From 9ad32125c0ee8701e76101ec96c5c3b91ea6c86b Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Tue, 26 May 2020 23:35:05 +0200 Subject: [PATCH 01/33] Window decorations: initial implementation (incomplete) (issues #47 and #82) TODO - move window - resize window - window icon - window border --- .../java/com/formdev/flatlaf/FlatLaf.java | 5 + .../com/formdev/flatlaf/IntelliJTheme.java | 4 + .../formdev/flatlaf/ui/FlatRootPaneUI.java | 186 ++++++- .../com/formdev/flatlaf/ui/FlatTitlePane.java | 306 ++++++++++++ .../com/formdev/flatlaf/FlatLaf.properties | 14 + .../testing/FlatWindowDecorationsTest.java | 467 ++++++++++++++++++ .../testing/FlatWindowDecorationsTest.jfd | 336 +++++++++++++ .../flatlaf/testing/FlatTestLaf.properties | 8 + 8 files changed, 1322 insertions(+), 4 deletions(-) create mode 100644 flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTitlePane.java create mode 100644 flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatWindowDecorationsTest.java create mode 100644 flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatWindowDecorationsTest.jfd diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatLaf.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatLaf.java index 2f97c616..cac59091 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatLaf.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatLaf.java @@ -110,6 +110,11 @@ public abstract class FlatLaf public abstract boolean isDark(); + @Override + public boolean getSupportsWindowDecorations() { + return SystemInfo.IS_WINDOWS; + } + @Override public boolean isNativeLookAndFeel() { return false; diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/IntelliJTheme.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/IntelliJTheme.java index 5c5119ff..f6cc7240 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/IntelliJTheme.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/IntelliJTheme.java @@ -492,6 +492,10 @@ public class IntelliJTheme // Slider uiKeyMapping.put( "Slider.trackWidth", "" ); // ignore (used in Material Theme UI Lite) + // TitlePane + uiKeyMapping.put( "TitlePane.infoForeground", "TitlePane.foreground" ); + uiKeyMapping.put( "TitlePane.inactiveInfoForeground", "TitlePane.inactiveForeground" ); + for( Map.Entry e : uiKeyMapping.entrySet() ) uiKeyInverseMapping.put( e.getValue(), e.getKey() ); 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 1f473487..6396890d 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 @@ -16,7 +16,18 @@ package com.formdev.flatlaf.ui; +import java.awt.Component; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.Insets; +import java.awt.LayoutManager; +import java.awt.LayoutManager2; +import java.beans.PropertyChangeEvent; +import java.util.function.Function; import javax.swing.JComponent; +import javax.swing.JLayeredPane; +import javax.swing.JMenuBar; +import javax.swing.JRootPane; import javax.swing.plaf.ComponentUI; import javax.swing.plaf.basic.BasicRootPaneUI; @@ -28,11 +39,178 @@ import javax.swing.plaf.basic.BasicRootPaneUI; public class FlatRootPaneUI extends BasicRootPaneUI { - private static ComponentUI instance; + private JRootPane rootPane; + private JComponent titlePane; + private LayoutManager oldLayout; public static ComponentUI createUI( JComponent c ) { - if( instance == null ) - instance = new FlatRootPaneUI(); - return instance; + return new FlatRootPaneUI(); + } + + @Override + public void installUI( JComponent c ) { + super.installUI( c ); + + rootPane = (JRootPane) c; + + if( rootPane.getWindowDecorationStyle() != JRootPane.NONE ) + installClientDecorations(); + } + + @Override + public void uninstallUI( JComponent c ) { + super.uninstallUI( c ); + + uninstallClientDecorations(); + rootPane = null; + } + + private void installClientDecorations() { + // install title pane + setTitlePane( new FlatTitlePane( rootPane ) ); + + // install layout + oldLayout = rootPane.getLayout(); + rootPane.setLayout( new FlatRootLayout() ); + } + + private void uninstallClientDecorations() { + setTitlePane( null ); + + if( oldLayout != null ) { + rootPane.setLayout( oldLayout ); + oldLayout = null; + } + + if( rootPane.getWindowDecorationStyle() == JRootPane.NONE ) { + rootPane.revalidate(); + rootPane.repaint(); + } + } + + private void setTitlePane( JComponent newTitlePane ) { + JLayeredPane layeredPane = rootPane.getLayeredPane(); + + if( titlePane != null ) + layeredPane.remove( titlePane ); + + if( newTitlePane != null ) + layeredPane.add( newTitlePane, JLayeredPane.FRAME_CONTENT_LAYER ); + + titlePane = newTitlePane; + } + + @Override + public void propertyChange( PropertyChangeEvent e ) { + super.propertyChange( e ); + + switch( e.getPropertyName() ) { + case "windowDecorationStyle": + uninstallClientDecorations(); + if( rootPane.getWindowDecorationStyle() != JRootPane.NONE ) + installClientDecorations(); + break; + } + } + + //---- class FlatRootLayout ----------------------------------------------- + + private static class FlatRootLayout + implements LayoutManager2 + { + @Override public void addLayoutComponent( String name, Component comp ) {} + @Override public void addLayoutComponent( Component comp, Object constraints ) {} + @Override public void removeLayoutComponent( Component comp ) {} + @Override public void invalidateLayout( Container target ) {} + + @Override + public Dimension preferredLayoutSize( Container parent ) { + return computeLayoutSize( parent, c -> c.getPreferredSize() ); + } + + @Override + public Dimension minimumLayoutSize( Container parent ) { + return computeLayoutSize( parent, c -> c.getMinimumSize() ); + } + + @Override + public Dimension maximumLayoutSize( Container parent ) { + return new Dimension( Integer.MAX_VALUE, Integer.MAX_VALUE ); + } + + private Dimension computeLayoutSize( Container parent, Function getSizeFunc ) { + JRootPane rootPane = (JRootPane) parent; + JComponent titlePane = getTitlePane( rootPane ); + + Dimension titlePaneSize = (titlePane != null) + ? getSizeFunc.apply( titlePane ) + : new Dimension(); + Dimension menuBarSize = (rootPane.getJMenuBar() != null) + ? getSizeFunc.apply( rootPane.getJMenuBar() ) + : new Dimension(); + Dimension contentSize = (rootPane.getContentPane() != null) + ? getSizeFunc.apply( rootPane.getContentPane() ) + : rootPane.getSize(); + + int width = Math.max( titlePaneSize.width, Math.max( menuBarSize.width, contentSize.width ) ); + int height = titlePaneSize.height + menuBarSize.height + contentSize.height; + Insets insets = rootPane.getInsets(); + + return new Dimension( + width + insets.left + insets.right, + height + insets.top + insets.bottom ); + } + + private JComponent getTitlePane( JRootPane rootPane ) { + return (rootPane.getWindowDecorationStyle() != JRootPane.NONE && + rootPane.getUI() instanceof FlatRootPaneUI) + ? ((FlatRootPaneUI)rootPane.getUI()).titlePane + : null; + } + + @Override + public void layoutContainer( Container parent ) { + JRootPane rootPane = (JRootPane) parent; + + Insets insets = rootPane.getInsets(); + int x = insets.left; + int y = insets.top; + int width = rootPane.getWidth() - insets.left - insets.right; + int height = rootPane.getHeight() - insets.top - insets.bottom; + + if( rootPane.getLayeredPane() != null ) + rootPane.getLayeredPane().setBounds( x, y, width, height ); + if( rootPane.getGlassPane() != null ) + rootPane.getGlassPane().setBounds( x, y, width, height ); + + int nextY = 0; + JComponent titlePane = getTitlePane( rootPane ); + if( titlePane != null ) { + Dimension prefSize = titlePane.getPreferredSize(); + titlePane.setBounds( 0, 0, width, prefSize.height ); + nextY += prefSize.height; + } + + JMenuBar menuBar = rootPane.getJMenuBar(); + if( menuBar != null ) { + Dimension prefSize = menuBar.getPreferredSize(); + menuBar.setBounds( 0, nextY, width, prefSize.height ); + nextY += prefSize.height; + } + + Container contentPane = rootPane.getContentPane(); + if( contentPane != null ) + contentPane.setBounds( 0, nextY, width, Math.max( height - nextY, 0 ) ); + } + + @Override + public float getLayoutAlignmentX( Container target ) { + return 0; + } + + @Override + public float getLayoutAlignmentY( Container target ) { + return 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 new file mode 100644 index 00000000..014b5d53 --- /dev/null +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTitlePane.java @@ -0,0 +1,306 @@ +/* + * 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.Dialog; +import java.awt.Frame; +import java.awt.Graphics; +import java.awt.Window; +import java.awt.event.ActionListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import javax.accessibility.AccessibleContext; +import javax.swing.BorderFactory; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JRootPane; +import javax.swing.SwingUtilities; +import javax.swing.UIManager; +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.closeIcon Icon + * @uiDefault TitlePane.iconifyIcon Icon + * @uiDefault TitlePane.maximizeIcon Icon + * @uiDefault TitlePane.minimizeIcon Icon + * + * @author Karl Tauber + */ +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 JRootPane rootPane; + + private JLabel titleLabel; + private JPanel buttonPanel; + private JButton iconifyButton; + private JButton maximizeButton; + private JButton restoreButton; + private JButton closeButton; + + private final Handler handler = new Handler(); + private Window window; + + FlatTitlePane( JRootPane rootPane ) { + this.rootPane = rootPane; + + addSubComponents(); + activeChanged( true ); + + addMouseListener( handler ); + } + + private void addSubComponents() { + titleLabel = new JLabel(); + titleLabel.setBorder( new FlatEmptyBorder( UIManager.getInsets( "TitlePane.titleMargins" ) ) ); + + createButtons(); + + setLayout( new BorderLayout( UIScale.scale( 4 ), 0 ) ); + add( titleLabel, BorderLayout.CENTER ); + add( buttonPanel, BorderLayout.EAST ); + } + + private void createButtons() { + iconifyButton = createButton( "TitlePane.iconifyIcon", "Iconify", e -> iconify() ); + maximizeButton = createButton( "TitlePane.maximizeIcon", "Maximize", e -> maximize() ); + restoreButton = createButton( "TitlePane.minimizeIcon", "Restore", e -> restore() ); + closeButton = createButton( "TitlePane.closeIcon", "Close", e -> close() ); + + buttonPanel = new JPanel(); + buttonPanel.setOpaque( false ); + buttonPanel.setLayout( new BoxLayout( buttonPanel, BoxLayout.X_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 ); + } + + private 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; + } + + private void activeChanged( boolean active ) { + setBackground( active ? activeBackground : inactiveBackground ); + titleLabel.setForeground( FlatUIUtils.nonUIResource( active ? activeForeground : inactiveForeground ) ); + } + + private 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(); + } + } + + @Override + public void addNotify() { + super.addNotify(); + + uninstallWindowListeners(); + + window = SwingUtilities.getWindowAncestor( this ); + if( window != null ) { + frameStateChanged(); + activeChanged( window.isActive() ); + titleLabel.setText( getWindowTitle() ); + installWindowListeners(); + } + } + + @Override + public void removeNotify() { + super.removeNotify(); + + uninstallWindowListeners(); + window = null; + } + + private String getWindowTitle() { + if( window instanceof Frame ) + return ((Frame)window).getTitle(); + if( window instanceof Dialog ) + return ((Dialog)window).getTitle(); + return null; + } + + private void installWindowListeners() { + if( window == null ) + return; + + window.addPropertyChangeListener( handler ); + window.addWindowListener( handler ); + window.addWindowStateListener( handler ); + } + + private void uninstallWindowListeners() { + if( window == null ) + return; + + window.removePropertyChangeListener( handler ); + window.removeWindowListener( handler ); + window.removeWindowStateListener( handler ); + } + + @Override + protected void paintComponent( Graphics g ) { + g.setColor( getBackground() ); + g.fillRect( 0, 0, getWidth(), getHeight() ); + } + + private void iconify() { + if( window instanceof Frame ) { + Frame frame = (Frame) window; + frame.setExtendedState( frame.getExtendedState() | Frame.ICONIFIED ); + } + } + + private void maximize() { + if( window instanceof Frame ) { + Frame frame = (Frame) window; + frame.setExtendedState( frame.getExtendedState() | Frame.MAXIMIZED_BOTH ); + } + } + + private 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) ); + } + } + + private void close() { + if( window != null ) + window.dispatchEvent( new WindowEvent( window, WindowEvent.WINDOW_CLOSING ) ); + } + + //---- class Handler ------------------------------------------------------ + + private class Handler + extends WindowAdapter + implements PropertyChangeListener, MouseListener + { + //---- 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; + } + } + + //---- interface WindowListener ---- + + @Override + public void windowActivated( WindowEvent e ) { + activeChanged( true ); + } + + @Override + public void windowDeactivated( WindowEvent e ) { + activeChanged( false ); + } + + @Override + public void windowStateChanged( WindowEvent e ) { + frameStateChanged(); + } + + //---- interface MouseListener ---- + + @Override + public void mouseClicked( MouseEvent e ) { + if( e.getClickCount() == 2 && + SwingUtilities.isLeftMouseButton( e ) && + window instanceof Frame && + ((Frame)window).isResizable() ) + { + // maximize/restore on double-click + Frame frame = (Frame) window; + int state = frame.getExtendedState(); + frame.setExtendedState( ((state & Frame.MAXIMIZED_BOTH) != 0) + ? (state & ~Frame.MAXIMIZED_BOTH) + : (state | Frame.MAXIMIZED_BOTH) ); + } + } + + @Override public void mousePressed( MouseEvent e ) {} + @Override public void mouseReleased( MouseEvent e ) {} + @Override public void mouseEntered( MouseEvent e ) {} + @Override public void mouseExited( MouseEvent e ) {} + } +} 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 d5c6dcb0..98094770 100644 --- a/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLaf.properties +++ b/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLaf.properties @@ -574,6 +574,20 @@ TitledBorder.titleColor=@foreground TitledBorder.border=1,1,1,1,$Separator.foreground +#---- TitlePane ---- + +TitlePane.titleMargins=3,8,3,8 +TitlePane.closeIcon=$InternalFrame.closeIcon +TitlePane.iconifyIcon=$InternalFrame.iconifyIcon +TitlePane.maximizeIcon=$InternalFrame.maximizeIcon +TitlePane.minimizeIcon=$InternalFrame.minimizeIcon + +TitlePane.background=$MenuBar.background +TitlePane.inactiveBackground=$TitlePane.background +TitlePane.foreground=@foreground +TitlePane.inactiveForeground=@disabledText + + #---- ToggleButton ---- ToggleButton.border=com.formdev.flatlaf.ui.FlatButtonBorder 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 new file mode 100644 index 00000000..6d4efdab --- /dev/null +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatWindowDecorationsTest.java @@ -0,0 +1,467 @@ +/* + * 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.testing; + +import java.awt.*; +import java.awt.event.*; +import javax.swing.*; +import net.miginfocom.swing.*; + +/** + * @author Karl Tauber + */ +public class FlatWindowDecorationsTest + extends FlatTestPanel +{ + public static void main( String[] args ) { + SwingUtilities.invokeLater( () -> { + // enable custom window decoration (if LaF supports it) + JFrame.setDefaultLookAndFeelDecorated( true ); + JDialog.setDefaultLookAndFeelDecorated( true ); + + FlatTestFrame frame = FlatTestFrame.create( args, "FlatWindowDecorationsTest" ); + + // WARNING: Do not this in real-world programs. +// frame.setUndecorated( true ); +// frame.getRootPane().setWindowDecorationStyle( JRootPane.FRAME ); + + frame.showFrame( FlatWindowDecorationsTest::new, panel -> ((FlatWindowDecorationsTest)panel).menuBar ); + } ); + } + + FlatWindowDecorationsTest() { + initComponents(); + } + + @Override + public void addNotify() { + super.addNotify(); + + JRootPane rootPane = getWindowRootPane(); + if( rootPane != null ) { + int style = rootPane.getWindowDecorationStyle(); + if( style == JRootPane.NONE ) + styleNoneRadioButton.setSelected( true ); + else if( style == JRootPane.FRAME ) + styleFrameRadioButton.setSelected( true ); + else if( style == JRootPane.PLAIN_DIALOG ) + stylePlainRadioButton.setSelected( true ); + else if( style == JRootPane.INFORMATION_DIALOG ) + styleInfoRadioButton.setSelected( true ); + else + throw new RuntimeException(); // not used + } + } + + private void menuBarChanged() { + Window window = SwingUtilities.windowForComponent( this ); + if( window instanceof JFrame ) { + ((JFrame)window).setJMenuBar( menuBarCheckBox.isSelected() ? menuBar : null ); + window.revalidate(); + window.repaint(); + } + } + + private void resizableChanged() { + Window window = SwingUtilities.windowForComponent( this ); + if( window instanceof Frame ) + ((Frame)window).setResizable( resizableCheckBox.isSelected() ); + } + + private void menuItemActionPerformed(ActionEvent e) { + SwingUtilities.invokeLater( () -> { + JOptionPane.showMessageDialog( this, e.getActionCommand(), "Menu Item", JOptionPane.PLAIN_MESSAGE ); + } ); + } + + private void openDialog() { + JOptionPane.showMessageDialog( this, new FlatWindowDecorationsTest() ); + } + + private void decorationStyleChanged() { + int style; + if( styleFrameRadioButton.isSelected() ) + style = JRootPane.FRAME; + else if( stylePlainRadioButton.isSelected() ) + style = JRootPane.PLAIN_DIALOG; + else if( styleInfoRadioButton.isSelected() ) + style = JRootPane.INFORMATION_DIALOG; + else if( styleErrorRadioButton.isSelected() ) + style = JRootPane.ERROR_DIALOG; + else if( styleQuestionRadioButton.isSelected() ) + style = JRootPane.QUESTION_DIALOG; + else if( styleWarningRadioButton.isSelected() ) + style = JRootPane.WARNING_DIALOG; + else if( styleColorChooserRadioButton.isSelected() ) + style = JRootPane.COLOR_CHOOSER_DIALOG; + else if( styleFileChooserRadioButton.isSelected() ) + style = JRootPane.FILE_CHOOSER_DIALOG; + else + style = JRootPane.NONE; + + JRootPane rootPane = getWindowRootPane(); + if( rootPane != null ) + rootPane.setWindowDecorationStyle( style ); + } + + 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; + } + + private void initComponents() { + // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents + menuBarCheckBox = new JCheckBox(); + resizableCheckBox = new JCheckBox(); + JLabel label1 = new JLabel(); + JPanel panel1 = new JPanel(); + styleNoneRadioButton = new JRadioButton(); + styleFrameRadioButton = new JRadioButton(); + stylePlainRadioButton = new JRadioButton(); + styleInfoRadioButton = new JRadioButton(); + styleErrorRadioButton = new JRadioButton(); + styleQuestionRadioButton = new JRadioButton(); + styleWarningRadioButton = new JRadioButton(); + styleColorChooserRadioButton = new JRadioButton(); + styleFileChooserRadioButton = new JRadioButton(); + JButton openDialogButton = new JButton(); + menuBar = new JMenuBar(); + JMenu fileMenu = new JMenu(); + JMenuItem newMenuItem = new JMenuItem(); + JMenuItem openMenuItem = new JMenuItem(); + JMenuItem closeMenuItem = new JMenuItem(); + JMenuItem closeMenuItem2 = new JMenuItem(); + JMenuItem exitMenuItem = new JMenuItem(); + JMenu editMenu = new JMenu(); + JMenuItem undoMenuItem = new JMenuItem(); + JMenuItem redoMenuItem = new JMenuItem(); + JMenuItem cutMenuItem = new JMenuItem(); + JMenuItem copyMenuItem = new JMenuItem(); + JMenuItem pasteMenuItem = new JMenuItem(); + JMenuItem deleteMenuItem = new JMenuItem(); + JMenu viewMenu = new JMenu(); + JCheckBoxMenuItem checkBoxMenuItem1 = new JCheckBoxMenuItem(); + JMenu menu1 = new JMenu(); + JMenu subViewsMenu = new JMenu(); + JMenu subSubViewsMenu = new JMenu(); + JMenuItem errorLogViewMenuItem = new JMenuItem(); + JMenuItem searchViewMenuItem = new JMenuItem(); + JMenuItem projectViewMenuItem = new JMenuItem(); + JMenuItem structureViewMenuItem = new JMenuItem(); + JMenuItem propertiesViewMenuItem = new JMenuItem(); + JMenu helpMenu = new JMenu(); + JMenuItem aboutMenuItem = new JMenuItem(); + + //======== this ======== + setLayout(new MigLayout( + "ltr,insets dialog,hidemode 3", + // columns + "[left]para", + // rows + "para[]" + + "[]" + + "[]" + + "[]" + + "[]")); + + //---- menuBarCheckBox ---- + menuBarCheckBox.setText("menu bar"); + menuBarCheckBox.setSelected(true); + menuBarCheckBox.addActionListener(e -> menuBarChanged()); + add(menuBarCheckBox, "cell 0 0"); + + //---- resizableCheckBox ---- + resizableCheckBox.setText("resizable"); + resizableCheckBox.setSelected(true); + resizableCheckBox.addActionListener(e -> resizableChanged()); + add(resizableCheckBox, "cell 0 1"); + + //---- label1 ---- + label1.setText("Style:"); + add(label1, "cell 0 2"); + + //======== panel1 ======== + { + panel1.setLayout(new MigLayout( + "ltr,insets 0,hidemode 3,gap 0 0", + // columns + "[fill]", + // rows + "[]" + + "[]" + + "[]" + + "[]" + + "[]" + + "[]" + + "[]" + + "[]" + + "[]")); + + //---- styleNoneRadioButton ---- + styleNoneRadioButton.setText("none"); + styleNoneRadioButton.addActionListener(e -> decorationStyleChanged()); + panel1.add(styleNoneRadioButton, "cell 0 0"); + + //---- styleFrameRadioButton ---- + styleFrameRadioButton.setText("frame"); + styleFrameRadioButton.setSelected(true); + styleFrameRadioButton.addActionListener(e -> decorationStyleChanged()); + panel1.add(styleFrameRadioButton, "cell 0 1"); + + //---- stylePlainRadioButton ---- + stylePlainRadioButton.setText("plain dialog"); + stylePlainRadioButton.addActionListener(e -> decorationStyleChanged()); + panel1.add(stylePlainRadioButton, "cell 0 2"); + + //---- styleInfoRadioButton ---- + styleInfoRadioButton.setText("info dialog"); + styleInfoRadioButton.addActionListener(e -> decorationStyleChanged()); + panel1.add(styleInfoRadioButton, "cell 0 3"); + + //---- styleErrorRadioButton ---- + styleErrorRadioButton.setText("error dialog"); + styleErrorRadioButton.addActionListener(e -> decorationStyleChanged()); + panel1.add(styleErrorRadioButton, "cell 0 4"); + + //---- styleQuestionRadioButton ---- + styleQuestionRadioButton.setText("question dialog"); + styleQuestionRadioButton.addActionListener(e -> decorationStyleChanged()); + panel1.add(styleQuestionRadioButton, "cell 0 5"); + + //---- styleWarningRadioButton ---- + styleWarningRadioButton.setText("warning dialog"); + styleWarningRadioButton.addActionListener(e -> decorationStyleChanged()); + panel1.add(styleWarningRadioButton, "cell 0 6"); + + //---- styleColorChooserRadioButton ---- + styleColorChooserRadioButton.setText("color chooser dialog"); + styleColorChooserRadioButton.addActionListener(e -> decorationStyleChanged()); + panel1.add(styleColorChooserRadioButton, "cell 0 7"); + + //---- styleFileChooserRadioButton ---- + styleFileChooserRadioButton.setText("file chooser dialog"); + styleFileChooserRadioButton.addActionListener(e -> decorationStyleChanged()); + panel1.add(styleFileChooserRadioButton, "cell 0 8"); + } + add(panel1, "cell 0 3"); + + //---- openDialogButton ---- + openDialogButton.setText("Open Dialog"); + openDialogButton.addActionListener(e -> openDialog()); + add(openDialogButton, "cell 0 4"); + + //======== menuBar ======== + { + + //======== fileMenu ======== + { + fileMenu.setText("File"); + fileMenu.setMnemonic('F'); + + //---- newMenuItem ---- + newMenuItem.setText("New"); + newMenuItem.setMnemonic('N'); + newMenuItem.addActionListener(e -> menuItemActionPerformed(e)); + fileMenu.add(newMenuItem); + + //---- openMenuItem ---- + openMenuItem.setText("Open"); + openMenuItem.setMnemonic('O'); + openMenuItem.addActionListener(e -> menuItemActionPerformed(e)); + fileMenu.add(openMenuItem); + fileMenu.addSeparator(); + + //---- closeMenuItem ---- + closeMenuItem.setText("Close"); + closeMenuItem.setMnemonic('C'); + closeMenuItem.addActionListener(e -> menuItemActionPerformed(e)); + fileMenu.add(closeMenuItem); + + //---- closeMenuItem2 ---- + closeMenuItem2.setText("Close All"); + closeMenuItem2.addActionListener(e -> menuItemActionPerformed(e)); + fileMenu.add(closeMenuItem2); + fileMenu.addSeparator(); + + //---- exitMenuItem ---- + exitMenuItem.setText("Exit"); + exitMenuItem.setMnemonic('X'); + exitMenuItem.addActionListener(e -> menuItemActionPerformed(e)); + fileMenu.add(exitMenuItem); + } + menuBar.add(fileMenu); + + //======== editMenu ======== + { + editMenu.setText("Edit"); + editMenu.setMnemonic('E'); + + //---- undoMenuItem ---- + undoMenuItem.setText("Undo"); + undoMenuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Z, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); + undoMenuItem.setMnemonic('U'); + undoMenuItem.addActionListener(e -> menuItemActionPerformed(e)); + editMenu.add(undoMenuItem); + + //---- redoMenuItem ---- + redoMenuItem.setText("Redo"); + redoMenuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Y, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); + redoMenuItem.setMnemonic('R'); + redoMenuItem.addActionListener(e -> menuItemActionPerformed(e)); + editMenu.add(redoMenuItem); + editMenu.addSeparator(); + + //---- cutMenuItem ---- + cutMenuItem.setText("Cut"); + cutMenuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_X, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); + cutMenuItem.setMnemonic('C'); + editMenu.add(cutMenuItem); + + //---- copyMenuItem ---- + copyMenuItem.setText("Copy"); + copyMenuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_C, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); + copyMenuItem.setMnemonic('O'); + editMenu.add(copyMenuItem); + + //---- pasteMenuItem ---- + pasteMenuItem.setText("Paste"); + pasteMenuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_V, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); + pasteMenuItem.setMnemonic('P'); + editMenu.add(pasteMenuItem); + editMenu.addSeparator(); + + //---- deleteMenuItem ---- + deleteMenuItem.setText("Delete"); + deleteMenuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0)); + deleteMenuItem.setMnemonic('D'); + deleteMenuItem.addActionListener(e -> menuItemActionPerformed(e)); + editMenu.add(deleteMenuItem); + } + menuBar.add(editMenu); + + //======== viewMenu ======== + { + viewMenu.setText("View"); + viewMenu.setMnemonic('V'); + + //---- checkBoxMenuItem1 ---- + checkBoxMenuItem1.setText("Show Toolbar"); + checkBoxMenuItem1.setSelected(true); + checkBoxMenuItem1.setMnemonic('T'); + checkBoxMenuItem1.addActionListener(e -> menuItemActionPerformed(e)); + viewMenu.add(checkBoxMenuItem1); + + //======== menu1 ======== + { + menu1.setText("Show View"); + menu1.setMnemonic('V'); + + //======== subViewsMenu ======== + { + subViewsMenu.setText("Sub Views"); + subViewsMenu.setMnemonic('S'); + + //======== subSubViewsMenu ======== + { + subSubViewsMenu.setText("Sub sub Views"); + subSubViewsMenu.setMnemonic('U'); + + //---- errorLogViewMenuItem ---- + errorLogViewMenuItem.setText("Error Log"); + errorLogViewMenuItem.setMnemonic('E'); + errorLogViewMenuItem.addActionListener(e -> menuItemActionPerformed(e)); + subSubViewsMenu.add(errorLogViewMenuItem); + } + subViewsMenu.add(subSubViewsMenu); + + //---- searchViewMenuItem ---- + searchViewMenuItem.setText("Search"); + searchViewMenuItem.setMnemonic('S'); + searchViewMenuItem.addActionListener(e -> menuItemActionPerformed(e)); + subViewsMenu.add(searchViewMenuItem); + } + menu1.add(subViewsMenu); + + //---- projectViewMenuItem ---- + projectViewMenuItem.setText("Project"); + projectViewMenuItem.setMnemonic('P'); + projectViewMenuItem.addActionListener(e -> menuItemActionPerformed(e)); + menu1.add(projectViewMenuItem); + + //---- structureViewMenuItem ---- + structureViewMenuItem.setText("Structure"); + structureViewMenuItem.setMnemonic('T'); + structureViewMenuItem.addActionListener(e -> menuItemActionPerformed(e)); + menu1.add(structureViewMenuItem); + + //---- propertiesViewMenuItem ---- + propertiesViewMenuItem.setText("Properties"); + propertiesViewMenuItem.setMnemonic('O'); + propertiesViewMenuItem.addActionListener(e -> menuItemActionPerformed(e)); + menu1.add(propertiesViewMenuItem); + } + viewMenu.add(menu1); + } + menuBar.add(viewMenu); + + //======== helpMenu ======== + { + helpMenu.setText("Help"); + helpMenu.setMnemonic('H'); + + //---- aboutMenuItem ---- + aboutMenuItem.setText("About"); + aboutMenuItem.setMnemonic('A'); + aboutMenuItem.addActionListener(e -> menuItemActionPerformed(e)); + helpMenu.add(aboutMenuItem); + } + menuBar.add(helpMenu); + } + + //---- styleButtonGroup ---- + ButtonGroup styleButtonGroup = new ButtonGroup(); + styleButtonGroup.add(styleNoneRadioButton); + styleButtonGroup.add(styleFrameRadioButton); + styleButtonGroup.add(stylePlainRadioButton); + styleButtonGroup.add(styleInfoRadioButton); + styleButtonGroup.add(styleErrorRadioButton); + styleButtonGroup.add(styleQuestionRadioButton); + styleButtonGroup.add(styleWarningRadioButton); + styleButtonGroup.add(styleColorChooserRadioButton); + styleButtonGroup.add(styleFileChooserRadioButton); + // JFormDesigner - End of component initialization //GEN-END:initComponents + } + + // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables + private JCheckBox menuBarCheckBox; + private JCheckBox resizableCheckBox; + private JRadioButton styleNoneRadioButton; + private JRadioButton styleFrameRadioButton; + private JRadioButton stylePlainRadioButton; + private JRadioButton styleInfoRadioButton; + private JRadioButton styleErrorRadioButton; + private JRadioButton styleQuestionRadioButton; + private JRadioButton styleWarningRadioButton; + private JRadioButton styleColorChooserRadioButton; + private JRadioButton styleFileChooserRadioButton; + 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 new file mode 100644 index 00000000..dfaee803 --- /dev/null +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatWindowDecorationsTest.jfd @@ -0,0 +1,336 @@ +JFDML JFormDesigner: "7.0.1.0.272" Java: "13.0.2" encoding: "UTF-8" + +new FormModel { + contentType: "form/swing" + root: new FormRoot { + auxiliary() { + "JavaCodeGenerator.defaultVariableLocal": true + } + add( new FormContainer( "com.formdev.flatlaf.testing.FlatTestPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { + "$layoutConstraints": "ltr,insets dialog,hidemode 3" + "$columnConstraints": "[left]para" + "$rowConstraints": "para[][][][][]" + } ) { + name: "this" + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "menuBarCheckBox" + "text": "menu bar" + "selected": true + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "menuBarChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0" + } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "resizableCheckBox" + "text": "resizable" + "selected": true + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "resizableChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 1" + } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "label1" + "text": "Style:" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 2" + } ) + add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { + "$columnConstraints": "[fill]" + "$rowConstraints": "[][][][][][][][][]" + "$layoutConstraints": "ltr,insets 0,hidemode 3,gap 0 0" + } ) { + name: "panel1" + add( new FormComponent( "javax.swing.JRadioButton" ) { + name: "styleNoneRadioButton" + "text": "none" + "$buttonGroup": new FormReference( "styleButtonGroup" ) + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "decorationStyleChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0" + } ) + add( new FormComponent( "javax.swing.JRadioButton" ) { + name: "styleFrameRadioButton" + "text": "frame" + "$buttonGroup": new FormReference( "styleButtonGroup" ) + "selected": true + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "decorationStyleChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 1" + } ) + add( new FormComponent( "javax.swing.JRadioButton" ) { + name: "stylePlainRadioButton" + "text": "plain dialog" + "$buttonGroup": new FormReference( "styleButtonGroup" ) + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "decorationStyleChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 2" + } ) + add( new FormComponent( "javax.swing.JRadioButton" ) { + name: "styleInfoRadioButton" + "text": "info dialog" + "$buttonGroup": new FormReference( "styleButtonGroup" ) + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "decorationStyleChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 3" + } ) + add( new FormComponent( "javax.swing.JRadioButton" ) { + name: "styleErrorRadioButton" + "text": "error dialog" + "$buttonGroup": new FormReference( "styleButtonGroup" ) + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "decorationStyleChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 4" + } ) + add( new FormComponent( "javax.swing.JRadioButton" ) { + name: "styleQuestionRadioButton" + "text": "question dialog" + "$buttonGroup": new FormReference( "styleButtonGroup" ) + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "decorationStyleChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 5" + } ) + add( new FormComponent( "javax.swing.JRadioButton" ) { + name: "styleWarningRadioButton" + "text": "warning dialog" + "$buttonGroup": new FormReference( "styleButtonGroup" ) + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "decorationStyleChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 6" + } ) + add( new FormComponent( "javax.swing.JRadioButton" ) { + name: "styleColorChooserRadioButton" + "text": "color chooser dialog" + "$buttonGroup": new FormReference( "styleButtonGroup" ) + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "decorationStyleChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 7" + } ) + add( new FormComponent( "javax.swing.JRadioButton" ) { + name: "styleFileChooserRadioButton" + "text": "file chooser dialog" + "$buttonGroup": new FormReference( "styleButtonGroup" ) + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "decorationStyleChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 8" + } ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 3" + } ) + add( new FormComponent( "javax.swing.JButton" ) { + name: "openDialogButton" + "text": "Open Dialog" + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "openDialog", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 4" + } ) + }, new FormLayoutConstraints( null ) { + "location": new java.awt.Point( 0, 0 ) + "size": new java.awt.Dimension( 450, 380 ) + } ) + add( new FormContainer( "javax.swing.JMenuBar", new FormLayoutManager( class javax.swing.JMenuBar ) ) { + name: "menuBar" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + add( new FormContainer( "javax.swing.JMenu", new FormLayoutManager( class javax.swing.JMenu ) ) { + name: "fileMenu" + "text": "File" + "mnemonic": 70 + add( new FormComponent( "javax.swing.JMenuItem" ) { + name: "newMenuItem" + "text": "New" + "mnemonic": 78 + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "menuItemActionPerformed", true ) ) + } ) + add( new FormComponent( "javax.swing.JMenuItem" ) { + name: "openMenuItem" + "text": "Open" + "mnemonic": 79 + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "menuItemActionPerformed", true ) ) + } ) + add( new FormComponent( "javax.swing.JPopupMenu$Separator" ) { + name: "separator2" + } ) + add( new FormComponent( "javax.swing.JMenuItem" ) { + name: "closeMenuItem" + "text": "Close" + "mnemonic": 67 + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "menuItemActionPerformed", true ) ) + } ) + add( new FormComponent( "javax.swing.JMenuItem" ) { + name: "closeMenuItem2" + "text": "Close All" + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "menuItemActionPerformed", true ) ) + } ) + add( new FormComponent( "javax.swing.JPopupMenu$Separator" ) { + name: "separator1" + } ) + add( new FormComponent( "javax.swing.JMenuItem" ) { + name: "exitMenuItem" + "text": "Exit" + "mnemonic": 88 + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "menuItemActionPerformed", true ) ) + } ) + } ) + add( new FormContainer( "javax.swing.JMenu", new FormLayoutManager( class javax.swing.JMenu ) ) { + name: "editMenu" + "text": "Edit" + "mnemonic": 69 + add( new FormComponent( "javax.swing.JMenuItem" ) { + name: "undoMenuItem" + "text": "Undo" + "accelerator": static javax.swing.KeyStroke getKeyStroke( 90, 4226, false ) + "mnemonic": 85 + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "menuItemActionPerformed", true ) ) + } ) + add( new FormComponent( "javax.swing.JMenuItem" ) { + name: "redoMenuItem" + "text": "Redo" + "accelerator": static javax.swing.KeyStroke getKeyStroke( 89, 4226, false ) + "mnemonic": 82 + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "menuItemActionPerformed", true ) ) + } ) + add( new FormComponent( "javax.swing.JPopupMenu$Separator" ) { + name: "separator4" + } ) + add( new FormComponent( "javax.swing.JMenuItem" ) { + name: "cutMenuItem" + "text": "Cut" + "accelerator": static javax.swing.KeyStroke getKeyStroke( 88, 4226, false ) + "mnemonic": 67 + } ) + add( new FormComponent( "javax.swing.JMenuItem" ) { + name: "copyMenuItem" + "text": "Copy" + "accelerator": static javax.swing.KeyStroke getKeyStroke( 67, 4226, false ) + "mnemonic": 79 + } ) + add( new FormComponent( "javax.swing.JMenuItem" ) { + name: "pasteMenuItem" + "text": "Paste" + "accelerator": static javax.swing.KeyStroke getKeyStroke( 86, 4226, false ) + "mnemonic": 80 + } ) + add( new FormComponent( "javax.swing.JPopupMenu$Separator" ) { + name: "separator3" + } ) + add( new FormComponent( "javax.swing.JMenuItem" ) { + name: "deleteMenuItem" + "text": "Delete" + "accelerator": static javax.swing.KeyStroke getKeyStroke( 127, 0, false ) + "mnemonic": 68 + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "menuItemActionPerformed", true ) ) + } ) + } ) + add( new FormContainer( "javax.swing.JMenu", new FormLayoutManager( class javax.swing.JMenu ) ) { + name: "viewMenu" + "text": "View" + "mnemonic": 86 + add( new FormComponent( "javax.swing.JCheckBoxMenuItem" ) { + name: "checkBoxMenuItem1" + "text": "Show Toolbar" + "selected": true + "mnemonic": 84 + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "menuItemActionPerformed", true ) ) + } ) + add( new FormContainer( "javax.swing.JMenu", new FormLayoutManager( class javax.swing.JMenu ) ) { + name: "menu1" + "text": "Show View" + "mnemonic": 86 + add( new FormContainer( "javax.swing.JMenu", new FormLayoutManager( class javax.swing.JMenu ) ) { + name: "subViewsMenu" + "text": "Sub Views" + "mnemonic": 83 + add( new FormContainer( "javax.swing.JMenu", new FormLayoutManager( class javax.swing.JMenu ) ) { + name: "subSubViewsMenu" + "text": "Sub sub Views" + "mnemonic": 85 + add( new FormComponent( "javax.swing.JMenuItem" ) { + name: "errorLogViewMenuItem" + "text": "Error Log" + "mnemonic": 69 + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "menuItemActionPerformed", true ) ) + } ) + } ) + add( new FormComponent( "javax.swing.JMenuItem" ) { + name: "searchViewMenuItem" + "text": "Search" + "mnemonic": 83 + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "menuItemActionPerformed", true ) ) + } ) + } ) + add( new FormComponent( "javax.swing.JMenuItem" ) { + name: "projectViewMenuItem" + "text": "Project" + "mnemonic": 80 + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "menuItemActionPerformed", true ) ) + } ) + add( new FormComponent( "javax.swing.JMenuItem" ) { + name: "structureViewMenuItem" + "text": "Structure" + "mnemonic": 84 + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "menuItemActionPerformed", true ) ) + } ) + add( new FormComponent( "javax.swing.JMenuItem" ) { + name: "propertiesViewMenuItem" + "text": "Properties" + "mnemonic": 79 + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "menuItemActionPerformed", true ) ) + } ) + } ) + } ) + add( new FormContainer( "javax.swing.JMenu", new FormLayoutManager( class javax.swing.JMenu ) ) { + name: "helpMenu" + "text": "Help" + "mnemonic": 72 + add( new FormComponent( "javax.swing.JMenuItem" ) { + name: "aboutMenuItem" + "text": "About" + "mnemonic": 65 + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "menuItemActionPerformed", true ) ) + } ) + } ) + }, new FormLayoutConstraints( null ) { + "location": new java.awt.Point( 0, 410 ) + "size": new java.awt.Dimension( 255, 30 ) + } ) + add( new FormNonVisual( "javax.swing.ButtonGroup" ) { + name: "styleButtonGroup" + }, new FormLayoutConstraints( null ) { + "location": new java.awt.Point( 0, 450 ) + } ) + } +} diff --git a/flatlaf-testing/src/main/resources/com/formdev/flatlaf/testing/FlatTestLaf.properties b/flatlaf-testing/src/main/resources/com/formdev/flatlaf/testing/FlatTestLaf.properties index 9c007558..ae8aa809 100644 --- a/flatlaf-testing/src/main/resources/com/formdev/flatlaf/testing/FlatTestLaf.properties +++ b/flatlaf-testing/src/main/resources/com/formdev/flatlaf/testing/FlatTestLaf.properties @@ -307,6 +307,14 @@ TitledBorder.titleColor=#ff00ff TitledBorder.border=1,1,1,1,#ff00ff +#---- TitlePane ---- + +TitlePane.background=#0f0 +TitlePane.inactiveBackground=#080 +TitlePane.foreground=#00f +TitlePane.inactiveForeground=#fff + + #---- ToggleButton ---- ToggleButton.background=#ddddff From 626601f6aa4cc84048e25301d18d9e6592e9c7fd Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Wed, 27 May 2020 11:36:11 +0200 Subject: [PATCH 02/33] Window decorations: added window icon (issues #47 and #82) --- .../com/formdev/flatlaf/ui/FlatTitlePane.java | 40 ++++++- .../formdev/flatlaf/ui/FlatTitlePaneIcon.java | 70 ++++++++++++ .../formdev/flatlaf/util/ScaledImageIcon.java | 28 +++-- .../com/formdev/flatlaf/FlatLaf.properties | 2 + .../Window Icon Test Images.sketch | Bin 0 -> 14593 bytes .../testing/FlatWindowDecorationsTest.java | 104 +++++++++++++++++- .../testing/FlatWindowDecorationsTest.jfd | 58 +++++++++- .../com/formdev/flatlaf/testing/test128.png | Bin 0 -> 3934 bytes .../com/formdev/flatlaf/testing/test16.png | Bin 0 -> 428 bytes .../com/formdev/flatlaf/testing/test24.png | Bin 0 -> 624 bytes .../com/formdev/flatlaf/testing/test32.png | Bin 0 -> 1128 bytes .../com/formdev/flatlaf/testing/test48.png | Bin 0 -> 1494 bytes .../com/formdev/flatlaf/testing/test64.png | Bin 0 -> 1985 bytes 13 files changed, 286 insertions(+), 16 deletions(-) create mode 100644 flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTitlePaneIcon.java create mode 100644 flatlaf-testing/Window Icon Test Images.sketch create mode 100644 flatlaf-testing/src/main/resources/com/formdev/flatlaf/testing/test128.png create mode 100644 flatlaf-testing/src/main/resources/com/formdev/flatlaf/testing/test16.png create mode 100644 flatlaf-testing/src/main/resources/com/formdev/flatlaf/testing/test24.png create mode 100644 flatlaf-testing/src/main/resources/com/formdev/flatlaf/testing/test32.png create mode 100644 flatlaf-testing/src/main/resources/com/formdev/flatlaf/testing/test48.png create mode 100644 flatlaf-testing/src/main/resources/com/formdev/flatlaf/testing/test64.png 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 014b5d53..568d9594 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 @@ -19,8 +19,10 @@ package com.formdev.flatlaf.ui; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Dialog; +import java.awt.Dimension; import java.awt.Frame; import java.awt.Graphics; +import java.awt.Image; import java.awt.Window; import java.awt.event.ActionListener; import java.awt.event.MouseEvent; @@ -29,6 +31,7 @@ import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; +import java.util.List; import javax.accessibility.AccessibleContext; import javax.swing.BorderFactory; import javax.swing.BoxLayout; @@ -39,7 +42,6 @@ import javax.swing.JPanel; import javax.swing.JRootPane; import javax.swing.SwingUtilities; import javax.swing.UIManager; -import com.formdev.flatlaf.util.UIScale; /** * Provides the Flat LaF title bar. @@ -48,6 +50,9 @@ import com.formdev.flatlaf.util.UIScale; * @uiDefault TitlePane.inactiveBackground Color * @uiDefault TitlePane.foreground Color * @uiDefault TitlePane.inactiveForeground Color + * @uiDefault TitlePane.iconSize Dimension + * @uiDefault TitlePane.iconMargins Insets + * @uiDefault TitlePane.titleMargins Insets * @uiDefault TitlePane.closeIcon Icon * @uiDefault TitlePane.iconifyIcon Icon * @uiDefault TitlePane.maximizeIcon Icon @@ -63,8 +68,11 @@ class FlatTitlePane private final Color activeForeground = UIManager.getColor( "TitlePane.foreground" ); private final Color inactiveForeground = UIManager.getColor( "TitlePane.inactiveForeground" ); + private final Dimension iconSize = UIManager.getDimension( "TitlePane.iconSize" ); + private final JRootPane rootPane; + private JLabel iconLabel; private JLabel titleLabel; private JPanel buttonPanel; private JButton iconifyButton; @@ -85,12 +93,15 @@ class FlatTitlePane } private void addSubComponents() { + iconLabel = new JLabel(); titleLabel = new JLabel(); + iconLabel.setBorder( new FlatEmptyBorder( UIManager.getInsets( "TitlePane.iconMargins" ) ) ); titleLabel.setBorder( new FlatEmptyBorder( UIManager.getInsets( "TitlePane.titleMargins" ) ) ); createButtons(); - setLayout( new BorderLayout( UIScale.scale( 4 ), 0 ) ); + setLayout( new BorderLayout() ); + add( iconLabel, BorderLayout.WEST ); add( titleLabel, BorderLayout.CENTER ); add( buttonPanel, BorderLayout.EAST ); } @@ -157,6 +168,26 @@ class FlatTitlePane } } + private void updateIcon() { + // get window images + List 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; + } + } + + // show/hide icon + boolean hasImages = !images.isEmpty(); + iconLabel.setVisible( hasImages ); + + if( hasImages ) + iconLabel.setIcon( FlatTitlePaneIcon.create( images, iconSize ) ); + } + @Override public void addNotify() { super.addNotify(); @@ -167,6 +198,7 @@ class FlatTitlePane if( window != null ) { frameStateChanged(); activeChanged( window.isActive() ); + updateIcon(); titleLabel.setText( getWindowTitle() ); installWindowListeners(); } @@ -260,6 +292,10 @@ class FlatTitlePane if( window instanceof Frame ) frameStateChanged(); break; + + case "iconImage": + updateIcon(); + break; } } diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTitlePaneIcon.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTitlePaneIcon.java new file mode 100644 index 00000000..93084ccc --- /dev/null +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTitlePaneIcon.java @@ -0,0 +1,70 @@ +/* + * 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.Dimension; +import java.awt.Image; +import java.util.ArrayList; +import java.util.List; +import javax.swing.Icon; +import javax.swing.ImageIcon; +import com.formdev.flatlaf.util.MultiResolutionImageSupport; +import com.formdev.flatlaf.util.ScaledImageIcon; + +/** + * @author Karl Tauber + */ +class FlatTitlePaneIcon + extends ScaledImageIcon +{ + static Icon create( List images, Dimension size ) { + // collect all images including multi-resolution variants + List allImages = new ArrayList<>(); + for( Image image : images ) { + if( MultiResolutionImageSupport.isMultiResolutionImage( image ) ) + allImages.addAll( MultiResolutionImageSupport.getResolutionVariants( image ) ); + else + allImages.add( image ); + } + + // sort images by size + allImages.sort( (image1, image2) -> { + return image1.getWidth( null ) - image2.getWidth( null ); + } ); + + // create icon + return new FlatTitlePaneIcon( allImages, size ); + } + + private final List images; + + FlatTitlePaneIcon( List images, Dimension size ) { + super( new ImageIcon( images.get( 0 ) ), size.width, size.height ); + this.images = images; + } + + @Override + protected Image getResolutionVariant( int destImageWidth, int destImageHeight ) { + for( Image image : images ) { + if( destImageWidth <= image.getWidth( null ) && + destImageHeight <= image.getHeight( null ) ) + return image; + } + + return images.get( images.size() - 1 ); + } +} diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/ScaledImageIcon.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/ScaledImageIcon.java index 91c5549f..cb503c31 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/ScaledImageIcon.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/ScaledImageIcon.java @@ -37,30 +37,38 @@ public class ScaledImageIcon implements Icon { private final ImageIcon imageIcon; + private final int iconWidth; + private final int iconHeight; private double lastSystemScaleFactor; private float lastUserScaleFactor; private Image lastImage; public ScaledImageIcon( ImageIcon imageIcon ) { + this( imageIcon, imageIcon.getIconWidth(), imageIcon.getIconHeight() ); + } + + public ScaledImageIcon( ImageIcon imageIcon, int iconWidth, int iconHeight ) { this.imageIcon = imageIcon; + this.iconWidth = iconWidth; + this.iconHeight = iconHeight; } @Override public int getIconWidth() { - return UIScale.scale( imageIcon.getIconWidth() ); + return UIScale.scale( iconWidth ); } @Override public int getIconHeight() { - return UIScale.scale( imageIcon.getIconHeight() ); + return UIScale.scale( iconHeight ); } @Override public void paintIcon( Component c, Graphics g, int x, int y ) { /*debug g.setColor( Color.red ); - g.drawRect( x, y, getIconWidth(), getIconHeight() ); + g.drawRect( x, y, getIconWidth() - 1, getIconHeight() - 1 ); debug*/ // scale factor @@ -69,7 +77,7 @@ debug*/ double scaleFactor = systemScaleFactor * userScaleFactor; // paint input image icon if not necessary to scale - if( scaleFactor == 1 ) { + if( scaleFactor == 1 && iconWidth == imageIcon.getIconWidth() && iconHeight == imageIcon.getIconHeight() ) { imageIcon.paintIcon( c, g, x, y ); return; } @@ -84,12 +92,11 @@ debug*/ } // destination image size - int destImageWidth = (int) Math.round( imageIcon.getIconWidth() * scaleFactor ); - int destImageHeight = (int) Math.round( imageIcon.getIconHeight() * scaleFactor ); + int destImageWidth = (int) Math.round( iconWidth * scaleFactor ); + int destImageHeight = (int) Math.round( iconHeight * scaleFactor ); // get resolution variant of image if it is a multi-resolution image - Image image = MultiResolutionImageSupport.getResolutionVariant( - imageIcon.getImage(), destImageWidth, destImageHeight ); + Image image = getResolutionVariant( destImageWidth, destImageHeight ); // size of image int imageWidth = image.getWidth( null ); @@ -124,6 +131,11 @@ debug*/ paintLastImage( g, x, y ); } + protected Image getResolutionVariant( int destImageWidth, int destImageHeight ) { + return MultiResolutionImageSupport.getResolutionVariant( + imageIcon.getImage(), destImageWidth, destImageHeight ); + } + private void paintLastImage( Graphics g, int x, int y ) { if( lastSystemScaleFactor > 1 ) { HiDPIUtils.paintAtScale1x( (Graphics2D) g, x, y, 100, 100, // width and height are not used 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 98094770..5b6f4d2a 100644 --- a/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLaf.properties +++ b/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLaf.properties @@ -576,6 +576,8 @@ TitledBorder.border=1,1,1,1,$Separator.foreground #---- TitlePane ---- +TitlePane.iconSize=16,16 +TitlePane.iconMargins=3,8,3,0 TitlePane.titleMargins=3,8,3,8 TitlePane.closeIcon=$InternalFrame.closeIcon TitlePane.iconifyIcon=$InternalFrame.iconifyIcon diff --git a/flatlaf-testing/Window Icon Test Images.sketch b/flatlaf-testing/Window Icon Test Images.sketch new file mode 100644 index 0000000000000000000000000000000000000000..3fa6d1df56f50ed39d2ce3e845349a10dc9cf990 GIT binary patch literal 14593 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 LpX}zbeLejjp|~*| literal 0 HcmV?d00001 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 6d4efdab..58f35611 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 @@ -17,7 +17,11 @@ package com.formdev.flatlaf.testing; import java.awt.*; +import java.awt.Dialog.ModalityType; import java.awt.event.*; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import javax.swing.*; import net.miginfocom.swing.*; @@ -39,10 +43,25 @@ public class FlatWindowDecorationsTest // frame.setUndecorated( true ); // frame.getRootPane().setWindowDecorationStyle( JRootPane.FRAME ); + Class cls = FlatWindowDecorationsTest.class; + List images = Arrays.asList( + new ImageIcon( cls.getResource( "/com/formdev/flatlaf/testing/test16.png" ) ).getImage(), + new ImageIcon( cls.getResource( "/com/formdev/flatlaf/testing/test24.png" ) ).getImage(), + new ImageIcon( cls.getResource( "/com/formdev/flatlaf/testing/test32.png" ) ).getImage(), + new ImageIcon( cls.getResource( "/com/formdev/flatlaf/testing/test48.png" ) ).getImage(), + new ImageIcon( cls.getResource( "/com/formdev/flatlaf/testing/test64.png" ) ).getImage(), + new ImageIcon( cls.getResource( "/com/formdev/flatlaf/testing/test128.png" ) ).getImage() + ); + // shuffle to test whether FlatLaf chooses the right size + Collections.shuffle( images ); + frame.setIconImages( images ); + frame.showFrame( FlatWindowDecorationsTest::new, panel -> ((FlatWindowDecorationsTest)panel).menuBar ); } ); } + private List images; + FlatWindowDecorationsTest() { initComponents(); } @@ -51,6 +70,14 @@ public class FlatWindowDecorationsTest public void addNotify() { super.addNotify(); + Window window = SwingUtilities.windowForComponent( this ); + menuBarCheckBox.setEnabled( window instanceof JFrame ); + + boolean windowHasIcons = (window != null && !window.getIconImages().isEmpty()); + iconNoneRadioButton.setEnabled( windowHasIcons ); + iconTestAllRadioButton.setEnabled( windowHasIcons ); + iconTestRandomRadioButton.setEnabled( windowHasIcons ); + JRootPane rootPane = getWindowRootPane(); if( rootPane != null ) { int style = rootPane.getWindowDecorationStyle(); @@ -89,7 +116,12 @@ public class FlatWindowDecorationsTest } private void openDialog() { - JOptionPane.showMessageDialog( this, new FlatWindowDecorationsTest() ); + Window owner = SwingUtilities.windowForComponent( this ); + JDialog dialog = new JDialog( owner, "Dialog", ModalityType.APPLICATION_MODAL ); + dialog.add( new FlatWindowDecorationsTest() ); + dialog.pack(); + dialog.setLocationRelativeTo( this ); + dialog.setVisible( true ); } private void decorationStyleChanged() { @@ -118,6 +150,22 @@ public class FlatWindowDecorationsTest rootPane.setWindowDecorationStyle( style ); } + private void iconChanged() { + Window window = SwingUtilities.windowForComponent( this ); + if( window == null ) + return; + + if( images == null ) + images = window.getIconImages(); + + if( iconNoneRadioButton.isSelected() ) + window.setIconImage( null ); + else if( iconTestAllRadioButton.isSelected() ) + window.setIconImages( images ); + else if( iconTestRandomRadioButton.isSelected() ) + window.setIconImage( images.get( (int) (Math.random() * images.size()) ) ); + } + private JRootPane getWindowRootPane() { Window window = SwingUtilities.windowForComponent( this ); if( window instanceof JFrame ) @@ -132,6 +180,7 @@ public class FlatWindowDecorationsTest menuBarCheckBox = new JCheckBox(); resizableCheckBox = new JCheckBox(); JLabel label1 = new JLabel(); + JLabel label2 = new JLabel(); JPanel panel1 = new JPanel(); styleNoneRadioButton = new JRadioButton(); styleFrameRadioButton = new JRadioButton(); @@ -142,6 +191,10 @@ public class FlatWindowDecorationsTest styleWarningRadioButton = new JRadioButton(); styleColorChooserRadioButton = new JRadioButton(); styleFileChooserRadioButton = new JRadioButton(); + JPanel panel2 = new JPanel(); + iconNoneRadioButton = new JRadioButton(); + iconTestAllRadioButton = new JRadioButton(); + iconTestRandomRadioButton = new JRadioButton(); JButton openDialogButton = new JButton(); menuBar = new JMenuBar(); JMenu fileMenu = new JMenu(); @@ -174,12 +227,13 @@ public class FlatWindowDecorationsTest setLayout(new MigLayout( "ltr,insets dialog,hidemode 3", // columns - "[left]para", + "[left]para" + + "[fill]", // rows - "para[]" + - "[]" + + "para[]0" + "[]" + "[]" + + "[top]" + "[]")); //---- menuBarCheckBox ---- @@ -198,6 +252,10 @@ public class FlatWindowDecorationsTest label1.setText("Style:"); add(label1, "cell 0 2"); + //---- label2 ---- + label2.setText("Icon:"); + add(label2, "cell 1 2"); + //======== panel1 ======== { panel1.setLayout(new MigLayout( @@ -263,6 +321,35 @@ public class FlatWindowDecorationsTest } add(panel1, "cell 0 3"); + //======== panel2 ======== + { + panel2.setLayout(new MigLayout( + "ltr,insets 0,hidemode 3,gap 0 0", + // columns + "[fill]", + // rows + "[]" + + "[]" + + "[]")); + + //---- iconNoneRadioButton ---- + iconNoneRadioButton.setText("none"); + iconNoneRadioButton.addActionListener(e -> iconChanged()); + panel2.add(iconNoneRadioButton, "cell 0 0"); + + //---- iconTestAllRadioButton ---- + iconTestAllRadioButton.setText("test all"); + iconTestAllRadioButton.setSelected(true); + iconTestAllRadioButton.addActionListener(e -> iconChanged()); + panel2.add(iconTestAllRadioButton, "cell 0 1"); + + //---- iconTestRandomRadioButton ---- + iconTestRandomRadioButton.setText("test random"); + iconTestRandomRadioButton.addActionListener(e -> iconChanged()); + panel2.add(iconTestRandomRadioButton, "cell 0 2"); + } + add(panel2, "cell 1 3"); + //---- openDialogButton ---- openDialogButton.setText("Open Dialog"); openDialogButton.addActionListener(e -> openDialog()); @@ -447,6 +534,12 @@ public class FlatWindowDecorationsTest styleButtonGroup.add(styleWarningRadioButton); styleButtonGroup.add(styleColorChooserRadioButton); styleButtonGroup.add(styleFileChooserRadioButton); + + //---- iconButtonGroup ---- + ButtonGroup iconButtonGroup = new ButtonGroup(); + iconButtonGroup.add(iconNoneRadioButton); + iconButtonGroup.add(iconTestAllRadioButton); + iconButtonGroup.add(iconTestRandomRadioButton); // JFormDesigner - End of component initialization //GEN-END:initComponents } @@ -462,6 +555,9 @@ public class FlatWindowDecorationsTest private JRadioButton styleWarningRadioButton; private JRadioButton styleColorChooserRadioButton; private JRadioButton styleFileChooserRadioButton; + private JRadioButton iconNoneRadioButton; + private JRadioButton iconTestAllRadioButton; + private JRadioButton iconTestRandomRadioButton; 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 dfaee803..a9824228 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 @@ -8,8 +8,8 @@ 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]para" - "$rowConstraints": "para[][][][][]" + "$columnConstraints": "[left]para[fill]" + "$rowConstraints": "para[]0[][][top][]" } ) { name: "this" add( new FormComponent( "javax.swing.JCheckBox" ) { @@ -40,6 +40,12 @@ new FormModel { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 2" } ) + add( new FormComponent( "javax.swing.JLabel" ) { + name: "label2" + "text": "Icon:" + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 2" + } ) add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { "$columnConstraints": "[fill]" "$rowConstraints": "[][][][][][][][][]" @@ -149,6 +155,49 @@ new FormModel { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 3" } ) + add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { + "$columnConstraints": "[fill]" + "$rowConstraints": "[][][]" + "$layoutConstraints": "ltr,insets 0,hidemode 3,gap 0 0" + } ) { + name: "panel2" + add( new FormComponent( "javax.swing.JRadioButton" ) { + name: "iconNoneRadioButton" + "text": "none" + "$buttonGroup": new FormReference( "iconButtonGroup" ) + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "iconChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 0" + } ) + add( new FormComponent( "javax.swing.JRadioButton" ) { + name: "iconTestAllRadioButton" + "text": "test all" + "selected": true + "$buttonGroup": new FormReference( "iconButtonGroup" ) + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "iconChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 1" + } ) + add( new FormComponent( "javax.swing.JRadioButton" ) { + name: "iconTestRandomRadioButton" + "text": "test random" + "$buttonGroup": new FormReference( "iconButtonGroup" ) + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "iconChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 2" + } ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 3" + } ) add( new FormComponent( "javax.swing.JButton" ) { name: "openDialogButton" "text": "Open Dialog" @@ -332,5 +381,10 @@ new FormModel { }, new FormLayoutConstraints( null ) { "location": new java.awt.Point( 0, 450 ) } ) + add( new FormNonVisual( "javax.swing.ButtonGroup" ) { + name: "iconButtonGroup" + }, new FormLayoutConstraints( null ) { + "location": new java.awt.Point( 0, 502 ) + } ) } } diff --git a/flatlaf-testing/src/main/resources/com/formdev/flatlaf/testing/test128.png b/flatlaf-testing/src/main/resources/com/formdev/flatlaf/testing/test128.png new file mode 100644 index 0000000000000000000000000000000000000000..b26ffcea6b442087894dbb1f5e34a7401bc1824f GIT binary patch literal 3934 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 literal 0 HcmV?d00001 diff --git a/flatlaf-testing/src/main/resources/com/formdev/flatlaf/testing/test64.png b/flatlaf-testing/src/main/resources/com/formdev/flatlaf/testing/test64.png new file mode 100644 index 0000000000000000000000000000000000000000..ad67439b8db856341bd4a96b37460916693e07b5 GIT binary patch literal 1985 zcmV;y2R`_TP)Px+dr3q=RCodH8)Gtn&HLW9E!Vzdfer(B@B{el#?f;M$XUPu*Ff$H3~&R0oCV}8V1R2NcLfHx z0py%up!XKQ0be*C_{cEeg2l#iTJ$t%o%Nu{8W7K3ttFJs0WOedJX24!fZFn>@8k4X z0h}}hf@2;6q3m|xrNNF5*3|^ar?x?|_G17w>d4bmqEL1#glE1;#g*niuATzK^2HDw z_ZVtFOgG>JaDoKE%B2t(z7Uc%n?X7EofDgH8nLv{u>Xwfsq zo#YFvATVqph{NVH50$2@0kyprbo3ED9b);t5Gc8^r{XDco)y3sj0Lgs0cHWMqYh&G z*MOe9Na=d=5~vsUb$yz4gEVC|a6-tK=K>K3O@9Gmdsb3@KLDRs0l}&#{fM~odQ5~k zkS47{^A_lWTIZ>h!_&#jfYR_IBu{+EkOhnwTzEJ`^Mr5#kfyCiWd=R5du8&Z0HI_y z2!kduYsL?4V%|J6X+h*uTR}@STPOg5(wiCF3qr1+P&kgVYOzKN_XhB!fWXiN%roSM zpFnG^W@z0ax`zJYH)dKWxrxEu5Ml5P5J%rn+0>T5K|b>}#k+u>^dZr#b2P%GgGKlZtLuFJnQT;aP@$(e#E#P_u_<}LaGkTJ4G7I=X$knrk zQl)VxC}+R*v4T6Rs|E1+mCP#S{yC07KJek|0E94T9sdEKsrF`Y_|~OK&(b9{%=P0( zUZ;4b0CxoxunJ(zPb>s+_#z|K6)b!tBQ(sutp*fynCkgmKKIK(XzKI8iTPA6arjjL z^dFfX?h1&oF`GCsGKA8(5SsEFrlf;X_Cy>OhjKiA#G&)hWAA`O^;=jF#Zq{k2;%6a zAQVrhQ*!-wOsw`&Jd;3rAArdRhf%;fbh#bGG0Px4YlGwAt-K~Ij#vbdx$hZevq|gj zn=crLh1R2Vnxxl>ns+IlX&}7?VCrfgBq@?C8`Lq*layF%KW@|ld}IXh1y#W3S77M6 zqF-VSp_wm1bl39kOJh_vNtHRiYg@XmLj0eN$RD?FBV%}41(-R!^=ZOlGDy3x$NUH!) zoV|A9*k%k#t)BKoYJa(IE5j>bzrZ?3G_khTk~QyR&!FDQk~KK30`zYBcuM0hzBXj< z0+IMmdWvKOM&60F#w~O`rTGBJ_1h?(Z6K`^bc)+X8R{{fo0}(HV*4P?4GkM5()g$7 zl#bp$dHe$mug*nN2sTK7G^CECIIW6AgO*YG1>8}7jTx&bQY=Mi61i&K5PhrA^@mC{DR!A1q4F%|+ zUOLRI?-d~mwe|@}ciP^k@X~m3Re*9~4~F9;ZANmufL{AHP|keib*~^`R%jh0kI*_! z0<@oiSkoTslxJA1d=R9n<&nQFOaJ>Sgu2-?WZ5_eUOVAaFR#NvcntyQY z!_2(W@H3i#!qBc2;;sPPnS6RXvp+s>6dr0BL+cI^E0%OMN-PZW+3zjbwFNyXfV9En zb3ZUUB|nS?$3DW)>VU{NTGNZ<^;ti$uMnU$PJJ@`6;OQ z>#UJDVQWk%nh4^kyO|7+2$5VMar6x;-xmPS3LsuTwr@3rXK!M>uP|sb{h8Y676l4u z3)^}?;-%vUH~5MkR{3nfp00rTa8lUB_O1e@W^!H~#yLiw_ z?h~+g3%I;p(iDy352Asg^Fge*56wNbwl_~k8YXi6m)H~h#&bW=l)YNtTyOthfRjqm zH_R}qJ5r43N{7)A)}m*#-p$E`l6+f${h@lI3IFn=?}Rn`{@ie1KEQ3U{y3jgK!1E| zncQ>E0y3%1{PvlCqY TGQLQf00000NkvXXu0mjflUTaU literal 0 HcmV?d00001 From 506a1e6b62667885ec7222fa17e54229901f44a9 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Thu, 28 May 2020 11:35:30 +0200 Subject: [PATCH 03/33] Window decorations: iconify/maximize/restore/close button icons in Windows 10 style (issues #47 and #82) --- .../flatlaf/icons/FlatWindowAbstractIcon.java | 76 ++++++++++++++++++ .../flatlaf/icons/FlatWindowCloseIcon.java | 70 ++++++++++++++++ .../flatlaf/icons/FlatWindowIconifyIcon.java | 41 ++++++++++ .../flatlaf/icons/FlatWindowMaximizeIcon.java | 42 ++++++++++ .../flatlaf/icons/FlatWindowRestoreIcon.java | 55 +++++++++++++ .../com/formdev/flatlaf/ui/FlatTitlePane.java | 17 +++- .../formdev/flatlaf/FlatDarkLaf.properties | 6 ++ .../com/formdev/flatlaf/FlatLaf.properties | 14 +++- .../formdev/flatlaf/FlatLightLaf.properties | 6 ++ flatlaf-testing/Windows 10 decorations.png | Bin 0 -> 107753 bytes 10 files changed, 319 insertions(+), 8 deletions(-) create mode 100644 flatlaf-core/src/main/java/com/formdev/flatlaf/icons/FlatWindowAbstractIcon.java create mode 100644 flatlaf-core/src/main/java/com/formdev/flatlaf/icons/FlatWindowCloseIcon.java create mode 100644 flatlaf-core/src/main/java/com/formdev/flatlaf/icons/FlatWindowIconifyIcon.java create mode 100644 flatlaf-core/src/main/java/com/formdev/flatlaf/icons/FlatWindowMaximizeIcon.java create mode 100644 flatlaf-core/src/main/java/com/formdev/flatlaf/icons/FlatWindowRestoreIcon.java create mode 100644 flatlaf-testing/Windows 10 decorations.png diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/icons/FlatWindowAbstractIcon.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/icons/FlatWindowAbstractIcon.java new file mode 100644 index 00000000..15a8d6ab --- /dev/null +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/icons/FlatWindowAbstractIcon.java @@ -0,0 +1,76 @@ +/* + * 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.icons; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.Graphics2D; +import javax.swing.UIManager; +import com.formdev.flatlaf.ui.FlatButtonUI; +import com.formdev.flatlaf.ui.FlatUIUtils; +import com.formdev.flatlaf.util.HiDPIUtils; + +/** + * Base class for window icons. + * + * @uiDefault TitlePane.buttonSize Dimension + * @uiDefault TitlePane.buttonHoverBackground Color + * @uiDefault TitlePane.buttonPressedBackground Color + * + * @author Karl Tauber + */ +public abstract class FlatWindowAbstractIcon + extends FlatAbstractIcon +{ + private final Color hoverBackground; + private final Color pressedBackground; + + public FlatWindowAbstractIcon() { + this( UIManager.getDimension( "TitlePane.buttonSize" ), + UIManager.getColor( "TitlePane.buttonHoverBackground" ), + UIManager.getColor( "TitlePane.buttonPressedBackground" ) ); + } + + public FlatWindowAbstractIcon( Dimension size, Color hoverBackground, Color pressedBackground ) { + super( size.width, size.height, null ); + this.hoverBackground = hoverBackground; + this.pressedBackground = pressedBackground; + } + + @Override + protected void paintIcon( Component c, Graphics2D g ) { + paintBackground( c, g ); + + g.setColor( getForeground( c ) ); + HiDPIUtils.paintAtScale1x( g, 0, 0, width, height, this::paintIconAt1x ); + } + + protected abstract void paintIconAt1x( Graphics2D g, int x, int y, int width, int height, double scaleFactor ); + + protected void paintBackground( Component c, Graphics2D g ) { + Color background = FlatButtonUI.buttonStateColor( c, null, null, null, hoverBackground, pressedBackground ); + if( background != null ) { + g.setColor( FlatUIUtils.deriveColor( background, c.getBackground() ) ); + g.fillRect( 0, 0, width, height ); + } + } + + protected Color getForeground( Component c ) { + return c.getForeground(); + } +} diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/icons/FlatWindowCloseIcon.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/icons/FlatWindowCloseIcon.java new file mode 100644 index 00000000..f4bdc5ca --- /dev/null +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/icons/FlatWindowCloseIcon.java @@ -0,0 +1,70 @@ +/* + * 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.icons; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Component; +import java.awt.Graphics2D; +import java.awt.geom.Line2D; +import java.awt.geom.Path2D; +import javax.swing.UIManager; +import com.formdev.flatlaf.ui.FlatButtonUI; + +/** + * "close" icon for windows (frames and dialogs). + * + * @uiDefault TitlePane.closeHoverBackground Color + * @uiDefault TitlePane.closePressedBackground Color + * @uiDefault TitlePane.closeHoverForeground Color + * @uiDefault TitlePane.closePressedForeground Color + * + * @author Karl Tauber + */ +public class FlatWindowCloseIcon + extends FlatWindowAbstractIcon +{ + private final Color hoverForeground = UIManager.getColor( "TitlePane.closeHoverForeground" ); + private final Color pressedForeground = UIManager.getColor( "TitlePane.closePressedForeground" ); + + public FlatWindowCloseIcon() { + super( UIManager.getDimension( "TitlePane.buttonSize" ), + UIManager.getColor( "TitlePane.closeHoverBackground" ), + UIManager.getColor( "TitlePane.closePressedBackground" ) ); + } + + @Override + protected void paintIconAt1x( Graphics2D g, int x, int y, int width, int height, double scaleFactor ) { + int iwh = (int) (10 * scaleFactor); + int ix = x + ((width - iwh) / 2); + int iy = y + ((height - iwh) / 2); + int ix2 = ix + iwh - 1; + int iy2 = iy + iwh - 1; + int thickness = (int) scaleFactor; + + Path2D path = new Path2D.Float( Path2D.WIND_EVEN_ODD ); + path.append( new Line2D.Float( ix, iy, ix2, iy2 ), false ); + path.append( new Line2D.Float( ix, iy2, ix2, iy ), false ); + g.setStroke( new BasicStroke( thickness ) ); + g.draw( path ); + } + + @Override + protected Color getForeground( Component c ) { + return FlatButtonUI.buttonStateColor( c, c.getForeground(), null, null, hoverForeground, pressedForeground ); + } +} diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/icons/FlatWindowIconifyIcon.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/icons/FlatWindowIconifyIcon.java new file mode 100644 index 00000000..54cab7d7 --- /dev/null +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/icons/FlatWindowIconifyIcon.java @@ -0,0 +1,41 @@ +/* + * 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.icons; + +import java.awt.Graphics2D; + +/** + * "iconify" icon for windows (frames and dialogs). + * + * @author Karl Tauber + */ +public class FlatWindowIconifyIcon + extends FlatWindowAbstractIcon +{ + public FlatWindowIconifyIcon() { + } + + @Override + protected void paintIconAt1x( Graphics2D g, int x, int y, int width, int height, double scaleFactor ) { + int iw = (int) (10 * scaleFactor); + int ih = (int) scaleFactor; + int ix = x + ((width - iw) / 2); + int iy = y + ((height - ih) / 2); + + g.fillRect( ix, iy, iw, ih ); + } +} diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/icons/FlatWindowMaximizeIcon.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/icons/FlatWindowMaximizeIcon.java new file mode 100644 index 00000000..5035d43d --- /dev/null +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/icons/FlatWindowMaximizeIcon.java @@ -0,0 +1,42 @@ +/* + * 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.icons; + +import java.awt.Graphics2D; +import com.formdev.flatlaf.ui.FlatUIUtils; + +/** + * "maximize" icon for windows (frames and dialogs). + * + * @author Karl Tauber + */ +public class FlatWindowMaximizeIcon + extends FlatWindowAbstractIcon +{ + public FlatWindowMaximizeIcon() { + } + + @Override + protected void paintIconAt1x( Graphics2D g, int x, int y, int width, int height, double scaleFactor ) { + int iwh = (int) (10 * scaleFactor); + int ix = x + ((width - iwh) / 2); + int iy = y + ((height - iwh) / 2); + int thickness = (int) scaleFactor; + + g.fill( FlatUIUtils.createRectangle( ix, iy, iwh, iwh, thickness ) ); + } +} diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/icons/FlatWindowRestoreIcon.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/icons/FlatWindowRestoreIcon.java new file mode 100644 index 00000000..b62087ef --- /dev/null +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/icons/FlatWindowRestoreIcon.java @@ -0,0 +1,55 @@ +/* + * 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.icons; + +import java.awt.Graphics2D; +import java.awt.geom.Area; +import java.awt.geom.Path2D; +import java.awt.geom.Rectangle2D; +import com.formdev.flatlaf.ui.FlatUIUtils; + +/** + * "restore" icon for windows (frames and dialogs). + * + * @author Karl Tauber + */ +public class FlatWindowRestoreIcon + extends FlatWindowAbstractIcon +{ + public FlatWindowRestoreIcon() { + } + + @Override + protected void paintIconAt1x( Graphics2D g, int x, int y, int width, int height, double scaleFactor ) { + int iwh = (int) (10 * scaleFactor); + int ix = x + ((width - iwh) / 2); + int iy = y + ((height - iwh) / 2); + int thickness = (int) scaleFactor; + + int rwh = (int) (8 * scaleFactor); + int ro2 = iwh - rwh; + + Path2D r1 = FlatUIUtils.createRectangle( ix + ro2, iy, rwh, rwh, thickness ); + Path2D r2 = FlatUIUtils.createRectangle( ix, iy + ro2, rwh, rwh, thickness ); + + Area area = new Area( r1 ); + area.subtract( new Area( new Rectangle2D.Float( ix, iy + ro2, rwh, rwh ) ) ); + g.fill( area ); + + g.fill( r2 ); + } +} 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 568d9594..bf7eadea 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 @@ -56,7 +56,7 @@ import javax.swing.UIManager; * @uiDefault TitlePane.closeIcon Icon * @uiDefault TitlePane.iconifyIcon Icon * @uiDefault TitlePane.maximizeIcon Icon - * @uiDefault TitlePane.minimizeIcon Icon + * @uiDefault TitlePane.restoreIcon Icon * * @author Karl Tauber */ @@ -109,7 +109,7 @@ class FlatTitlePane private void createButtons() { iconifyButton = createButton( "TitlePane.iconifyIcon", "Iconify", e -> iconify() ); maximizeButton = createButton( "TitlePane.maximizeIcon", "Maximize", e -> maximize() ); - restoreButton = createButton( "TitlePane.minimizeIcon", "Restore", e -> restore() ); + restoreButton = createButton( "TitlePane.restoreIcon", "Restore", e -> restore() ); closeButton = createButton( "TitlePane.closeIcon", "Close", e -> close() ); buttonPanel = new JPanel(); @@ -141,8 +141,17 @@ class FlatTitlePane } private void activeChanged( boolean active ) { - setBackground( active ? activeBackground : inactiveBackground ); - titleLabel.setForeground( FlatUIUtils.nonUIResource( active ? activeForeground : inactiveForeground ) ); + Color background = FlatUIUtils.nonUIResource( active ? activeBackground : inactiveBackground ); + Color foreground = FlatUIUtils.nonUIResource( active ? 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 ); } private void frameStateChanged() { diff --git a/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatDarkLaf.properties b/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatDarkLaf.properties index a3f6ad8f..4eeadea6 100644 --- a/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatDarkLaf.properties +++ b/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatDarkLaf.properties @@ -266,6 +266,12 @@ TableHeader.separatorColor=lighten($TableHeader.background,10%) TableHeader.bottomSeparatorColor=$TableHeader.separatorColor +#---- TitlePane ---- + +TitlePane.buttonHoverBackground=lighten($TitlePane.background,10%,derived) +TitlePane.buttonPressedBackground=lighten($TitlePane.background,20%,derived) + + #---- ToggleButton ---- ToggleButton.selectedBackground=lighten($ToggleButton.background,10%,derived) 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 5b6f4d2a..e545d990 100644 --- a/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLaf.properties +++ b/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLaf.properties @@ -579,16 +579,22 @@ TitledBorder.border=1,1,1,1,$Separator.foreground TitlePane.iconSize=16,16 TitlePane.iconMargins=3,8,3,0 TitlePane.titleMargins=3,8,3,8 -TitlePane.closeIcon=$InternalFrame.closeIcon -TitlePane.iconifyIcon=$InternalFrame.iconifyIcon -TitlePane.maximizeIcon=$InternalFrame.maximizeIcon -TitlePane.minimizeIcon=$InternalFrame.minimizeIcon +TitlePane.buttonSize=44,30 +TitlePane.closeIcon=com.formdev.flatlaf.icons.FlatWindowCloseIcon +TitlePane.iconifyIcon=com.formdev.flatlaf.icons.FlatWindowIconifyIcon +TitlePane.maximizeIcon=com.formdev.flatlaf.icons.FlatWindowMaximizeIcon +TitlePane.restoreIcon=com.formdev.flatlaf.icons.FlatWindowRestoreIcon TitlePane.background=$MenuBar.background TitlePane.inactiveBackground=$TitlePane.background TitlePane.foreground=@foreground TitlePane.inactiveForeground=@disabledText +TitlePane.closeHoverBackground=#e81123 +TitlePane.closePressedBackground=rgba($TitlePane.closeHoverBackground,60%) +TitlePane.closeHoverForeground=#fff +TitlePane.closePressedForeground=#fff + #---- ToggleButton ---- diff --git a/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLightLaf.properties b/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLightLaf.properties index 938997ea..8982df4d 100644 --- a/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLightLaf.properties +++ b/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLightLaf.properties @@ -273,6 +273,12 @@ TableHeader.separatorColor=darken($TableHeader.background,10%) TableHeader.bottomSeparatorColor=$TableHeader.separatorColor +#---- TitlePane ---- + +TitlePane.buttonHoverBackground=darken($TitlePane.background,10%,derived) +TitlePane.buttonPressedBackground=darken($TitlePane.background,20%,derived) + + #---- ToggleButton ---- ToggleButton.selectedBackground=darken($ToggleButton.background,20%,derived) diff --git a/flatlaf-testing/Windows 10 decorations.png b/flatlaf-testing/Windows 10 decorations.png new file mode 100644 index 0000000000000000000000000000000000000000..04af6c4c81ea003d89bddf200fda2986455bd8db GIT binary patch literal 107753 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

N&d&_GZiM#Y4;w;B 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 Lbkq^IE#Lef{#Eg9 literal 0 HcmV?d00001 From 954cae87382701c834eb47a32a8b07c381e01694 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Thu, 28 May 2020 23:04:43 +0200 Subject: [PATCH 04/33] Window decorations: limit size of moximized windows so that they do not overlap the Windows task bar (issues #47 and #82) --- .../com/formdev/flatlaf/ui/FlatTitlePane.java | 51 +++++++++++++++++-- 1 file changed, 47 insertions(+), 4 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 bf7eadea..54d25004 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 @@ -22,13 +22,17 @@ import java.awt.Dialog; import java.awt.Dimension; import java.awt.Frame; import java.awt.Graphics; +import java.awt.GraphicsConfiguration; import java.awt.Image; +import java.awt.Insets; +import java.awt.Rectangle; import java.awt.Window; import java.awt.event.ActionListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; 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.List; @@ -263,7 +267,46 @@ class FlatTitlePane private void maximize() { if( window instanceof Frame ) { Frame frame = (Frame) window; + GraphicsConfiguration gc = window.getGraphicsConfiguration(); + + // remember current maximized bounds + Rectangle oldMaximizedBounds = frame.getMaximizedBounds(); + + // Scaled screen size, which may be smaller than physical size on Java 9+. + // E.g. if running a 3840x2160 screen at 200%, scaledSreenSize is 1920x1080. + // In Java 9+, each screen can have its own scale factor. + // + // On Java 8, which does not scale, scaledSreenSize of the primary screen + // is identical to its physical size. But when the primary screen is scaled, + // then scaledSreenSize 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 scaledSreenSize is 3840x2160 on primary and 2880x1560 on secondary. + Dimension scaledSreenSize = gc.getBounds().getSize(); + + // scale screen bounds to get physical screen size + AffineTransform defaultTransform = gc.getDefaultTransform(); + int screenWidth = (int) (scaledSreenSize.width * defaultTransform.getScaleX()); + int screenHeight = (int) (scaledSreenSize.height * defaultTransform.getScaleY()); + + // screen insets are in physical size, + // 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 8 on secondary screens where primary screen is scaled + Rectangle maximizedBounds = new Rectangle( screenInsets.left, screenInsets.top, + screenWidth - screenInsets.left - screenInsets.right, + screenHeight - screenInsets.top - screenInsets.bottom ); + + // temporary change maximized bounds + frame.setMaximizedBounds( maximizedBounds ); + + // maximize window frame.setExtendedState( frame.getExtendedState() | Frame.MAXIMIZED_BOTH ); + + // restore old maximized bounds + frame.setMaximizedBounds( oldMaximizedBounds ); } } @@ -336,10 +379,10 @@ class FlatTitlePane { // maximize/restore on double-click Frame frame = (Frame) window; - int state = frame.getExtendedState(); - frame.setExtendedState( ((state & Frame.MAXIMIZED_BOTH) != 0) - ? (state & ~Frame.MAXIMIZED_BOTH) - : (state | Frame.MAXIMIZED_BOTH) ); + if( (frame.getExtendedState() & Frame.MAXIMIZED_BOTH) != 0 ) + restore(); + else + maximize(); } } From 4f79cdad50eb554729c28f6d24874ee99177b4b0 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Thu, 28 May 2020 23:49:46 +0200 Subject: [PATCH 05/33] Window decorations: support moving window (issues #47 and #82) --- .../com/formdev/flatlaf/ui/FlatTitlePane.java | 58 ++++++++++++++++++- 1 file changed, 56 insertions(+), 2 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 54d25004..7dca8e46 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 @@ -30,6 +30,7 @@ import java.awt.Window; import java.awt.event.ActionListener; 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; @@ -94,6 +95,7 @@ class FlatTitlePane activeChanged( true ); addMouseListener( handler ); + addMouseMotionListener( handler ); } private void addSubComponents() { @@ -329,7 +331,7 @@ class FlatTitlePane private class Handler extends WindowAdapter - implements PropertyChangeListener, MouseListener + implements PropertyChangeListener, MouseListener, MouseMotionListener { //---- interface PropertyChangeListener ---- @@ -370,6 +372,9 @@ class FlatTitlePane //---- interface MouseListener ---- + private int lastXOnScreen; + private int lastYOnScreen; + @Override public void mouseClicked( MouseEvent e ) { if( e.getClickCount() == 2 && @@ -386,9 +391,58 @@ class FlatTitlePane } } - @Override public void mousePressed( MouseEvent e ) {} + @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 ) { + 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; + if( xOnScreen >= maximizedX + restoredWidth - buttonPanel.getWidth() - 10 ) + newX = xOnScreen + buttonPanel.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 ) {} } } From 576c0048d06babacc7468af2499ea3f6c9875ceb Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Fri, 29 May 2020 00:26:10 +0200 Subject: [PATCH 06/33] Window decorations: make title pane height smaller when frame is maximized (issues #47 and #82) --- .../com/formdev/flatlaf/ui/FlatTitlePane.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) 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 7dca8e46..1de1b986 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 @@ -47,6 +47,7 @@ import javax.swing.JPanel; import javax.swing.JRootPane; import javax.swing.SwingUtilities; import javax.swing.UIManager; +import com.formdev.flatlaf.util.UIScale; /** * Provides the Flat LaF title bar. @@ -58,6 +59,7 @@ import javax.swing.UIManager; * @uiDefault TitlePane.iconSize Dimension * @uiDefault TitlePane.iconMargins Insets * @uiDefault TitlePane.titleMargins Insets + * @uiDefault TitlePane.buttonMaximizedHeight int * @uiDefault TitlePane.closeIcon Icon * @uiDefault TitlePane.iconifyIcon Icon * @uiDefault TitlePane.maximizeIcon Icon @@ -74,6 +76,7 @@ class FlatTitlePane private final Color inactiveForeground = UIManager.getColor( "TitlePane.inactiveForeground" ); private final Dimension iconSize = UIManager.getDimension( "TitlePane.iconSize" ); + private final int buttonMaximizedHeight = UIManager.getInt( "TitlePane.buttonMaximizedHeight" ); private final JRootPane rootPane; @@ -118,7 +121,20 @@ class FlatTitlePane restoreButton = createButton( "TitlePane.restoreIcon", "Restore", e -> restore() ); closeButton = createButton( "TitlePane.closeIcon", "Close", e -> close() ); - buttonPanel = new JPanel(); + 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.X_AXIS ) ); if( rootPane.getWindowDecorationStyle() == JRootPane.FRAME ) { From 023d781daf34eb63a7243fb781426a30eb66f108 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Fri, 29 May 2020 11:17:46 +0200 Subject: [PATCH 07/33] Window decorations: set maximized title pane height to 22px (issues #47 and #82) --- .../src/main/resources/com/formdev/flatlaf/FlatLaf.properties | 1 + 1 file changed, 1 insertion(+) 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 e545d990..6624ee2d 100644 --- a/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLaf.properties +++ b/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLaf.properties @@ -580,6 +580,7 @@ TitlePane.iconSize=16,16 TitlePane.iconMargins=3,8,3,0 TitlePane.titleMargins=3,8,3,8 TitlePane.buttonSize=44,30 +TitlePane.buttonMaximizedHeight=22 TitlePane.closeIcon=com.formdev.flatlaf.icons.FlatWindowCloseIcon TitlePane.iconifyIcon=com.formdev.flatlaf.icons.FlatWindowIconifyIcon TitlePane.maximizeIcon=com.formdev.flatlaf.icons.FlatWindowMaximizeIcon From 436fc545c03314377af6d6dfe084fd38117783c2 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Fri, 29 May 2020 16:44:33 +0200 Subject: [PATCH 08/33] Window decorations: support native Windows 10 custom window decorations with JetBrains Runtime 11 (issues #47 and #82) --- .../java/com/formdev/flatlaf/FlatLaf.java | 27 +++ .../formdev/flatlaf/ui/FlatRootPaneUI.java | 4 + .../com/formdev/flatlaf/ui/FlatTitlePane.java | 80 ++++++- .../flatlaf/ui/JBRCustomDecorations.java | 199 ++++++++++++++++++ .../com/formdev/flatlaf/util/SystemInfo.java | 6 + 5 files changed, 313 insertions(+), 3 deletions(-) create mode 100644 flatlaf-core/src/main/java/com/formdev/flatlaf/ui/JBRCustomDecorations.java diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatLaf.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatLaf.java index cac59091..a7b1c2a7 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatLaf.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatLaf.java @@ -43,6 +43,8 @@ import javax.swing.BorderFactory; import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.JComponent; +import javax.swing.JDialog; +import javax.swing.JFrame; import javax.swing.LookAndFeel; import javax.swing.PopupFactory; import javax.swing.SwingUtilities; @@ -57,6 +59,7 @@ import javax.swing.plaf.basic.BasicLookAndFeel; import javax.swing.text.StyleContext; import javax.swing.text.html.HTMLEditorKit; import com.formdev.flatlaf.ui.FlatPopupFactory; +import com.formdev.flatlaf.ui.JBRCustomDecorations; import com.formdev.flatlaf.util.GrayFilter; import com.formdev.flatlaf.util.MultiResolutionImageSupport; import com.formdev.flatlaf.util.SystemInfo; @@ -110,8 +113,32 @@ public abstract class FlatLaf public abstract boolean isDark(); + /** + * Returns whether FlatLaf supports custom window decorations. + *

+ * To use custom window decorations in your application, enable them with + * (before creating any frames or dialogs): + *

+	 * JFrame.setDefaultLookAndFeelDecorated( true );
+	 * JDialog.setDefaultLookAndFeelDecorated( true );
+	 * 
+ *

+ * Returns {@code true} on Windows, {@code false} otherwise. + *

+ * Return also {@code false} if running on Windows 10 in + * JetBrains Runtime 11 (or later) + * (source code on github) + * and JBR supports custom window decorations. In this case, JBR custom decorations + * are enabled if {@link JFrame#isDefaultLookAndFeelDecorated()} or + * {@link JDialog#isDefaultLookAndFeelDecorated()} return {@code true}. + */ @Override public boolean getSupportsWindowDecorations() { + if( SystemInfo.IS_JETBRAINS_JVM_11_OR_LATER && + SystemInfo.IS_WINDOWS_10_OR_LATER && + JBRCustomDecorations.isSupported() ) + return false; + return SystemInfo.IS_WINDOWS; } 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 6396890d..a1952205 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 @@ -30,6 +30,7 @@ import javax.swing.JMenuBar; import javax.swing.JRootPane; import javax.swing.plaf.ComponentUI; import javax.swing.plaf.basic.BasicRootPaneUI; +import com.formdev.flatlaf.util.SystemInfo; /** * Provides the Flat LaF UI delegate for {@link javax.swing.JRootPane}. @@ -55,6 +56,9 @@ public class FlatRootPaneUI if( rootPane.getWindowDecorationStyle() != JRootPane.NONE ) installClientDecorations(); + + if( SystemInfo.IS_JETBRAINS_JVM_11_OR_LATER && SystemInfo.IS_WINDOWS_10_OR_LATER ) + JBRCustomDecorations.install( rootPane ); } @Override 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 1de1b986..45130581 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 @@ -20,14 +20,18 @@ import java.awt.BorderLayout; import java.awt.Color; 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; @@ -36,6 +40,7 @@ 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 javax.accessibility.AccessibleContext; import javax.swing.BorderFactory; @@ -233,6 +238,8 @@ class FlatTitlePane titleLabel.setText( getWindowTitle() ); installWindowListeners(); } + + updateJBRHitTestSpotsAndTitleBarHeight(); } @Override @@ -258,6 +265,7 @@ class FlatTitlePane window.addPropertyChangeListener( handler ); window.addWindowListener( handler ); window.addWindowStateListener( handler ); + window.addComponentListener( handler ); } private void uninstallWindowListeners() { @@ -267,6 +275,7 @@ class FlatTitlePane window.removePropertyChangeListener( handler ); window.removeWindowListener( handler ); window.removeWindowStateListener( handler ); + window.removeComponentListener( handler ); } @Override @@ -283,8 +292,12 @@ class FlatTitlePane } private void maximize() { - if( window instanceof Frame ) { - Frame frame = (Frame) window; + if( !(window instanceof Frame) ) + return; + + Frame frame = (Frame) window; + + if( !hasJBRCustomDecoration() ) { GraphicsConfiguration gc = window.getGraphicsConfiguration(); // remember current maximized bounds @@ -325,6 +338,11 @@ class FlatTitlePane // restore old maximized bounds frame.setMaximizedBounds( oldMaximizedBounds ); + } else { + // not necessary to set maximized bounds when running in JBR + + // maximize window + frame.setExtendedState( frame.getExtendedState() | Frame.MAXIMIZED_BOTH ); } } @@ -343,11 +361,45 @@ class FlatTitlePane window.dispatchEvent( new WindowEvent( window, WindowEvent.WINDOW_CLOSING ) ); } + private boolean hasJBRCustomDecoration() { + return window != null && JBRCustomDecorations.hasCustomDecoration( window ); + } + + private void updateJBRHitTestSpotsAndTitleBarHeight() { + if( !isDisplayable() ) + return; + + if( !hasJBRCustomDecoration() ) + return; + + List hitTestSpots = new ArrayList<>(); + addJBRHitTestSpot( buttonPanel, hitTestSpots ); + + int titleBarHeight = getHeight(); + // slightly reduce height so that component receives mouseExit events + if( titleBarHeight > 0 ) + titleBarHeight--; + + JBRCustomDecorations.setHitTestSpotsAndTitleBarHeight( window, hitTestSpots, titleBarHeight ); + } + + private void addJBRHitTestSpot( JComponent c, List 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 ); + // slightly increase rectangle so that component receives mouseExit events + r.grow( 2, 2 ); + hitTestSpots.add( r ); + } + //---- class Handler ------------------------------------------------------ private class Handler extends WindowAdapter - implements PropertyChangeListener, MouseListener, MouseMotionListener + implements PropertyChangeListener, MouseListener, MouseMotionListener, ComponentListener { //---- interface PropertyChangeListener ---- @@ -374,16 +426,19 @@ class FlatTitlePane @Override public void windowActivated( WindowEvent e ) { activeChanged( true ); + updateJBRHitTestSpotsAndTitleBarHeight(); } @Override public void windowDeactivated( WindowEvent e ) { activeChanged( false ); + updateJBRHitTestSpotsAndTitleBarHeight(); } @Override public void windowStateChanged( WindowEvent e ) { frameStateChanged(); + updateJBRHitTestSpotsAndTitleBarHeight(); } //---- interface MouseListener ---- @@ -393,6 +448,9 @@ class FlatTitlePane @Override public void mouseClicked( MouseEvent e ) { + if( hasJBRCustomDecoration() ) + return; // do nothing if running in JBR + if( e.getClickCount() == 2 && SwingUtilities.isLeftMouseButton( e ) && window instanceof Frame && @@ -421,6 +479,9 @@ class FlatTitlePane @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 ) @@ -460,5 +521,18 @@ class FlatTitlePane } @Override public void mouseMoved( MouseEvent e ) {} + + //---- interface ComponentListener ---- + + @Override + public void componentResized( ComponentEvent e ) { + EventQueue.invokeLater( () -> { + updateJBRHitTestSpotsAndTitleBarHeight(); + } ); + } + + @Override public void componentMoved( ComponentEvent e ) {} + @Override public void componentShown( ComponentEvent e ) {} + @Override public void componentHidden( ComponentEvent e ) {} } } diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/JBRCustomDecorations.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/JBRCustomDecorations.java new file mode 100644 index 00000000..a19bd339 --- /dev/null +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/JBRCustomDecorations.java @@ -0,0 +1,199 @@ +/* + * 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.Component; +import java.awt.Container; +import java.awt.EventQueue; +import java.awt.Rectangle; +import java.awt.Window; +import java.awt.event.HierarchyEvent; +import java.awt.event.HierarchyListener; +import java.lang.reflect.Method; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.swing.JDialog; +import javax.swing.JFrame; +import javax.swing.JRootPane; +import javax.swing.UIManager; +import com.formdev.flatlaf.FlatLaf; +import com.formdev.flatlaf.util.SystemInfo; + +/** + * Support for custom window decorations provided by JetBrains Runtime (based on OpenJDK). + * Requires that the application runs on Windows 10 in a JetBrains Runtime 11 or later. + *

+ * + * @author Karl Tauber + */ +public class JBRCustomDecorations +{ + private static boolean initialized; + private static Method Window_hasCustomDecoration; + private static Method Window_setHasCustomDecoration; + private static Method WWindowPeer_setCustomDecorationHitTestSpots; + private static Method WWindowPeer_setCustomDecorationTitleBarHeight; + private static Method AWTAccessor_getComponentAccessor; + private static Method AWTAccessor_ComponentAccessor_getPeer; + + public static boolean isSupported() { + initialize(); + return Window_setHasCustomDecoration != null; + } + + static void install( JRootPane rootPane ) { + boolean frameIsDefaultLookAndFeelDecorated = JFrame.isDefaultLookAndFeelDecorated(); + boolean dialogIsDefaultLookAndFeelDecorated = JDialog.isDefaultLookAndFeelDecorated(); + boolean lafSupportsWindowDecorations = UIManager.getLookAndFeel().getSupportsWindowDecorations(); + + // check whether decorations are enabled + if( !frameIsDefaultLookAndFeelDecorated && !dialogIsDefaultLookAndFeelDecorated ) + return; + + // do not enable JBR decorations if JFrame and JDialog will use LaF decorations + if( lafSupportsWindowDecorations && + frameIsDefaultLookAndFeelDecorated && + dialogIsDefaultLookAndFeelDecorated ) + return; + + if( !isSupported() ) + return; + + // use hierarchy listener to wait until the root pane is added to a window + HierarchyListener addListener = new HierarchyListener() { + @Override + public void hierarchyChanged( HierarchyEvent e ) { + if( (e.getChangeFlags() & HierarchyEvent.PARENT_CHANGED) == 0 ) + return; + + Container parent = e.getChangedParent(); + if( parent instanceof JFrame ) { + JFrame frame = (JFrame) parent; + + // do not enable JBR decorations if JFrame will use LaF decorations + if( lafSupportsWindowDecorations && frameIsDefaultLookAndFeelDecorated ) + return; + + // do not enable JBR decorations if frame is undecorated + if( frame.isUndecorated() ) + return; + + // enable JBR custom window decoration for window + setHasCustomDecoration( frame ); + + // enable Swing window decoration + rootPane.setWindowDecorationStyle( JRootPane.FRAME ); + + } else if( parent instanceof JDialog ) { + JDialog dialog = (JDialog)parent; + + // do not enable JBR decorations if JDialog will use LaF decorations + if( lafSupportsWindowDecorations && dialogIsDefaultLookAndFeelDecorated ) + return; + + // do not enable JBR decorations if dialog is undecorated + if( dialog.isUndecorated() ) + return; + + // enable JBR custom window decoration for window + setHasCustomDecoration( dialog ); + + // enable Swing window decoration + rootPane.setWindowDecorationStyle( JRootPane.PLAIN_DIALOG ); + } + + // use invokeLater to remove listener to avoid that listener + // is removed while listener queue is processed + EventQueue.invokeLater( () -> { + rootPane.removeHierarchyListener( this ); + } ); + } + }; + rootPane.addHierarchyListener( addListener ); + } + + static boolean hasCustomDecoration( Window window ) { + if( !isSupported() ) + return false; + + try { + return (Boolean) Window_hasCustomDecoration.invoke( window ); + } catch( Exception ex ) { + Logger.getLogger( FlatLaf.class.getName() ).log( Level.SEVERE, null, ex ); + return false; + } + } + + static void setHasCustomDecoration( Window window ) { + if( !isSupported() ) + return; + + try { + Window_setHasCustomDecoration.invoke( window ); + } catch( Exception ex ) { + Logger.getLogger( FlatLaf.class.getName() ).log( Level.SEVERE, null, ex ); + } + } + + static void setHitTestSpotsAndTitleBarHeight( Window window, List hitTestSpots, int titleBarHeight ) { + if( !isSupported() ) + return; + + try { + Object compAccessor = AWTAccessor_getComponentAccessor.invoke( null ); + Object peer = AWTAccessor_ComponentAccessor_getPeer.invoke( compAccessor, window ); + WWindowPeer_setCustomDecorationHitTestSpots.invoke( peer, hitTestSpots ); + WWindowPeer_setCustomDecorationTitleBarHeight.invoke( peer, titleBarHeight ); + } catch( Exception ex ) { + Logger.getLogger( FlatLaf.class.getName() ).log( Level.SEVERE, null, ex ); + } + } + + private static void initialize() { + if( initialized ) + return; + initialized = true; + + // requires JetBrains Runtime 11 and Windows 10 + if( !SystemInfo.IS_JETBRAINS_JVM_11_OR_LATER || !SystemInfo.IS_WINDOWS_10_OR_LATER ) + return; + + try { + Class awtAcessorClass = Class.forName( "sun.awt.AWTAccessor" ); + Class compAccessorClass = Class.forName( "sun.awt.AWTAccessor$ComponentAccessor" ); + AWTAccessor_getComponentAccessor = awtAcessorClass.getDeclaredMethod( "getComponentAccessor" ); + AWTAccessor_ComponentAccessor_getPeer = compAccessorClass.getDeclaredMethod( "getPeer", Component.class ); + + Class peerClass = Class.forName( "sun.awt.windows.WWindowPeer" ); + WWindowPeer_setCustomDecorationHitTestSpots = peerClass.getDeclaredMethod( "setCustomDecorationHitTestSpots", List.class ); + WWindowPeer_setCustomDecorationTitleBarHeight = peerClass.getDeclaredMethod( "setCustomDecorationTitleBarHeight", int.class ); + WWindowPeer_setCustomDecorationHitTestSpots.setAccessible( true ); + WWindowPeer_setCustomDecorationTitleBarHeight.setAccessible( true ); + + Window_hasCustomDecoration = Window.class.getDeclaredMethod( "hasCustomDecoration" ); + Window_setHasCustomDecoration = Window.class.getDeclaredMethod( "setHasCustomDecoration" ); + Window_hasCustomDecoration.setAccessible( true ); + Window_setHasCustomDecoration.setAccessible( true ); + } catch( Exception ex ) { + // ignore + } + } +} diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/SystemInfo.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/SystemInfo.java index 0909fdb1..91aedb84 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/SystemInfo.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/SystemInfo.java @@ -32,13 +32,16 @@ public class SystemInfo public static final boolean IS_LINUX; // OS versions + public static final boolean IS_WINDOWS_10_OR_LATER; public static final boolean IS_MAC_OS_10_11_EL_CAPITAN_OR_LATER; // Java versions public static final boolean IS_JAVA_9_OR_LATER; + public static final boolean IS_JAVA_11_OR_LATER; // Java VMs public static final boolean IS_JETBRAINS_JVM; + public static final boolean IS_JETBRAINS_JVM_11_OR_LATER; // UI toolkits public static final boolean IS_KDE; @@ -52,15 +55,18 @@ public class SystemInfo // OS versions long osVersion = scanVersion( System.getProperty( "os.version" ) ); + IS_WINDOWS_10_OR_LATER = (IS_WINDOWS && osVersion >= toVersion( 10, 0, 0, 0 )); IS_MAC_OS_10_11_EL_CAPITAN_OR_LATER = (IS_MAC && osVersion >= toVersion( 10, 11, 0, 0 )); // Java versions long javaVersion = scanVersion( System.getProperty( "java.version" ) ); IS_JAVA_9_OR_LATER = (javaVersion >= toVersion( 9, 0, 0, 0 )); + IS_JAVA_11_OR_LATER = (javaVersion >= toVersion( 11, 0, 0, 0 )); // Java VMs IS_JETBRAINS_JVM = System.getProperty( "java.vm.vendor", "Unknown" ) .toLowerCase( Locale.ENGLISH ).contains( "jetbrains" ); + IS_JETBRAINS_JVM_11_OR_LATER = IS_JETBRAINS_JVM && IS_JAVA_11_OR_LATER; // UI toolkits IS_KDE = (IS_LINUX && System.getenv( "KDE_FULL_SESSION" ) != null); From 5add7238520a6d35fe58fc4045cb78be566c95a5 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Sat, 30 May 2020 12:41:22 +0200 Subject: [PATCH 09/33] Window decorations: support right-to-left component orientation (issues #47 and #82) --- .../com/formdev/flatlaf/ui/FlatTitlePane.java | 17 ++++++++++++----- 1 file changed, 12 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 45130581..7a67ad8e 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 @@ -115,9 +115,9 @@ class FlatTitlePane createButtons(); setLayout( new BorderLayout() ); - add( iconLabel, BorderLayout.WEST ); + add( iconLabel, BorderLayout.LINE_START ); add( titleLabel, BorderLayout.CENTER ); - add( buttonPanel, BorderLayout.EAST ); + add( buttonPanel, BorderLayout.LINE_END ); } private void createButtons() { @@ -141,7 +141,7 @@ class FlatTitlePane } }; buttonPanel.setOpaque( false ); - buttonPanel.setLayout( new BoxLayout( buttonPanel, BoxLayout.X_AXIS ) ); + 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) @@ -418,6 +418,12 @@ class FlatTitlePane case "iconImage": updateIcon(); break; + + case "componentOrientation": + EventQueue.invokeLater( () -> { + updateJBRHitTestSpotsAndTitleBarHeight(); + } ); + break; } } @@ -500,8 +506,9 @@ class FlatTitlePane int restoredWidth = window.getWidth(); int newX = maximizedX; - if( xOnScreen >= maximizedX + restoredWidth - buttonPanel.getWidth() - 10 ) - newX = xOnScreen + buttonPanel.getWidth() + 10 - restoredWidth; + JComponent rightComp = getComponentOrientation().isLeftToRight() ? buttonPanel : iconLabel; + if( xOnScreen >= maximizedX + restoredWidth - rightComp.getWidth() - 10 ) + newX = xOnScreen + rightComp.getWidth() + 10 - restoredWidth; // move window near mouse window.setLocation( newX, maximizedY ); From 99c99b9218dc4e2d9307ba36a785f791fa14c096 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Sun, 31 May 2020 14:10:58 +0200 Subject: [PATCH 10/33] Window decorations: support embedding menu bar into title pane (enabled by default) (issues #47 and #82) --- .../formdev/flatlaf/FlatClientProperties.java | 9 +++ .../formdev/flatlaf/ui/FlatRootPaneUI.java | 59 ++++++++++++---- .../com/formdev/flatlaf/ui/FlatTitlePane.java | 68 +++++++++++++++++-- .../formdev/flatlaf/FlatDarkLaf.properties | 1 + .../com/formdev/flatlaf/FlatLaf.properties | 2 + .../formdev/flatlaf/FlatLightLaf.properties | 1 + .../com/formdev/flatlaf/demo/DemoFrame.java | 14 ++++ .../com/formdev/flatlaf/demo/DemoFrame.jfd | 9 +++ .../testing/FlatWindowDecorationsTest.java | 28 ++++++-- .../testing/FlatWindowDecorationsTest.jfd | 25 +++++-- .../uidefaults/FlatDarkLaf_1.8.0_202.txt | 26 +++++++ .../uidefaults/FlatLightLaf_1.8.0_202.txt | 26 +++++++ 12 files changed, 235 insertions(+), 33 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 ae53d724..baf2625d 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatClientProperties.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatClientProperties.java @@ -175,6 +175,15 @@ public interface FlatClientProperties */ String PROGRESS_BAR_SQUARE = "JProgressBar.square"; + /** + * Specifies whether the menu bar is embedded into the title pane if custom + * window decorations are enabled. Default is {@code true}. + *

+ * Component {@link javax.swing.JRootPane}
+ * Value type {@link java.lang.Boolean} + */ + String MENU_BAR_EMBEDDED = "JRootPane.menuBarEmbedded"; + /** * Specifies whether the decrease/increase arrow buttons of a scrollbar are shown. *

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 a1952205..1d2b6ce7 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 @@ -30,6 +30,7 @@ import javax.swing.JMenuBar; import javax.swing.JRootPane; import javax.swing.plaf.ComponentUI; import javax.swing.plaf.basic.BasicRootPaneUI; +import com.formdev.flatlaf.FlatClientProperties; import com.formdev.flatlaf.util.SystemInfo; /** @@ -41,7 +42,7 @@ public class FlatRootPaneUI extends BasicRootPaneUI { private JRootPane rootPane; - private JComponent titlePane; + private FlatTitlePane titlePane; private LayoutManager oldLayout; public static ComponentUI createUI( JComponent c ) { @@ -92,14 +93,17 @@ public class FlatRootPaneUI } } - private void setTitlePane( JComponent newTitlePane ) { + // layer title pane under frame content layer to allow placing menu bar over title pane + private final static Integer TITLE_PANE_LAYER = JLayeredPane.FRAME_CONTENT_LAYER - 1; + + private void setTitlePane( FlatTitlePane newTitlePane ) { JLayeredPane layeredPane = rootPane.getLayeredPane(); if( titlePane != null ) layeredPane.remove( titlePane ); if( newTitlePane != null ) - layeredPane.add( newTitlePane, JLayeredPane.FRAME_CONTENT_LAYER ); + layeredPane.add( newTitlePane, TITLE_PANE_LAYER ); titlePane = newTitlePane; } @@ -114,6 +118,14 @@ public class FlatRootPaneUI if( rootPane.getWindowDecorationStyle() != JRootPane.NONE ) installClientDecorations(); break; + + case FlatClientProperties.MENU_BAR_EMBEDDED: + if( titlePane != null ) { + titlePane.menuBarChanged(); + rootPane.revalidate(); + rootPane.repaint(); + } + break; } } @@ -125,7 +137,6 @@ public class FlatRootPaneUI @Override public void addLayoutComponent( String name, Component comp ) {} @Override public void addLayoutComponent( Component comp, Object constraints ) {} @Override public void removeLayoutComponent( Component comp ) {} - @Override public void invalidateLayout( Container target ) {} @Override public Dimension preferredLayoutSize( Container parent ) { @@ -144,20 +155,26 @@ public class FlatRootPaneUI private Dimension computeLayoutSize( Container parent, Function getSizeFunc ) { JRootPane rootPane = (JRootPane) parent; - JComponent titlePane = getTitlePane( rootPane ); + FlatTitlePane titlePane = getTitlePane( rootPane ); Dimension titlePaneSize = (titlePane != null) ? getSizeFunc.apply( titlePane ) : new Dimension(); - Dimension menuBarSize = (rootPane.getJMenuBar() != null) - ? getSizeFunc.apply( rootPane.getJMenuBar() ) - : new Dimension(); Dimension contentSize = (rootPane.getContentPane() != null) ? getSizeFunc.apply( rootPane.getContentPane() ) : rootPane.getSize(); - int width = Math.max( titlePaneSize.width, Math.max( menuBarSize.width, contentSize.width ) ); - int height = titlePaneSize.height + menuBarSize.height + contentSize.height; + int width = Math.max( titlePaneSize.width, contentSize.width ); + int height = titlePaneSize.height + contentSize.height; + if( titlePane == null || !titlePane.isMenuBarEmbedded() ) { + Dimension menuBarSize = (rootPane.getJMenuBar() != null) + ? getSizeFunc.apply( rootPane.getJMenuBar() ) + : new Dimension(); + + width = Math.max( width, menuBarSize.width ); + height += menuBarSize.height; + } + Insets insets = rootPane.getInsets(); return new Dimension( @@ -165,7 +182,7 @@ public class FlatRootPaneUI height + insets.top + insets.bottom ); } - private JComponent getTitlePane( JRootPane rootPane ) { + private FlatTitlePane getTitlePane( JRootPane rootPane ) { return (rootPane.getWindowDecorationStyle() != JRootPane.NONE && rootPane.getUI() instanceof FlatRootPaneUI) ? ((FlatRootPaneUI)rootPane.getUI()).titlePane @@ -188,7 +205,7 @@ public class FlatRootPaneUI rootPane.getGlassPane().setBounds( x, y, width, height ); int nextY = 0; - JComponent titlePane = getTitlePane( rootPane ); + FlatTitlePane titlePane = getTitlePane( rootPane ); if( titlePane != null ) { Dimension prefSize = titlePane.getPreferredSize(); titlePane.setBounds( 0, 0, width, prefSize.height ); @@ -197,9 +214,14 @@ public class FlatRootPaneUI JMenuBar menuBar = rootPane.getJMenuBar(); if( menuBar != null ) { - Dimension prefSize = menuBar.getPreferredSize(); - menuBar.setBounds( 0, nextY, width, prefSize.height ); - nextY += prefSize.height; + if( titlePane != null && titlePane.isMenuBarEmbedded() ) { + titlePane.validate(); + menuBar.setBounds( titlePane.getMenuBarBounds() ); + } else { + Dimension prefSize = menuBar.getPreferredSize(); + menuBar.setBounds( 0, nextY, width, prefSize.height ); + nextY += prefSize.height; + } } Container contentPane = rootPane.getContentPane(); @@ -207,6 +229,13 @@ public class FlatRootPaneUI contentPane.setBounds( 0, nextY, width, Math.max( height - nextY, 0 ) ); } + @Override + public void invalidateLayout( Container parent ) { + FlatTitlePane titlePane = getTitlePane( (JRootPane) parent ); + if( titlePane != null && titlePane.isMenuBarEmbedded() ) + titlePane.menuBarChanged(); + } + @Override public float getLayoutAlignmentX( Container target ) { return 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 7a67ad8e..a656cb3f 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 @@ -48,10 +48,12 @@ import javax.swing.BoxLayout; 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 com.formdev.flatlaf.FlatClientProperties; import com.formdev.flatlaf.util.UIScale; /** @@ -61,9 +63,12 @@ import com.formdev.flatlaf.util.UIScale; * @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 @@ -79,13 +84,18 @@ class FlatTitlePane 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 boolean menuBarEmbedded = UIManager.getBoolean( "TitlePane.menuBarEmbedded" ); + 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; @@ -107,15 +117,32 @@ class FlatTitlePane } private 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.setBorder( new LineBorder( Color.red ) ); + 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( iconLabel, BorderLayout.LINE_START ); + add( leftPanel, BorderLayout.LINE_START ); add( titleLabel, BorderLayout.CENTER ); add( buttonPanel, BorderLayout.LINE_END ); } @@ -169,7 +196,9 @@ class FlatTitlePane private void activeChanged( boolean active ) { Color background = FlatUIUtils.nonUIResource( active ? activeBackground : inactiveBackground ); - Color foreground = FlatUIUtils.nonUIResource( active ? activeForeground : inactiveForeground ); + Color foreground = FlatUIUtils.nonUIResource( active + ? (rootPane.getJMenuBar() != null && isMenuBarEmbedded() ? embeddedForeground : activeForeground) + : inactiveForeground ); setBackground( background ); titleLabel.setForeground( foreground ); @@ -278,6 +307,32 @@ class FlatTitlePane window.removeComponentListener( handler ); } + boolean isMenuBarEmbedded() { + return menuBarEmbedded && FlatClientProperties.clientPropertyBoolean( + rootPane, FlatClientProperties.MENU_BAR_EMBEDDED, true ); + } + + Rectangle getMenuBarBounds() { + Rectangle bounds = menuBarPlaceholder.getBounds(); + bounds = SwingUtilities.convertRectangle( menuBarPlaceholder.getParent(), bounds, rootPane ); + return FlatUIUtils.subtractInsets( bounds, UIScale.scale( getMenuBarMargins() ) ); + } + + void menuBarChanged() { + menuBarPlaceholder.invalidate(); + + // update title foreground color + EventQueue.invokeLater( () -> { + activeChanged( window == null || window.isActive() ); + } ); + } + + private 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() ); @@ -373,7 +428,8 @@ class FlatTitlePane return; List hitTestSpots = new ArrayList<>(); - addJBRHitTestSpot( buttonPanel, hitTestSpots ); + addJBRHitTestSpot( buttonPanel, false, hitTestSpots ); + addJBRHitTestSpot( menuBarPlaceholder, true, hitTestSpots );//TOOD int titleBarHeight = getHeight(); // slightly reduce height so that component receives mouseExit events @@ -383,13 +439,15 @@ class FlatTitlePane JBRCustomDecorations.setHitTestSpotsAndTitleBarHeight( window, hitTestSpots, titleBarHeight ); } - private void addJBRHitTestSpot( JComponent c, List hitTestSpots ) { + private void addJBRHitTestSpot( JComponent c, boolean subtractMenuBarMargins, List 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 ); @@ -506,7 +564,7 @@ class FlatTitlePane int restoredWidth = window.getWidth(); int newX = maximizedX; - JComponent rightComp = getComponentOrientation().isLeftToRight() ? buttonPanel : iconLabel; + JComponent rightComp = getComponentOrientation().isLeftToRight() ? buttonPanel : leftPanel; if( xOnScreen >= maximizedX + restoredWidth - rightComp.getWidth() - 10 ) newX = xOnScreen + rightComp.getWidth() + 10 - restoredWidth; diff --git a/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatDarkLaf.properties b/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatDarkLaf.properties index 4eeadea6..99d7e083 100644 --- a/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatDarkLaf.properties +++ b/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatDarkLaf.properties @@ -268,6 +268,7 @@ TableHeader.bottomSeparatorColor=$TableHeader.separatorColor #---- TitlePane ---- +TitlePane.embeddedForeground=darken($TitlePane.foreground,15%) TitlePane.buttonHoverBackground=lighten($TitlePane.background,10%,derived) TitlePane.buttonPressedBackground=lighten($TitlePane.background,20%,derived) 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 6624ee2d..0f9748ec 100644 --- a/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLaf.properties +++ b/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLaf.properties @@ -576,8 +576,10 @@ TitledBorder.border=1,1,1,1,$Separator.foreground #---- TitlePane ---- +TitlePane.menuBarEmbedded=true TitlePane.iconSize=16,16 TitlePane.iconMargins=3,8,3,0 +TitlePane.menuBarMargins=0,8,0,22 TitlePane.titleMargins=3,8,3,8 TitlePane.buttonSize=44,30 TitlePane.buttonMaximizedHeight=22 diff --git a/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLightLaf.properties b/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLightLaf.properties index 8982df4d..7f759756 100644 --- a/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLightLaf.properties +++ b/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLightLaf.properties @@ -275,6 +275,7 @@ TableHeader.bottomSeparatorColor=$TableHeader.separatorColor #---- TitlePane ---- +TitlePane.embeddedForeground=lighten($TitlePane.foreground,35%) TitlePane.buttonHoverBackground=darken($TitlePane.background,10%,derived) TitlePane.buttonPressedBackground=darken($TitlePane.background,20%,derived) 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 bf02a76e..51aa1967 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 @@ -23,6 +23,7 @@ import java.util.Arrays; import javax.swing.*; import javax.swing.text.DefaultEditorKit; import javax.swing.text.StyleContext; +import com.formdev.flatlaf.FlatClientProperties; import com.formdev.flatlaf.FlatLaf; import com.formdev.flatlaf.demo.extras.*; import com.formdev.flatlaf.demo.intellijthemes.*; @@ -71,6 +72,11 @@ class DemoFrame } ); } + private void menuBarEmbeddedChanged() { + getRootPane().putClientProperty( FlatClientProperties.MENU_BAR_EMBEDDED, + menuBarEmbeddedCheckBoxMenuItem.isSelected() ? null : false ); + } + private void underlineMenuSelection() { UIManager.put( "MenuItem.selectionType", underlineMenuSelectionMenuItem.isSelected() ? "underline" : null ); } @@ -219,6 +225,7 @@ class DemoFrame JMenuItem incrFontMenuItem = new JMenuItem(); JMenuItem decrFontMenuItem = new JMenuItem(); JMenu optionsMenu = new JMenu(); + menuBarEmbeddedCheckBoxMenuItem = new JCheckBoxMenuItem(); underlineMenuSelectionMenuItem = new JCheckBoxMenuItem(); alwaysShowMnemonicsMenuItem = new JCheckBoxMenuItem(); JMenu helpMenu = new JMenu(); @@ -453,6 +460,12 @@ class DemoFrame { optionsMenu.setText("Options"); + //---- menuBarEmbeddedCheckBoxMenuItem ---- + menuBarEmbeddedCheckBoxMenuItem.setText("Embedded menu bar"); + menuBarEmbeddedCheckBoxMenuItem.setSelected(true); + menuBarEmbeddedCheckBoxMenuItem.addActionListener(e -> menuBarEmbeddedChanged()); + optionsMenu.add(menuBarEmbeddedCheckBoxMenuItem); + //---- underlineMenuSelectionMenuItem ---- underlineMenuSelectionMenuItem.setText("Use underline menu selection"); underlineMenuSelectionMenuItem.addActionListener(e -> underlineMenuSelection()); @@ -572,6 +585,7 @@ class DemoFrame // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables private JMenu fontMenu; + private JCheckBoxMenuItem menuBarEmbeddedCheckBoxMenuItem; private JCheckBoxMenuItem underlineMenuSelectionMenuItem; private JCheckBoxMenuItem alwaysShowMnemonicsMenuItem; private JTabbedPane tabbedPane; 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 c2aec3f5..7675da4c 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 @@ -322,6 +322,15 @@ new FormModel { add( new FormContainer( "javax.swing.JMenu", new FormLayoutManager( class javax.swing.JMenu ) ) { name: "optionsMenu" "text": "Options" + add( new FormComponent( "javax.swing.JCheckBoxMenuItem" ) { + name: "menuBarEmbeddedCheckBoxMenuItem" + "text": "Embedded menu bar" + "selected": true + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "menuBarEmbeddedChanged", false ) ) + } ) add( new FormComponent( "javax.swing.JCheckBoxMenuItem" ) { name: "underlineMenuSelectionMenuItem" "text": "Use underline menu selection" 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 58f35611..2816e732 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 @@ -23,6 +23,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; import javax.swing.*; +import com.formdev.flatlaf.FlatClientProperties; import net.miginfocom.swing.*; /** @@ -103,6 +104,12 @@ public class FlatWindowDecorationsTest } } + private void menuBarEmbeddedChanged() { + JRootPane rootPane = getWindowRootPane(); + if( rootPane != null ) + rootPane.putClientProperty( FlatClientProperties.MENU_BAR_EMBEDDED, menuBarEmbeddedCheckBox.isSelected() ); + } + private void resizableChanged() { Window window = SwingUtilities.windowForComponent( this ); if( window instanceof Frame ) @@ -178,6 +185,7 @@ public class FlatWindowDecorationsTest private void initComponents() { // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents menuBarCheckBox = new JCheckBox(); + menuBarEmbeddedCheckBox = new JCheckBox(); resizableCheckBox = new JCheckBox(); JLabel label1 = new JLabel(); JLabel label2 = new JLabel(); @@ -231,6 +239,7 @@ public class FlatWindowDecorationsTest "[fill]", // rows "para[]0" + + "[]0" + "[]" + "[]" + "[top]" + @@ -242,19 +251,25 @@ public class FlatWindowDecorationsTest menuBarCheckBox.addActionListener(e -> menuBarChanged()); add(menuBarCheckBox, "cell 0 0"); + //---- menuBarEmbeddedCheckBox ---- + menuBarEmbeddedCheckBox.setText("embedded menu bar"); + menuBarEmbeddedCheckBox.setSelected(true); + menuBarEmbeddedCheckBox.addActionListener(e -> menuBarEmbeddedChanged()); + add(menuBarEmbeddedCheckBox, "cell 0 1"); + //---- resizableCheckBox ---- resizableCheckBox.setText("resizable"); resizableCheckBox.setSelected(true); resizableCheckBox.addActionListener(e -> resizableChanged()); - add(resizableCheckBox, "cell 0 1"); + add(resizableCheckBox, "cell 0 2"); //---- label1 ---- label1.setText("Style:"); - add(label1, "cell 0 2"); + add(label1, "cell 0 3"); //---- label2 ---- label2.setText("Icon:"); - add(label2, "cell 1 2"); + add(label2, "cell 1 3"); //======== panel1 ======== { @@ -319,7 +334,7 @@ public class FlatWindowDecorationsTest styleFileChooserRadioButton.addActionListener(e -> decorationStyleChanged()); panel1.add(styleFileChooserRadioButton, "cell 0 8"); } - add(panel1, "cell 0 3"); + add(panel1, "cell 0 4"); //======== panel2 ======== { @@ -348,12 +363,12 @@ public class FlatWindowDecorationsTest iconTestRandomRadioButton.addActionListener(e -> iconChanged()); panel2.add(iconTestRandomRadioButton, "cell 0 2"); } - add(panel2, "cell 1 3"); + add(panel2, "cell 1 4"); //---- openDialogButton ---- openDialogButton.setText("Open Dialog"); openDialogButton.addActionListener(e -> openDialog()); - add(openDialogButton, "cell 0 4"); + add(openDialogButton, "cell 0 5"); //======== menuBar ======== { @@ -545,6 +560,7 @@ public class FlatWindowDecorationsTest // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables private JCheckBox menuBarCheckBox; + private JCheckBox menuBarEmbeddedCheckBox; private JCheckBox resizableCheckBox; private JRadioButton styleNoneRadioButton; private JRadioButton styleFrameRadioButton; 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 a9824228..5675caf2 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]para[fill]" - "$rowConstraints": "para[]0[][][top][]" + "$rowConstraints": "para[]0[]0[][][top][]" } ) { name: "this" add( new FormComponent( "javax.swing.JCheckBox" ) { @@ -23,6 +23,17 @@ new FormModel { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 0" } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "menuBarEmbeddedCheckBox" + "text": "embedded menu bar" + "selected": true + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "menuBarEmbeddedChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 1" + } ) add( new FormComponent( "javax.swing.JCheckBox" ) { name: "resizableCheckBox" "text": "resizable" @@ -32,19 +43,19 @@ new FormModel { } addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "resizableChanged", false ) ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 1" + "value": "cell 0 2" } ) add( new FormComponent( "javax.swing.JLabel" ) { name: "label1" "text": "Style:" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 2" + "value": "cell 0 3" } ) add( new FormComponent( "javax.swing.JLabel" ) { name: "label2" "text": "Icon:" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 2" + "value": "cell 1 3" } ) add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { "$columnConstraints": "[fill]" @@ -153,7 +164,7 @@ new FormModel { "value": "cell 0 8" } ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 3" + "value": "cell 0 4" } ) add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { "$columnConstraints": "[fill]" @@ -196,14 +207,14 @@ new FormModel { "value": "cell 0 2" } ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 3" + "value": "cell 1 4" } ) add( new FormComponent( "javax.swing.JButton" ) { name: "openDialogButton" "text": "Open Dialog" addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "openDialog", false ) ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 4" + "value": "cell 0 5" } ) }, new FormLayoutConstraints( null ) { "location": new java.awt.Point( 0, 0 ) diff --git a/flatlaf-testing/src/main/resources/com/formdev/flatlaf/testing/uidefaults/FlatDarkLaf_1.8.0_202.txt b/flatlaf-testing/src/main/resources/com/formdev/flatlaf/testing/uidefaults/FlatDarkLaf_1.8.0_202.txt index cb6711f4..3d818fb4 100644 --- a/flatlaf-testing/src/main/resources/com/formdev/flatlaf/testing/uidefaults/FlatDarkLaf_1.8.0_202.txt +++ b/flatlaf-testing/src/main/resources/com/formdev/flatlaf/testing/uidefaults/FlatDarkLaf_1.8.0_202.txt @@ -1038,6 +1038,32 @@ TextPane.selectionForeground #bbbbbb javax.swing.plaf.ColorUIResource [UI] TextPaneUI com.formdev.flatlaf.ui.FlatTextPaneUI +#---- TitlePane ---- + +TitlePane.background #303234 javax.swing.plaf.ColorUIResource [UI] +TitlePane.buttonHoverBackground #484c4f com.formdev.flatlaf.util.DerivedColor [UI] lighten(10% autoInverse) +TitlePane.buttonMaximizedHeight 22 +TitlePane.buttonPressedBackground #616569 com.formdev.flatlaf.util.DerivedColor [UI] lighten(20% autoInverse) +TitlePane.buttonSize 44,30 javax.swing.plaf.DimensionUIResource [UI] +TitlePane.closeHoverBackground #e81123 javax.swing.plaf.ColorUIResource [UI] +TitlePane.closeHoverForeground #ffffff javax.swing.plaf.ColorUIResource [UI] +TitlePane.closeIcon [lazy] 44,30 com.formdev.flatlaf.icons.FlatWindowCloseIcon [UI] +TitlePane.closePressedBackground #99e81123 javax.swing.plaf.ColorUIResource [UI] +TitlePane.closePressedForeground #ffffff javax.swing.plaf.ColorUIResource [UI] +TitlePane.embeddedForeground #959595 javax.swing.plaf.ColorUIResource [UI] +TitlePane.foreground #bbbbbb javax.swing.plaf.ColorUIResource [UI] +TitlePane.iconMargins 3,8,3,0 javax.swing.plaf.InsetsUIResource [UI] +TitlePane.iconSize 16,16 javax.swing.plaf.DimensionUIResource [UI] +TitlePane.iconifyIcon [lazy] 44,30 com.formdev.flatlaf.icons.FlatWindowIconifyIcon [UI] +TitlePane.inactiveBackground #303234 javax.swing.plaf.ColorUIResource [UI] +TitlePane.inactiveForeground #777777 javax.swing.plaf.ColorUIResource [UI] +TitlePane.maximizeIcon [lazy] 44,30 com.formdev.flatlaf.icons.FlatWindowMaximizeIcon [UI] +TitlePane.menuBarEmbedded true +TitlePane.menuBarMargins 0,8,0,22 javax.swing.plaf.InsetsUIResource [UI] +TitlePane.restoreIcon [lazy] 44,30 com.formdev.flatlaf.icons.FlatWindowRestoreIcon [UI] +TitlePane.titleMargins 3,8,3,8 javax.swing.plaf.InsetsUIResource [UI] + + #---- TitledBorder ---- TitledBorder.border [lazy] 1,1,1,1 false com.formdev.flatlaf.ui.FlatLineBorder [UI] lineColor=#515151 javax.swing.plaf.ColorUIResource [UI] lineThickness=1.000000 diff --git a/flatlaf-testing/src/main/resources/com/formdev/flatlaf/testing/uidefaults/FlatLightLaf_1.8.0_202.txt b/flatlaf-testing/src/main/resources/com/formdev/flatlaf/testing/uidefaults/FlatLightLaf_1.8.0_202.txt index f113f6fb..f584833e 100644 --- a/flatlaf-testing/src/main/resources/com/formdev/flatlaf/testing/uidefaults/FlatLightLaf_1.8.0_202.txt +++ b/flatlaf-testing/src/main/resources/com/formdev/flatlaf/testing/uidefaults/FlatLightLaf_1.8.0_202.txt @@ -1040,6 +1040,32 @@ TextPane.selectionForeground #ffffff javax.swing.plaf.ColorUIResource [UI] TextPaneUI com.formdev.flatlaf.ui.FlatTextPaneUI +#---- TitlePane ---- + +TitlePane.background #ffffff javax.swing.plaf.ColorUIResource [UI] +TitlePane.buttonHoverBackground #e6e6e6 com.formdev.flatlaf.util.DerivedColor [UI] darken(10% autoInverse) +TitlePane.buttonMaximizedHeight 22 +TitlePane.buttonPressedBackground #cccccc com.formdev.flatlaf.util.DerivedColor [UI] darken(20% autoInverse) +TitlePane.buttonSize 44,30 javax.swing.plaf.DimensionUIResource [UI] +TitlePane.closeHoverBackground #e81123 javax.swing.plaf.ColorUIResource [UI] +TitlePane.closeHoverForeground #ffffff javax.swing.plaf.ColorUIResource [UI] +TitlePane.closeIcon [lazy] 44,30 com.formdev.flatlaf.icons.FlatWindowCloseIcon [UI] +TitlePane.closePressedBackground #99e81123 javax.swing.plaf.ColorUIResource [UI] +TitlePane.closePressedForeground #ffffff javax.swing.plaf.ColorUIResource [UI] +TitlePane.embeddedForeground #595959 javax.swing.plaf.ColorUIResource [UI] +TitlePane.foreground #000000 javax.swing.plaf.ColorUIResource [UI] +TitlePane.iconMargins 3,8,3,0 javax.swing.plaf.InsetsUIResource [UI] +TitlePane.iconSize 16,16 javax.swing.plaf.DimensionUIResource [UI] +TitlePane.iconifyIcon [lazy] 44,30 com.formdev.flatlaf.icons.FlatWindowIconifyIcon [UI] +TitlePane.inactiveBackground #ffffff javax.swing.plaf.ColorUIResource [UI] +TitlePane.inactiveForeground #8c8c8c javax.swing.plaf.ColorUIResource [UI] +TitlePane.maximizeIcon [lazy] 44,30 com.formdev.flatlaf.icons.FlatWindowMaximizeIcon [UI] +TitlePane.menuBarEmbedded true +TitlePane.menuBarMargins 0,8,0,22 javax.swing.plaf.InsetsUIResource [UI] +TitlePane.restoreIcon [lazy] 44,30 com.formdev.flatlaf.icons.FlatWindowRestoreIcon [UI] +TitlePane.titleMargins 3,8,3,8 javax.swing.plaf.InsetsUIResource [UI] + + #---- TitledBorder ---- TitledBorder.border [lazy] 1,1,1,1 false com.formdev.flatlaf.ui.FlatLineBorder [UI] lineColor=#d1d1d1 javax.swing.plaf.ColorUIResource [UI] lineThickness=1.000000 From 7ccd32dfbd256d4a215d7dee31a9557545e62f4e Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Sun, 31 May 2020 14:45:44 +0200 Subject: [PATCH 11/33] Window decorations: fixed menu bar border if embedded (issues #47 and #82) --- .../com/formdev/flatlaf/ui/FlatTitlePane.java | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) 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 a656cb3f..ecac58bd 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 @@ -18,6 +18,7 @@ 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; @@ -53,6 +54,8 @@ 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.util.UIScale; @@ -109,6 +112,8 @@ class FlatTitlePane FlatTitlePane( JRootPane rootPane ) { this.rootPane = rootPane; + setBorder( new TitlePaneBorder() ); + addSubComponents(); activeChanged( true ); @@ -123,7 +128,6 @@ class FlatTitlePane iconLabel.setBorder( new FlatEmptyBorder( UIManager.getInsets( "TitlePane.iconMargins" ) ) ); titleLabel.setBorder( new FlatEmptyBorder( UIManager.getInsets( "TitlePane.titleMargins" ) ) ); - leftPanel.setBorder( new LineBorder( Color.red ) ); leftPanel.setLayout( new BoxLayout( leftPanel, BoxLayout.LINE_AXIS ) ); leftPanel.setOpaque( false ); leftPanel.add( iconLabel ); @@ -315,6 +319,12 @@ class FlatTitlePane Rectangle getMenuBarBounds() { Rectangle bounds = menuBarPlaceholder.getBounds(); bounds = SwingUtilities.convertRectangle( menuBarPlaceholder.getParent(), bounds, rootPane ); + + // 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() ) ); } @@ -453,6 +463,39 @@ class FlatTitlePane hitTestSpots.add( r ); } + //---- class TitlePaneBorder ---------------------------------------------- + + private class TitlePaneBorder + 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; + } + + 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 ); + } + + private Border getMenuBarBorder() { + JMenuBar menuBar = rootPane.getJMenuBar(); + return (menuBar != null && isMenuBarEmbedded()) ? menuBar.getBorder() : null; + } + } + //---- class Handler ------------------------------------------------------ private class Handler From 10c948d33c1dce0e7f77c5cbbfd54a5adc8523ca Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Sun, 31 May 2020 14:53:13 +0200 Subject: [PATCH 12/33] Window decorations: nested class FlatRootPaneUI.FlatRootLayout is no longer static (issues #47 and #82) --- .../java/com/formdev/flatlaf/ui/FlatRootPaneUI.java | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) 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 1d2b6ce7..3ce1f6c4 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 @@ -131,7 +131,7 @@ public class FlatRootPaneUI //---- class FlatRootLayout ----------------------------------------------- - private static class FlatRootLayout + private class FlatRootLayout implements LayoutManager2 { @Override public void addLayoutComponent( String name, Component comp ) {} @@ -155,7 +155,6 @@ public class FlatRootPaneUI private Dimension computeLayoutSize( Container parent, Function getSizeFunc ) { JRootPane rootPane = (JRootPane) parent; - FlatTitlePane titlePane = getTitlePane( rootPane ); Dimension titlePaneSize = (titlePane != null) ? getSizeFunc.apply( titlePane ) @@ -182,13 +181,6 @@ public class FlatRootPaneUI height + insets.top + insets.bottom ); } - private FlatTitlePane getTitlePane( JRootPane rootPane ) { - return (rootPane.getWindowDecorationStyle() != JRootPane.NONE && - rootPane.getUI() instanceof FlatRootPaneUI) - ? ((FlatRootPaneUI)rootPane.getUI()).titlePane - : null; - } - @Override public void layoutContainer( Container parent ) { JRootPane rootPane = (JRootPane) parent; @@ -205,7 +197,6 @@ public class FlatRootPaneUI rootPane.getGlassPane().setBounds( x, y, width, height ); int nextY = 0; - FlatTitlePane titlePane = getTitlePane( rootPane ); if( titlePane != null ) { Dimension prefSize = titlePane.getPreferredSize(); titlePane.setBounds( 0, 0, width, prefSize.height ); @@ -231,7 +222,6 @@ public class FlatRootPaneUI @Override public void invalidateLayout( Container parent ) { - FlatTitlePane titlePane = getTitlePane( (JRootPane) parent ); if( titlePane != null && titlePane.isMenuBarEmbedded() ) titlePane.menuBarChanged(); } From 16d2e27d0517226e87ff0ed26db3207d9b572076 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Sun, 31 May 2020 15:31:28 +0200 Subject: [PATCH 13/33] Window decorations: require Windows 10 (issues #47 and #82) --- .../src/main/java/com/formdev/flatlaf/FlatLaf.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatLaf.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatLaf.java index a7b1c2a7..d8c72c3a 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatLaf.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatLaf.java @@ -115,15 +115,17 @@ public abstract class FlatLaf /** * Returns whether FlatLaf supports custom window decorations. + * This depends on the operating system and on the used Java runtime. *

* To use custom window decorations in your application, enable them with - * (before creating any frames or dialogs): + * following code (before creating any frames or dialogs). Then custom window + * decorations are only enabled if this method returns {@code true}. *

 	 * JFrame.setDefaultLookAndFeelDecorated( true );
 	 * JDialog.setDefaultLookAndFeelDecorated( true );
 	 * 
*

- * Returns {@code true} on Windows, {@code false} otherwise. + * Returns {@code true} on Windows 10, {@code false} otherwise. *

* Return also {@code false} if running on Windows 10 in * JetBrains Runtime 11 (or later) @@ -139,7 +141,7 @@ public abstract class FlatLaf JBRCustomDecorations.isSupported() ) return false; - return SystemInfo.IS_WINDOWS; + return SystemInfo.IS_WINDOWS_10_OR_LATER; } @Override From 1fffc67d13ed158e4ae6673fc579a60b47068f5d Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Tue, 2 Jun 2020 17:49:30 +0200 Subject: [PATCH 14/33] Window decorations: added border (issues #47 and #82) --- .../formdev/flatlaf/ui/FlatRootPaneUI.java | 39 +++++++++++++++++++ .../com/formdev/flatlaf/ui/FlatTitlePane.java | 6 ++- .../com/formdev/flatlaf/FlatLaf.properties | 5 +++ .../flatlaf/testing/FlatInspector.java | 3 +- .../uidefaults/FlatDarkLaf_1.8.0_202.txt | 1 + .../uidefaults/FlatLightLaf_1.8.0_202.txt | 1 + 6 files changed, 52 insertions(+), 3 deletions(-) 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 3362753b..8a331a5b 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 @@ -20,9 +20,12 @@ import java.awt.Color; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Graphics2D; import java.awt.Insets; import java.awt.LayoutManager; import java.awt.LayoutManager2; +import java.awt.Toolkit; import java.beans.PropertyChangeEvent; import java.util.function.Function; import javax.swing.JComponent; @@ -31,11 +34,14 @@ import javax.swing.JFrame; import javax.swing.JLayeredPane; import javax.swing.JMenuBar; import javax.swing.JRootPane; +import javax.swing.LookAndFeel; import javax.swing.UIManager; +import javax.swing.plaf.BorderUIResource; import javax.swing.plaf.ComponentUI; import javax.swing.plaf.UIResource; import javax.swing.plaf.basic.BasicRootPaneUI; import com.formdev.flatlaf.FlatClientProperties; +import com.formdev.flatlaf.util.HiDPIUtils; import com.formdev.flatlaf.util.SystemInfo; /** @@ -93,6 +99,12 @@ public class FlatRootPaneUI } private void installClientDecorations() { + // install border + if( rootPane.getWindowDecorationStyle() != JRootPane.NONE ) + LookAndFeel.installBorder( rootPane, "RootPane.border" ); + else + LookAndFeel.uninstallBorder( rootPane ); + // install title pane setTitlePane( new FlatTitlePane( rootPane ) ); @@ -102,6 +114,7 @@ public class FlatRootPaneUI } private void uninstallClientDecorations() { + LookAndFeel.uninstallBorder( rootPane ); setTitlePane( null ); if( oldLayout != null ) { @@ -258,4 +271,30 @@ public class FlatRootPaneUI return 0; } } + + //---- class FlatWindowBorder --------------------------------------------- + + public static class FlatWindowBorder + extends BorderUIResource.EmptyBorderUIResource + { + public FlatWindowBorder() { + super( 1, 1, 1, 1 ); + } + + @Override + public void paintBorder( Component c, Graphics g, int x, int y, int width, int height ) { + Object borderColorObj = Toolkit.getDefaultToolkit().getDesktopProperty( + "win.frame.activeBorderColor" ); + Color borderColor = (borderColorObj instanceof Color) + ? (Color) borderColorObj + : UIManager.getColor( "windowBorder" ); + + g.setColor( borderColor ); + HiDPIUtils.paintAtScale1x( (Graphics2D) g, x, y, width, height, this::paintImpl ); + } + + private void paintImpl( Graphics2D g, int x, int y, int width, int height, double scaleFactor ) { + g.drawRect( x, y, width - 1, height - 1 ); + } + } } 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 ecac58bd..fdf45e2d 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 @@ -317,8 +317,10 @@ class FlatTitlePane } Rectangle getMenuBarBounds() { - Rectangle bounds = menuBarPlaceholder.getBounds(); - bounds = SwingUtilities.convertRectangle( menuBarPlaceholder.getParent(), bounds, rootPane ); + 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) 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 bb199a39..9d43cd19 100644 --- a/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLaf.properties +++ b/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLaf.properties @@ -423,6 +423,11 @@ RadioButtonMenuItem.borderPainted=true RadioButtonMenuItem.background=@menuBackground +#---- RootPane ---- + +RootPane.border=com.formdev.flatlaf.ui.FlatRootPaneUI$FlatWindowBorder + + #---- ScrollBar ---- ScrollBar.width=10 diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatInspector.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatInspector.java index 58a88d4c..8b38a164 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatInspector.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatInspector.java @@ -284,8 +284,9 @@ public class FlatInspector highlightFigure.setVisible( c != null ); if( c != null ) { + Insets insets = rootPane.getInsets(); highlightFigure.setBounds( new Rectangle( - SwingUtilities.convertPoint( c, 0, 0, rootPane ), + SwingUtilities.convertPoint( c, -insets.left, -insets.top, rootPane ), c.getSize() ) ); } } diff --git a/flatlaf-testing/src/main/resources/com/formdev/flatlaf/testing/uidefaults/FlatDarkLaf_1.8.0_202.txt b/flatlaf-testing/src/main/resources/com/formdev/flatlaf/testing/uidefaults/FlatDarkLaf_1.8.0_202.txt index 957de7b7..0513ce0c 100644 --- a/flatlaf-testing/src/main/resources/com/formdev/flatlaf/testing/uidefaults/FlatDarkLaf_1.8.0_202.txt +++ b/flatlaf-testing/src/main/resources/com/formdev/flatlaf/testing/uidefaults/FlatDarkLaf_1.8.0_202.txt @@ -743,6 +743,7 @@ Resizable.resizeBorder [lazy] 4,4,4,4 false com.formdev.flatlaf.ui.F #---- RootPane ---- +RootPane.border [lazy] 1,1,1,1 false com.formdev.flatlaf.ui.FlatRootPaneUI$FlatWindowBorder [UI] RootPane.defaultButtonWindowKeyBindings length=8 [Ljava.lang.Object; [0] ENTER [1] press diff --git a/flatlaf-testing/src/main/resources/com/formdev/flatlaf/testing/uidefaults/FlatLightLaf_1.8.0_202.txt b/flatlaf-testing/src/main/resources/com/formdev/flatlaf/testing/uidefaults/FlatLightLaf_1.8.0_202.txt index 8861a633..5f4b56d5 100644 --- a/flatlaf-testing/src/main/resources/com/formdev/flatlaf/testing/uidefaults/FlatLightLaf_1.8.0_202.txt +++ b/flatlaf-testing/src/main/resources/com/formdev/flatlaf/testing/uidefaults/FlatLightLaf_1.8.0_202.txt @@ -745,6 +745,7 @@ Resizable.resizeBorder [lazy] 4,4,4,4 false com.formdev.flatlaf.ui.F #---- RootPane ---- +RootPane.border [lazy] 1,1,1,1 false com.formdev.flatlaf.ui.FlatRootPaneUI$FlatWindowBorder [UI] RootPane.defaultButtonWindowKeyBindings length=8 [Ljava.lang.Object; [0] ENTER [1] press From 8e77eb0519d8d663a3e9aa2b33b235eab023abdf Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Sat, 6 Jun 2020 12:20:33 +0200 Subject: [PATCH 15/33] Window decorations: support resizing window (issues #47 and #82) --- .../formdev/flatlaf/ui/FlatRootPaneUI.java | 10 + .../formdev/flatlaf/ui/FlatWindowResizer.java | 310 ++++++++++++++++++ .../com/formdev/flatlaf/FlatLaf.properties | 3 + .../testing/FlatWindowDecorationsTest.java | 2 + .../uidefaults/FlatDarkLaf_1.8.0_202.txt | 3 + .../uidefaults/FlatLightLaf_1.8.0_202.txt | 3 + 6 files changed, 331 insertions(+) create mode 100644 flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatWindowResizer.java 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 8a331a5b..32e94c84 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 @@ -55,6 +55,7 @@ public class FlatRootPaneUI private JRootPane rootPane; private FlatTitlePane titlePane; private LayoutManager oldLayout; + private FlatWindowResizer windowResizer; public static ComponentUI createUI( JComponent c ) { return new FlatRootPaneUI(); @@ -111,12 +112,21 @@ public class FlatRootPaneUI // install layout oldLayout = rootPane.getLayout(); rootPane.setLayout( new FlatRootLayout() ); + + // install window resizer + if( !JBRCustomDecorations.isSupported() ) + windowResizer = new FlatWindowResizer( rootPane ); } private void uninstallClientDecorations() { LookAndFeel.uninstallBorder( rootPane ); setTitlePane( null ); + if( windowResizer != null ) { + windowResizer.uninstall(); + windowResizer = null; + } + if( oldLayout != null ) { rootPane.setLayout( oldLayout ); oldLayout = null; diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatWindowResizer.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatWindowResizer.java new file mode 100644 index 00000000..3a5c8e65 --- /dev/null +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatWindowResizer.java @@ -0,0 +1,310 @@ +/* + * 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 static java.awt.Cursor.*; +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.Container; +import java.awt.Dialog; +import java.awt.Dimension; +import java.awt.Frame; +import java.awt.Graphics; +import java.awt.Rectangle; +import java.awt.Toolkit; +import java.awt.Window; +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.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import javax.swing.JComponent; +import javax.swing.JLayeredPane; +import javax.swing.JRootPane; +import javax.swing.UIManager; +import com.formdev.flatlaf.util.UIScale; + +/** + * Resizes frames and dialogs. + * + * @author Karl Tauber + */ +class FlatWindowResizer + extends JComponent + implements PropertyChangeListener, ComponentListener +{ + private final static Integer WINDOW_RESIZER_LAYER = JLayeredPane.DRAG_LAYER + 1; + + private final JRootPane rootPane; + + private final int borderDragThickness = FlatUIUtils.getUIInt( "RootPane.borderDragThickness", 5 ); + private final int cornerDragWidth = FlatUIUtils.getUIInt( "RootPane.cornerDragWidth", 16 ); + private final boolean honorMinimumSizeOnResize = UIManager.getBoolean( "RootPane.honorMinimumSizeOnResize" ); + + private Window window; + + FlatWindowResizer( JRootPane rootPane ) { + this.rootPane = rootPane; + + setLayout( new BorderLayout() ); + add( new DragBorderComponent( NW_RESIZE_CURSOR, N_RESIZE_CURSOR, NE_RESIZE_CURSOR ), BorderLayout.NORTH ); + add( new DragBorderComponent( SW_RESIZE_CURSOR, S_RESIZE_CURSOR, SE_RESIZE_CURSOR ), BorderLayout.SOUTH ); + add( new DragBorderComponent( NW_RESIZE_CURSOR, W_RESIZE_CURSOR, SW_RESIZE_CURSOR ), BorderLayout.WEST ); + add( new DragBorderComponent( NE_RESIZE_CURSOR, E_RESIZE_CURSOR, SE_RESIZE_CURSOR ), BorderLayout.EAST ); + + rootPane.addComponentListener( this ); + rootPane.getLayeredPane().add( this, WINDOW_RESIZER_LAYER ); + + if( rootPane.isDisplayable() ) + setBounds( 0, 0, rootPane.getWidth(), rootPane.getHeight() ); + } + + void uninstall() { + rootPane.removeComponentListener( this ); + rootPane.getLayeredPane().remove( this ); + } + + @Override + public void addNotify() { + super.addNotify(); + + Container parent = rootPane.getParent(); + window = (parent instanceof Window) ? (Window) parent : null; + if( window instanceof Frame ) + window.addPropertyChangeListener( "resizable", this ); + + updateVisibility(); + } + + @Override + public void removeNotify() { + super.removeNotify(); + + if( window instanceof Frame ) + window.removePropertyChangeListener( "resizable", this ); + window = null; + + updateVisibility(); + } + + @Override + protected void paintChildren( Graphics g ) { + super.paintChildren( g ); + + // this is necessary because Dialog.setResizable() does not fire events + if( window instanceof Dialog ) + updateVisibility(); + } + + private void updateVisibility() { + boolean visible = isWindowResizable(); + if( visible == getComponent( 0 ).isVisible() ) + return; + + for( Component c : getComponents() ) + c.setVisible( visible ); + } + + private boolean isWindowResizable() { + if( window instanceof Frame ) + return ((Frame)window).isResizable(); + if( window instanceof Dialog ) + return ((Dialog)window).isResizable(); + return false; + } + + @Override + public void propertyChange( PropertyChangeEvent e ) { + updateVisibility(); + } + + @Override + public void componentResized( ComponentEvent e ) { + setBounds( 0, 0, rootPane.getWidth(), rootPane.getHeight() ); + validate(); + } + + @Override public void componentMoved( ComponentEvent e ) {} + @Override public void componentShown( ComponentEvent e ) {} + @Override public void componentHidden( ComponentEvent e ) {} + + //---- class DragBorderComponent ------------------------------------------ + + private class DragBorderComponent + extends JComponent + implements MouseListener, MouseMotionListener + { + private final int leadingResizeDir; + private final int centerResizeDir; + private final int trailingResizeDir; + + private int resizeDir = -1; + private int dragStartMouseX; + private int dragStartMouseY; + private Rectangle dragStartWindowBounds; + + DragBorderComponent( int leadingResizeDir, int centerResizeDir, int trailingResizeDir ) { + this.leadingResizeDir = leadingResizeDir; + this.centerResizeDir = centerResizeDir; + this.trailingResizeDir = trailingResizeDir; + + setResizeDir( centerResizeDir ); + setVisible( false ); + + addMouseListener( this ); + addMouseMotionListener( this ); + } + + private void setResizeDir( int resizeDir ) { + if( this.resizeDir == resizeDir ) + return; + this.resizeDir = resizeDir; + + setCursor( getPredefinedCursor( resizeDir ) ); + } + + @Override + public Dimension getPreferredSize() { + int thickness = UIScale.scale( borderDragThickness ); + return new Dimension( thickness, thickness ); + } + +/*debug + @Override + protected void paintComponent( Graphics g ) { + g.setColor( java.awt.Color.red ); + g.drawRect( 0, 0, getWidth() - 1, getHeight() - 1 ); + } +debug*/ + + @Override + public void mouseClicked( MouseEvent e ) { + } + + @Override + public void mousePressed( MouseEvent e ) { + if( window == null ) + return; + + dragStartMouseX = e.getXOnScreen(); + dragStartMouseY = e.getYOnScreen(); + dragStartWindowBounds = window.getBounds(); + } + + @Override + public void mouseReleased( MouseEvent e ) { + dragStartWindowBounds = null; + } + + @Override + public void mouseEntered( MouseEvent e ) { + } + + @Override + public void mouseExited( MouseEvent e ) { + } + + @Override + public void mouseMoved( MouseEvent e ) { + boolean topBottom = (centerResizeDir == N_RESIZE_CURSOR || centerResizeDir == S_RESIZE_CURSOR); + int xy = topBottom ? e.getX() : e.getY(); + int wh = topBottom ? getWidth() : getHeight(); + int cornerWH = UIScale.scale( cornerDragWidth - (topBottom ? 0 : borderDragThickness) ); + + setResizeDir( xy <= cornerWH + ? leadingResizeDir + : (xy >= wh - cornerWH + ? trailingResizeDir + : centerResizeDir) ); + } + + @Override + public void mouseDragged( MouseEvent e ) { + if( dragStartWindowBounds == null ) + return; + + if( !isWindowResizable() ) + return; + + int mouseDeltaX = e.getXOnScreen() - dragStartMouseX; + int mouseDeltaY = e.getYOnScreen() - dragStartMouseY; + + int deltaX = 0; + int deltaY = 0; + int deltaWidth = 0; + int deltaHeight = 0; + + // north + if( resizeDir == N_RESIZE_CURSOR || resizeDir == NW_RESIZE_CURSOR || resizeDir == NE_RESIZE_CURSOR ) { + deltaY = mouseDeltaY; + deltaHeight = -mouseDeltaY; + } + + // south + if( resizeDir == S_RESIZE_CURSOR || resizeDir == SW_RESIZE_CURSOR || resizeDir == SE_RESIZE_CURSOR ) + deltaHeight = mouseDeltaY; + + // west + if( resizeDir == W_RESIZE_CURSOR || resizeDir == NW_RESIZE_CURSOR || resizeDir == SW_RESIZE_CURSOR ) { + deltaX = mouseDeltaX; + deltaWidth = -mouseDeltaX; + } + + // east + if( resizeDir == E_RESIZE_CURSOR || resizeDir == NE_RESIZE_CURSOR || resizeDir == SE_RESIZE_CURSOR ) + deltaWidth = mouseDeltaX; + + // compute new window bounds + Rectangle newBounds = new Rectangle( dragStartWindowBounds ); + newBounds.x += deltaX; + newBounds.y += deltaY; + newBounds.width += deltaWidth; + newBounds.height += deltaHeight; + + // apply minimum window size + Dimension minimumSize = honorMinimumSizeOnResize ? window.getMinimumSize() : null; + if( minimumSize == null ) + minimumSize = UIScale.scale( new Dimension( 150, 50 ) ); + if( newBounds.width < minimumSize.width ) { + if( deltaX != 0 ) + newBounds.x -= (minimumSize.width - newBounds.width); + newBounds.width = minimumSize.width; + } + if( newBounds.height < minimumSize.height ) { + if( deltaY != 0 ) + newBounds.y -= (minimumSize.height - newBounds.height); + newBounds.height = minimumSize.height; + } + + // set window bounds + if( !newBounds.equals( dragStartWindowBounds ) ) { + window.setBounds( newBounds ); + + // immediately layout drag border components + FlatWindowResizer.this.setBounds( 0, 0, newBounds.width, newBounds.height ); + FlatWindowResizer.this.validate(); + + if( Toolkit.getDefaultToolkit().isDynamicLayoutActive() ) { + window.validate(); + rootPane.repaint(); + } + } + } + } +} 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 9d43cd19..3c63f90d 100644 --- a/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLaf.properties +++ b/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLaf.properties @@ -426,6 +426,9 @@ RadioButtonMenuItem.background=@menuBackground #---- RootPane ---- RootPane.border=com.formdev.flatlaf.ui.FlatRootPaneUI$FlatWindowBorder +RootPane.borderDragThickness=5 +RootPane.cornerDragWidth=16 +RootPane.honorMinimumSizeOnResize=true #---- ScrollBar ---- 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 2816e732..9b7830f0 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 @@ -114,6 +114,8 @@ public class FlatWindowDecorationsTest Window window = SwingUtilities.windowForComponent( this ); if( window instanceof Frame ) ((Frame)window).setResizable( resizableCheckBox.isSelected() ); + else if( window instanceof Dialog ) + ((Dialog)window).setResizable( resizableCheckBox.isSelected() ); } private void menuItemActionPerformed(ActionEvent e) { diff --git a/flatlaf-testing/src/main/resources/com/formdev/flatlaf/testing/uidefaults/FlatDarkLaf_1.8.0_202.txt b/flatlaf-testing/src/main/resources/com/formdev/flatlaf/testing/uidefaults/FlatDarkLaf_1.8.0_202.txt index 0513ce0c..13f547e7 100644 --- a/flatlaf-testing/src/main/resources/com/formdev/flatlaf/testing/uidefaults/FlatDarkLaf_1.8.0_202.txt +++ b/flatlaf-testing/src/main/resources/com/formdev/flatlaf/testing/uidefaults/FlatDarkLaf_1.8.0_202.txt @@ -744,6 +744,8 @@ Resizable.resizeBorder [lazy] 4,4,4,4 false com.formdev.flatlaf.ui.F #---- RootPane ---- RootPane.border [lazy] 1,1,1,1 false com.formdev.flatlaf.ui.FlatRootPaneUI$FlatWindowBorder [UI] +RootPane.borderDragThickness 5 +RootPane.cornerDragWidth 16 RootPane.defaultButtonWindowKeyBindings length=8 [Ljava.lang.Object; [0] ENTER [1] press @@ -753,6 +755,7 @@ RootPane.defaultButtonWindowKeyBindings length=8 [Ljava.lang.Object; [5] press [6] ctrl released ENTER [7] release +RootPane.honorMinimumSizeOnResize true RootPaneUI com.formdev.flatlaf.ui.FlatRootPaneUI diff --git a/flatlaf-testing/src/main/resources/com/formdev/flatlaf/testing/uidefaults/FlatLightLaf_1.8.0_202.txt b/flatlaf-testing/src/main/resources/com/formdev/flatlaf/testing/uidefaults/FlatLightLaf_1.8.0_202.txt index 5f4b56d5..9ed1bf26 100644 --- a/flatlaf-testing/src/main/resources/com/formdev/flatlaf/testing/uidefaults/FlatLightLaf_1.8.0_202.txt +++ b/flatlaf-testing/src/main/resources/com/formdev/flatlaf/testing/uidefaults/FlatLightLaf_1.8.0_202.txt @@ -746,6 +746,8 @@ Resizable.resizeBorder [lazy] 4,4,4,4 false com.formdev.flatlaf.ui.F #---- RootPane ---- RootPane.border [lazy] 1,1,1,1 false com.formdev.flatlaf.ui.FlatRootPaneUI$FlatWindowBorder [UI] +RootPane.borderDragThickness 5 +RootPane.cornerDragWidth 16 RootPane.defaultButtonWindowKeyBindings length=8 [Ljava.lang.Object; [0] ENTER [1] press @@ -755,6 +757,7 @@ RootPane.defaultButtonWindowKeyBindings length=8 [Ljava.lang.Object; [5] press [6] ctrl released ENTER [7] release +RootPane.honorMinimumSizeOnResize true RootPaneUI com.formdev.flatlaf.ui.FlatRootPaneUI From 2ad0aba382b39f03864a46b96003db3d8461b13d Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Sat, 6 Jun 2020 13:53:22 +0200 Subject: [PATCH 16/33] Window decorations: enable dark window appearance on macOS when running in JetBrains Runtime (issues #47 and #82) --- .../main/java/com/formdev/flatlaf/ui/FlatRootPaneUI.java | 8 ++++++++ .../main/java/com/formdev/flatlaf/util/SystemInfo.java | 2 ++ 2 files changed, 10 insertions(+) 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 32e94c84..0e55558d 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 @@ -41,6 +41,7 @@ import javax.swing.plaf.ComponentUI; import javax.swing.plaf.UIResource; import javax.swing.plaf.basic.BasicRootPaneUI; import com.formdev.flatlaf.FlatClientProperties; +import com.formdev.flatlaf.FlatLaf; import com.formdev.flatlaf.util.HiDPIUtils; import com.formdev.flatlaf.util.SystemInfo; @@ -97,6 +98,13 @@ public class FlatRootPaneUI if( background == null || background instanceof UIResource ) parent.setBackground( UIManager.getColor( "control" ) ); } + + // enable dark window appearance on macOS when running in JetBrains Runtime + if( SystemInfo.IS_JETBRAINS_JVM && SystemInfo.IS_MAC_OS_10_14_MOJAVE ) { + LookAndFeel laf = UIManager.getLookAndFeel(); + boolean isDark = laf instanceof FlatLaf && ((FlatLaf)laf).isDark(); + c.putClientProperty( "jetbrains.awt.windowDarkAppearance", isDark ); + } } private void installClientDecorations() { diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/SystemInfo.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/SystemInfo.java index 24a54fff..d443079d 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/SystemInfo.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/SystemInfo.java @@ -34,6 +34,7 @@ public class SystemInfo // OS versions public static final boolean IS_WINDOWS_10_OR_LATER; public static final boolean IS_MAC_OS_10_11_EL_CAPITAN_OR_LATER; + public static final boolean IS_MAC_OS_10_14_MOJAVE; public static final boolean IS_MAC_OS_10_15_CATALINA_OR_LATER; // Java versions @@ -58,6 +59,7 @@ public class SystemInfo long osVersion = scanVersion( System.getProperty( "os.version" ) ); IS_WINDOWS_10_OR_LATER = (IS_WINDOWS && osVersion >= toVersion( 10, 0, 0, 0 )); IS_MAC_OS_10_11_EL_CAPITAN_OR_LATER = (IS_MAC && osVersion >= toVersion( 10, 11, 0, 0 )); + IS_MAC_OS_10_14_MOJAVE = (IS_MAC && osVersion >= toVersion( 10, 14, 0, 0 )); IS_MAC_OS_10_15_CATALINA_OR_LATER = (IS_MAC && osVersion >= toVersion( 10, 15, 0, 0 )); // Java versions From dd96712c2a6281dcb7032ed46f3932e96f591efd Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Sat, 6 Jun 2020 15:31:11 +0200 Subject: [PATCH 17/33] Menu: no longer add 1px to bottom insets of JMenu contained in JMenuBar to fix vertical alignment of JMenu text with FlatTitlePane title text on Java 9+ on HiDPI screens (due to rounding) (this extra 1px was actually not necessary) --- .../main/java/com/formdev/flatlaf/ui/FlatMenuItemBorder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatMenuItemBorder.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatMenuItemBorder.java index 9d51a5af..aa8079a6 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatMenuItemBorder.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatMenuItemBorder.java @@ -40,7 +40,7 @@ public class FlatMenuItemBorder if( c.getParent() instanceof JMenuBar ) { insets.top = scale( menuBarItemMargins.top ); insets.left = scale( menuBarItemMargins.left ); - insets.bottom = scale( menuBarItemMargins.bottom + 1 ); + insets.bottom = scale( menuBarItemMargins.bottom ); insets.right = scale( menuBarItemMargins.right ); return insets; } else From 1381a347521ebfb5a54b8326c3baaf7dc05d0a1d Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Sat, 6 Jun 2020 15:38:22 +0200 Subject: [PATCH 18/33] FlatInspector: ignore FlatWindowResizer --- .../main/java/com/formdev/flatlaf/testing/FlatInspector.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatInspector.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatInspector.java index 8b38a164..d7f4edbb 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatInspector.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatInspector.java @@ -268,6 +268,9 @@ public class FlatInspector if( c.getParent() instanceof JRootPane && c == ((JRootPane)c.getParent()).getGlassPane() ) continue; + if( "com.formdev.flatlaf.ui.FlatWindowResizer".equals( c.getClass().getName() ) ) + continue; + return c; } } From 732ca8be562cd52f24fb56a0bf30a51b38de49e2 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Sat, 6 Jun 2020 22:00:54 +0200 Subject: [PATCH 19/33] FlatLaf.isLafDark() added --- .../src/main/java/com/formdev/flatlaf/FlatLaf.java | 8 ++++++++ .../main/java/com/formdev/flatlaf/extras/FlatSVGIcon.java | 4 +--- .../formdev/flatlaf/testing/FlatDisabledIconsTest.java | 7 +------ .../java/com/formdev/flatlaf/testing/FlatTestFrame.java | 3 +-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatLaf.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatLaf.java index e535daf6..f40db140 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatLaf.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatLaf.java @@ -113,6 +113,14 @@ public abstract class FlatLaf public abstract boolean isDark(); + /** + * Checks whether the current look and feel is dark. + */ + public static boolean isLafDark() { + LookAndFeel lookAndFeel = UIManager.getLookAndFeel(); + return lookAndFeel instanceof FlatLaf && ((FlatLaf)lookAndFeel).isDark(); + } + /** * Returns whether FlatLaf supports custom window decorations. * This depends on the operating system and on the used Java runtime. diff --git a/flatlaf-extras/src/main/java/com/formdev/flatlaf/extras/FlatSVGIcon.java b/flatlaf-extras/src/main/java/com/formdev/flatlaf/extras/FlatSVGIcon.java index f343b51a..ca354603 100644 --- a/flatlaf-extras/src/main/java/com/formdev/flatlaf/extras/FlatSVGIcon.java +++ b/flatlaf-extras/src/main/java/com/formdev/flatlaf/extras/FlatSVGIcon.java @@ -29,7 +29,6 @@ import java.net.URL; import java.util.HashMap; import java.util.Map; import javax.swing.Icon; -import javax.swing.LookAndFeel; import javax.swing.UIManager; import com.formdev.flatlaf.FlatIconColors; import com.formdev.flatlaf.FlatLaf; @@ -164,8 +163,7 @@ public class FlatSVGIcon } private static void lafChanged() { - LookAndFeel lookAndFeel = UIManager.getLookAndFeel(); - darkLaf = (lookAndFeel instanceof FlatLaf && ((FlatLaf)lookAndFeel).isDark()); + darkLaf = FlatLaf.isLafDark(); } //---- class ColorFilter -------------------------------------------------- diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatDisabledIconsTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatDisabledIconsTest.java index e375530d..95e960b2 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatDisabledIconsTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatDisabledIconsTest.java @@ -598,12 +598,7 @@ public class FlatDisabledIconsTest } private ImageIcon getCurrentIcon() { - return isDark() ? darkIcon : lightIcon; - } - - private boolean isDark() { - LookAndFeel lookAndFeel = UIManager.getLookAndFeel(); - return lookAndFeel instanceof FlatLaf && ((FlatLaf)lookAndFeel).isDark(); + return FlatLaf.isLafDark() ? darkIcon : lightIcon; } @Override diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatTestFrame.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatTestFrame.java index aa9b8087..4b7407e4 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatTestFrame.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatTestFrame.java @@ -399,8 +399,7 @@ public class FlatTestFrame boolean explicit = explicitColorsCheckBox.isSelected(); ColorUIResource restoreColor = new ColorUIResource( Color.white ); - LookAndFeel lookAndFeel = UIManager.getLookAndFeel(); - boolean dark = (lookAndFeel instanceof FlatLaf && ((FlatLaf)lookAndFeel).isDark()); + boolean dark = FlatLaf.isLafDark(); Color magenta = dark ? Color.magenta.darker() : Color.magenta; Color orange = dark ? Color.orange.darker() : Color.orange; Color blue = dark ? Color.blue.darker() : Color.blue; From 97577e835e796f4d1d064687dccc20d4815feb57 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Sat, 6 Jun 2020 22:16:26 +0200 Subject: [PATCH 20/33] Window decorations: fixed top border when running in JetBrains Runtime (issues #47 and #82) --- .../formdev/flatlaf/ui/FlatRootPaneUI.java | 2 +- .../com/formdev/flatlaf/ui/FlatTitlePane.java | 13 +++ .../flatlaf/ui/JBRCustomDecorations.java | 103 ++++++++++++++++++ 3 files changed, 117 insertions(+), 1 deletion(-) 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 0e55558d..c812f993 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 @@ -109,7 +109,7 @@ public class FlatRootPaneUI private void installClientDecorations() { // install border - if( rootPane.getWindowDecorationStyle() != JRootPane.NONE ) + if( rootPane.getWindowDecorationStyle() != JRootPane.NONE && !JBRCustomDecorations.isSupported() ) LookAndFeel.installBorder( rootPane, "RootPane.border" ); else LookAndFeel.uninstallBorder( rootPane ); 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 fdf45e2d..4d8789b8 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 @@ -57,6 +57,7 @@ import javax.swing.UIManager; import javax.swing.border.AbstractBorder; import javax.swing.border.Border; import com.formdev.flatlaf.FlatClientProperties; +import com.formdev.flatlaf.ui.JBRCustomDecorations.JBRWindowTopBorder; import com.formdev.flatlaf.util.UIScale; /** @@ -481,6 +482,9 @@ class FlatTitlePane insets.bottom += menuBarInsets.bottom; } + if( hasJBRCustomDecoration() ) + insets = FlatUIUtils.addInsets( insets, JBRWindowTopBorder.getInstance().getBorderInsets() ); + return insets; } @@ -490,6 +494,9 @@ class FlatTitlePane 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 ); } private Border getMenuBarBorder() { @@ -536,12 +543,18 @@ class FlatTitlePane 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 diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/JBRCustomDecorations.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/JBRCustomDecorations.java index a19bd339..2ded0eb8 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/JBRCustomDecorations.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/JBRCustomDecorations.java @@ -16,13 +16,18 @@ package com.formdev.flatlaf.ui; +import java.awt.Color; import java.awt.Component; import java.awt.Container; import java.awt.EventQueue; +import java.awt.Graphics; +import java.awt.Graphics2D; import java.awt.Rectangle; +import java.awt.Toolkit; import java.awt.Window; import java.awt.event.HierarchyEvent; import java.awt.event.HierarchyListener; +import java.beans.PropertyChangeListener; import java.lang.reflect.Method; import java.util.List; import java.util.logging.Level; @@ -30,8 +35,11 @@ import java.util.logging.Logger; import javax.swing.JDialog; import javax.swing.JFrame; import javax.swing.JRootPane; +import javax.swing.SwingUtilities; import javax.swing.UIManager; +import javax.swing.plaf.BorderUIResource; import com.formdev.flatlaf.FlatLaf; +import com.formdev.flatlaf.util.HiDPIUtils; import com.formdev.flatlaf.util.SystemInfo; /** @@ -196,4 +204,99 @@ public class JBRCustomDecorations // ignore } } + + //---- class JBRWindowTopBorder ------------------------------------------- + + static class JBRWindowTopBorder + extends BorderUIResource.EmptyBorderUIResource + { + private static JBRWindowTopBorder instance; + + private final Color defaultActiveBorder = new Color( 0x707070 ); + private final Color inactiveLightColor = new Color( 0xaaaaaa ); + private final Color inactiveDarkColor = new Color( 0x3f3f3f ); + + private boolean colorizationAffectsBorders; + private Color activeColor = defaultActiveBorder; + + static JBRWindowTopBorder getInstance() { + if( instance == null ) + instance = new JBRWindowTopBorder(); + return instance; + } + + private JBRWindowTopBorder() { + super( 1, 0, 0, 0 ); + + colorizationAffectsBorders = calculateAffectsBorders(); + activeColor = calculateActiveBorderColor(); + + Toolkit toolkit = Toolkit.getDefaultToolkit(); + toolkit.addPropertyChangeListener( "win.dwm.colorizationColor.affects.borders", e -> { + colorizationAffectsBorders = calculateAffectsBorders(); + activeColor = calculateActiveBorderColor(); + } ); + + PropertyChangeListener l = e -> { + activeColor = calculateActiveBorderColor(); + }; + toolkit.addPropertyChangeListener( "win.dwm.colorizationColor", l ); + toolkit.addPropertyChangeListener( "win.dwm.colorizationColorBalance", l ); + toolkit.addPropertyChangeListener( "win.frame.activeBorderColor", l ); + } + + private boolean calculateAffectsBorders() { + Object value = Toolkit.getDefaultToolkit().getDesktopProperty( "win.dwm.colorizationColor.affects.borders" ); + return (value instanceof Boolean) ? (Boolean) value : true; + } + + private Color calculateActiveBorderColor() { + if( !colorizationAffectsBorders ) + return defaultActiveBorder; + + Toolkit toolkit = Toolkit.getDefaultToolkit(); + Color colorizationColor = (Color) toolkit.getDesktopProperty( "win.dwm.colorizationColor" ); + if( colorizationColor != null ) { + Object colorizationColorBalanceObj = toolkit.getDesktopProperty( "win.dwm.colorizationColorBalance" ); + if( colorizationColorBalanceObj instanceof Integer ) { + int colorizationColorBalance = (Integer) colorizationColorBalanceObj; + if( colorizationColorBalance < 0 ) + colorizationColorBalance = 100; + + if( colorizationColorBalance == 0 ) + return new Color( 0xD9D9D9 ); + if( colorizationColorBalance == 100 ) + return colorizationColor; + + float alpha = colorizationColorBalance / 100.0f; + float remainder = 1 - alpha; + int r = Math.round( (colorizationColor.getRed() * alpha + 0xD9 * remainder) ); + int g = Math.round( (colorizationColor.getGreen() * alpha + 0xD9 * remainder) ); + int b = Math.round( (colorizationColor.getBlue() * alpha + 0xD9 * remainder) ); + return new Color( r, g, b ); + } + return colorizationColor; + } + + Color activeBorderColor = (Color) toolkit.getDesktopProperty( "win.frame.activeBorderColor" ); + return (activeBorderColor != null) ? activeBorderColor : UIManager.getColor( "MenuBar.borderColor" ); + } + + @Override + public void paintBorder( Component c, Graphics g, int x, int y, int width, int height ) { + Window window = SwingUtilities.windowForComponent( c ); + boolean active = (window != null) ? window.isActive() : false; + + g.setColor( active ? activeColor : (FlatLaf.isLafDark() ? inactiveDarkColor : inactiveLightColor) ); + HiDPIUtils.paintAtScale1x( (Graphics2D) g, x, y, width, height, this::paintImpl ); + } + + private void paintImpl( Graphics2D g, int x, int y, int width, int height, double scaleFactor ) { + g.drawRect( x, y, width - 1, 0 ); + } + + void repaintBorder( Component c ) { + c.repaint( 0, 0, c.getWidth(), 1 ); + } + } } From 5e5b9f0990c62259c353065d2e42e73058062303 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Sun, 7 Jun 2020 11:03:22 +0200 Subject: [PATCH 21/33] Window decorations: fixed maximized bounds on Java 15 (issues #47 and #82) --- .../com/formdev/flatlaf/ui/FlatTitlePane.java | 51 ++++++++++++------- .../com/formdev/flatlaf/util/SystemInfo.java | 2 + 2 files changed, 36 insertions(+), 17 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 4d8789b8..fd8ab46b 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 @@ -58,6 +58,7 @@ import javax.swing.border.AbstractBorder; import javax.swing.border.Border; import com.formdev.flatlaf.FlatClientProperties; import com.formdev.flatlaf.ui.JBRCustomDecorations.JBRWindowTopBorder; +import com.formdev.flatlaf.util.SystemInfo; import com.formdev.flatlaf.util.UIScale; /** @@ -371,32 +372,48 @@ class FlatTitlePane // remember current maximized bounds Rectangle oldMaximizedBounds = frame.getMaximizedBounds(); - // Scaled screen size, which may be smaller than physical size on Java 9+. - // E.g. if running a 3840x2160 screen at 200%, scaledSreenSize is 1920x1080. + // 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, scaledSreenSize of the primary screen + // 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 scaledSreenSize of secondary screens is scaled with the scale factor + // 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 scaledSreenSize is 3840x2160 on primary and 2880x1560 on secondary. - Dimension scaledSreenSize = gc.getBounds().getSize(); + // then screenBounds.size is 3840x2160 on primary and 2880x1560 on secondary. + Rectangle screenBounds = gc.getBounds(); - // scale screen bounds to get physical screen size - AffineTransform defaultTransform = gc.getDefaultTransform(); - int screenWidth = (int) (scaledSreenSize.width * defaultTransform.getScaleX()); - int screenHeight = (int) (scaledSreenSize.height * defaultTransform.getScaleY()); + int maximizedX = screenBounds.x; + int maximizedY = screenBounds.y; + int maximizedWidth = screenBounds.width; + int maximizedHeight = screenBounds.height; - // screen insets are in physical size, - // except for Java 8 on secondary screens where primary screen is scaled + 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 8 on secondary screens where primary screen is scaled - Rectangle maximizedBounds = new Rectangle( screenInsets.left, screenInsets.top, - screenWidth - screenInsets.left - screenInsets.right, - screenHeight - screenInsets.top - screenInsets.bottom ); + // 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 ); // temporary change maximized bounds frame.setMaximizedBounds( maximizedBounds ); diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/SystemInfo.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/SystemInfo.java index d443079d..0ae20747 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/SystemInfo.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/SystemInfo.java @@ -40,6 +40,7 @@ public class SystemInfo // Java versions public static final boolean IS_JAVA_9_OR_LATER; public static final boolean IS_JAVA_11_OR_LATER; + public static final boolean IS_JAVA_15_OR_LATER; // Java VMs public static final boolean IS_JETBRAINS_JVM; @@ -66,6 +67,7 @@ public class SystemInfo long javaVersion = scanVersion( System.getProperty( "java.version" ) ); IS_JAVA_9_OR_LATER = (javaVersion >= toVersion( 9, 0, 0, 0 )); IS_JAVA_11_OR_LATER = (javaVersion >= toVersion( 11, 0, 0, 0 )); + IS_JAVA_15_OR_LATER = (javaVersion >= toVersion( 15, 0, 0, 0 )); // Java VMs IS_JETBRAINS_JVM = System.getProperty( "java.vm.vendor", "Unknown" ) From d081b9e182177eee0489ddd421c77cbcde055014 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Sun, 7 Jun 2020 11:27:29 +0200 Subject: [PATCH 22/33] Window decorations: do not restore maximized bounds in method maximize() because when restoring an iconified frame by clicking on the Windows 10 taskbar the maximize() method is not invoked and the frame size becomes full screen size and overlaps taskbar --- .../com/formdev/flatlaf/ui/FlatTitlePane.java | 20 +++++-------------- 1 file changed, 5 insertions(+), 15 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 fd8ab46b..1addf9d6 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 @@ -366,12 +366,10 @@ class FlatTitlePane Frame frame = (Frame) window; + // set maximized bounds to avoid that maximized window overlaps Windows task bar if( !hasJBRCustomDecoration() ) { GraphicsConfiguration gc = window.getGraphicsConfiguration(); - // remember current maximized bounds - Rectangle oldMaximizedBounds = frame.getMaximizedBounds(); - // 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. @@ -415,20 +413,12 @@ class FlatTitlePane maximizedWidth - screenInsets.left - screenInsets.right, maximizedHeight - screenInsets.top - screenInsets.bottom ); - // temporary change maximized bounds + // change maximized bounds frame.setMaximizedBounds( maximizedBounds ); - - // maximize window - frame.setExtendedState( frame.getExtendedState() | Frame.MAXIMIZED_BOTH ); - - // restore old maximized bounds - frame.setMaximizedBounds( oldMaximizedBounds ); - } else { - // not necessary to set maximized bounds when running in JBR - - // maximize window - frame.setExtendedState( frame.getExtendedState() | Frame.MAXIMIZED_BOTH ); } + + // maximize window + frame.setExtendedState( frame.getExtendedState() | Frame.MAXIMIZED_BOTH ); } private void restore() { From ff55cc1a2af2a1c884048a98df1944adae82561c Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Sun, 7 Jun 2020 11:57:05 +0200 Subject: [PATCH 23/33] Window decorations: do not overwrite maximized bounds if controlled from the application --- .../com/formdev/flatlaf/ui/FlatTitlePane.java | 11 ++++++++++- .../testing/FlatWindowDecorationsTest.java | 18 ++++++++++++++++++ .../testing/FlatWindowDecorationsTest.jfd | 12 +++++++++++- 3 files changed, 39 insertions(+), 2 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 1addf9d6..399d1949 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 @@ -43,6 +43,7 @@ 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; @@ -367,7 +368,11 @@ class FlatTitlePane Frame frame = (Frame) window; // set maximized bounds to avoid that maximized window overlaps Windows task bar - if( !hasJBRCustomDecoration() ) { + // (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+. @@ -415,6 +420,10 @@ class FlatTitlePane // 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 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 9b7830f0..aab62652 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 @@ -73,6 +73,8 @@ public class FlatWindowDecorationsTest Window window = SwingUtilities.windowForComponent( this ); menuBarCheckBox.setEnabled( window instanceof JFrame ); + menuBarEmbeddedCheckBox.setEnabled( window instanceof JFrame ); + maximizedBoundsCheckBox.setEnabled( window instanceof Frame ); boolean windowHasIcons = (window != null && !window.getIconImages().isEmpty()); iconNoneRadioButton.setEnabled( windowHasIcons ); @@ -118,6 +120,15 @@ public class FlatWindowDecorationsTest ((Dialog)window).setResizable( resizableCheckBox.isSelected() ); } + private void maximizedBoundsChanged() { + Window window = SwingUtilities.windowForComponent( this ); + if( window instanceof Frame ) { + ((Frame)window).setMaximizedBounds( maximizedBoundsCheckBox.isSelected() + ? new Rectangle( 50, 100, 1000, 700 ) + : null ); + } + } + private void menuItemActionPerformed(ActionEvent e) { SwingUtilities.invokeLater( () -> { JOptionPane.showMessageDialog( this, e.getActionCommand(), "Menu Item", JOptionPane.PLAIN_MESSAGE ); @@ -189,6 +200,7 @@ public class FlatWindowDecorationsTest menuBarCheckBox = new JCheckBox(); menuBarEmbeddedCheckBox = new JCheckBox(); resizableCheckBox = new JCheckBox(); + maximizedBoundsCheckBox = new JCheckBox(); JLabel label1 = new JLabel(); JLabel label2 = new JLabel(); JPanel panel1 = new JPanel(); @@ -265,6 +277,11 @@ public class FlatWindowDecorationsTest resizableCheckBox.addActionListener(e -> resizableChanged()); add(resizableCheckBox, "cell 0 2"); + //---- maximizedBoundsCheckBox ---- + maximizedBoundsCheckBox.setText("maximized bounds (50,100, 1000,700)"); + maximizedBoundsCheckBox.addActionListener(e -> maximizedBoundsChanged()); + add(maximizedBoundsCheckBox, "cell 1 2"); + //---- label1 ---- label1.setText("Style:"); add(label1, "cell 0 3"); @@ -564,6 +581,7 @@ public class FlatWindowDecorationsTest private JCheckBox menuBarCheckBox; private JCheckBox menuBarEmbeddedCheckBox; private JCheckBox resizableCheckBox; + private JCheckBox maximizedBoundsCheckBox; private JRadioButton styleNoneRadioButton; private JRadioButton styleFrameRadioButton; private JRadioButton stylePlainRadioButton; 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 5675caf2..436718fe 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 @@ -1,4 +1,4 @@ -JFDML JFormDesigner: "7.0.1.0.272" Java: "13.0.2" encoding: "UTF-8" +JFDML JFormDesigner: "7.0.2.0.298" Java: "13.0.2" encoding: "UTF-8" new FormModel { contentType: "form/swing" @@ -45,6 +45,16 @@ new FormModel { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 2" } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "maximizedBoundsCheckBox" + "text": "maximized bounds (50,100, 1000,700)" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "maximizedBoundsChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 1 2" + } ) add( new FormComponent( "javax.swing.JLabel" ) { name: "label1" "text": "Style:" From 0880a3380c841409e244ff8afe867f510410eb04 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Sun, 7 Jun 2020 23:22:57 +0200 Subject: [PATCH 24/33] Window decorations: hide drag border components if frame is maximized --- .../formdev/flatlaf/ui/FlatWindowResizer.java | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatWindowResizer.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatWindowResizer.java index 3a5c8e65..969f27e5 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatWindowResizer.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatWindowResizer.java @@ -32,6 +32,8 @@ import java.awt.event.ComponentListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; +import java.awt.event.WindowEvent; +import java.awt.event.WindowStateListener; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import javax.swing.JComponent; @@ -47,7 +49,7 @@ import com.formdev.flatlaf.util.UIScale; */ class FlatWindowResizer extends JComponent - implements PropertyChangeListener, ComponentListener + implements PropertyChangeListener, WindowStateListener, ComponentListener { private final static Integer WINDOW_RESIZER_LAYER = JLayeredPane.DRAG_LAYER + 1; @@ -86,8 +88,10 @@ class FlatWindowResizer Container parent = rootPane.getParent(); window = (parent instanceof Window) ? (Window) parent : null; - if( window instanceof Frame ) + if( window instanceof Frame ) { window.addPropertyChangeListener( "resizable", this ); + window.addWindowStateListener( this ); + } updateVisibility(); } @@ -96,8 +100,10 @@ class FlatWindowResizer public void removeNotify() { super.removeNotify(); - if( window instanceof Frame ) + if( window instanceof Frame ) { window.removePropertyChangeListener( "resizable", this ); + window.removeWindowStateListener( this ); + } window = null; updateVisibility(); @@ -123,7 +129,7 @@ class FlatWindowResizer private boolean isWindowResizable() { if( window instanceof Frame ) - return ((Frame)window).isResizable(); + return ((Frame)window).isResizable() && (((Frame)window).getExtendedState() & Frame.MAXIMIZED_BOTH) == 0; if( window instanceof Dialog ) return ((Dialog)window).isResizable(); return false; @@ -134,6 +140,11 @@ class FlatWindowResizer updateVisibility(); } + @Override + public void windowStateChanged( WindowEvent e ) { + updateVisibility(); + } + @Override public void componentResized( ComponentEvent e ) { setBounds( 0, 0, rootPane.getWidth(), rootPane.getHeight() ); From 293b76f04b172ffb839ac7570cbcdb1b2c8333e5 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Thu, 25 Jun 2020 17:55:42 +0200 Subject: [PATCH 25/33] Window decorations: FlatWindowDecorationsTest: added "undecorated" checkbox --- .../testing/FlatWindowDecorationsTest.java | 38 ++++++++++++++++--- .../testing/FlatWindowDecorationsTest.jfd | 22 ++++++++--- 2 files changed, 49 insertions(+), 11 deletions(-) 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 aab62652..13b77661 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 @@ -81,6 +81,11 @@ public class FlatWindowDecorationsTest iconTestAllRadioButton.setEnabled( windowHasIcons ); iconTestRandomRadioButton.setEnabled( windowHasIcons ); + if( window instanceof Frame ) + undecoratedCheckBox.setSelected( ((Frame)window).isUndecorated() ); + else if( window instanceof Dialog ) + undecoratedCheckBox.setSelected( ((Dialog)window).isUndecorated() ); + JRootPane rootPane = getWindowRootPane(); if( rootPane != null ) { int style = rootPane.getWindowDecorationStyle(); @@ -120,6 +125,21 @@ public class FlatWindowDecorationsTest ((Dialog)window).setResizable( resizableCheckBox.isSelected() ); } + private void undecoratedChanged() { + Window window = SwingUtilities.windowForComponent( this ); + if( window == null ) + return; + + window.dispose(); + + if( window instanceof Frame ) + ((Frame)window).setUndecorated( undecoratedCheckBox.isSelected() ); + else if( window instanceof Dialog ) + ((Dialog)window).setUndecorated( undecoratedCheckBox.isSelected() ); + + window.setVisible( true ); + } + private void maximizedBoundsChanged() { Window window = SwingUtilities.windowForComponent( this ); if( window instanceof Frame ) { @@ -201,6 +221,7 @@ public class FlatWindowDecorationsTest menuBarEmbeddedCheckBox = new JCheckBox(); resizableCheckBox = new JCheckBox(); maximizedBoundsCheckBox = new JCheckBox(); + undecoratedCheckBox = new JCheckBox(); JLabel label1 = new JLabel(); JLabel label2 = new JLabel(); JPanel panel1 = new JPanel(); @@ -254,6 +275,7 @@ public class FlatWindowDecorationsTest // rows "para[]0" + "[]0" + + "[]0" + "[]" + "[]" + "[top]" + @@ -282,13 +304,18 @@ public class FlatWindowDecorationsTest maximizedBoundsCheckBox.addActionListener(e -> maximizedBoundsChanged()); add(maximizedBoundsCheckBox, "cell 1 2"); + //---- undecoratedCheckBox ---- + undecoratedCheckBox.setText("undecorated"); + undecoratedCheckBox.addActionListener(e -> undecoratedChanged()); + add(undecoratedCheckBox, "cell 0 3"); + //---- label1 ---- label1.setText("Style:"); - add(label1, "cell 0 3"); + add(label1, "cell 0 4"); //---- label2 ---- label2.setText("Icon:"); - add(label2, "cell 1 3"); + add(label2, "cell 1 4"); //======== panel1 ======== { @@ -353,7 +380,7 @@ public class FlatWindowDecorationsTest styleFileChooserRadioButton.addActionListener(e -> decorationStyleChanged()); panel1.add(styleFileChooserRadioButton, "cell 0 8"); } - add(panel1, "cell 0 4"); + add(panel1, "cell 0 5"); //======== panel2 ======== { @@ -382,12 +409,12 @@ public class FlatWindowDecorationsTest iconTestRandomRadioButton.addActionListener(e -> iconChanged()); panel2.add(iconTestRandomRadioButton, "cell 0 2"); } - add(panel2, "cell 1 4"); + add(panel2, "cell 1 5"); //---- openDialogButton ---- openDialogButton.setText("Open Dialog"); openDialogButton.addActionListener(e -> openDialog()); - add(openDialogButton, "cell 0 5"); + add(openDialogButton, "cell 0 6"); //======== menuBar ======== { @@ -582,6 +609,7 @@ public class FlatWindowDecorationsTest private JCheckBox menuBarEmbeddedCheckBox; private JCheckBox resizableCheckBox; private JCheckBox maximizedBoundsCheckBox; + private JCheckBox undecoratedCheckBox; private JRadioButton styleNoneRadioButton; private JRadioButton styleFrameRadioButton; private JRadioButton stylePlainRadioButton; 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 436718fe..e5021fd8 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]para[fill]" - "$rowConstraints": "para[]0[]0[][][top][]" + "$rowConstraints": "para[]0[]0[]0[][][top][]" } ) { name: "this" add( new FormComponent( "javax.swing.JCheckBox" ) { @@ -55,17 +55,27 @@ new FormModel { }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 1 2" } ) + add( new FormComponent( "javax.swing.JCheckBox" ) { + name: "undecoratedCheckBox" + "text": "undecorated" + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "undecoratedChanged", false ) ) + }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { + "value": "cell 0 3" + } ) add( new FormComponent( "javax.swing.JLabel" ) { name: "label1" "text": "Style:" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 3" + "value": "cell 0 4" } ) add( new FormComponent( "javax.swing.JLabel" ) { name: "label2" "text": "Icon:" }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 3" + "value": "cell 1 4" } ) add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { "$columnConstraints": "[fill]" @@ -174,7 +184,7 @@ new FormModel { "value": "cell 0 8" } ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 4" + "value": "cell 0 5" } ) add( new FormContainer( "javax.swing.JPanel", new FormLayoutManager( class net.miginfocom.swing.MigLayout ) { "$columnConstraints": "[fill]" @@ -217,14 +227,14 @@ new FormModel { "value": "cell 0 2" } ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 1 4" + "value": "cell 1 5" } ) add( new FormComponent( "javax.swing.JButton" ) { name: "openDialogButton" "text": "Open Dialog" addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "openDialog", false ) ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { - "value": "cell 0 5" + "value": "cell 0 6" } ) }, new FormLayoutConstraints( null ) { "location": new java.awt.Point( 0, 0 ) From 7720d4258427a271cefc6161d0ce8fadbe3e58aa Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Fri, 26 Jun 2020 00:22:28 +0200 Subject: [PATCH 26/33] Window decorations: reworked/fixed initialization when running in JetBrains Runtime --- .../formdev/flatlaf/ui/FlatRootPaneUI.java | 22 +++- .../com/formdev/flatlaf/ui/FlatTitlePane.java | 4 +- .../flatlaf/ui/JBRCustomDecorations.java | 105 +++++++++--------- 3 files changed, 77 insertions(+), 54 deletions(-) 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 c812f993..db5fa9c5 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 @@ -48,11 +48,25 @@ import com.formdev.flatlaf.util.SystemInfo; /** * Provides the Flat LaF UI delegate for {@link javax.swing.JRootPane}. * + * + * + * @uiDefault RootPane.border Border + * + * + * + * @uiDefault RootPane.borderDragThickness int + * @uiDefault RootPane.cornerDragWidth int + * @uiDefault RootPane.honorMinimumSizeOnResize boolean + * * @author Karl Tauber */ public class FlatRootPaneUI extends BasicRootPaneUI { + // check this field before using class JBRCustomDecorations to avoid unnecessary loading of that class + static final boolean canUseJBRCustomDecorations + = SystemInfo.IS_JETBRAINS_JVM_11_OR_LATER && SystemInfo.IS_WINDOWS_10_OR_LATER; + private JRootPane rootPane; private FlatTitlePane titlePane; private LayoutManager oldLayout; @@ -71,7 +85,7 @@ public class FlatRootPaneUI if( rootPane.getWindowDecorationStyle() != JRootPane.NONE ) installClientDecorations(); - if( SystemInfo.IS_JETBRAINS_JVM_11_OR_LATER && SystemInfo.IS_WINDOWS_10_OR_LATER ) + if( canUseJBRCustomDecorations ) JBRCustomDecorations.install( rootPane ); } @@ -108,8 +122,10 @@ public class FlatRootPaneUI } private void installClientDecorations() { + boolean isJBRSupported = canUseJBRCustomDecorations && JBRCustomDecorations.isSupported(); + // install border - if( rootPane.getWindowDecorationStyle() != JRootPane.NONE && !JBRCustomDecorations.isSupported() ) + if( rootPane.getWindowDecorationStyle() != JRootPane.NONE && !isJBRSupported ) LookAndFeel.installBorder( rootPane, "RootPane.border" ); else LookAndFeel.uninstallBorder( rootPane ); @@ -122,7 +138,7 @@ public class FlatRootPaneUI rootPane.setLayout( new FlatRootLayout() ); // install window resizer - if( !JBRCustomDecorations.isSupported() ) + if( !isJBRSupported ) windowResizer = new FlatWindowResizer( rootPane ); } 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 399d1949..793027cd 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 @@ -446,7 +446,9 @@ class FlatTitlePane } private boolean hasJBRCustomDecoration() { - return window != null && JBRCustomDecorations.hasCustomDecoration( window ); + return window != null && + FlatRootPaneUI.canUseJBRCustomDecorations && + JBRCustomDecorations.hasCustomDecoration( window ); } private void updateJBRHitTestSpotsAndTitleBarHeight() { diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/JBRCustomDecorations.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/JBRCustomDecorations.java index 2ded0eb8..d06069f8 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/JBRCustomDecorations.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/JBRCustomDecorations.java @@ -68,65 +68,26 @@ public class JBRCustomDecorations } static void install( JRootPane rootPane ) { - boolean frameIsDefaultLookAndFeelDecorated = JFrame.isDefaultLookAndFeelDecorated(); - boolean dialogIsDefaultLookAndFeelDecorated = JDialog.isDefaultLookAndFeelDecorated(); - boolean lafSupportsWindowDecorations = UIManager.getLookAndFeel().getSupportsWindowDecorations(); - - // check whether decorations are enabled - if( !frameIsDefaultLookAndFeelDecorated && !dialogIsDefaultLookAndFeelDecorated ) - return; - - // do not enable JBR decorations if JFrame and JDialog will use LaF decorations - if( lafSupportsWindowDecorations && - frameIsDefaultLookAndFeelDecorated && - dialogIsDefaultLookAndFeelDecorated ) - return; - if( !isSupported() ) return; - // use hierarchy listener to wait until the root pane is added to a window + // check whether root pane already has a parent, which is the case when switching LaF + if( rootPane.getParent() != null ) + return; + + // Use hierarchy listener to wait until the root pane is added to a window. + // Enabling JBR decorations must be done very early, probably before + // window becomes displayable (window.isDisplayable()). Tried also using + // "ancestor" property change event on root pane, but this is invoked too late. HierarchyListener addListener = new HierarchyListener() { @Override public void hierarchyChanged( HierarchyEvent e ) { - if( (e.getChangeFlags() & HierarchyEvent.PARENT_CHANGED) == 0 ) + if( e.getChanged() != rootPane || (e.getChangeFlags() & HierarchyEvent.PARENT_CHANGED) == 0 ) return; Container parent = e.getChangedParent(); - if( parent instanceof JFrame ) { - JFrame frame = (JFrame) parent; - - // do not enable JBR decorations if JFrame will use LaF decorations - if( lafSupportsWindowDecorations && frameIsDefaultLookAndFeelDecorated ) - return; - - // do not enable JBR decorations if frame is undecorated - if( frame.isUndecorated() ) - return; - - // enable JBR custom window decoration for window - setHasCustomDecoration( frame ); - - // enable Swing window decoration - rootPane.setWindowDecorationStyle( JRootPane.FRAME ); - - } else if( parent instanceof JDialog ) { - JDialog dialog = (JDialog)parent; - - // do not enable JBR decorations if JDialog will use LaF decorations - if( lafSupportsWindowDecorations && dialogIsDefaultLookAndFeelDecorated ) - return; - - // do not enable JBR decorations if dialog is undecorated - if( dialog.isUndecorated() ) - return; - - // enable JBR custom window decoration for window - setHasCustomDecoration( dialog ); - - // enable Swing window decoration - rootPane.setWindowDecorationStyle( JRootPane.PLAIN_DIALOG ); - } + if( parent instanceof Window ) + install( (Window) parent ); // use invokeLater to remove listener to avoid that listener // is removed while listener queue is processed @@ -138,6 +99,50 @@ public class JBRCustomDecorations rootPane.addHierarchyListener( addListener ); } + static void install( Window window ) { + if( !isSupported() ) + return; + + // do not enable JBR decorations if LaF provides decorations + if( UIManager.getLookAndFeel().getSupportsWindowDecorations() ) + return; + + if( window instanceof JFrame ) { + JFrame frame = (JFrame) window; + + // do not enable JBR decorations if JFrame should use system window decorations + if( !JFrame.isDefaultLookAndFeelDecorated() ) + return; + + // do not enable JBR decorations if frame is undecorated + if( frame.isUndecorated() ) + return; + + // enable JBR custom window decoration for window + setHasCustomDecoration( frame ); + + // enable Swing window decoration + frame.getRootPane().setWindowDecorationStyle( JRootPane.FRAME ); + + } else if( window instanceof JDialog ) { + JDialog dialog = (JDialog) window; + + // do not enable JBR decorations if JDialog should use system window decorations + if( !JDialog.isDefaultLookAndFeelDecorated() ) + return; + + // do not enable JBR decorations if dialog is undecorated + if( dialog.isUndecorated() ) + return; + + // enable JBR custom window decoration for window + setHasCustomDecoration( dialog ); + + // enable Swing window decoration + dialog.getRootPane().setWindowDecorationStyle( JRootPane.PLAIN_DIALOG ); + } + } + static boolean hasCustomDecoration( Window window ) { if( !isSupported() ) return false; From 7e8aaffb92563f5c9d1aa9cdc54c8ccb0d0f54db Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Fri, 26 Jun 2020 10:49:49 +0200 Subject: [PATCH 27/33] Window decorations: - double-click on icon closes window - after switching LaF is was not possible to move window when running in JetBrains Runtime --- .../com/formdev/flatlaf/ui/FlatTitlePane.java | 58 +++++++++++-------- 1 file changed, 34 insertions(+), 24 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 793027cd..502f3af5 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 @@ -122,6 +122,9 @@ class FlatTitlePane addMouseListener( handler ); addMouseMotionListener( handler ); + + // necessary for closing window with double-click on icon + iconLabel.addMouseListener( handler ); } private void addSubComponents() { @@ -258,6 +261,8 @@ class FlatTitlePane if( hasImages ) iconLabel.setIcon( FlatTitlePaneIcon.create( images, iconSize ) ); + + updateJBRHitTestSpotsAndTitleBarHeightLater(); } @Override @@ -275,7 +280,7 @@ class FlatTitlePane installWindowListeners(); } - updateJBRHitTestSpotsAndTitleBarHeight(); + updateJBRHitTestSpotsAndTitleBarHeightLater(); } @Override @@ -446,11 +451,17 @@ class FlatTitlePane } private boolean hasJBRCustomDecoration() { - return window != null && - FlatRootPaneUI.canUseJBRCustomDecorations && + return FlatRootPaneUI.canUseJBRCustomDecorations && + window != null && JBRCustomDecorations.hasCustomDecoration( window ); } + private void updateJBRHitTestSpotsAndTitleBarHeightLater() { + EventQueue.invokeLater( () -> { + updateJBRHitTestSpotsAndTitleBarHeight(); + } ); + } + private void updateJBRHitTestSpotsAndTitleBarHeight() { if( !isDisplayable() ) return; @@ -459,8 +470,10 @@ class FlatTitlePane return; List hitTestSpots = new ArrayList<>(); + if( iconLabel.isVisible() ) + addJBRHitTestSpot( iconLabel, false, hitTestSpots ); addJBRHitTestSpot( buttonPanel, false, hitTestSpots ); - addJBRHitTestSpot( menuBarPlaceholder, true, hitTestSpots );//TOOD + addJBRHitTestSpot( menuBarPlaceholder, true, hitTestSpots ); int titleBarHeight = getHeight(); // slightly reduce height so that component receives mouseExit events @@ -548,9 +561,7 @@ class FlatTitlePane break; case "componentOrientation": - EventQueue.invokeLater( () -> { - updateJBRHitTestSpotsAndTitleBarHeight(); - } ); + updateJBRHitTestSpotsAndTitleBarHeightLater(); break; } } @@ -588,20 +599,21 @@ class FlatTitlePane @Override public void mouseClicked( MouseEvent e ) { - if( hasJBRCustomDecoration() ) - return; // do nothing if running in JBR - - if( e.getClickCount() == 2 && - SwingUtilities.isLeftMouseButton( e ) && - 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(); + 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(); + } } } @@ -667,9 +679,7 @@ class FlatTitlePane @Override public void componentResized( ComponentEvent e ) { - EventQueue.invokeLater( () -> { - updateJBRHitTestSpotsAndTitleBarHeight(); - } ); + updateJBRHitTestSpotsAndTitleBarHeightLater(); } @Override public void componentMoved( ComponentEvent e ) {} From 8b4786ad1865c5b756c0df6bcfb84fa4c300094b Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Sat, 27 Jun 2020 17:57:59 +0200 Subject: [PATCH 28/33] added class FlatSystemProperties to define/document own system properties used in FlatLaf --- .../formdev/flatlaf/FlatSystemProperties.java | 66 +++++++++++++++++++ .../com/formdev/flatlaf/LinuxFontPolicy.java | 2 +- .../com/formdev/flatlaf/util/HiDPIUtils.java | 3 +- .../com/formdev/flatlaf/util/UIScale.java | 6 +- .../testing/FlatPaintingStringTest.java | 3 +- .../flatlaf/testing/FlatTestFrame.java | 11 ++-- .../testing/uidefaults/UIDefaultsDump.java | 2 +- 7 files changed, 81 insertions(+), 12 deletions(-) create mode 100644 flatlaf-core/src/main/java/com/formdev/flatlaf/FlatSystemProperties.java diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatSystemProperties.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatSystemProperties.java new file mode 100644 index 00000000..b6cc16e0 --- /dev/null +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatSystemProperties.java @@ -0,0 +1,66 @@ +/* + * 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; + +/** + * Defines/documents own system properties used in FlatLaf. + * + * @author Karl Tauber + */ +public interface FlatSystemProperties +{ + /** + * Specifies a custom scale factor used to scale the UI. + *

+ * If Java runtime scales (Java 9 or later), this scale factor is applied on top + * of the Java system scale factor. Java 8 does not scale and this scale factor + * replaces the user scale factor that FlatLaf computes based on the font. + * To replace the Java 9+ system scale factor, use system property "sun.java2d.uiScale", + * which has the same syntax as this one. + *

+ * Allowed Values e.g. {@code 1.5}, {@code 1.5x}, {@code 150%} or {@code 144dpi} (96dpi is 100%)
+ */ + String UI_SCALE = "flatlaf.uiScale"; + + /** + * Specifies whether Ubuntu font should be used on Ubuntu Linux. + * By default, if not running in a JetBrains Runtime, the Liberation Sans font + * is used because there are rendering issues (in Java) with Ubuntu fonts. + *

+ * Allowed Values {@code false} and {@code true}
+ * Default {@code false} + */ + String USE_UBUNTU_FONT = "flatlaf.useUbuntuFont"; + + /** + * Specifies whether vertical text position is corrected when UI is scaled on HiDPI screens. + *

+ * Allowed Values {@code false} and {@code true}
+ * Default {@code true} + */ + String USE_TEXT_Y_CORRECTION = "flatlaf.useTextYCorrection"; + + /** + * Checks whether a system property is set and returns {@code true} if its value + * is {@code "true"} (case-insensitive), otherwise it returns {@code false}. + * If the system property is not set, {@code defaultValue} is returned. + */ + static boolean getBoolean( String key, boolean defaultValue ) { + String value = System.getProperty( key ); + return (value != null) ? Boolean.parseBoolean( value ) : defaultValue; + } +} diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/LinuxFontPolicy.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/LinuxFontPolicy.java index 1ba7e683..8642b4ff 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/LinuxFontPolicy.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/LinuxFontPolicy.java @@ -78,7 +78,7 @@ class LinuxFontPolicy // --> use Liberation Sans font if( family.startsWith( "Ubuntu" ) && !SystemInfo.IS_JETBRAINS_JVM && - !Boolean.parseBoolean( System.getProperty( "flatlaf.useUbuntuFont" ) ) ) + !FlatSystemProperties.getBoolean( FlatSystemProperties.USE_UBUNTU_FONT, false ) ) family = "Liberation Sans"; // scale font size diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/HiDPIUtils.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/HiDPIUtils.java index 93b417a6..12b2f441 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/HiDPIUtils.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/HiDPIUtils.java @@ -23,6 +23,7 @@ import java.awt.geom.AffineTransform; import java.awt.geom.Rectangle2D; import java.text.AttributedCharacterIterator; import javax.swing.JComponent; +import com.formdev.flatlaf.FlatSystemProperties; /** * @author Karl Tauber @@ -104,7 +105,7 @@ public class HiDPIUtils private static boolean useTextYCorrection() { if( useTextYCorrection == null ) - useTextYCorrection = Boolean.valueOf( System.getProperty( "flatlaf.useTextYCorrection", "true" ) ); + useTextYCorrection = FlatSystemProperties.getBoolean( FlatSystemProperties.USE_TEXT_Y_CORRECTION, true ); return useTextYCorrection; } diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/UIScale.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/UIScale.java index 2338f25e..b1ded6bd 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/UIScale.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/UIScale.java @@ -32,6 +32,7 @@ import javax.swing.plaf.DimensionUIResource; import javax.swing.plaf.FontUIResource; import javax.swing.plaf.InsetsUIResource; import javax.swing.plaf.UIResource; +import com.formdev.flatlaf.FlatSystemProperties; /** * Two scaling modes are supported for HiDPI displays: @@ -195,8 +196,7 @@ public class UIScale private static boolean isUserScalingEnabled() { // same as in IntelliJ IDEA - String hidpi = System.getProperty( "hidpi" ); - return (hidpi != null) ? Boolean.parseBoolean( hidpi ) : true; + return FlatSystemProperties.getBoolean( "hidpi", true ); } /** @@ -204,7 +204,7 @@ public class UIScale * to the given font. */ public static FontUIResource applyCustomScaleFactor( FontUIResource font ) { - String uiScale = System.getProperty( "flatlaf.uiScale" ); + String uiScale = System.getProperty( FlatSystemProperties.UI_SCALE ); float scaleFactor = parseScaleFactor( uiScale ); if( scaleFactor <= 0 ) return font; diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatPaintingStringTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatPaintingStringTest.java index 60c6246b..3a9ab3cd 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatPaintingStringTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatPaintingStringTest.java @@ -26,6 +26,7 @@ import java.awt.Insets; import java.awt.geom.AffineTransform; import javax.swing.*; import javax.swing.border.EmptyBorder; +import com.formdev.flatlaf.FlatSystemProperties; import com.formdev.flatlaf.ui.FlatUIUtils; import com.formdev.flatlaf.util.Graphics2DProxy; import com.formdev.flatlaf.util.HiDPIUtils; @@ -40,7 +41,7 @@ public class FlatPaintingStringTest extends JPanel { public static void main( String[] args ) { - System.setProperty( "flatlaf.uiScale", "1x" ); + System.setProperty( FlatSystemProperties.UI_SCALE, "1x" ); System.setProperty( "sun.java2d.uiScale", "1x" ); SwingUtilities.invokeLater( () -> { diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatTestFrame.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatTestFrame.java index 8998eedc..eae07aea 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatTestFrame.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatTestFrame.java @@ -35,6 +35,7 @@ import com.formdev.flatlaf.FlatDarkLaf; import com.formdev.flatlaf.FlatIntelliJLaf; import com.formdev.flatlaf.FlatLaf; import com.formdev.flatlaf.FlatLightLaf; +import com.formdev.flatlaf.FlatSystemProperties; import com.formdev.flatlaf.IntelliJTheme; import com.formdev.flatlaf.demo.LookAndFeelsComboBox; import com.formdev.flatlaf.demo.DemoPrefs; @@ -66,10 +67,10 @@ public class FlatTestFrame DemoPrefs.init( PREFS_ROOT_PATH ); // set scale factor - if( System.getProperty( "flatlaf.uiScale", System.getProperty( "sun.java2d.uiScale" ) ) == null ) { + if( System.getProperty( FlatSystemProperties.UI_SCALE, System.getProperty( "sun.java2d.uiScale" ) ) == null ) { String scaleFactor = DemoPrefs.getState().get( KEY_SCALE_FACTOR, null ); if( scaleFactor != null ) - System.setProperty( "flatlaf.uiScale", scaleFactor ); + System.setProperty( FlatSystemProperties.UI_SCALE, scaleFactor ); } // set look and feel @@ -145,7 +146,7 @@ public class FlatTestFrame lookAndFeelComboBox.setModel( lafModel ); updateScaleFactorComboBox(); - String scaleFactor = System.getProperty( "flatlaf.uiScale", System.getProperty( "sun.java2d.uiScale" ) ); + String scaleFactor = System.getProperty( FlatSystemProperties.UI_SCALE, System.getProperty( "sun.java2d.uiScale" ) ); if( scaleFactor != null ) scaleFactorComboBox.setSelectedItem( scaleFactor ); @@ -472,10 +473,10 @@ public class FlatTestFrame scaleFactorComboBox.setPopupVisible( false ); if( scaleFactor != null ) { - System.setProperty( "flatlaf.uiScale", scaleFactor ); + System.setProperty( FlatSystemProperties.UI_SCALE, scaleFactor ); DemoPrefs.getState().put( KEY_SCALE_FACTOR, scaleFactor ); } else { - System.clearProperty( "flatlaf.uiScale" ); + System.clearProperty( FlatSystemProperties.UI_SCALE ); DemoPrefs.getState().remove( KEY_SCALE_FACTOR ); } diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/uidefaults/UIDefaultsDump.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/uidefaults/UIDefaultsDump.java index 53a01ecf..b552165f 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/uidefaults/UIDefaultsDump.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/uidefaults/UIDefaultsDump.java @@ -84,7 +84,7 @@ public class UIDefaultsDump public static void main( String[] args ) { Locale.setDefault( Locale.ENGLISH ); System.setProperty( "sun.java2d.uiScale", "1x" ); - System.setProperty( "flatlaf.uiScale", "1x" ); + System.setProperty( FlatSystemProperties.UI_SCALE, "1x" ); File dir = new File( "src/main/resources/com/formdev/flatlaf/testing/uidefaults" ); From 332f05b6e13c41af81153d8fb38552b8071b83df Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Sat, 27 Jun 2020 19:36:36 +0200 Subject: [PATCH 29/33] Window decorations: allow enabling/disabling custom window decorations via system properties "flatlaf.useWindowDecorations", "flatlaf.useJetBrainsCustomDecorations" and "flatlaf.menuBarEmbedded" (all boolean) --- .../java/com/formdev/flatlaf/FlatLaf.java | 21 ++++++++ .../formdev/flatlaf/FlatSystemProperties.java | 52 +++++++++++++++++++ .../com/formdev/flatlaf/ui/FlatTitlePane.java | 10 ++-- .../flatlaf/ui/JBRCustomDecorations.java | 16 ++++-- 4 files changed, 91 insertions(+), 8 deletions(-) diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatLaf.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatLaf.java index 92a9a144..ff319ea2 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatLaf.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatLaf.java @@ -88,6 +88,9 @@ public abstract class FlatLaf private Consumer postInitialization; + private Boolean oldFrameWindowDecorated; + private Boolean oldDialogWindowDecorated; + public static boolean install( LookAndFeel newLookAndFeel ) { try { UIManager.setLookAndFeel( newLookAndFeel ); @@ -242,6 +245,16 @@ public abstract class FlatLaf String.format( "a { color: #%06x; }", linkColor.getRGB() & 0xffffff ) ); } }; + + // enable/disable window decorations, but only if system property is either + // "true" or "false"; in other cases it is not changed + Boolean useWindowDecorations = FlatSystemProperties.getBooleanStrict( FlatSystemProperties.USE_WINDOW_DECORATIONS, null ); + if( useWindowDecorations != null ) { + oldFrameWindowDecorated = JFrame.isDefaultLookAndFeelDecorated(); + oldDialogWindowDecorated = JDialog.isDefaultLookAndFeelDecorated(); + JFrame.setDefaultLookAndFeelDecorated( useWindowDecorations ); + JDialog.setDefaultLookAndFeelDecorated( useWindowDecorations ); + } } @Override @@ -274,6 +287,14 @@ public abstract class FlatLaf new HTMLEditorKit().getStyleSheet().addRule( "a { color: blue; }" ); postInitialization = null; + // restore enable/disable window decorations + if( oldFrameWindowDecorated != null ) { + JFrame.setDefaultLookAndFeelDecorated( oldFrameWindowDecorated ); + JDialog.setDefaultLookAndFeelDecorated( oldDialogWindowDecorated ); + oldFrameWindowDecorated = null; + oldDialogWindowDecorated = null; + } + super.uninitialize(); } diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatSystemProperties.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatSystemProperties.java index b6cc16e0..c8a6d858 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatSystemProperties.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatSystemProperties.java @@ -16,6 +16,9 @@ package com.formdev.flatlaf; +import javax.swing.JDialog; +import javax.swing.JFrame; + /** * Defines/documents own system properties used in FlatLaf. * @@ -46,6 +49,41 @@ public interface FlatSystemProperties */ String USE_UBUNTU_FONT = "flatlaf.useUbuntuFont"; + /** + * Specifies whether custom look and feel window decorations should be used + * when creating {@code JFrame} or {@code JDialog}. + *

+ * If this system property is set, FlatLaf invokes {@link JFrame#setDefaultLookAndFeelDecorated(boolean)} + * and {@link JDialog#setDefaultLookAndFeelDecorated(boolean)} on LaF initialization. + *

+ * Allowed Values {@code false} and {@code true}
+ * Default none + */ + String USE_WINDOW_DECORATIONS = "flatlaf.useWindowDecorations"; + + /** + * Specifies whether JetBrains Runtime custom window decorations should be used + * when creating {@code JFrame} or {@code JDialog}. + * Requires that the application runs in a + * JetBrains Runtime + * (based on OpenJDK). + *

+ * Setting this to {@code true} forces using JetBrains Runtime custom window decorations + * even if they are not enabled by the application. + *

+ * Allowed Values {@code false} and {@code true}
+ * Default {@code true} + */ + String USE_JETBRAINS_CUSTOM_DECORATIONS = "flatlaf.useJetBrainsCustomDecorations"; + + /** + * Specifies whether menubar is embedded into custom window decorations. + *

+ * Allowed Values {@code false} and {@code true}
+ * Default {@code true} + */ + String MENUBAR_EMBEDDED = "flatlaf.menuBarEmbedded"; + /** * Specifies whether vertical text position is corrected when UI is scaled on HiDPI screens. *

@@ -63,4 +101,18 @@ public interface FlatSystemProperties String value = System.getProperty( key ); return (value != null) ? Boolean.parseBoolean( value ) : defaultValue; } + + /** + * Checks whether a system property is set and returns {@code Boolean.TRUE} if its value + * is {@code "true"} (case-insensitive) or returns {@code Boolean.FALSE} if its value + * is {@code "false"} (case-insensitive). Otherwise {@code defaultValue} is returned. + */ + static Boolean getBooleanStrict( String key, Boolean defaultValue ) { + String value = System.getProperty( key ); + if( "true".equalsIgnoreCase( value ) ) + return Boolean.TRUE; + if( "false".equalsIgnoreCase( value ) ) + return Boolean.FALSE; + return defaultValue; + } } 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 502f3af5..81c84c7d 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 @@ -58,6 +58,7 @@ 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.SystemInfo; import com.formdev.flatlaf.util.UIScale; @@ -320,8 +321,9 @@ class FlatTitlePane } boolean isMenuBarEmbedded() { - return menuBarEmbedded && FlatClientProperties.clientPropertyBoolean( - rootPane, FlatClientProperties.MENU_BAR_EMBEDDED, true ); + return menuBarEmbedded && + FlatClientProperties.clientPropertyBoolean( rootPane, FlatClientProperties.MENU_BAR_EMBEDDED, true ) && + FlatSystemProperties.getBoolean( FlatSystemProperties.MENUBAR_EMBEDDED, true ); } Rectangle getMenuBarBounds() { @@ -376,7 +378,7 @@ class FlatTitlePane // (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" ) )) ) + Objects.equals( frame.getMaximizedBounds(), rootPane.getClientProperty( "_flatlaf.maximizedBounds" ) )) ) { GraphicsConfiguration gc = window.getGraphicsConfiguration(); @@ -428,7 +430,7 @@ class FlatTitlePane // remember maximized bounds in client property to be able to detect // whether maximized bounds are modified from the application - rootPane.putClientProperty( "flatlaf.maximizedBounds", maximizedBounds ); + rootPane.putClientProperty( "_flatlaf.maximizedBounds", maximizedBounds ); } // maximize window diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/JBRCustomDecorations.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/JBRCustomDecorations.java index d06069f8..9054c3f4 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/JBRCustomDecorations.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/JBRCustomDecorations.java @@ -39,6 +39,7 @@ import javax.swing.SwingUtilities; import javax.swing.UIManager; import javax.swing.plaf.BorderUIResource; import com.formdev.flatlaf.FlatLaf; +import com.formdev.flatlaf.FlatSystemProperties; import com.formdev.flatlaf.util.HiDPIUtils; import com.formdev.flatlaf.util.SystemInfo; @@ -111,8 +112,10 @@ public class JBRCustomDecorations JFrame frame = (JFrame) window; // do not enable JBR decorations if JFrame should use system window decorations - if( !JFrame.isDefaultLookAndFeelDecorated() ) - return; + // and if not forced to use JBR decorations + if( !JFrame.isDefaultLookAndFeelDecorated() && + !FlatSystemProperties.getBoolean( FlatSystemProperties.USE_JETBRAINS_CUSTOM_DECORATIONS, false )) + return; // do not enable JBR decorations if frame is undecorated if( frame.isUndecorated() ) @@ -128,8 +131,10 @@ public class JBRCustomDecorations JDialog dialog = (JDialog) window; // do not enable JBR decorations if JDialog should use system window decorations - if( !JDialog.isDefaultLookAndFeelDecorated() ) - return; + // and if not forced to use JBR decorations + if( !JDialog.isDefaultLookAndFeelDecorated() && + !FlatSystemProperties.getBoolean( FlatSystemProperties.USE_JETBRAINS_CUSTOM_DECORATIONS, false )) + return; // do not enable JBR decorations if dialog is undecorated if( dialog.isUndecorated() ) @@ -189,6 +194,9 @@ public class JBRCustomDecorations if( !SystemInfo.IS_JETBRAINS_JVM_11_OR_LATER || !SystemInfo.IS_WINDOWS_10_OR_LATER ) return; + if( !FlatSystemProperties.getBoolean( FlatSystemProperties.USE_JETBRAINS_CUSTOM_DECORATIONS, true ) ) + return; + try { Class awtAcessorClass = Class.forName( "sun.awt.AWTAccessor" ); Class compAccessorClass = Class.forName( "sun.awt.AWTAccessor$ComponentAccessor" ); From 6669d0e59db6fb50b981edc3d8fb3decb4a7e62d Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Sun, 28 Jun 2020 11:34:30 +0200 Subject: [PATCH 30/33] Window decorations: support enabling/disabling embedding menu bar via UI value at runtime --- .../main/java/com/formdev/flatlaf/ui/FlatRootPaneUI.java | 2 +- .../main/java/com/formdev/flatlaf/ui/FlatTitlePane.java | 4 ++-- .../main/java/com/formdev/flatlaf/demo/DemoFrame.java | 9 +++++++++ 3 files changed, 12 insertions(+), 3 deletions(-) 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 db5fa9c5..4f418b4f 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 @@ -291,7 +291,7 @@ public class FlatRootPaneUI @Override public void invalidateLayout( Container parent ) { - if( titlePane != null && titlePane.isMenuBarEmbedded() ) + if( titlePane != null ) titlePane.menuBarChanged(); } 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 81c84c7d..836fcf22 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 @@ -93,7 +93,6 @@ class FlatTitlePane private final Color inactiveForeground = UIManager.getColor( "TitlePane.inactiveForeground" ); private final Color embeddedForeground = UIManager.getColor( "TitlePane.embeddedForeground" ); - private final boolean menuBarEmbedded = UIManager.getBoolean( "TitlePane.menuBarEmbedded" ); private final Insets menuBarMargins = UIManager.getInsets( "TitlePane.menuBarMargins" ); private final Dimension iconSize = UIManager.getDimension( "TitlePane.iconSize" ); private final int buttonMaximizedHeight = UIManager.getInt( "TitlePane.buttonMaximizedHeight" ); @@ -321,7 +320,8 @@ class FlatTitlePane } boolean isMenuBarEmbedded() { - return menuBarEmbedded && + // 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 ); } 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 51aa1967..be299934 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 @@ -28,6 +28,7 @@ import com.formdev.flatlaf.FlatLaf; import com.formdev.flatlaf.demo.extras.*; import com.formdev.flatlaf.demo.intellijthemes.*; import com.formdev.flatlaf.extras.FlatSVGIcon; +import com.formdev.flatlaf.ui.JBRCustomDecorations; import net.miginfocom.swing.*; /** @@ -75,6 +76,11 @@ class DemoFrame private void menuBarEmbeddedChanged() { getRootPane().putClientProperty( FlatClientProperties.MENU_BAR_EMBEDDED, menuBarEmbeddedCheckBoxMenuItem.isSelected() ? null : false ); + +// alternative method for all frames and menu bars in an application +// UIManager.put( "TitlePane.menuBarEmbedded", menuBarEmbeddedCheckBoxMenuItem.isSelected() ); +// revalidate(); +// repaint(); } private void underlineMenuSelection() { @@ -581,6 +587,9 @@ class DemoFrame cutMenuItem.addActionListener( new DefaultEditorKit.CutAction() ); copyMenuItem.addActionListener( new DefaultEditorKit.CopyAction() ); pasteMenuItem.addActionListener( new DefaultEditorKit.PasteAction() ); + + menuBarEmbeddedCheckBoxMenuItem.setEnabled( UIManager.getLookAndFeel() + .getSupportsWindowDecorations() || JBRCustomDecorations.isSupported() ); } // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables From 14ddc2f6299e20e66e392d2bcec55c3c95b7f767 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Sun, 28 Jun 2020 12:12:58 +0200 Subject: [PATCH 31/33] Demo: use window decorations by default and added "Options > Window decorations" to menu --- .../com/formdev/flatlaf/demo/DemoFrame.java | 24 +++++++++++++++++-- .../com/formdev/flatlaf/demo/DemoFrame.jfd | 9 +++++++ .../com/formdev/flatlaf/demo/FlatLafDemo.java | 6 +++++ 3 files changed, 37 insertions(+), 2 deletions(-) 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 be299934..f68a47c8 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,16 @@ class DemoFrame } ); } + private void windowDecorationsChanged() { + boolean windowDecorations = windowDecorationsCheckBoxMenuItem.isSelected(); + + dispose(); + setUndecorated( windowDecorations ); + getRootPane().setWindowDecorationStyle( windowDecorations ? JRootPane.FRAME : JRootPane.NONE ); + menuBarEmbeddedCheckBoxMenuItem.setEnabled( windowDecorations ); + setVisible( true ); + } + private void menuBarEmbeddedChanged() { getRootPane().putClientProperty( FlatClientProperties.MENU_BAR_EMBEDDED, menuBarEmbeddedCheckBoxMenuItem.isSelected() ? null : false ); @@ -231,6 +241,7 @@ class DemoFrame JMenuItem incrFontMenuItem = new JMenuItem(); JMenuItem decrFontMenuItem = new JMenuItem(); JMenu optionsMenu = new JMenu(); + windowDecorationsCheckBoxMenuItem = new JCheckBoxMenuItem(); menuBarEmbeddedCheckBoxMenuItem = new JCheckBoxMenuItem(); underlineMenuSelectionMenuItem = new JCheckBoxMenuItem(); alwaysShowMnemonicsMenuItem = new JCheckBoxMenuItem(); @@ -466,6 +477,12 @@ class DemoFrame { optionsMenu.setText("Options"); + //---- windowDecorationsCheckBoxMenuItem ---- + windowDecorationsCheckBoxMenuItem.setText("Window decorations"); + windowDecorationsCheckBoxMenuItem.setSelected(true); + windowDecorationsCheckBoxMenuItem.addActionListener(e -> windowDecorationsChanged()); + optionsMenu.add(windowDecorationsCheckBoxMenuItem); + //---- menuBarEmbeddedCheckBoxMenuItem ---- menuBarEmbeddedCheckBoxMenuItem.setText("Embedded menu bar"); menuBarEmbeddedCheckBoxMenuItem.setSelected(true); @@ -588,12 +605,15 @@ class DemoFrame copyMenuItem.addActionListener( new DefaultEditorKit.CopyAction() ); pasteMenuItem.addActionListener( new DefaultEditorKit.PasteAction() ); - menuBarEmbeddedCheckBoxMenuItem.setEnabled( UIManager.getLookAndFeel() - .getSupportsWindowDecorations() || JBRCustomDecorations.isSupported() ); + boolean supportsWindowDecorations = UIManager.getLookAndFeel() + .getSupportsWindowDecorations() || JBRCustomDecorations.isSupported(); + windowDecorationsCheckBoxMenuItem.setEnabled( supportsWindowDecorations && !JBRCustomDecorations.isSupported() ); + menuBarEmbeddedCheckBoxMenuItem.setEnabled( supportsWindowDecorations ); } // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables private JMenu fontMenu; + private JCheckBoxMenuItem windowDecorationsCheckBoxMenuItem; private JCheckBoxMenuItem menuBarEmbeddedCheckBoxMenuItem; private JCheckBoxMenuItem underlineMenuSelectionMenuItem; private JCheckBoxMenuItem alwaysShowMnemonicsMenuItem; 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 7675da4c..0146ff6b 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 @@ -322,6 +322,15 @@ new FormModel { add( new FormContainer( "javax.swing.JMenu", new FormLayoutManager( class javax.swing.JMenu ) ) { name: "optionsMenu" "text": "Options" + add( new FormComponent( "javax.swing.JCheckBoxMenuItem" ) { + name: "windowDecorationsCheckBoxMenuItem" + "text": "Window decorations" + "selected": true + auxiliary() { + "JavaCodeGenerator.variableLocal": false + } + addEvent( new FormEvent( "java.awt.event.ActionListener", "actionPerformed", "windowDecorationsChanged", false ) ) + } ) add( new FormComponent( "javax.swing.JCheckBoxMenuItem" ) { name: "menuBarEmbeddedCheckBoxMenuItem" "text": "Embedded menu bar" diff --git a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/FlatLafDemo.java b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/FlatLafDemo.java index 5e5bb17e..94e927d6 100644 --- a/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/FlatLafDemo.java +++ b/flatlaf-demo/src/main/java/com/formdev/flatlaf/demo/FlatLafDemo.java @@ -16,6 +16,8 @@ package com.formdev.flatlaf.demo; +import javax.swing.JDialog; +import javax.swing.JFrame; import javax.swing.SwingUtilities; import com.formdev.flatlaf.extras.FlatInspector; import com.formdev.flatlaf.util.SystemInfo; @@ -36,6 +38,10 @@ public class FlatLafDemo SwingUtilities.invokeLater( () -> { DemoPrefs.init( PREFS_ROOT_PATH ); + // enable window decorations + JFrame.setDefaultLookAndFeelDecorated( true ); + JDialog.setDefaultLookAndFeelDecorated( true ); + // set look and feel DemoPrefs.initLaf( args ); From e8d5210606f87231ef9149cccfef9f4fcd436876 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Mon, 29 Jun 2020 12:20:57 +0200 Subject: [PATCH 32/33] Window decorations: use default icon if no icon set on window --- .../com/formdev/flatlaf/ui/FlatTitlePane.java | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 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 836fcf22..f223ae5e 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 @@ -47,6 +47,8 @@ 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; @@ -60,6 +62,7 @@ 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; @@ -255,12 +258,24 @@ class FlatTitlePane } } - // show/hide icon - boolean hasImages = !images.isEmpty(); - iconLabel.setVisible( hasImages ); + boolean hasIcon = true; - if( hasImages ) + // 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(); } From a31a8a03c1dbf4c8b37a76763ab082e5976da255 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Mon, 29 Jun 2020 15:45:26 +0200 Subject: [PATCH 33/33] Window decorations: made most classes/methods public/protected for extending/subclassing --- .../formdev/flatlaf/ui/FlatRootPaneUI.java | 30 +++++--- .../com/formdev/flatlaf/ui/FlatTitlePane.java | 77 ++++++++++++------- .../formdev/flatlaf/ui/FlatTitlePaneIcon.java | 6 +- .../formdev/flatlaf/ui/FlatWindowResizer.java | 24 +++--- 4 files changed, 87 insertions(+), 50 deletions(-) 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 4f418b4f..06891bce 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 @@ -121,7 +121,7 @@ public class FlatRootPaneUI } } - private void installClientDecorations() { + protected void installClientDecorations() { boolean isJBRSupported = canUseJBRCustomDecorations && JBRCustomDecorations.isSupported(); // install border @@ -131,18 +131,18 @@ public class FlatRootPaneUI LookAndFeel.uninstallBorder( rootPane ); // install title pane - setTitlePane( new FlatTitlePane( rootPane ) ); + setTitlePane( createTitlePane() ); // install layout oldLayout = rootPane.getLayout(); - rootPane.setLayout( new FlatRootLayout() ); + rootPane.setLayout( createRootLayout() ); // install window resizer if( !isJBRSupported ) - windowResizer = new FlatWindowResizer( rootPane ); + windowResizer = createWindowResizer(); } - private void uninstallClientDecorations() { + protected void uninstallClientDecorations() { LookAndFeel.uninstallBorder( rootPane ); setTitlePane( null ); @@ -162,10 +162,22 @@ public class FlatRootPaneUI } } - // layer title pane under frame content layer to allow placing menu bar over title pane - private final static Integer TITLE_PANE_LAYER = JLayeredPane.FRAME_CONTENT_LAYER - 1; + protected FlatRootLayout createRootLayout() { + return new FlatRootLayout(); + } - private void setTitlePane( FlatTitlePane newTitlePane ) { + protected FlatWindowResizer createWindowResizer() { + return new FlatWindowResizer( rootPane ); + } + + protected FlatTitlePane createTitlePane() { + return new FlatTitlePane( rootPane ); + } + + // 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; + + protected void setTitlePane( FlatTitlePane newTitlePane ) { JLayeredPane layeredPane = rootPane.getLayeredPane(); if( titlePane != null ) @@ -200,7 +212,7 @@ public class FlatRootPaneUI //---- class FlatRootLayout ----------------------------------------------- - private class FlatRootLayout + protected class FlatRootLayout implements LayoutManager2 { @Override public void addLayoutComponent( String name, Component comp ) {} 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 f223ae5e..a2a76824 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 @@ -87,7 +87,7 @@ import com.formdev.flatlaf.util.UIScale; * * @author Karl Tauber */ -class FlatTitlePane +public class FlatTitlePane extends JComponent { private final Color activeBackground = UIManager.getColor( "TitlePane.background" ); @@ -112,13 +112,14 @@ class FlatTitlePane private JButton restoreButton; private JButton closeButton; - private final Handler handler = new Handler(); + private final Handler handler; private Window window; - FlatTitlePane( JRootPane rootPane ) { + public FlatTitlePane( JRootPane rootPane ) { this.rootPane = rootPane; - setBorder( new TitlePaneBorder() ); + handler = createHandler(); + setBorder( createTitlePaneBorder() ); addSubComponents(); activeChanged( true ); @@ -130,7 +131,15 @@ class FlatTitlePane iconLabel.addMouseListener( handler ); } - private void addSubComponents() { + protected FlatTitlePaneBorder createTitlePaneBorder() { + return new FlatTitlePaneBorder(); + } + + protected Handler createHandler() { + return new Handler(); + } + + protected void addSubComponents() { leftPanel = new JPanel(); iconLabel = new JLabel(); titleLabel = new JLabel(); @@ -160,7 +169,7 @@ class FlatTitlePane add( buttonPanel, BorderLayout.LINE_END ); } - private void createButtons() { + protected void createButtons() { iconifyButton = createButton( "TitlePane.iconifyIcon", "Iconify", e -> iconify() ); maximizeButton = createButton( "TitlePane.maximizeIcon", "Maximize", e -> maximize() ); restoreButton = createButton( "TitlePane.restoreIcon", "Restore", e -> restore() ); @@ -197,7 +206,7 @@ class FlatTitlePane buttonPanel.add( closeButton ); } - private JButton createButton( String iconKey, String accessibleName, ActionListener action ) { + protected JButton createButton( String iconKey, String accessibleName, ActionListener action ) { JButton button = new JButton( UIManager.getIcon( iconKey ) ); button.setFocusable( false ); button.setContentAreaFilled( false ); @@ -207,7 +216,7 @@ class FlatTitlePane return button; } - private void activeChanged( boolean active ) { + protected void activeChanged( boolean active ) { Color background = FlatUIUtils.nonUIResource( active ? activeBackground : inactiveBackground ); Color foreground = FlatUIUtils.nonUIResource( active ? (rootPane.getJMenuBar() != null && isMenuBarEmbedded() ? embeddedForeground : activeForeground) @@ -223,7 +232,7 @@ class FlatTitlePane closeButton.setBackground( background ); } - private void frameStateChanged() { + protected void frameStateChanged() { if( window == null || rootPane.getWindowDecorationStyle() != JRootPane.FRAME ) return; @@ -246,7 +255,7 @@ class FlatTitlePane } } - private void updateIcon() { + protected void updateIcon() { // get window images List images = window.getIconImages(); if( images.isEmpty() ) { @@ -306,7 +315,7 @@ class FlatTitlePane window = null; } - private String getWindowTitle() { + protected String getWindowTitle() { if( window instanceof Frame ) return ((Frame)window).getTitle(); if( window instanceof Dialog ) @@ -314,7 +323,7 @@ class FlatTitlePane return null; } - private void installWindowListeners() { + protected void installWindowListeners() { if( window == null ) return; @@ -324,7 +333,7 @@ class FlatTitlePane window.addComponentListener( handler ); } - private void uninstallWindowListeners() { + protected void uninstallWindowListeners() { if( window == null ) return; @@ -334,14 +343,14 @@ class FlatTitlePane window.removeComponentListener( handler ); } - boolean isMenuBarEmbedded() { + 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 ); } - Rectangle getMenuBarBounds() { + protected Rectangle getMenuBarBounds() { Insets insets = rootPane.getInsets(); Rectangle bounds = new Rectangle( SwingUtilities.convertPoint( menuBarPlaceholder, -insets.left, -insets.top, rootPane ), @@ -355,7 +364,7 @@ class FlatTitlePane return FlatUIUtils.subtractInsets( bounds, UIScale.scale( getMenuBarMargins() ) ); } - void menuBarChanged() { + protected void menuBarChanged() { menuBarPlaceholder.invalidate(); // update title foreground color @@ -364,7 +373,7 @@ class FlatTitlePane } ); } - private Insets getMenuBarMargins() { + protected Insets getMenuBarMargins() { return getComponentOrientation().isLeftToRight() ? menuBarMargins : new Insets( menuBarMargins.top, menuBarMargins.right, menuBarMargins.bottom, menuBarMargins.left ); @@ -376,14 +385,20 @@ class FlatTitlePane g.fillRect( 0, 0, getWidth(), getHeight() ); } - private void iconify() { + /** + * Iconifies the window. + */ + protected void iconify() { if( window instanceof Frame ) { Frame frame = (Frame) window; frame.setExtendedState( frame.getExtendedState() | Frame.ICONIFIED ); } } - private void maximize() { + /** + * Maximizes the window. + */ + protected void maximize() { if( !(window instanceof Frame) ) return; @@ -452,7 +467,10 @@ class FlatTitlePane frame.setExtendedState( frame.getExtendedState() | Frame.MAXIMIZED_BOTH ); } - private void restore() { + /** + * Restores the window size. + */ + protected void restore() { if( window instanceof Frame ) { Frame frame = (Frame) window; int state = frame.getExtendedState(); @@ -462,24 +480,27 @@ class FlatTitlePane } } - private void close() { + /** + * Closes the window. + */ + protected void close() { if( window != null ) window.dispatchEvent( new WindowEvent( window, WindowEvent.WINDOW_CLOSING ) ); } - private boolean hasJBRCustomDecoration() { + protected boolean hasJBRCustomDecoration() { return FlatRootPaneUI.canUseJBRCustomDecorations && window != null && JBRCustomDecorations.hasCustomDecoration( window ); } - private void updateJBRHitTestSpotsAndTitleBarHeightLater() { + protected void updateJBRHitTestSpotsAndTitleBarHeightLater() { EventQueue.invokeLater( () -> { updateJBRHitTestSpotsAndTitleBarHeight(); } ); } - private void updateJBRHitTestSpotsAndTitleBarHeight() { + protected void updateJBRHitTestSpotsAndTitleBarHeight() { if( !isDisplayable() ) return; @@ -500,7 +521,7 @@ class FlatTitlePane JBRCustomDecorations.setHitTestSpotsAndTitleBarHeight( window, hitTestSpots, titleBarHeight ); } - private void addJBRHitTestSpot( JComponent c, boolean subtractMenuBarMargins, List hitTestSpots ) { + protected void addJBRHitTestSpot( JComponent c, boolean subtractMenuBarMargins, List hitTestSpots ) { Dimension size = c.getSize(); if( size.width <= 0 || size.height <= 0 ) return; @@ -516,7 +537,7 @@ class FlatTitlePane //---- class TitlePaneBorder ---------------------------------------------- - private class TitlePaneBorder + protected class FlatTitlePaneBorder extends AbstractBorder { @Override @@ -547,7 +568,7 @@ class FlatTitlePane JBRWindowTopBorder.getInstance().paintBorder( c, g, x, y, width, height ); } - private Border getMenuBarBorder() { + protected Border getMenuBarBorder() { JMenuBar menuBar = rootPane.getJMenuBar(); return (menuBar != null && isMenuBarEmbedded()) ? menuBar.getBorder() : null; } @@ -555,7 +576,7 @@ class FlatTitlePane //---- class Handler ------------------------------------------------------ - private class Handler + protected class Handler extends WindowAdapter implements PropertyChangeListener, MouseListener, MouseMotionListener, ComponentListener { diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTitlePaneIcon.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTitlePaneIcon.java index 93084ccc..fbe166a0 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTitlePaneIcon.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatTitlePaneIcon.java @@ -28,10 +28,10 @@ import com.formdev.flatlaf.util.ScaledImageIcon; /** * @author Karl Tauber */ -class FlatTitlePaneIcon +public class FlatTitlePaneIcon extends ScaledImageIcon { - static Icon create( List images, Dimension size ) { + public static Icon create( List images, Dimension size ) { // collect all images including multi-resolution variants List allImages = new ArrayList<>(); for( Image image : images ) { @@ -52,7 +52,7 @@ class FlatTitlePaneIcon private final List images; - FlatTitlePaneIcon( List images, Dimension size ) { + private FlatTitlePaneIcon( List images, Dimension size ) { super( new ImageIcon( images.get( 0 ) ), size.width, size.height ); this.images = images; } diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatWindowResizer.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatWindowResizer.java index 969f27e5..c8f0365b 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatWindowResizer.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatWindowResizer.java @@ -47,7 +47,7 @@ import com.formdev.flatlaf.util.UIScale; * * @author Karl Tauber */ -class FlatWindowResizer +public class FlatWindowResizer extends JComponent implements PropertyChangeListener, WindowStateListener, ComponentListener { @@ -61,14 +61,14 @@ class FlatWindowResizer private Window window; - FlatWindowResizer( JRootPane rootPane ) { + public FlatWindowResizer( JRootPane rootPane ) { this.rootPane = rootPane; setLayout( new BorderLayout() ); - add( new DragBorderComponent( NW_RESIZE_CURSOR, N_RESIZE_CURSOR, NE_RESIZE_CURSOR ), BorderLayout.NORTH ); - add( new DragBorderComponent( SW_RESIZE_CURSOR, S_RESIZE_CURSOR, SE_RESIZE_CURSOR ), BorderLayout.SOUTH ); - add( new DragBorderComponent( NW_RESIZE_CURSOR, W_RESIZE_CURSOR, SW_RESIZE_CURSOR ), BorderLayout.WEST ); - add( new DragBorderComponent( NE_RESIZE_CURSOR, E_RESIZE_CURSOR, SE_RESIZE_CURSOR ), BorderLayout.EAST ); + add( createDragBorderComponent( NW_RESIZE_CURSOR, N_RESIZE_CURSOR, NE_RESIZE_CURSOR ), BorderLayout.NORTH ); + add( createDragBorderComponent( SW_RESIZE_CURSOR, S_RESIZE_CURSOR, SE_RESIZE_CURSOR ), BorderLayout.SOUTH ); + add( createDragBorderComponent( NW_RESIZE_CURSOR, W_RESIZE_CURSOR, SW_RESIZE_CURSOR ), BorderLayout.WEST ); + add( createDragBorderComponent( NE_RESIZE_CURSOR, E_RESIZE_CURSOR, SE_RESIZE_CURSOR ), BorderLayout.EAST ); rootPane.addComponentListener( this ); rootPane.getLayeredPane().add( this, WINDOW_RESIZER_LAYER ); @@ -77,7 +77,11 @@ class FlatWindowResizer setBounds( 0, 0, rootPane.getWidth(), rootPane.getHeight() ); } - void uninstall() { + protected DragBorderComponent createDragBorderComponent( int leadingResizeDir, int centerResizeDir, int trailingResizeDir ) { + return new DragBorderComponent( leadingResizeDir, centerResizeDir, trailingResizeDir ); + } + + public void uninstall() { rootPane.removeComponentListener( this ); rootPane.getLayeredPane().remove( this ); } @@ -157,7 +161,7 @@ class FlatWindowResizer //---- class DragBorderComponent ------------------------------------------ - private class DragBorderComponent + protected class DragBorderComponent extends JComponent implements MouseListener, MouseMotionListener { @@ -170,7 +174,7 @@ class FlatWindowResizer private int dragStartMouseY; private Rectangle dragStartWindowBounds; - DragBorderComponent( int leadingResizeDir, int centerResizeDir, int trailingResizeDir ) { + protected DragBorderComponent( int leadingResizeDir, int centerResizeDir, int trailingResizeDir ) { this.leadingResizeDir = leadingResizeDir; this.centerResizeDir = centerResizeDir; this.trailingResizeDir = trailingResizeDir; @@ -182,7 +186,7 @@ class FlatWindowResizer addMouseMotionListener( this ); } - private void setResizeDir( int resizeDir ) { + protected void setResizeDir( int resizeDir ) { if( this.resizeDir == resizeDir ) return; this.resizeDir = resizeDir;