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