From eea341fb332b199b68cfc33c6b6dea24ea9bba3d Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Thu, 13 May 2021 12:10:11 +0200 Subject: [PATCH] Native window decorations: fixed broken maximizing window (under special conditions) when restoring frame state at startup (issue #283) --- CHANGELOG.md | 11 +- .../ui/FlatWindowsNativeWindowBorder.java | 13 +- .../FlatWindowsNativeWindowBorder.java | 41 ++- .../src/main/cpp/FlatWndProc.cpp | 36 ++- .../src/main/cpp/FlatWndProc.h | 3 +- ...ui_FlatWindowsNativeWindowBorder_WndProc.h | 4 +- .../testing/FlatWindowMaximizeTest.java | 264 ++++++++++++++++++ 7 files changed, 338 insertions(+), 34 deletions(-) create mode 100644 flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatWindowMaximizeTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index a5419679..8190efd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,10 +51,13 @@ FlatLaf Change Log (issue #322) - IntelliJ Themes: Fixed background colors of DesktopPane and DesktopIcon in all themes. -- Native window decorations: Fixed occasional double window title bar when - creating many frames or dialogs. (issue #315) -- Native window decorations: Fixed slow application startup under particular - conditions. (e.g. incomplete custom JRE) (issue #319) +- Native window decorations: + - Fixed slow application startup under particular conditions. (e.g. incomplete + custom JRE) (issue #319) + - Fixed occasional double window title bar when creating many frames or + dialogs. (issue #315) + - Fixed broken maximizing window (under special conditions) when restoring + frame state at startup. - Linux: Fixed/improved detection of user font settings. (issue #309) diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatWindowsNativeWindowBorder.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatWindowsNativeWindowBorder.java index 87977450..458877bd 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatWindowsNativeWindowBorder.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatWindowsNativeWindowBorder.java @@ -312,16 +312,7 @@ class FlatWindowsNativeWindowBorder return; // remove the OS window title bar - if( window instanceof JFrame && ((JFrame)window).getExtendedState() != 0 ) { - // In case that the frame should be maximized or minimized immediately - // when showing, then it is necessary to defer ::SetWindowPos() invocation. - // Otherwise the frame will not be maximized or minimized. - // This occurs only if frame.pack() was no invoked. - EventQueue.invokeLater( () -> { - updateFrame( hwnd ); - }); - } else - updateFrame( hwnd ); + updateFrame( hwnd, (window instanceof JFrame) ? ((JFrame)window).getExtendedState() : 0 ); } void uninstall() { @@ -333,7 +324,7 @@ class FlatWindowsNativeWindowBorder private native long installImpl( Window window ); private native void uninstallImpl( long hwnd ); - private native void updateFrame( long hwnd ); + private native void updateFrame( long hwnd, int state ); private native void showWindow( long hwnd, int cmd ); // invoked from native code diff --git a/flatlaf-natives/flatlaf-natives-jna/src/main/java/com/formdev/flatlaf/natives/jna/windows/FlatWindowsNativeWindowBorder.java b/flatlaf-natives/flatlaf-natives-jna/src/main/java/com/formdev/flatlaf/natives/jna/windows/FlatWindowsNativeWindowBorder.java index 87bb06f0..148c204e 100644 --- a/flatlaf-natives/flatlaf-natives-jna/src/main/java/com/formdev/flatlaf/natives/jna/windows/FlatWindowsNativeWindowBorder.java +++ b/flatlaf-natives/flatlaf-natives-jna/src/main/java/com/formdev/flatlaf/natives/jna/windows/FlatWindowsNativeWindowBorder.java @@ -292,6 +292,11 @@ public class FlatWindowsNativeWindowBorder WM_NCRBUTTONUP = 0x00A5, WM_DWMCOLORIZATIONCOLORCHANGED = 0x0320; + // WM_SIZE wParam + private static final int + SIZE_MINIMIZED = 1, + SIZE_MAXIMIZED = 2; + // WM_NCHITTEST mouse position codes private static final int HTCLIENT = 1, @@ -320,6 +325,7 @@ public class FlatWindowsNativeWindowBorder private Window window; private final HWND hwnd; private final BaseTSD.LONG_PTR defaultWndProc; + private int wmSizeWParam = -1; private int titleBarHeight; private Rectangle[] hitTestSpots; @@ -338,16 +344,7 @@ public class FlatWindowsNativeWindowBorder defaultWndProc = User32Ex.INSTANCE.SetWindowLong( hwnd, GWLP_WNDPROC, this ); // remove the OS window title bar - if( window instanceof JFrame && ((JFrame)window).getExtendedState() != 0 ) { - // In case that the frame should be maximized or minimized immediately - // when showing, then it is necessary to defer ::SetWindowPos() invocation. - // Otherwise the frame will not be maximized or minimized. - // This occurs only if frame.pack() was no invoked. - EventQueue.invokeLater( () -> { - updateFrame(); - }); - } else - updateFrame(); + updateFrame( (window instanceof JFrame) ? ((JFrame)window).getExtendedState() : 0 ); } void uninstall() { @@ -358,16 +355,31 @@ public class FlatWindowsNativeWindowBorder User32Ex.INSTANCE.SetWindowLong( hwnd, GWLP_WNDPROC, defaultWndProc ); // show the OS window title bar - updateFrame(); + updateFrame( 0 ); // cleanup window = null; } - private void updateFrame() { + private void updateFrame( int state ) { + // Following SetWindowPos() sends a WM_SIZE(SIZE_RESTORED) message to the window + // (although SWP_NOSIZE is set), which would prevent maximizing/minimizing + // when making the frame visible. + // AWT uses WM_SIZE wParam SIZE_RESTORED to update JFrame.extendedState and + // removes MAXIMIZED_BOTH and ICONIFIED. (see method AwtFrame::WmSize() in awt_Frame.cpp) + // To avoid this, change WM_SIZE wParam to SIZE_MAXIMIZED or SIZE_MINIMIZED if necessary. + if( (state & JFrame.ICONIFIED) != 0 ) + wmSizeWParam = SIZE_MINIMIZED; + else if( (state & JFrame.MAXIMIZED_BOTH) == JFrame.MAXIMIZED_BOTH ) + wmSizeWParam = SIZE_MAXIMIZED; + else + wmSizeWParam = -1; + // this sends WM_NCCALCSIZE and removes/shows the window title bar User32.INSTANCE.SetWindowPos( hwnd, hwnd, 0, 0, 0, 0, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE ); + + wmSizeWParam = -1; } /** @@ -391,6 +403,11 @@ public class FlatWindowsNativeWindowBorder fireStateChangedLaterOnce(); break; + case WM_SIZE: + if( wmSizeWParam >= 0 ) + wParam = new WPARAM( wmSizeWParam ); + break; + case WM_DESTROY: return WmDestroy( hwnd, uMsg, wParam, lParam ); } diff --git a/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/FlatWndProc.cpp b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/FlatWndProc.cpp index db545406..5708b1a0 100644 --- a/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/FlatWndProc.cpp +++ b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/FlatWndProc.cpp @@ -47,9 +47,9 @@ JNIEXPORT void JNICALL Java_com_formdev_flatlaf_ui_FlatWindowsNativeWindowBorder extern "C" JNIEXPORT void JNICALL Java_com_formdev_flatlaf_ui_FlatWindowsNativeWindowBorder_00024WndProc_updateFrame - ( JNIEnv* env, jobject obj, jlong hwnd ) + ( JNIEnv* env, jobject obj, jlong hwnd, jint state ) { - FlatWndProc::updateFrame( reinterpret_cast( hwnd ) ); + FlatWndProc::updateFrame( reinterpret_cast( hwnd ), state ); } extern "C" @@ -68,6 +68,9 @@ jmethodID FlatWndProc::fireStateChangedLaterOnceMID; HWNDMap* FlatWndProc::hwndMap; +#define java_awt_Frame_ICONIFIED 1 +#define java_awt_Frame_MAXIMIZED_BOTH (4 | 2) + //---- class FlatWndProc methods ---------------------------------------------- FlatWndProc::FlatWndProc() { @@ -76,6 +79,7 @@ FlatWndProc::FlatWndProc() { obj = NULL; hwnd = NULL; defaultWndProc = NULL; + wmSizeWParam = -1; } HWND FlatWndProc::install( JNIEnv *env, jobject obj, jobject window ) { @@ -120,7 +124,7 @@ void FlatWndProc::uninstall( JNIEnv *env, jobject obj, HWND hwnd ) { ::SetWindowLongPtr( hwnd, GWLP_WNDPROC, (LONG_PTR) fwp->defaultWndProc ); // show the OS window title bar - updateFrame( hwnd ); + updateFrame( hwnd, 0 ); // cleanup env->DeleteGlobalRef( fwp->obj ); @@ -145,10 +149,29 @@ void FlatWndProc::initIDs( JNIEnv *env, jobject obj ) { initialized = 1; } -void FlatWndProc::updateFrame( HWND hwnd ) { +void FlatWndProc::updateFrame( HWND hwnd, int state ) { + // Following SetWindowPos() sends a WM_SIZE(SIZE_RESTORED) message to the window + // (although SWP_NOSIZE is set), which would prevent maximizing/minimizing + // when making the frame visible. + // AWT uses WM_SIZE wParam SIZE_RESTORED to update JFrame.extendedState and + // removes MAXIMIZED_BOTH and ICONIFIED. (see method AwtFrame::WmSize() in awt_Frame.cpp) + // To avoid this, change WM_SIZE wParam to SIZE_MAXIMIZED or SIZE_MINIMIZED if necessary. + FlatWndProc* fwp = (FlatWndProc*) hwndMap->get( hwnd ); + if( fwp != NULL ) { + if( (state & java_awt_Frame_ICONIFIED) != 0 ) + fwp->wmSizeWParam = SIZE_MINIMIZED; + else if( (state & java_awt_Frame_MAXIMIZED_BOTH) == java_awt_Frame_MAXIMIZED_BOTH ) + fwp->wmSizeWParam = SIZE_MAXIMIZED; + else + fwp->wmSizeWParam = -1; + } + // this sends WM_NCCALCSIZE and removes/shows the window title bar ::SetWindowPos( hwnd, hwnd, 0, 0, 0, 0, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE ); + + if( fwp != NULL ) + fwp->wmSizeWParam = -1; } LRESULT CALLBACK FlatWndProc::StaticWindowProc( HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam ) { @@ -176,6 +199,11 @@ LRESULT CALLBACK FlatWndProc::WindowProc( HWND hwnd, UINT uMsg, WPARAM wParam, L fireStateChangedLaterOnce(); break; + case WM_SIZE: + if( wmSizeWParam >= 0 ) + wParam = wmSizeWParam; + break; + case WM_DESTROY: return WmDestroy( hwnd, uMsg, wParam, lParam ); } diff --git a/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/FlatWndProc.h b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/FlatWndProc.h index 6c7bc675..52d68789 100644 --- a/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/FlatWndProc.h +++ b/flatlaf-natives/flatlaf-natives-windows/src/main/cpp/FlatWndProc.h @@ -25,7 +25,7 @@ class FlatWndProc public: static HWND install( JNIEnv *env, jobject obj, jobject window ); static void uninstall( JNIEnv *env, jobject obj, HWND hwnd ); - static void updateFrame( HWND hwnd ); + static void updateFrame( HWND hwnd, int state ); private: static int initialized; @@ -40,6 +40,7 @@ private: jobject obj; HWND hwnd; WNDPROC defaultWndProc; + int wmSizeWParam; FlatWndProc(); static void initIDs( JNIEnv *env, jobject obj ); diff --git a/flatlaf-natives/flatlaf-natives-windows/src/main/headers/com_formdev_flatlaf_ui_FlatWindowsNativeWindowBorder_WndProc.h b/flatlaf-natives/flatlaf-natives-windows/src/main/headers/com_formdev_flatlaf_ui_FlatWindowsNativeWindowBorder_WndProc.h index fe37a2f7..7e586697 100644 --- a/flatlaf-natives/flatlaf-natives-windows/src/main/headers/com_formdev_flatlaf_ui_FlatWindowsNativeWindowBorder_WndProc.h +++ b/flatlaf-natives/flatlaf-natives-windows/src/main/headers/com_formdev_flatlaf_ui_FlatWindowsNativeWindowBorder_WndProc.h @@ -34,10 +34,10 @@ JNIEXPORT void JNICALL Java_com_formdev_flatlaf_ui_FlatWindowsNativeWindowBorder /* * Class: com_formdev_flatlaf_ui_FlatWindowsNativeWindowBorder_WndProc * Method: updateFrame - * Signature: (J)V + * Signature: (JI)V */ JNIEXPORT void JNICALL Java_com_formdev_flatlaf_ui_FlatWindowsNativeWindowBorder_00024WndProc_updateFrame - (JNIEnv *, jobject, jlong); + (JNIEnv *, jobject, jlong, jint); /* * Class: com_formdev_flatlaf_ui_FlatWindowsNativeWindowBorder_WndProc diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatWindowMaximizeTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatWindowMaximizeTest.java new file mode 100644 index 00000000..74402902 --- /dev/null +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatWindowMaximizeTest.java @@ -0,0 +1,264 @@ +/* + * Copyright 2021 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.Component; +import java.awt.EventQueue; +import java.awt.event.ComponentEvent; +import java.awt.event.ComponentListener; +import java.awt.event.KeyEvent; +import java.awt.event.WindowEvent; +import java.awt.event.WindowListener; +import java.util.function.Consumer; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JDialog; +import javax.swing.JFrame; +import javax.swing.KeyStroke; +import javax.swing.UIManager; +import com.formdev.flatlaf.FlatDarkLaf; + +/** + * @author Karl Tauber + */ +public class FlatWindowMaximizeTest +{ + public static void main( String[] args ) { + System.out.println( "Java version: " + System.getProperty( "java.version" ) ); + + // Windows Laf + try { + UIManager.setLookAndFeel( "com.sun.java.swing.plaf.windows.WindowsLookAndFeel" ); + } catch( Exception ex ) { + ex.printStackTrace(); + } + testMaximize( JFrame.MAXIMIZED_BOTH ); +// testMaximize( JFrame.ICONIFIED ); +// testMaximize( JFrame.ICONIFIED | JFrame.MAXIMIZED_BOTH ); +// testMaximize( JFrame.NORMAL ); + + // FlatLaf + FlatDarkLaf.setup(); + testMaximize( JFrame.MAXIMIZED_BOTH ); +// testMaximize( JFrame.ICONIFIED ); +// testMaximize( JFrame.ICONIFIED | JFrame.MAXIMIZED_BOTH ); +// testMaximize( JFrame.NORMAL ); + + System.exit( 0 ); + } + + private static void testMaximize( int state ) { + System.out.println(); + System.out.println( "---- " + state + " - " + UIManager.getLookAndFeel().getClass().getSimpleName() + " ----" ); + + // only maximize + testMaximize( "MAX", state, frame -> { + frame.setExtendedState( state ); + } ); + + // pack/size before maximize + testMaximize( "pack MAX", state, frame -> { + frame.pack(); + frame.setExtendedState( state ); + } ); + testMaximize( "size MAX", state, frame -> { + frame.setSize( 1000, 500 ); + frame.setExtendedState( state ); + } ); + + // pack/size after maximize + testMaximize( "MAX pack", state, frame -> { + frame.setExtendedState( state ); + frame.pack(); + } ); + testMaximize( "MAX size", state, frame -> { + frame.setExtendedState( state ); + frame.setSize( 1000, 500 ); + } ); + + // pack and size before maximize + testMaximize( "pack size MAX", state, frame -> { + frame.pack(); + frame.setSize( 1000, 500 ); + frame.setExtendedState( state ); + } ); + testMaximize( "size pack MAX", state, frame -> { + frame.setSize( 1000, 500 ); + frame.pack(); + frame.setExtendedState( state ); + } ); + + // pack/size before maximize and size/pack after maximize + testMaximize( "pack MAX size", state, frame -> { + frame.pack(); + frame.setExtendedState( state ); + frame.setSize( 1000, 500 ); + } ); + testMaximize( "size MAX pack", state, frame -> { + frame.setSize( 1000, 500 ); + frame.setExtendedState( state ); + frame.pack(); + } ); + + // pack and size after maximize + testMaximize( "MAX size pack", state, frame -> { + frame.setExtendedState( state ); + frame.setSize( 1000, 500 ); + frame.pack(); + } ); + testMaximize( "MAX pack size", state, frame -> { + frame.setExtendedState( state ); + frame.pack(); + frame.setSize( 1000, 500 ); + } ); + + // 1. create invisible frame + // 2. create dialog with invisible frame as owner + // 3. pack dialog, which invokes frame.addNotify() + // 4. show frame + testMaximize( "MAX dialog.pack", state, true, frame -> { + frame.setExtendedState( state ); + + JDialog dialog = new JDialog( frame ); + dialog.pack(); // this invokes frame.addNotify() + } ); + } + + private static void testMaximize( String msg, int expectedState, Consumer testFunc ) { + testMaximize( msg, expectedState, false, testFunc ); + } + + private static void testMaximize( String msg, int expectedState, boolean showLater, Consumer testFunc ) { + JFrame[] pFrame = new JFrame[1]; + EventQueue.invokeLater( () -> { + JFrame frame = new JFrame( "test" ); + frame.setName( msg ); + frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); + +// addWindowListener( frame ); +// addComponentListener( frame ); + + ((JComponent) frame.getContentPane()).registerKeyboardAction( e -> { + System.exit( 0 ); + }, KeyStroke.getKeyStroke( KeyEvent.VK_ESCAPE, 0, false ), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ); + + JButton button = new JButton( msg ); + button.setName( "button - " + msg ); +// addComponentListener( button ); + frame.add( button ); + frame.setLocation( 100, 50 ); + testFunc.accept( frame ); + if( !showLater ) + frame.setVisible( true ); + pFrame[0] = frame; + } ); + + try { + if( showLater ) { + Thread.sleep( 500 ); + + EventQueue.invokeLater( () -> { + pFrame[0].setVisible( true ); + } ); + } + + Thread.sleep( 500 ); + + EventQueue.invokeAndWait( () -> { + int state = pFrame[0].getExtendedState(); + System.out.printf( " %-15s: %d %s\n", msg, state, (state != expectedState ? " FAILED" : "") ); + } ); + } catch( Exception ex ) { + ex.printStackTrace(); + } + } + + @SuppressWarnings( "unused" ) + private static void addWindowListener( JFrame frame ) { + frame.addWindowListener( new WindowListener() { + @Override + public void windowOpened( WindowEvent e ) { + print( "windowOpened", e.getWindow().getName() ); + } + + @Override + public void windowClosing( WindowEvent e ) { + print( "windowClosing", e.getWindow().getName() ); + } + + @Override + public void windowClosed( WindowEvent e ) { + print( "windowClosed", e.getWindow().getName() ); + } + + @Override + public void windowIconified( WindowEvent e ) { + print( "windowIconified", e.getWindow().getName() ); + } + + @Override + public void windowDeiconified( WindowEvent e ) { + print( "windowDeiconified", e.getWindow().getName() ); + } + + @Override + public void windowActivated( WindowEvent e ) { + print( "windowActivated", e.getWindow().getName() ); + } + + @Override + public void windowDeactivated( WindowEvent e ) { + print( "windowDeactivated", e.getWindow().getName() ); + } + } ); + + frame.addWindowStateListener( e -> { + print( "windowStateChanged", e.getOldState() + " -> " + e.getNewState() + " " + e.getWindow().getName() ); + } ); + } + + @SuppressWarnings( "unused" ) + private static void addComponentListener( Component comp ) { + comp.addComponentListener( new ComponentListener() { + @Override + public void componentResized( ComponentEvent e ) { + Component c = e.getComponent(); + print( "componentResized", c.getName() + " " + c.getWidth() + "," + c.getHeight() ); + } + + @Override + public void componentMoved( ComponentEvent e ) { + Component c = e.getComponent(); + print( "componentMoved", e.getComponent().getName() + " " + c.getX() + "," + c.getY() ); + } + + @Override + public void componentShown( ComponentEvent e ) { + print( "componentShown", e.getComponent().getName() ); + } + + @Override + public void componentHidden( ComponentEvent e ) { + print( "componentHidden", e.getComponent().getName() ); + } + } ); + } + + private static void print( String key, String value ) { + System.out.printf( " %-20s %s\n", key, value ); + } +}