From fb4576fc1b4b7e1247787613f700b5b11d930198 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Sat, 20 Aug 2022 21:09:49 +0200 Subject: [PATCH 1/5] Linux: use X11 window manager events to move window and to show window menu (right-click on window title bar), if custom window decorations are enabled (issue #482) --- .github/workflows/natives.yml | 22 +- CHANGELOG.md | 3 + .../kotlin/flatlaf-cpp-library.gradle.kts | 46 +++++ .../kotlin/flatlaf-jni-headers.gradle.kts | 36 ++++ .../formdev/flatlaf/ui/FlatNativeLibrary.java | 4 + .../flatlaf/ui/FlatNativeLinuxLibrary.java | 104 ++++++++++ .../com/formdev/flatlaf/ui/FlatTitlePane.java | 62 +++++- .../formdev/flatlaf/util/NativeLibrary.java | 2 +- flatlaf-natives/README.md | 1 + .../flatlaf-natives-jna/build.gradle.kts | 4 +- .../flatlaf/natives/jna/linux/X11WmUtils.java | 135 +++++++++++++ .../flatlaf-natives-linux/README.md | 39 ++++ .../flatlaf-natives-linux/build.gradle.kts | 89 +++++++++ .../src/main/cpp/X11WmUtils.cpp | 189 ++++++++++++++++++ ...ormdev_flatlaf_ui_FlatNativeLinuxLibrary.h | 31 +++ .../flatlaf-natives-windows/build.gradle.kts | 50 ++--- .../testing/FlatWindowDecorationsTest.java | 7 + settings.gradle.kts | 4 +- 18 files changed, 769 insertions(+), 59 deletions(-) create mode 100644 buildSrc/src/main/kotlin/flatlaf-cpp-library.gradle.kts create mode 100644 buildSrc/src/main/kotlin/flatlaf-jni-headers.gradle.kts create mode 100644 flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeLinuxLibrary.java create mode 100644 flatlaf-natives/flatlaf-natives-jna/src/main/java/com/formdev/flatlaf/natives/jna/linux/X11WmUtils.java create mode 100644 flatlaf-natives/flatlaf-natives-linux/README.md create mode 100644 flatlaf-natives/flatlaf-natives-linux/build.gradle.kts create mode 100644 flatlaf-natives/flatlaf-natives-linux/src/main/cpp/X11WmUtils.cpp create mode 100644 flatlaf-natives/flatlaf-natives-linux/src/main/headers/com_formdev_flatlaf_ui_FlatNativeLinuxLibrary.h diff --git a/.github/workflows/natives.yml b/.github/workflows/natives.yml index 8d4c91b8..4b5ff69d 100644 --- a/.github/workflows/natives.yml +++ b/.github/workflows/natives.yml @@ -9,20 +9,26 @@ on: tags: - '[0-9]*' paths: - - 'flatlaf-natives/flatlaf-natives-windows/**' + - 'flatlaf-natives/**' - '.github/workflows/natives.yml' - 'gradle/wrapper/gradle-wrapper.properties' pull_request: branches: - '*' paths: - - 'flatlaf-natives/flatlaf-natives-windows/**' + - 'flatlaf-natives/**' - '.github/workflows/natives.yml' - 'gradle/wrapper/gradle-wrapper.properties' jobs: - Windows: - runs-on: windows-latest + Natives: + strategy: + matrix: + os: + - windows + - ubuntu + + runs-on: ${{ matrix.os }}-latest steps: - uses: actions/checkout@v3 @@ -37,14 +43,14 @@ jobs: cache: gradle - name: Build with Gradle - # --no-daemon is necessary on Windows otherwise caching Gradle would fail with: + # --no-daemon is necessary on Windows otherwise caching Gradle would fail with: # tar.exe: Couldn't open ~/.gradle/caches/modules-2/modules-2.lock: Permission denied - run: ./gradlew :flatlaf-natives-windows:build-natives --no-daemon + run: ./gradlew build-natives --no-daemon - name: Upload artifacts uses: actions/upload-artifact@v3 with: - name: FlatLaf-natives-windows-build-artifacts + name: FlatLaf-natives-build-artifacts-${{ matrix.os }} path: | flatlaf-core/src/main/resources/com/formdev/flatlaf/natives - flatlaf-natives/flatlaf-natives-windows/build + flatlaf-natives/flatlaf-natives-*/build diff --git a/CHANGELOG.md b/CHANGELOG.md index b44ee9e2..4f6b1422 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ FlatLaf Change Log #### New features and improvements +- Linux: Use X11 window manager events to move window and to show window menu + (right-click on window title bar), if custom window decorations are enabled. + This gives FlatLaf windows a more "native" feeling. (issue #482) - TabbedPane: New option to disable tab run rotation in wrap layout. Set UI value `TabbedPane.rotateTabRuns` to `false`. (issue #574) - Native window decorations (Windows 10/11 only): Added client property to mark diff --git a/buildSrc/src/main/kotlin/flatlaf-cpp-library.gradle.kts b/buildSrc/src/main/kotlin/flatlaf-cpp-library.gradle.kts new file mode 100644 index 00000000..abacf9ec --- /dev/null +++ b/buildSrc/src/main/kotlin/flatlaf-cpp-library.gradle.kts @@ -0,0 +1,46 @@ +/* + * Copyright 2022 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. + */ + +plugins { + `cpp-library` +} + +library { + // disable debuggable for release builds to make shared libraries smaller + binaries.configureEach( CppSharedLibrary::class ) { + with( compileTask.get() ) { + if( name.contains( "Release" ) ) + isDebuggable = false + } + with( linkTask.get() ) { + if( name.contains( "Release" ) ) + debuggable.set( false ) + } + } +} + +tasks { + withType().configureEach { + doFirst { + println( "Used Tool Chain:" ) + println( " - ${toolChain.get()}" ) + println( "Available Tool Chains:" ) + toolChains.forEach { + println( " - $it" ) + } + } + } +} diff --git a/buildSrc/src/main/kotlin/flatlaf-jni-headers.gradle.kts b/buildSrc/src/main/kotlin/flatlaf-jni-headers.gradle.kts new file mode 100644 index 00000000..4219f4c4 --- /dev/null +++ b/buildSrc/src/main/kotlin/flatlaf-jni-headers.gradle.kts @@ -0,0 +1,36 @@ +/* + * Copyright 2022 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. + */ + +open class JniHeadersExtension { + var headers: List = emptyList() +} + +val extension = project.extensions.create( "flatlafJniHeaders" ) + + +tasks { + register( "jni-headers" ) { + // depend on :flatlaf-core:compileJava because it generates the JNI headers + dependsOn( ":flatlaf-core:compileJava" ) + + from( project( ":flatlaf-core" ).buildDir.resolve( "generated/jni-headers" ) ) + into( "src/main/headers" ) + include( extension.headers ) + filter( + "eol" to org.apache.tools.ant.filters.FixCrLfFilter.CrLf.newInstance( "lf" ) + ) + } +} diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeLibrary.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeLibrary.java index 9462f910..3bfd3064 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeLibrary.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeLibrary.java @@ -56,6 +56,10 @@ class FlatNativeLibrary // load jawt native library loadJAWT(); + } else if( SystemInfo.isLinux && SystemInfo.isX86_64 ) { + // Linux: requires x86_64 + + libraryName = "flatlaf-linux-x86_64"; } else return; // no native library available for current OS or CPU architecture diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeLinuxLibrary.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeLinuxLibrary.java new file mode 100644 index 00000000..b5cfa333 --- /dev/null +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeLinuxLibrary.java @@ -0,0 +1,104 @@ +/* + * Copyright 2022 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.Point; +import java.awt.Toolkit; +import java.awt.Window; +import java.awt.event.MouseEvent; +import java.awt.geom.AffineTransform; +import javax.swing.JDialog; +import javax.swing.JFrame; + +/** + * Native methods for Linux. + *

+ * Note: This is private API. Do not use! + * + * @author Karl Tauber + * @since 2.5 + */ +class FlatNativeLinuxLibrary +{ + static boolean isLoaded() { + return FlatNativeLibrary.isLoaded(); + } + + // direction for _NET_WM_MOVERESIZE message + // see https://specifications.freedesktop.org/wm-spec/wm-spec-latest.html + static final int MOVE = 8; + + private static Boolean isXWindowSystem; + + private static boolean isXWindowSystem() { + if( isXWindowSystem == null ) + isXWindowSystem = Toolkit.getDefaultToolkit().getClass().getName().endsWith( ".XToolkit" ); + return isXWindowSystem; + } + + static boolean isWMUtilsSupported( Window window ) { + return hasCustomDecoration( window ) && isXWindowSystem() && isLoaded(); + } + + static boolean moveOrResizeWindow( Window window, MouseEvent e, int direction ) { + Point pt = scale( window, e.getLocationOnScreen() ); + return xMoveOrResizeWindow( window, pt.x, pt.y, direction ); + +/* + try { + Class cls = Class.forName( "com.formdev.flatlaf.natives.jna.linux.X11WmUtils" ); + java.lang.reflect.Method m = cls.getMethod( "xMoveOrResizeWindow", Window.class, int.class, int.class, int.class ); + return (Boolean) m.invoke( null, window, pt.x, pt.y, direction ); + } catch (Exception ex) { + ex.printStackTrace(); + return false; + } +*/ + } + + static boolean showWindowMenu( Window window, MouseEvent e ) { + Point pt = scale( window, e.getLocationOnScreen() ); + return xShowWindowMenu( window, pt.x, pt.y ); + +/* + try { + Class cls = Class.forName( "com.formdev.flatlaf.natives.jna.linux.X11WmUtils" ); + java.lang.reflect.Method m = cls.getMethod( "xShowWindowMenu", Window.class, int.class, int.class ); + return (Boolean) m.invoke( null, window, pt.x, pt.y ); + } catch (Exception ex) { + ex.printStackTrace(); + return false; + } +*/ + } + + private static Point scale( Window window, Point pt ) { + AffineTransform transform = window.getGraphicsConfiguration().getDefaultTransform(); + int x = (int) Math.round( pt.x * transform.getScaleX() ); + int y = (int) Math.round( pt.y * transform.getScaleY() ); + return new Point( x, y ); + } + + // X Window System + private static native boolean xMoveOrResizeWindow( Window window, int x, int y, int direction ); + private static native boolean xShowWindowMenu( Window window, int x, int y ); + + private static boolean hasCustomDecoration( Window window ) { + return (window instanceof JFrame && JFrame.isDefaultLookAndFeelDecorated() && ((JFrame)window).isUndecorated()) || + (window instanceof JDialog && JDialog.isDefaultLookAndFeelDecorated() && ((JDialog)window).isUndecorated()); + } +} 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 aa40c63b..d0707971 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 @@ -357,6 +357,7 @@ public class FlatTitlePane restoreButton.setVisible( resizable && maximized ); if( maximized && + !(SystemInfo.isLinux && FlatNativeLinuxLibrary.isWMUtilsSupported( window )) && rootPane.getClientProperty( "_flatlaf.maximizedBoundsUpToDate" ) == null ) { rootPane.putClientProperty( "_flatlaf.maximizedBoundsUpToDate", null ); @@ -737,6 +738,17 @@ debug*/ } } + private void maximizeOrRestore() { + if( !(window instanceof Frame) || !((Frame)window).isResizable() ) + return; + + Frame frame = (Frame) window; + if( (frame.getExtendedState() & Frame.MAXIMIZED_BOTH) != 0 ) + restore(); + else + maximize(); + } + /** * Closes the window. */ @@ -1154,23 +1166,23 @@ debug*/ //---- interface MouseListener ---- private Point dragOffset; + private boolean nativeMove; @Override public void mouseClicked( MouseEvent e ) { + // on Linux, when using native library, the mouse clicked event + // is usually not sent and maximize/restore is done in mouse pressed event + // this check is here for the case that a mouse clicked event comes thru for some reason + if( SystemInfo.isLinux && FlatNativeLinuxLibrary.isWMUtilsSupported( window ) ) + return; + if( e.getClickCount() == 2 && SwingUtilities.isLeftMouseButton( e ) ) { if( e.getSource() == iconLabel ) { // double-click on icon closes window close(); - } else if( !hasNativeCustomDecoration() && - window instanceof Frame && - ((Frame)window).isResizable() ) - { + } else if( !hasNativeCustomDecoration() ) { // maximize/restore on double-click - Frame frame = (Frame) window; - if( (frame.getExtendedState() & Frame.MAXIMIZED_BOTH) != 0 ) - restore(); - else - maximize(); + maximizeOrRestore(); } } } @@ -1180,10 +1192,37 @@ debug*/ if( window == null ) return; // should newer occur + // on Linux, show window menu + if( SwingUtilities.isRightMouseButton( e ) && + SystemInfo.isLinux && FlatNativeLinuxLibrary.isWMUtilsSupported( window ) ) + { + e.consume(); + FlatNativeLinuxLibrary.showWindowMenu( window, e ); + return; + } + if( !SwingUtilities.isLeftMouseButton( e ) ) return; dragOffset = SwingUtilities.convertPoint( FlatTitlePane.this, e.getPoint(), window ); + nativeMove = false; + + // on Linux, move or maximize/restore window + if( SystemInfo.isLinux && FlatNativeLinuxLibrary.isWMUtilsSupported( window ) ) { + switch( e.getClickCount() ) { + case 1: + // move window via _NET_WM_MOVERESIZE event + e.consume(); + nativeMove = FlatNativeLinuxLibrary.moveOrResizeWindow( window, e, FlatNativeLinuxLibrary.MOVE ); + break; + + case 2: + // maximize/restore on double-click + // also done here because no mouse clicked event is sent when using _NET_WM_MOVERESIZE event + maximizeOrRestore(); + break; + } + } } @Override public void mouseReleased( MouseEvent e ) {} @@ -1194,9 +1233,12 @@ debug*/ @Override public void mouseDragged( MouseEvent e ) { - if( window == null ) + if( window == null || dragOffset == null ) return; // should newer occur + if( nativeMove ) + return; + if( !SwingUtilities.isLeftMouseButton( e ) ) return; diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/NativeLibrary.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/NativeLibrary.java index 018694fb..7b57f06a 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/util/NativeLibrary.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/util/NativeLibrary.java @@ -138,7 +138,7 @@ public class NativeLibrary System.load( libraryFile.getAbsolutePath() ); return true; } catch( Throwable ex ) { - log( null, ex ); + log( ex.getMessage(), ex ); return false; } } diff --git a/flatlaf-natives/README.md b/flatlaf-natives/README.md index 15e8acb8..f41473ee 100644 --- a/flatlaf-natives/README.md +++ b/flatlaf-natives/README.md @@ -2,4 +2,5 @@ FlatLaf Native Libraries ======================== - [Windows 10 Native Library](flatlaf-natives-windows) +- [Linux Native Library](flatlaf-natives-linux) - [Natives using JNA](flatlaf-natives-jna) (for development only) diff --git a/flatlaf-natives/flatlaf-natives-jna/build.gradle.kts b/flatlaf-natives/flatlaf-natives-jna/build.gradle.kts index 4cade0e8..03fcc59a 100644 --- a/flatlaf-natives/flatlaf-natives-jna/build.gradle.kts +++ b/flatlaf-natives/flatlaf-natives-jna/build.gradle.kts @@ -20,6 +20,6 @@ plugins { dependencies { implementation( project( ":flatlaf-core" ) ) - implementation( "net.java.dev.jna:jna:5.10.0" ) - implementation( "net.java.dev.jna:jna-platform:5.10.0" ) + implementation( "net.java.dev.jna:jna:5.12.1" ) + implementation( "net.java.dev.jna:jna-platform:5.12.1" ) } diff --git a/flatlaf-natives/flatlaf-natives-jna/src/main/java/com/formdev/flatlaf/natives/jna/linux/X11WmUtils.java b/flatlaf-natives/flatlaf-natives-jna/src/main/java/com/formdev/flatlaf/natives/jna/linux/X11WmUtils.java new file mode 100644 index 00000000..0363b662 --- /dev/null +++ b/flatlaf-natives/flatlaf-natives-jna/src/main/java/com/formdev/flatlaf/natives/jna/linux/X11WmUtils.java @@ -0,0 +1,135 @@ +/* + * Copyright 2022 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.natives.jna.linux; + +import java.awt.Window; +import com.sun.jna.Native; +import com.sun.jna.NativeLong; +import com.sun.jna.Pointer; +import com.sun.jna.platform.unix.X11; + +/** + * @author Karl Tauber + * @since 2.5 + */ +public class X11WmUtils +{ + /** + * Send _NET_WM_MOVERESIZE to window to initiate moving or resizing. + * + * Warning: Although the implementation of this method is (nearly) identical + * to the C++ implementation, this one does not work correctly. + * DO NOT USE. + * + * https://specifications.freedesktop.org/wm-spec/wm-spec-latest.html#idm45446104441728 + * https://gitlab.gnome.org/GNOME/gtk/-/blob/main/gdk/x11/gdksurface-x11.c#L3841-3881 + */ + public static boolean xMoveOrResizeWindow( Window window, int x, int y, int direction ) { + System.out.println( "---- move or resize window: " + x + "," + y ); + return sendEvent( window, + "_NET_WM_MOVERESIZE", + x, + y, + direction, + X11.Button1, // left mouse button + 1 ); // source indication + } + + /** + * Send _GTK_SHOW_WINDOW_MENU to window to show system window menu. + * + * Warning: Although the implementation of this method is (nearly) identical + * to the C++ implementation, this one does not work correctly. + * DO NOT USE. + * + * https://docs.gtk.org/gdk3/method.Window.show_window_menu.html + * https://gitlab.gnome.org/GNOME/gtk/-/blob/main/gdk/x11/gdksurface-x11.c#L4751-4801 + */ + public static boolean xShowWindowMenu( Window window, int x, int y ) { + System.out.println( "---- show window menu: " + x + "," + y ); + return sendEvent( window, + "_GTK_SHOW_WINDOW_MENU", + 0, // device id TODO + x, + y, + 0, + 0 ); + } + + private static boolean sendEvent( Window window, String atom_name, + long data0, long data1, long data2, long data3, long data4 ) + { + sun.awt.SunToolkit.awtLock(); + try { + + // open display and get root window + X11.Display display = X11.INSTANCE.XOpenDisplay( null ); + X11.Window root = X11.INSTANCE.XDefaultRootWindow( display ); + + // get X11 window ID for AWT window + Pointer p = Native.getComponentPointer( window ); + long windowsId = Pointer.nativeValue( p ); + System.out.println( "WindowId = " + windowsId ); + + // ungrab pointer and keyboard to allow the window manager to grab them + System.out.println( "Ungrab Pointer = " + X11Ext.INSTANCE.XUngrabPointer( display, new NativeLong( 0 ) ) ); + System.out.println( "Ungrab Keyboard = " + X11.INSTANCE.XUngrabKeyboard( display, new NativeLong( 0 ) ) ); + + // build event structure + X11.Window w = new X11.Window( windowsId ); + X11.XEvent event = new X11.XEvent(); + event.type = X11.ClientMessage; + event.setType( X11.XClientMessageEvent.class ); + event.xclient.type = X11.ClientMessage; + event.xclient.serial = new NativeLong( 0 ); + event.xclient.send_event = 1; + event.xclient.message_type = X11.INSTANCE.XInternAtom( display, atom_name, false ); + event.xclient.display = display; + event.xclient.window = w; + event.xclient.format = 32; + event.xclient.data.setType( NativeLong[].class ); + event.xclient.data.l[0] = new NativeLong( data0 ); + event.xclient.data.l[1] = new NativeLong( data1 ); + event.xclient.data.l[2] = new NativeLong( data2 ); + event.xclient.data.l[3] = new NativeLong( data3 ); + event.xclient.data.l[4] = new NativeLong( data4 ); + + // send event + System.out.println( "SendEvent = " + X11.INSTANCE.XSendEvent( display, root, 0, + new NativeLong( X11.SubstructureNotifyMask | X11.SubstructureRedirectMask ), event ) ); + + System.out.println( "Flush = " + X11.INSTANCE.XFlush( display ) ); + System.out.println( "CloseDisplay = " + X11.INSTANCE.XCloseDisplay( display ) ); + System.out.println( "Done" ); + + } finally { + sun.awt.SunToolkit.awtUnlock(); + } + + return true; + } + + //----- interface X11Ext -------------------------------------------------- + + interface X11Ext + extends X11 + { + X11Ext INSTANCE = Native.load( "X11", X11Ext.class ); + + int XUngrabPointer( Display display, NativeLong time ); + } +} diff --git a/flatlaf-natives/flatlaf-natives-linux/README.md b/flatlaf-natives/flatlaf-natives-linux/README.md new file mode 100644 index 00000000..3dd00c68 --- /dev/null +++ b/flatlaf-natives/flatlaf-natives-linux/README.md @@ -0,0 +1,39 @@ +FlatLaf Linux Native Library +============================ + +This sub-project contains the source code for the FlatLaf Linux native library. + +The native library can be built only on Linux and requires a C++ compiler. + +To be able to build FlatLaf on any platform, and without C++ compiler, the +pre-built native library is checked into Git at +[flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/](https://github.com/JFormDesigner/FlatLaf/tree/main/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives). + +The native library was built on a GitHub server with the help of GitHub Actions. +See: +[Native Libraries](https://github.com/JFormDesigner/FlatLaf/actions/workflows/natives.yml) +workflow. Then the produced Artifacts ZIP was downloaded and the native library +checked into Git. + + +## Development + +To build the library on Linux, some packages needs to be installed. + + +### Ubuntu + +`build-essential` contains GCC and development tools. `libxt-dev` contains the +X11 toolkit development headers. + +~~~ +sudo apt update +sudo apt install build-essential libxt-dev +~~~ + + +### CentOS + +~~~ +sudo yum install libXt-devel +~~~ diff --git a/flatlaf-natives/flatlaf-natives-linux/build.gradle.kts b/flatlaf-natives/flatlaf-natives-linux/build.gradle.kts new file mode 100644 index 00000000..b76fb670 --- /dev/null +++ b/flatlaf-natives/flatlaf-natives-linux/build.gradle.kts @@ -0,0 +1,89 @@ +/* + * Copyright 2022 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. + */ + +plugins { + `cpp-library` + `flatlaf-cpp-library` + `flatlaf-jni-headers` +} + +flatlafJniHeaders { + headers = listOf( "com_formdev_flatlaf_ui_FlatNativeLinuxLibrary.h" ) +} + +library { + targetMachines.set( listOf( machines.linux.x86_64 ) ) +} + +var javaHome = System.getProperty( "java.home" ) +if( javaHome.endsWith( "jre" ) ) + javaHome += "/.." + +tasks { + register( "build-natives" ) { + group = "build" + description = "Builds natives" + + if( org.gradle.internal.os.OperatingSystem.current().isLinux ) + dependsOn( "linkRelease" ) + } + + withType().configureEach { + onlyIf { name.contains( "Release" ) } + + // generate and copy needed JNI headers + dependsOn( "jni-headers" ) + + includes.from( + "${javaHome}/include", + "${javaHome}/include/linux" + ) + + compilerArgs.addAll( toolChain.map { + when( it ) { + is Gcc, is Clang -> listOf() + else -> emptyList() + } + } ) + } + + withType().configureEach { + onlyIf { name.contains( "Release" ) } + + val nativesDir = project( ":flatlaf-core" ).projectDir.resolve( "src/main/resources/com/formdev/flatlaf/natives" ) + val libraryName = "libflatlaf-linux-x86_64.so" + val jawt = "jawt" + var jawtPath = "${javaHome}/lib" + if( JavaVersion.current() == JavaVersion.VERSION_1_8 ) + jawtPath += "/amd64" + + linkerArgs.addAll( toolChain.map { + when( it ) { + is Gcc, is Clang -> listOf( "-L${jawtPath}", "-l${jawt}" ) + else -> emptyList() + } + } ) + + doLast { + // copy shared library to flatlaf-core resources + copy { + from( linkedFile ) + into( nativesDir ) + rename( "libflatlaf-natives-linux.so", libraryName ) + } + } + } +} diff --git a/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/X11WmUtils.cpp b/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/X11WmUtils.cpp new file mode 100644 index 00000000..8cbc57a9 --- /dev/null +++ b/flatlaf-natives/flatlaf-natives-linux/src/main/cpp/X11WmUtils.cpp @@ -0,0 +1,189 @@ +/* + * Copyright 2022 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. + */ + +#include +#include +#include +#include "com_formdev_flatlaf_ui_FlatNativeLinuxLibrary.h" + +/** + * @author Karl Tauber + * @since 2.5 + */ + + +bool sendEvent( JNIEnv *env, jobject window, const char *atom_name, + long data0, long data1, long data2, long data3, long data4 ); +bool isWMHintSupported( Display* display, Window rootWindow, Atom atom ); +Window getWindowHandle( JNIEnv* env, JAWT* awt, jobject window, Display** display_return ); + + +//---- JNI methods ------------------------------------------------------------ + +/** + * Send _NET_WM_MOVERESIZE to window to initiate moving or resizing. + * + * https://specifications.freedesktop.org/wm-spec/wm-spec-latest.html#idm45446104441728 + * https://gitlab.gnome.org/GNOME/gtk/-/blob/main/gdk/x11/gdksurface-x11.c#L3841-3881 + */ +extern "C" +JNIEXPORT jboolean JNICALL Java_com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_xMoveOrResizeWindow + ( JNIEnv *env, jclass cls, jobject window, jint x, jint y, jint direction ) +{ + return sendEvent( env, window, + "_NET_WM_MOVERESIZE", + x, + y, + direction, + Button1, // left mouse button + 1 ); // source indication +} + +/** + * Send _GTK_SHOW_WINDOW_MENU to window to show system window menu. + * + * https://docs.gtk.org/gdk3/method.Window.show_window_menu.html + * https://gitlab.gnome.org/GNOME/gtk/-/blob/main/gdk/x11/gdksurface-x11.c#L4751-4801 + */ +extern "C" +JNIEXPORT jboolean JNICALL Java_com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_xShowWindowMenu + ( JNIEnv *env, jclass cls, jobject window, jint x, jint y ) +{ + // TODO pass useful value for (input?) devide id ? + // + // not used in Mutter and Metacity window manager (but maybe in other WMs?): + // https://github.com/GNOME/mutter/blob/5e5480e620ed5b307902d913f89f5937cc01a28f/src/x11/window-x11.c#L3437 + // https://github.com/GNOME/metacity/blob/7c1cc3ca1d8131499b9cf2ef50b295602ffd6112/src/core/window.c#L5699 + // not used in KWin: + // https://github.com/KDE/kwin/blob/7e1617c2808b7c9b23a8c786327fc88212e10b32/src/netinfo.cpp#L222 + + return sendEvent( env, window, + "_GTK_SHOW_WINDOW_MENU", + 0, //TODO device id + x, + y, + 0, + 0 ); +} + +bool sendEvent( JNIEnv *env, jobject window, const char *atom_name, + long data0, long data1, long data2, long data3, long data4 ) +{ + // get the AWT + JAWT awt; + awt.version = JAWT_VERSION_1_4; + if( !JAWT_GetAWT( env, &awt ) ) + return false; + + // get Xlib window and display from AWT window + Display* display; + Window w = getWindowHandle( env, &awt, window, &display ); + if( w == 0 ) + return false; + + awt.Lock( env ); + + Window rootWindow = XDefaultRootWindow( display ); + + // check whether window manager supports message + Atom atom = XInternAtom( display, atom_name, false ); + if( !isWMHintSupported( display, rootWindow, atom ) ) { + awt.Unlock( env ); + return false; + } + + // ungrab (mouse) pointer and keyboard to allow the window manager to grab them + XUngrabPointer( display, CurrentTime ); + XUngrabKeyboard( display, CurrentTime ); + + // build event structure + XClientMessageEvent xclient = { 0 }; + xclient.type = ClientMessage; + xclient.window = w; + xclient.message_type = atom; + xclient.format = 32; + xclient.data.l[0] = data0; + xclient.data.l[1] = data1; + xclient.data.l[2] = data2; + xclient.data.l[3] = data3; + xclient.data.l[4] = data4; + + // send event + XSendEvent( display, rootWindow, False, + SubstructureRedirectMask | SubstructureNotifyMask, + (XEvent*) &xclient ); + + awt.Unlock( env ); + return true; +} + + +bool isWMHintSupported( Display* display, Window rootWindow, Atom atom ) { + Atom type; + int format; + unsigned long n_atoms; + unsigned long bytes_after; + Atom* atoms; + + // get all supported hints + XGetWindowProperty( display, rootWindow, + XInternAtom( display, "_NET_SUPPORTED", false ), + 0, 0xffff, False, XA_ATOM, + &type, &format, &n_atoms, &bytes_after, (unsigned char**) &atoms ); + + if( atoms == NULL ) + return false; + + if( type != XA_ATOM ) { + XFree( atoms ); + return false; + } + + bool supported = false; + for( int i = 0; i < n_atoms; i++ ) { + if( atoms[i] == atom ) { + supported = true; + break; + } + } + + XFree( atoms ); + return supported; +} + +Window getWindowHandle( JNIEnv* env, JAWT* awt, jobject window, Display** display_return ) { + jawt_DrawingSurface* ds = awt->GetDrawingSurface( env, window ); + if( ds == NULL ) + return 0; + + jint lock = ds->Lock( ds ); + if( (lock & JAWT_LOCK_ERROR) != 0 ) { + awt->FreeDrawingSurface( ds ); + return 0; + } + + JAWT_DrawingSurfaceInfo* dsi = ds->GetDrawingSurfaceInfo( ds ); + JAWT_X11DrawingSurfaceInfo* xdsi = (JAWT_X11DrawingSurfaceInfo*) dsi->platformInfo; + + Window handle = xdsi->drawable; + *display_return = xdsi->display; + + ds->FreeDrawingSurfaceInfo( dsi ); + ds->Unlock( ds ); + awt->FreeDrawingSurface( ds ); + + return handle; +} diff --git a/flatlaf-natives/flatlaf-natives-linux/src/main/headers/com_formdev_flatlaf_ui_FlatNativeLinuxLibrary.h b/flatlaf-natives/flatlaf-natives-linux/src/main/headers/com_formdev_flatlaf_ui_FlatNativeLinuxLibrary.h new file mode 100644 index 00000000..c3b8c4b2 --- /dev/null +++ b/flatlaf-natives/flatlaf-natives-linux/src/main/headers/com_formdev_flatlaf_ui_FlatNativeLinuxLibrary.h @@ -0,0 +1,31 @@ +/* DO NOT EDIT THIS FILE - it is machine generated */ +#include +/* Header for class com_formdev_flatlaf_ui_FlatNativeLinuxLibrary */ + +#ifndef _Included_com_formdev_flatlaf_ui_FlatNativeLinuxLibrary +#define _Included_com_formdev_flatlaf_ui_FlatNativeLinuxLibrary +#ifdef __cplusplus +extern "C" { +#endif +#undef com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_MOVE +#define com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_MOVE 8L +/* + * Class: com_formdev_flatlaf_ui_FlatNativeLinuxLibrary + * Method: xMoveOrResizeWindow + * Signature: (Ljava/awt/Window;III)Z + */ +JNIEXPORT jboolean JNICALL Java_com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_xMoveOrResizeWindow + (JNIEnv *, jclass, jobject, jint, jint, jint); + +/* + * Class: com_formdev_flatlaf_ui_FlatNativeLinuxLibrary + * Method: xShowWindowMenu + * Signature: (Ljava/awt/Window;II)Z + */ +JNIEXPORT jboolean JNICALL Java_com_formdev_flatlaf_ui_FlatNativeLinuxLibrary_xShowWindowMenu + (JNIEnv *, jclass, jobject, jint, jint); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/flatlaf-natives/flatlaf-natives-windows/build.gradle.kts b/flatlaf-natives/flatlaf-natives-windows/build.gradle.kts index 2a47c790..99acaf2a 100644 --- a/flatlaf-natives/flatlaf-natives-windows/build.gradle.kts +++ b/flatlaf-natives/flatlaf-natives-windows/build.gradle.kts @@ -16,22 +16,19 @@ plugins { `cpp-library` + `flatlaf-cpp-library` + `flatlaf-jni-headers` +} + +flatlafJniHeaders { + headers = listOf( + "com_formdev_flatlaf_ui_FlatWindowsNativeWindowBorder.h", + "com_formdev_flatlaf_ui_FlatWindowsNativeWindowBorder_WndProc.h" + ) } library { targetMachines.set( listOf( machines.windows.x86, machines.windows.x86_64 ) ) - - // disable debuggable for release builds to make shared libraries smaller - binaries.configureEach( CppSharedLibrary::class ) { - with( compileTask.get() ) { - if( name.contains( "Release" ) ) - isDebuggable = false - } - with( linkTask.get() ) { - if( name.contains( "Release" ) ) - debuggable.set( false ) - } - } } var javaHome = System.getProperty( "java.home" ) @@ -43,36 +40,15 @@ tasks { group = "build" description = "Builds natives" - dependsOn( "linkReleaseX86", "linkReleaseX86-64" ) + if( org.gradle.internal.os.OperatingSystem.current().isWindows() ) + dependsOn( "linkReleaseX86", "linkReleaseX86-64" ) } withType().configureEach { onlyIf { name.contains( "Release" ) } - // depend on :flatlaf-core:compileJava because it generates the JNI headers - dependsOn( ":flatlaf-core:compileJava" ) - - doFirst { - println( "Used Tool Chain:" ) - println( " - ${toolChain.get()}" ) - println( "Available Tool Chains:" ) - toolChains.forEach { - println( " - $it" ) - } - - // copy needed JNI headers - copy { - from( project( ":flatlaf-core" ).buildDir.resolve( "generated/jni-headers" ) ) - into( "src/main/headers" ) - include( - "com_formdev_flatlaf_ui_FlatWindowsNativeWindowBorder.h", - "com_formdev_flatlaf_ui_FlatWindowsNativeWindowBorder_WndProc.h" - ) - filter( - "eol" to org.apache.tools.ant.filters.FixCrLfFilter.CrLf.newInstance( "lf" ) - ) - } - } + // generate and copy needed JNI headers + dependsOn( "jni-headers" ) includes.from( "${javaHome}/include", 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 e605dd3c..aeec1663 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 @@ -30,6 +30,7 @@ import com.formdev.flatlaf.FlatClientProperties; import com.formdev.flatlaf.extras.components.FlatTriStateCheckBox; import com.formdev.flatlaf.ui.FlatUIUtils; import com.formdev.flatlaf.util.MultiResolutionImageSupport; +import com.formdev.flatlaf.util.SystemInfo; import net.miginfocom.swing.*; /** @@ -40,6 +41,12 @@ public class FlatWindowDecorationsTest { public static void main( String[] args ) { SwingUtilities.invokeLater( () -> { + if( SystemInfo.isLinux ) { + // enable custom window decorations + JFrame.setDefaultLookAndFeelDecorated( true ); + JDialog.setDefaultLookAndFeelDecorated( true ); + } + FlatTestFrame frame = FlatTestFrame.create( args, "FlatWindowDecorationsTest" ); frame.applyComponentOrientationToFrame = true; diff --git a/settings.gradle.kts b/settings.gradle.kts index 767617af..0ff4adda 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -26,7 +26,9 @@ include( "flatlaf-testing" ) include( "flatlaf-theme-editor" ) includeProject( "flatlaf-natives-windows", "flatlaf-natives/flatlaf-natives-windows" ) -includeProject( "flatlaf-natives-jna", "flatlaf-natives/flatlaf-natives-jna" ) +includeProject( "flatlaf-natives-linux", "flatlaf-natives/flatlaf-natives-linux" ) +includeProject( "flatlaf-natives-jna", "flatlaf-natives/flatlaf-natives-jna" ) + includeProject( "flatlaf-testing-modular-app", "flatlaf-testing/flatlaf-testing-modular-app" ) fun includeProject( projectPath: String, projectDir: String ) { From 0baae7da8b59f6e635841ccd71b5952f1746c20d Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Sat, 20 Aug 2022 23:44:41 +0200 Subject: [PATCH 2/5] Linux: load `jawt.so` explicitly before loading FlatLaf native library to fix `UnsatisfiedLinkError: ... libjawt.so: cannot open shared object file ...` (issue #482) --- .../formdev/flatlaf/ui/FlatNativeLibrary.java | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeLibrary.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeLibrary.java index 3bfd3064..e486b7a8 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeLibrary.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeLibrary.java @@ -55,11 +55,25 @@ class FlatNativeLibrary libraryName += "_64"; // load jawt native library - loadJAWT(); + if( !SystemInfo.isJava_9_orLater ) { + // In Java 8, load jawt.dll (part of JRE) explicitly because it + // is not found when running application with /bin/java.exe. + // When using /jre/bin/java.exe, it is found. + // jawt.dll is located in /jre/bin/. + // Java 9 and later do not have this problem. + loadJAWT(); + } } else if( SystemInfo.isLinux && SystemInfo.isX86_64 ) { // Linux: requires x86_64 libraryName = "flatlaf-linux-x86_64"; + + // Load jawt.so (part of JRE) explicitly because it is not found + // in all Java versions/distributions. + // E.g. not found in Java 13 and later from openjdk.java.net. + // There seems to be also differences between distributions. + // E.g. Adoptium Java 17 does not need this, but Java 17 from openjdk.java.net does. + loadJAWT(); } else return; // no native library available for current OS or CPU architecture @@ -81,14 +95,6 @@ class FlatNativeLibrary } private static void loadJAWT() { - if( SystemInfo.isJava_9_orLater ) - return; - - // In Java 8, load jawt.dll (part of JRE) explicitly because it - // is not found when running application with /bin/java.exe. - // When using /jre/bin/java.exe, it is found. - // jawt.dll is located in /jre/bin/. - // Java 9 and later do not have this problem. try { System.loadLibrary( "jawt" ); } catch( UnsatisfiedLinkError ex ) { From 218ea6ce4798dc02f75a62cab676dd862ba53165 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Sun, 21 Aug 2022 17:24:50 +0200 Subject: [PATCH 3/5] Linux: fixed double-click on title bar to maximize/restore (issue #482) --- .../com/formdev/flatlaf/ui/FlatTitlePane.java | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 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 d0707971..01ed07f9 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 @@ -32,6 +32,7 @@ import java.awt.Image; import java.awt.Insets; import java.awt.Point; import java.awt.Rectangle; +import java.awt.Toolkit; import java.awt.Window; import java.awt.event.ActionListener; import java.awt.event.ComponentEvent; @@ -1167,6 +1168,7 @@ debug*/ private Point dragOffset; private boolean nativeMove; + private long lastSingleClickWhen; @Override public void mouseClicked( MouseEvent e ) { @@ -1209,22 +1211,33 @@ debug*/ // on Linux, move or maximize/restore window if( SystemInfo.isLinux && FlatNativeLinuxLibrary.isWMUtilsSupported( window ) ) { - switch( e.getClickCount() ) { + // double-click is not always recognized in Java when using _NET_WM_MOVERESIZE message + int clickCount = e.getClickCount(); + if( clickCount == 1 && (e.getWhen() - lastSingleClickWhen) <= getMultiClickInterval() ) + clickCount = 2; + + switch( clickCount ) { case 1: - // move window via _NET_WM_MOVERESIZE event + // move window via _NET_WM_MOVERESIZE message e.consume(); nativeMove = FlatNativeLinuxLibrary.moveOrResizeWindow( window, e, FlatNativeLinuxLibrary.MOVE ); + lastSingleClickWhen = e.getWhen(); break; case 2: // maximize/restore on double-click - // also done here because no mouse clicked event is sent when using _NET_WM_MOVERESIZE event + // also done here because no mouse clicked event is sent when using _NET_WM_MOVERESIZE message maximizeOrRestore(); break; } } } + private int getMultiClickInterval() { + Object value = Toolkit.getDefaultToolkit().getDesktopProperty( "awt.multiClickInterval" ); + return (value instanceof Integer) ? (Integer) value : 200; + } + @Override public void mouseReleased( MouseEvent e ) {} @Override public void mouseEntered( MouseEvent e ) {} @Override public void mouseExited( MouseEvent e ) {} From 9f0554c883759f2133d0eca47e77a2ed1432b972 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Sun, 21 Aug 2022 17:35:44 +0200 Subject: [PATCH 4/5] Linux: added libflatlaf-linux-x86_64.so (issue #482) built by GitHub Actions: https://github.com/JFormDesigner/FlatLaf/actions/runs/2898994332 --- .../flatlaf/natives/libflatlaf-linux-x86_64.so | Bin 0 -> 17064 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/libflatlaf-linux-x86_64.so diff --git a/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/libflatlaf-linux-x86_64.so b/flatlaf-core/src/main/resources/com/formdev/flatlaf/natives/libflatlaf-linux-x86_64.so new file mode 100644 index 0000000000000000000000000000000000000000..9942ff5ca6c398c5996908eb3479a1444a9f5b04 GIT binary patch literal 17064 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 literal 0 HcmV?d00001 From bf4d4cc2c51894633d328ae4b9d14670d4d56743 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Sun, 21 Aug 2022 19:49:41 +0200 Subject: [PATCH 5/5] Linux: fixed double-click on title bar to maximize/restore on Ubuntu 22.04 (issue #482) --- .../com/formdev/flatlaf/ui/FlatTitlePane.java | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 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 01ed07f9..f7ca89ae 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 @@ -1175,8 +1175,14 @@ debug*/ // on Linux, when using native library, the mouse clicked event // is usually not sent and maximize/restore is done in mouse pressed event // this check is here for the case that a mouse clicked event comes thru for some reason - if( SystemInfo.isLinux && FlatNativeLinuxLibrary.isWMUtilsSupported( window ) ) + if( SystemInfo.isLinux && FlatNativeLinuxLibrary.isWMUtilsSupported( window ) ) { + // see comment in mousePressed() + if( lastSingleClickWhen != 0 && (e.getWhen() - lastSingleClickWhen) <= getMultiClickInterval() ) { + lastSingleClickWhen = 0; + maximizeOrRestore(); + } return; + } if( e.getClickCount() == 2 && SwingUtilities.isLeftMouseButton( e ) ) { if( e.getSource() == iconLabel ) { @@ -1211,9 +1217,16 @@ debug*/ // on Linux, move or maximize/restore window if( SystemInfo.isLinux && FlatNativeLinuxLibrary.isWMUtilsSupported( window ) ) { + // The fired Java mouse events, when doing a double-click and the first click + // sends a _NET_WM_MOVERESIZE message, are different for various Linux distributions: + // CentOS 7 (GNOME 3.28.2, X11): PRESSED(clickCount=1) PRESSED(clickCount=2) RELEASED(clickCount=2) + // Ubuntu 20.04 (GNOME 3.36.1, X11): PRESSED(clickCount=1) PRESSED(clickCount=2) RELEASED(clickCount=2) + // Ubuntu 22.04 (GNOME 42.2, Wayland): PRESSED(clickCount=1) RELEASED(clickCount=1) CLICKED(clickCount=1) + // Kubuntu 22.04 (KDE 5.24.4, X11): PRESSED(clickCount=1) PRESSED(clickCount=1) RELEASED(clickCount=1) + // double-click is not always recognized in Java when using _NET_WM_MOVERESIZE message int clickCount = e.getClickCount(); - if( clickCount == 1 && (e.getWhen() - lastSingleClickWhen) <= getMultiClickInterval() ) + if( clickCount == 1 && lastSingleClickWhen != 0 && (e.getWhen() - lastSingleClickWhen) <= getMultiClickInterval() ) clickCount = 2; switch( clickCount ) { @@ -1227,6 +1240,7 @@ debug*/ case 2: // maximize/restore on double-click // also done here because no mouse clicked event is sent when using _NET_WM_MOVERESIZE message + lastSingleClickWhen = 0; maximizeOrRestore(); break; } @@ -1235,7 +1249,7 @@ debug*/ private int getMultiClickInterval() { Object value = Toolkit.getDefaultToolkit().getDesktopProperty( "awt.multiClickInterval" ); - return (value instanceof Integer) ? (Integer) value : 200; + return (value instanceof Integer) ? (Integer) value : 500; } @Override public void mouseReleased( MouseEvent e ) {}