From 6f32236fb7f61eb5410ed2b8bce6dca551cd2256 Mon Sep 17 00:00:00 2001 From: Karl Tauber Date: Sat, 9 Dec 2023 16:12:35 +0100 Subject: [PATCH] macOS: native rounded borders for popups (issue #715) --- .github/workflows/natives.yml | 1 + CHANGELOG.md | 5 + flatlaf-core/build.gradle.kts | 10 +- .../formdev/flatlaf/FlatClientProperties.java | 31 ++++- .../formdev/flatlaf/ui/FlatLineBorder.java | 4 + .../formdev/flatlaf/ui/FlatNativeLibrary.java | 6 + .../flatlaf/ui/FlatNativeLinuxLibrary.java | 6 + .../flatlaf/ui/FlatNativeMacLibrary.java | 59 ++++++++ .../formdev/flatlaf/ui/FlatPopupFactory.java | 78 ++++++++--- .../formdev/flatlaf/FlatDarkLaf.properties | 1 + .../com/formdev/flatlaf/FlatLaf.properties | 4 + .../flatlaf-natives-macos/build.gradle.kts | 129 ++++++++++++++++++ .../src/main/headers/JNFRunLoop.h | 45 ++++++ .../src/main/headers/JNIUtils.h | 41 ++++++ ..._formdev_flatlaf_ui_FlatNativeMacLibrary.h | 29 ++++ .../src/main/objcpp/JNFRunLoop.mm | 76 +++++++++++ .../src/main/objcpp/MacWindow.mm | 90 ++++++++++++ .../flatlaf-natives-windows/build.gradle.kts | 6 +- .../uidefaults/FlatDarkLaf_1.8.0-mac.txt | 16 +++ .../uidefaults/FlatLightLaf_1.8.0-mac.txt | 16 +++ .../flatlaf/testing/FlatPopupTest.java | 12 +- .../flatlaf/themeeditor/FlatLafUIKeys.txt | 4 + settings.gradle.kts | 1 + 23 files changed, 639 insertions(+), 31 deletions(-) create mode 100644 flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeMacLibrary.java create mode 100644 flatlaf-natives/flatlaf-natives-macos/build.gradle.kts create mode 100644 flatlaf-natives/flatlaf-natives-macos/src/main/headers/JNFRunLoop.h create mode 100644 flatlaf-natives/flatlaf-natives-macos/src/main/headers/JNIUtils.h create mode 100644 flatlaf-natives/flatlaf-natives-macos/src/main/headers/com_formdev_flatlaf_ui_FlatNativeMacLibrary.h create mode 100644 flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/JNFRunLoop.mm create mode 100644 flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacWindow.mm diff --git a/.github/workflows/natives.yml b/.github/workflows/natives.yml index 060b431d..be0b57d0 100644 --- a/.github/workflows/natives.yml +++ b/.github/workflows/natives.yml @@ -20,6 +20,7 @@ jobs: matrix: os: - windows + - macos - ubuntu runs-on: ${{ matrix.os }}-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index fe889b12..10140bea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ FlatLaf Change Log #### New features and improvements +- macOS (10.14+): Popups (`JPopupMenu`, `JComboBox`, `JToolTip`, etc.) now use + native macOS rounded borders. (PR #772; issue #715) +- Native libraries: Added `libflatlaf-macos-arm64.dylib` and + `libflatlaf-macos-x86_64.dylib`. See also + https://www.formdev.com/flatlaf/native-libraries/. - ToolBar: Added styling properties `separatorWidth` and `separatorColor`. #### Fixed bugs diff --git a/flatlaf-core/build.gradle.kts b/flatlaf-core/build.gradle.kts index 5bb3fc49..eca0e2dd 100644 --- a/flatlaf-core/build.gradle.kts +++ b/flatlaf-core/build.gradle.kts @@ -127,9 +127,11 @@ flatlafPublish { val natives = "src/main/resources/com/formdev/flatlaf/natives" nativeArtifacts = listOf( - NativeArtifact( "${natives}/flatlaf-windows-x86.dll", "windows-x86", "dll" ), - NativeArtifact( "${natives}/flatlaf-windows-x86_64.dll", "windows-x86_64", "dll" ), - NativeArtifact( "${natives}/flatlaf-windows-arm64.dll", "windows-arm64", "dll" ), - NativeArtifact( "${natives}/libflatlaf-linux-x86_64.so", "linux-x86_64", "so" ), + NativeArtifact( "${natives}/flatlaf-windows-x86.dll", "windows-x86", "dll" ), + NativeArtifact( "${natives}/flatlaf-windows-x86_64.dll", "windows-x86_64", "dll" ), + NativeArtifact( "${natives}/flatlaf-windows-arm64.dll", "windows-arm64", "dll" ), + NativeArtifact( "${natives}/libflatlaf-macos-arm64.dylib", "macos-arm64", "dylib" ), + NativeArtifact( "${natives}/libflatlaf-macos-x86_64.dylib", "macos-x86_64", "dylib" ), + NativeArtifact( "${natives}/libflatlaf-linux-x86_64.so", "linux-x86_64", "so" ), ) } diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatClientProperties.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatClientProperties.java index 1aad7959..863404c0 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatClientProperties.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/FlatClientProperties.java @@ -278,12 +278,13 @@ public interface FlatClientProperties *

* Note that this is not available on all platforms since it requires special support. * Supported platforms: - *

- * Windows 11 (x86 or x86_64): Only two corner radiuses are supported - * by the OS: {@code DWMWCP_ROUND} is 8px and {@code DWMWCP_ROUNDSMALL} is 4px. - * If this value is {@code 1 - 4}, then {@code DWMWCP_ROUNDSMALL} is used. - * If it is {@code >= 5}, then {@code DWMWCP_ROUND} is used. - *

+ *

* Component {@link javax.swing.JComponent}
* Value type {@link java.lang.Integer}
* @@ -291,6 +292,24 @@ public interface FlatClientProperties */ String POPUP_BORDER_CORNER_RADIUS = "Popup.borderCornerRadius"; + /** + * Specifies the popup rounded border width if the component is shown in a popup + * or if the component is the owner of another component that is shown in a popup. + *

+ * Only used if popup uses rounded border. + *

+ * Note that this is not available on all platforms since it requires special support. + * Supported platforms: + *

+ * Component {@link javax.swing.JComponent}
+ * Value type {@link java.lang.Integer} or {@link java.lang.Float}
+ * + * @since 3.3 + */ + String POPUP_ROUNDED_BORDER_WIDTH = "Popup.roundedBorderWidth"; + /** * Specifies whether a drop shadow is painted if the component is shown in a popup * or if the component is the owner of another component that is shown in a popup. diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatLineBorder.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatLineBorder.java index fea886d8..888f2cb2 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatLineBorder.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatLineBorder.java @@ -22,6 +22,7 @@ import java.awt.Component; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Insets; +import javax.swing.JComponent; /** * Line border for various components. @@ -66,6 +67,9 @@ public class FlatLineBorder @Override public void paintBorder( Component c, Graphics g, int x, int y, int width, int height ) { + if( c instanceof JComponent && ((JComponent)c).getClientProperty( FlatPopupFactory.KEY_POPUP_USES_NATIVE_BORDER ) != null ) + return; + Graphics2D g2 = (Graphics2D) g.create(); try { FlatUIUtils.setRenderingHints( g2 ); 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 e977cae8..b119a4f7 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 @@ -81,6 +81,12 @@ class FlatNativeLibrary // Instead flatlaf.dll dynamically loads jawt.dll when first used, // which is guaranteed after AWT initialization. + } else if( SystemInfo.isMacOS_10_14_Mojave_orLater && (SystemInfo.isAARCH64 || SystemInfo.isX86_64) ) { + // macOS: requires macOS 10.14 or later (arm64 or x86_64) + + classifier = SystemInfo.isAARCH64 ? "macos-arm64" : "macos-x86_64"; + ext = "dylib"; + } else if( SystemInfo.isLinux && SystemInfo.isX86_64 ) { // Linux: requires x86_64 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 index 8a884d4e..b60922ec 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeLinuxLibrary.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeLinuxLibrary.java @@ -35,6 +35,12 @@ import com.formdev.flatlaf.util.SystemInfo; */ class FlatNativeLinuxLibrary { + /** + * Checks whether native library is loaded/available. + *

+ * Note: It is required to invoke this method before invoking any other + * method of this class. Otherwise, the native library may not be loaded. + */ static boolean isLoaded() { return SystemInfo.isLinux && FlatNativeLibrary.isLoaded(); } diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeMacLibrary.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeMacLibrary.java new file mode 100644 index 00000000..2a1b24c3 --- /dev/null +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatNativeMacLibrary.java @@ -0,0 +1,59 @@ +/* + * Copyright 2023 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.Window; + +/** + * Native methods for macOS. + *

+ * Note: This is private API. Do not use! + * + * @author Karl Tauber + * @since 3.3 + */ +public class FlatNativeMacLibrary +{ + /** + * Checks whether native library is loaded/available. + *

+ * Note: It is required to invoke this method before invoking any other + * method of this class. Otherwise, the native library may not be loaded. + */ + public static boolean isLoaded() { + return FlatNativeLibrary.isLoaded(); + } + + /** + * Gets the macOS window pointer (NSWindow) for the given Swing window. + *

+ * Note that the underlying macOS window must be already created, + * otherwise this method returns zero. Use following to ensure this: + *

{@code
+	 * if( !window.isDisplayable() )
+	 *     window.addNotify();
+	 * }
+ * or invoke this method after packing the window. E.g. + *
{@code
+	 * window.pack();
+	 * long windowPtr = getWindowPtr( window );
+	 * }
+ */ + public native static long getWindowPtr( Window window ); + + public native static void setWindowRoundedBorder( long windowPtr, float radius, float borderWidth, int borderColor ); +} diff --git a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatPopupFactory.java b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatPopupFactory.java index 211336de..8aa636f0 100644 --- a/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatPopupFactory.java +++ b/flatlaf-core/src/main/java/com/formdev/flatlaf/ui/FlatPopupFactory.java @@ -71,6 +71,8 @@ import com.formdev.flatlaf.util.UIScale; public class FlatPopupFactory extends PopupFactory { + static final String KEY_POPUP_USES_NATIVE_BORDER = "FlatLaf.internal.FlatPopupFactory.popupUsesNativeBorder"; + private MethodHandle java8getPopupMethod; private MethodHandle java9getPopupMethod; @@ -92,17 +94,20 @@ public class FlatPopupFactory return new NonFlashingPopup( getPopupForScreenOfOwner( owner, contents, x, y, forceHeavyWeight ), contents ); // macOS and Linux adds drop shadow to heavy weight popups - if( SystemInfo.isMacOS || SystemInfo.isLinux ) - return new NonFlashingPopup( getPopupForScreenOfOwner( owner, contents, x, y, true ), contents ); + if( SystemInfo.isMacOS || SystemInfo.isLinux ) { + NonFlashingPopup popup = new NonFlashingPopup( getPopupForScreenOfOwner( owner, contents, x, y, true ), contents ); + if( popup.popupWindow != null && SystemInfo.isMacOS && FlatNativeMacLibrary.isLoaded() ) + setupRoundedBorder( popup.popupWindow, owner, contents ); + return popup; + } // Windows 11 with FlatLaf native library can use rounded corners and shows drop shadow for heavy weight popups - int borderCornerRadius; if( isWindows11BorderSupported() && - (borderCornerRadius = getBorderCornerRadius( owner, contents )) > 0 ) + getBorderCornerRadius( owner, contents ) > 0 ) { NonFlashingPopup popup = new NonFlashingPopup( getPopupForScreenOfOwner( owner, contents, x, y, true ), contents ); if( popup.popupWindow != null ) - setupWindows11Border( popup.popupWindow, contents, borderCornerRadius ); + setupRoundedBorder( popup.popupWindow, owner, contents ); return popup; } @@ -197,7 +202,7 @@ public class FlatPopupFactory } } - private boolean isOptionEnabled( Component owner, Component contents, String clientKey, String uiKey ) { + private static boolean isOptionEnabled( Component owner, Component contents, String clientKey, String uiKey ) { Object value = getOption( owner, contents, clientKey, uiKey ); return (value instanceof Boolean) ? (Boolean) value : false; } @@ -210,7 +215,7 @@ public class FlatPopupFactory *
  • UI property {@code uiKey} * */ - private Object getOption( Component owner, Component contents, String clientKey, String uiKey ) { + private static Object getOption( Component owner, Component contents, String clientKey, String uiKey ) { for( Component c : new Component[] { owner, contents } ) { if( c instanceof JComponent ) { Object value = ((JComponent)c).getClientProperty( clientKey ); @@ -314,21 +319,22 @@ public class FlatPopupFactory return SystemInfo.isWindows_11_orLater && FlatNativeWindowsLibrary.isLoaded(); } - private static void setupWindows11Border( Window popupWindow, Component contents, int borderCornerRadius ) { - // make sure that the Windows 11 window is created + private static void setupRoundedBorder( Window popupWindow, Component owner, Component contents ) { + // make sure that the native window is created if( !popupWindow.isDisplayable() ) popupWindow.addNotify(); - // get window handle - long hwnd = FlatNativeWindowsLibrary.getHWND( popupWindow ); + // get native window handle/pointer + long hwnd = SystemInfo.isWindows + ? FlatNativeWindowsLibrary.getHWND( popupWindow ) + : FlatNativeMacLibrary.getWindowPtr( popupWindow ); + if( hwnd == 0 ) + return; - // set corner preference - int cornerPreference = (borderCornerRadius <= 4) - ? FlatNativeWindowsLibrary.DWMWCP_ROUNDSMALL // 4px - : FlatNativeWindowsLibrary.DWMWCP_ROUND; // 8px - FlatNativeWindowsLibrary.setWindowCornerPreference( hwnd, cornerPreference ); + int borderCornerRadius = getBorderCornerRadius( owner, contents ); + float borderWidth = getRoundedBorderWidth( owner, contents ); - // set border color + // get Swing border color Color borderColor = null; // use system default color if( contents instanceof JComponent ) { Border border = ((JComponent)contents).getBorder(); @@ -341,8 +347,28 @@ public class FlatPopupFactory borderColor = ((LineBorder)border).getLineColor(); else if( border instanceof EmptyBorder ) borderColor = FlatNativeWindowsLibrary.COLOR_NONE; // do not paint border + + // avoid that FlatLineBorder paints the Swing border + ((JComponent)contents).putClientProperty( KEY_POPUP_USES_NATIVE_BORDER, true ); + } + + if( SystemInfo.isWindows ) { + // set corner preference + int cornerPreference = (borderCornerRadius <= 4) + ? FlatNativeWindowsLibrary.DWMWCP_ROUNDSMALL // 4px + : FlatNativeWindowsLibrary.DWMWCP_ROUND; // 8px + FlatNativeWindowsLibrary.setWindowCornerPreference( hwnd, cornerPreference ); + + // set border color + FlatNativeWindowsLibrary.dwmSetWindowAttributeCOLORREF( hwnd, FlatNativeWindowsLibrary.DWMWA_BORDER_COLOR, borderColor ); + } else if( SystemInfo.isMacOS ) { + if( borderColor == null || borderColor == FlatNativeWindowsLibrary.COLOR_NONE ) + borderWidth = 0; + + // set corner radius, border width and color + FlatNativeMacLibrary.setWindowRoundedBorder( hwnd, borderCornerRadius, + borderWidth, (borderColor != null) ? borderColor.getRGB() : 0 ); } - FlatNativeWindowsLibrary.dwmSetWindowAttributeCOLORREF( hwnd, FlatNativeWindowsLibrary.DWMWA_BORDER_COLOR, borderColor ); } private static void resetWindows11Border( Window popupWindow ) { @@ -355,7 +381,7 @@ public class FlatPopupFactory FlatNativeWindowsLibrary.setWindowCornerPreference( hwnd, FlatNativeWindowsLibrary.DWMWCP_DONOTROUND ); } - private int getBorderCornerRadius( Component owner, Component contents ) { + private static int getBorderCornerRadius( Component owner, Component contents ) { String uiKey = (contents instanceof BasicComboPopup) ? "ComboBox.borderCornerRadius" : (contents instanceof JPopupMenu) ? "PopupMenu.borderCornerRadius" : @@ -366,6 +392,17 @@ public class FlatPopupFactory return (value instanceof Integer) ? (Integer) value : 0; } + private static float getRoundedBorderWidth( Component owner, Component contents ) { + String uiKey = + (contents instanceof BasicComboPopup) ? "ComboBox.roundedBorderWidth" : + (contents instanceof JPopupMenu) ? "PopupMenu.roundedBorderWidth" : + (contents instanceof JToolTip) ? "ToolTip.roundedBorderWidth" : + "Popup.roundedBorderWidth"; + + Object value = getOption( owner, contents, FlatClientProperties.POPUP_ROUNDED_BORDER_WIDTH, uiKey ); + return (value instanceof Number) ? ((Number)value).floatValue() : 0; + } + //---- fixes -------------------------------------------------------------- private static boolean overlapsHeavyWeightComponent( Component owner, Component contents, int x, int y ) { @@ -508,6 +545,9 @@ public class FlatPopupFactory @Override public void hide() { + if( contents instanceof JComponent ) + ((JComponent)contents).putClientProperty( KEY_POPUP_USES_NATIVE_BORDER, null ); + if( delegate != null ) { delegate.hide(); delegate = null; diff --git a/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatDarkLaf.properties b/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatDarkLaf.properties index 95e6b3cd..e6a3413d 100644 --- a/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatDarkLaf.properties +++ b/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatDarkLaf.properties @@ -246,6 +246,7 @@ PasswordField.revealIconColor = @foreground #---- Popup ---- +[mac]Popup.roundedBorderWidth = 1 Popup.dropShadowColor = #000 Popup.dropShadowOpacity = 0.25 diff --git a/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLaf.properties b/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLaf.properties index 36763714..2b0124e0 100644 --- a/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLaf.properties +++ b/flatlaf-core/src/main/resources/com/formdev/flatlaf/FlatLaf.properties @@ -289,6 +289,7 @@ ComboBox.popupInsets = 0,0,0,0 ComboBox.selectionInsets = 0,0,0,0 ComboBox.selectionArc = 0 ComboBox.borderCornerRadius = $Popup.borderCornerRadius +[mac]ComboBox.roundedBorderWidth = $Popup.roundedBorderWidth #---- Component ---- @@ -505,6 +506,7 @@ PasswordField.revealIcon = com.formdev.flatlaf.icons.FlatRevealIcon #---- Popup ---- Popup.borderCornerRadius = 4 +[mac]Popup.roundedBorderWidth = 0 Popup.dropShadowPainted = true Popup.dropShadowInsets = -4,-4,4,4 @@ -514,6 +516,7 @@ Popup.dropShadowInsets = -4,-4,4,4 PopupMenu.border = com.formdev.flatlaf.ui.FlatPopupMenuBorder PopupMenu.borderInsets = 4,1,4,1 PopupMenu.borderCornerRadius = $Popup.borderCornerRadius +[mac]PopupMenu.roundedBorderWidth = $Popup.roundedBorderWidth PopupMenu.background = @menuBackground PopupMenu.scrollArrowColor = @buttonArrowColor @@ -902,6 +905,7 @@ ToolTipManager.enableToolTipMode = activeApplication #---- ToolTip ---- ToolTip.borderCornerRadius = $Popup.borderCornerRadius +[mac]ToolTip.roundedBorderWidth = $Popup.roundedBorderWidth #---- Tree ---- diff --git a/flatlaf-natives/flatlaf-natives-macos/build.gradle.kts b/flatlaf-natives/flatlaf-natives-macos/build.gradle.kts new file mode 100644 index 00000000..b6b24745 --- /dev/null +++ b/flatlaf-natives/flatlaf-natives-macos/build.gradle.kts @@ -0,0 +1,129 @@ +/* + * Copyright 2023 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. + */ + +import java.io.FileOutputStream + +val minOsARM64 = "11.0" +val minOsX86_64 = "10.14" + +plugins { + `cpp-library` + `flatlaf-cpp-library` + `flatlaf-jni-headers` +} + +flatlafJniHeaders { + headers = listOf( "com_formdev_flatlaf_ui_FlatNativeMacLibrary.h" ) +} + +library { + targetMachines.set( listOf( + machines.macOS.architecture( "arm64" ), + machines.macOS.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().isMacOsX() ) + dependsOn( "linkReleaseArm64", "linkReleaseX86-64" ) + } + + withType().configureEach { + onlyIf { name.contains( "Release" ) } + + // generate and copy needed JNI headers + dependsOn( "jni-headers" ) + + includes.from( + "${javaHome}/include", + "${javaHome}/include/darwin" + ) + + // compile Objective-C++ sources + source.from( files( "src/main/objcpp" ) + .asFileTree.matching { include( "**/*.mm" ) } ) + + val isARM64 = name.contains( "Arm64" ) + val minOs = if( isARM64 ) minOsARM64 else minOsX86_64 + + compilerArgs.addAll( toolChain.map { + when( it ) { + is Gcc, is Clang -> listOf( "-x", "objective-c++", "-mmacosx-version-min=$minOs" ) + else -> emptyList() + } + } ) + } + + withType().configureEach { + onlyIf { name.contains( "Release" ) } + + val nativesDir = project( ":flatlaf-core" ).projectDir.resolve( "src/main/resources/com/formdev/flatlaf/natives" ) + val isARM64 = name.contains( "Arm64" ) + val minOs = if( isARM64 ) minOsARM64 else minOsX86_64 + val libraryName = if( isARM64 ) "flatlaf-macos-arm64.dylib" else "flatlaf-macos-x86_64.dylib" + + linkerArgs.addAll( toolChain.map { + when( it ) { + is Gcc, is Clang -> listOf( "-lobjc", "-framework", "Cocoa", "-mmacosx-version-min=$minOs" ) + else -> emptyList() + } + } ) + + doLast { + // copy shared library to flatlaf-core resources + copy { + from( linkedFile ) + into( nativesDir ) + rename( "flatlaf-natives-macos.dylib", libraryName ) + } + +///*dump + val dylib = linkedFile.asFile.get() + val dylibDir = dylib.parent + exec { commandLine( "size", dylib ) } + exec { commandLine( "size", "-m", dylib ) } + exec { + commandLine( "objdump", + // commands + "--archive-headers", + "--section-headers", + "--private-headers", + "--reloc", + "--dynamic-reloc", + "--raw-clang-ast", + "--syms", + "--unwind-info", + // options + "--bind", +// "--private-header", + // files + dylib ) + standardOutput = FileOutputStream( "$dylibDir/objdump.txt" ) + } + exec { commandLine( "objdump", "--disassemble-all", dylib ); standardOutput = FileOutputStream( "$dylibDir/disassemble.txt" ) } + exec { commandLine( "objdump", "--full-contents", dylib ); standardOutput = FileOutputStream( "$dylibDir/full-contents.txt" ) } +//dump*/ + } + } +} diff --git a/flatlaf-natives/flatlaf-natives-macos/src/main/headers/JNFRunLoop.h b/flatlaf-natives/flatlaf-natives-macos/src/main/headers/JNFRunLoop.h new file mode 100644 index 00000000..faf73d96 --- /dev/null +++ b/flatlaf-natives/flatlaf-natives-macos/src/main/headers/JNFRunLoop.h @@ -0,0 +1,45 @@ +// from https://github.com/apple/openjdk/blob/xcodejdk14-release/apple/JavaNativeFoundation/JavaNativeFoundation/JNFRunLoop.h + +/* + * Copyright (c) 2009-2020 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * -- + * + * Used to perform selectors and blocks in the Java runloop mode. + */ + +#import + +@interface FlatJNFRunLoop : NSObject { } + ++ (NSString *)javaRunLoopMode; ++ (void)performOnMainThread:(SEL)aSelector on:(id)target withObject:(id)arg waitUntilDone:(BOOL)wait; ++ (void)performOnMainThreadWaiting:(BOOL)waitUntilDone withBlock:(void (^)(void))block; + +@end diff --git a/flatlaf-natives/flatlaf-natives-macos/src/main/headers/JNIUtils.h b/flatlaf-natives/flatlaf-natives-macos/src/main/headers/JNIUtils.h new file mode 100644 index 00000000..8e5be892 --- /dev/null +++ b/flatlaf-natives/flatlaf-natives-macos/src/main/headers/JNIUtils.h @@ -0,0 +1,41 @@ +/* + * Copyright 2023 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. + */ + +#import +#import +#import + +/** + * @author Karl Tauber + */ + + +// from JNFJNI.h +#ifndef jlong_to_ptr +#define jlong_to_ptr(a) ((void *)(uintptr_t)(a)) +#endif + + +#define JNI_COCOA_ENTER() \ + @autoreleasepool { \ + @try { + +#define JNI_COCOA_EXIT() \ + } @catch( NSException *ex ) { \ + NSLog( @"Exception: %@\nReason: %@\nUser Info: %@\nStack: %@", \ + [ex name], [ex reason], [ex userInfo], [ex callStackSymbols] ); \ + } \ + } diff --git a/flatlaf-natives/flatlaf-natives-macos/src/main/headers/com_formdev_flatlaf_ui_FlatNativeMacLibrary.h b/flatlaf-natives/flatlaf-natives-macos/src/main/headers/com_formdev_flatlaf_ui_FlatNativeMacLibrary.h new file mode 100644 index 00000000..f4d55eed --- /dev/null +++ b/flatlaf-natives/flatlaf-natives-macos/src/main/headers/com_formdev_flatlaf_ui_FlatNativeMacLibrary.h @@ -0,0 +1,29 @@ +/* DO NOT EDIT THIS FILE - it is machine generated */ +#include +/* Header for class com_formdev_flatlaf_ui_FlatNativeMacLibrary */ + +#ifndef _Included_com_formdev_flatlaf_ui_FlatNativeMacLibrary +#define _Included_com_formdev_flatlaf_ui_FlatNativeMacLibrary +#ifdef __cplusplus +extern "C" { +#endif +/* + * Class: com_formdev_flatlaf_ui_FlatNativeMacLibrary + * Method: getWindowPtr + * Signature: (Ljava/awt/Window;)J + */ +JNIEXPORT jlong JNICALL Java_com_formdev_flatlaf_ui_FlatNativeMacLibrary_getWindowPtr + (JNIEnv *, jclass, jobject); + +/* + * Class: com_formdev_flatlaf_ui_FlatNativeMacLibrary + * Method: setWindowRoundedBorder + * Signature: (JFFI)V + */ +JNIEXPORT void JNICALL Java_com_formdev_flatlaf_ui_FlatNativeMacLibrary_setWindowRoundedBorder + (JNIEnv *, jclass, jlong, jfloat, jfloat, jint); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/JNFRunLoop.mm b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/JNFRunLoop.mm new file mode 100644 index 00000000..8bd74a00 --- /dev/null +++ b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/JNFRunLoop.mm @@ -0,0 +1,76 @@ +// from https://github.com/apple/openjdk/blob/xcodejdk14-release/apple/JavaNativeFoundation/JavaNativeFoundation/JNFRunLoop.m + +/* + * Copyright (c) 2009-2020 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#import "JNFRunLoop.h" + +#import + + +NSString *JNFRunLoopDidStartNotification = @"JNFRunLoopDidStartNotification"; + +static NSString *AWTRunLoopMode = @"AWTRunLoopMode"; +static NSArray *sPerformModes = nil; + +@implementation FlatJNFRunLoop + ++ (void)initialize { + if (sPerformModes) return; + sPerformModes = [[NSArray alloc] initWithObjects:NSDefaultRunLoopMode, NSModalPanelRunLoopMode, NSEventTrackingRunLoopMode, AWTRunLoopMode, nil]; +} + ++ (NSString *)javaRunLoopMode { + return AWTRunLoopMode; +} + ++ (void)performOnMainThread:(SEL)aSelector on:(id)target withObject:(id)arg waitUntilDone:(BOOL)waitUntilDone { + [target performSelectorOnMainThread:aSelector withObject:arg waitUntilDone:waitUntilDone modes:sPerformModes]; +} + ++ (void)_performDirectBlock:(void (^)(void))block { + block(); +} + ++ (void)_performCopiedBlock:(void (^)(void))newBlock { + newBlock(); + Block_release(newBlock); +} + ++ (void)performOnMainThreadWaiting:(BOOL)waitUntilDone withBlock:(void (^)(void))block { + if (waitUntilDone) { + [self performOnMainThread:@selector(_performDirectBlock:) on:self withObject:block waitUntilDone:YES]; + } else { + void (^newBlock)(void) = Block_copy(block); + [self performOnMainThread:@selector(_performCopiedBlock:) on:self withObject:newBlock waitUntilDone:NO]; + } +} + +@end diff --git a/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacWindow.mm b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacWindow.mm new file mode 100644 index 00000000..de0e8876 --- /dev/null +++ b/flatlaf-natives/flatlaf-natives-macos/src/main/objcpp/MacWindow.mm @@ -0,0 +1,90 @@ +/* + * Copyright 2023 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. + */ + +#import +#import +#import "JNIUtils.h" +#import "JNFRunLoop.h" +#import "com_formdev_flatlaf_ui_FlatNativeMacLibrary.h" + +/** + * @author Karl Tauber + */ + +extern "C" +JNIEXPORT jlong JNICALL Java_com_formdev_flatlaf_ui_FlatNativeMacLibrary_getWindowPtr + ( JNIEnv* env, jclass cls, jobject window ) +{ + if( window == NULL ) + return NULL; + + JNI_COCOA_ENTER() + + // get field java.awt.Component.peer + jfieldID peerID = env->GetFieldID( env->GetObjectClass( window ), "peer", "Ljava/awt/peer/ComponentPeer;" ); + jobject peer = (peerID != NULL) ? env->GetObjectField( window, peerID ) : NULL; + if( peer == NULL ) + return NULL; + + // get field sun.lwawt.LWWindowPeer.platformWindow + jfieldID platformWindowID = env->GetFieldID( env->GetObjectClass( peer ), "platformWindow", "Lsun/lwawt/PlatformWindow;" ); + jobject platformWindow = (platformWindowID != NULL) ? env->GetObjectField( peer, platformWindowID ) : NULL; + if( platformWindow == NULL ) + return NULL; + + // get field sun.lwawt.macosx.CFRetainedResource.ptr + jfieldID ptrID = env->GetFieldID( env->GetObjectClass( platformWindow ), "ptr", "J" ); + return (ptrID != NULL) ? env->GetLongField( platformWindow, ptrID ) : NULL; + + JNI_COCOA_EXIT() +} + +extern "C" +JNIEXPORT void JNICALL Java_com_formdev_flatlaf_ui_FlatNativeMacLibrary_setWindowRoundedBorder + ( JNIEnv* env, jclass cls, jlong windowPtr, jfloat radius, jfloat borderWidth, jint borderColor ) +{ + if( windowPtr == 0 ) + return; + + JNI_COCOA_ENTER() + + [FlatJNFRunLoop performOnMainThreadWaiting:NO withBlock:^(){ + NSWindow* window = (NSWindow *) jlong_to_ptr( windowPtr ); + + window.hasShadow = YES; + window.contentView.wantsLayer = YES; + window.contentView.layer.cornerRadius = radius; + window.contentView.layer.masksToBounds = YES; + + window.contentView.layer.borderWidth = borderWidth; + if( borderWidth > 0 ) { + CGFloat red = ((borderColor >> 16) & 0xff) / 255.; + CGFloat green = ((borderColor >> 8) & 0xff) / 255.; + CGFloat blue = (borderColor & 0xff) / 255.; + CGFloat alpha = ((borderColor >> 24) & 0xff) / 255.; + + window.contentView.layer.borderColor = [[NSColor colorWithDeviceRed:red green:green blue:blue alpha:alpha] CGColor]; + } + + window.backgroundColor = NSColor.clearColor; + window.opaque = NO; + + [window.contentView.layer removeAllAnimations]; + [window invalidateShadow]; + }]; + + JNI_COCOA_EXIT() +} diff --git a/flatlaf-natives/flatlaf-natives-windows/build.gradle.kts b/flatlaf-natives/flatlaf-natives-windows/build.gradle.kts index 1c1776b9..6849a62a 100644 --- a/flatlaf-natives/flatlaf-natives-windows/build.gradle.kts +++ b/flatlaf-natives/flatlaf-natives-windows/build.gradle.kts @@ -29,7 +29,11 @@ flatlafJniHeaders { } library { - targetMachines.set( listOf( machines.windows.x86, machines.windows.x86_64, machines.windows.architecture( "aarch64" ) ) ) + targetMachines.set( listOf( + machines.windows.x86, + machines.windows.x86_64, + machines.windows.architecture( "aarch64" ) + ) ) } var javaHome = System.getProperty( "java.home" ) diff --git a/flatlaf-testing/dumps/uidefaults/FlatDarkLaf_1.8.0-mac.txt b/flatlaf-testing/dumps/uidefaults/FlatDarkLaf_1.8.0-mac.txt index b5c8c403..7f0d7d07 100644 --- a/flatlaf-testing/dumps/uidefaults/FlatDarkLaf_1.8.0-mac.txt +++ b/flatlaf-testing/dumps/uidefaults/FlatDarkLaf_1.8.0-mac.txt @@ -6,6 +6,7 @@ #---- ComboBox ---- ++ ComboBox.roundedBorderWidth 1 + ComboBox.showPopupOnNavigation true @@ -46,6 +47,16 @@ + OptionPane.isYesLast true +#---- Popup ---- + ++ Popup.roundedBorderWidth 1 + + +#---- PopupMenu ---- + ++ PopupMenu.roundedBorderWidth 1 + + #---- ProgressBar ---- - ProgressBar.font [active] Segoe UI plain 10 javax.swing.plaf.FontUIResource [UI] @@ -77,6 +88,11 @@ - TitlePane.small.font [active] Segoe UI plain 11 javax.swing.plaf.FontUIResource [UI] + TitlePane.small.font [active] Helvetica Neue plain 12 javax.swing.plaf.FontUIResource [UI] + + +#---- ToolTip ---- + ++ ToolTip.roundedBorderWidth 1 - defaultFont Segoe UI plain 12 javax.swing.plaf.FontUIResource [UI] + defaultFont Helvetica Neue plain 13 javax.swing.plaf.FontUIResource [UI] diff --git a/flatlaf-testing/dumps/uidefaults/FlatLightLaf_1.8.0-mac.txt b/flatlaf-testing/dumps/uidefaults/FlatLightLaf_1.8.0-mac.txt index b5c8c403..46d779b2 100644 --- a/flatlaf-testing/dumps/uidefaults/FlatLightLaf_1.8.0-mac.txt +++ b/flatlaf-testing/dumps/uidefaults/FlatLightLaf_1.8.0-mac.txt @@ -6,6 +6,7 @@ #---- ComboBox ---- ++ ComboBox.roundedBorderWidth 0 + ComboBox.showPopupOnNavigation true @@ -46,6 +47,16 @@ + OptionPane.isYesLast true +#---- Popup ---- + ++ Popup.roundedBorderWidth 0 + + +#---- PopupMenu ---- + ++ PopupMenu.roundedBorderWidth 0 + + #---- ProgressBar ---- - ProgressBar.font [active] Segoe UI plain 10 javax.swing.plaf.FontUIResource [UI] @@ -77,6 +88,11 @@ - TitlePane.small.font [active] Segoe UI plain 11 javax.swing.plaf.FontUIResource [UI] + TitlePane.small.font [active] Helvetica Neue plain 12 javax.swing.plaf.FontUIResource [UI] + + +#---- ToolTip ---- + ++ ToolTip.roundedBorderWidth 0 - defaultFont Segoe UI plain 12 javax.swing.plaf.FontUIResource [UI] + defaultFont Helvetica Neue plain 13 javax.swing.plaf.FontUIResource [UI] diff --git a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatPopupTest.java b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatPopupTest.java index 05788bc9..a03a85b4 100644 --- a/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatPopupTest.java +++ b/flatlaf-testing/src/main/java/com/formdev/flatlaf/testing/FlatPopupTest.java @@ -17,7 +17,6 @@ package com.formdev.flatlaf.testing; import java.awt.*; -import java.awt.Point; import javax.swing.*; import com.formdev.flatlaf.util.Animator; import net.miginfocom.swing.*; @@ -88,6 +87,17 @@ public class FlatPopupTest animator.start(); } + @Override + public void updateUI() { + super.updateUI(); + + if( popupMenu1 != null ) { + SwingUtilities.updateComponentTreeUI( popupMenu1 ); + SwingUtilities.updateComponentTreeUI( popupMenu2 ); + SwingUtilities.updateComponentTreeUI( popupPanel ); + } + } + private void initComponents() { // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents label1 = new JLabel(); diff --git a/flatlaf-theme-editor/src/main/resources/com/formdev/flatlaf/themeeditor/FlatLafUIKeys.txt b/flatlaf-theme-editor/src/main/resources/com/formdev/flatlaf/themeeditor/FlatLafUIKeys.txt index 6545b7d2..9ada45ac 100644 --- a/flatlaf-theme-editor/src/main/resources/com/formdev/flatlaf/themeeditor/FlatLafUIKeys.txt +++ b/flatlaf-theme-editor/src/main/resources/com/formdev/flatlaf/themeeditor/FlatLafUIKeys.txt @@ -210,6 +210,7 @@ ComboBox.noActionOnKeyNavigation ComboBox.padding ComboBox.popupBackground ComboBox.popupInsets +ComboBox.roundedBorderWidth ComboBox.selectionArc ComboBox.selectionBackground ComboBox.selectionForeground @@ -604,6 +605,7 @@ Popup.dropShadowColor Popup.dropShadowInsets Popup.dropShadowOpacity Popup.dropShadowPainted +Popup.roundedBorderWidth PopupMenu.background PopupMenu.border PopupMenu.borderColor @@ -613,6 +615,7 @@ PopupMenu.consumeEventOnClose PopupMenu.font PopupMenu.foreground PopupMenu.hoverScrollArrowBackground +PopupMenu.roundedBorderWidth PopupMenu.scrollArrowColor PopupMenu.selectedWindowInputMapBindings PopupMenu.selectedWindowInputMapBindings.RightToLeft @@ -1161,6 +1164,7 @@ ToolTip.border ToolTip.borderCornerRadius ToolTip.font ToolTip.foreground +ToolTip.roundedBorderWidth ToolTipManager.enableToolTipMode ToolTipUI Tree.ancestorInputMap diff --git a/settings.gradle.kts b/settings.gradle.kts index 3d8d1e4d..23d937fa 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -31,6 +31,7 @@ includeProject( "flatlaf-fonts-roboto", "flatlaf-fonts/flatlaf-fonts-rob includeProject( "flatlaf-fonts-roboto-mono", "flatlaf-fonts/flatlaf-fonts-roboto-mono" ) includeProject( "flatlaf-natives-windows", "flatlaf-natives/flatlaf-natives-windows" ) +includeProject( "flatlaf-natives-macos", "flatlaf-natives/flatlaf-natives-macos" ) includeProject( "flatlaf-natives-linux", "flatlaf-natives/flatlaf-natives-linux" ) includeProject( "flatlaf-natives-jna", "flatlaf-natives/flatlaf-natives-jna" )