Skip to content

Commit

Permalink
Linux: use X11 window manager events to move window and to show windo…
Browse files Browse the repository at this point in the history
…w menu (right-click on window title bar), if custom window decorations are enabled (issue #482)
  • Loading branch information
DevCharly committed Aug 20, 2022
1 parent 16f3f9e commit fb4576f
Show file tree
Hide file tree
Showing 18 changed files with 769 additions and 59 deletions.
22 changes: 14 additions & 8 deletions .github/workflows/natives.yml
Expand Up @@ -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
Expand All @@ -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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Expand Up @@ -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
Expand Down
46 changes: 46 additions & 0 deletions 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<CppCompile>().configureEach {
doFirst {
println( "Used Tool Chain:" )
println( " - ${toolChain.get()}" )
println( "Available Tool Chains:" )
toolChains.forEach {
println( " - $it" )
}
}
}
}
36 changes: 36 additions & 0 deletions 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<String> = emptyList()
}

val extension = project.extensions.create<JniHeadersExtension>( "flatlafJniHeaders" )


tasks {
register<Copy>( "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<org.apache.tools.ant.filters.FixCrLfFilter>(
"eol" to org.apache.tools.ant.filters.FixCrLfFilter.CrLf.newInstance( "lf" )
)
}
}
Expand Up @@ -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

Expand Down
@@ -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.
* <p>
* <b>Note</b>: 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());
}
}
Expand Up @@ -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 );
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -1154,23 +1166,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();
}
}
}
Expand All @@ -1180,10 +1192,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 ) {}
Expand All @@ -1194,9 +1233,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;

Expand Down
Expand Up @@ -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;
}
}
Expand Down
1 change: 1 addition & 0 deletions flatlaf-natives/README.md
Expand Up @@ -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)
4 changes: 2 additions & 2 deletions flatlaf-natives/flatlaf-natives-jna/build.gradle.kts
Expand Up @@ -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" )
}

0 comments on commit fb4576f

Please sign in to comment.