From ca58440453b8b70b4247a65f5b26d7bbb9f1a6bd Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Tue, 19 Jul 2022 20:22:35 +0200 Subject: [PATCH] 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/ci.yml | 1 + .github/workflows/natives.yml | 22 +- CHANGELOG.md | 9 + .../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 +- 19 files changed, 776 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/ci.yml b/.github/workflows/ci.yml index 16bf57122..ac8a126d2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,6 +15,7 @@ on: jobs: build: runs-on: ubuntu-latest + if: false strategy: matrix: diff --git a/.github/workflows/natives.yml b/.github/workflows/natives.yml index 8d4c91b89..4b5ff69d9 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 f38de045b..1792cc7e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,15 @@ FlatLaf Change Log ================== +## 2.5-SNAPSHOT + +#### 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) + + ## 2.4 #### New features and improvements 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 000000000..abacf9ec9 --- /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 000000000..4219f4c4b --- /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 9462f910b..3bfd30647 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 @@ private static void initialize() { // 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 000000000..b5cfa333e --- /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 48301ab93..6ed351382 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 @@ protected void frameStateChanged() { 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 @@ protected void restore() { } } + 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. */ @@ -1148,23 +1160,23 @@ public void windowStateChanged( WindowEvent e ) { //---- 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(); } } } @@ -1174,10 +1186,37 @@ public void mousePressed( MouseEvent e ) { 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 ) {} @@ -1188,9 +1227,12 @@ public void mousePressed( MouseEvent e ) { @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 018694fb9..7b57f06a7 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 @@ private boolean loadLibraryFromFile( File libraryFile ) { 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 15e8acb86..f41473ee2 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 4cade0e8d..03fcc59a9 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 000000000..0363b6626 --- /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 000000000..3dd00c68a --- /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 000000000..4a1fb4bf8 --- /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( )//"-O2" ) + 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 000000000..8cbc57a97 --- /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 000000000..c3b8c4b25 --- /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 2a47c7900..99acaf2ab 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 c7e2937f5..cc24dd2e1 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.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 767617afb..0ff4adda0 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 ) {