JBR-4787 Rounded corners for native windows

This commit is contained in:
Alexander Lobas
2022-09-06 23:01:12 +03:00
committed by jbrbot
parent 524679411d
commit 8e4d4cd331
10 changed files with 328 additions and 166 deletions

View File

@@ -304,9 +304,7 @@ public class CPlatformWindow extends CFRetainedResource implements PlatformWindo
},
new Property<CPlatformWindow>(WINDOW_CORNER_RADIUS) {
public void applyProperty(final CPlatformWindow c, final Object value) {
if (value != null && (value instanceof Float)) {
c.execute(ptr -> nativeSetRoundedCorners(ptr, (float) value));
}
c.setRoundedCorners(value);
}
}
}) {
@@ -1508,4 +1506,23 @@ public class CPlatformWindow extends CFRetainedResource implements PlatformWindo
if (rootpane != null) rootpane.putClientProperty(WINDOW_TRANSPARENT_TITLE_BAR_HEIGHT, height);
}
}
// JBR API internals
private static void setRoundedCorners(Window window, Object params) {
Object peer = AWTAccessor.getComponentAccessor().getPeer(window);
if (peer instanceof CPlatformWindow) {
((CPlatformWindow)peer).setRoundedCorners(params);
} else if (window instanceof RootPaneContainer) {
JRootPane rootpane = ((RootPaneContainer)window).getRootPane();
if (rootpane != null) {
rootpane.putClientProperty(WINDOW_CORNER_RADIUS, params);
}
}
}
private void setRoundedCorners(Object params) {
if (params instanceof Float) {
execute(ptr -> nativeSetRoundedCorners(ptr, (float) params));
}
}
}

View File

@@ -32,6 +32,9 @@ public class JBRApiModule {
.withStatic("getFileDialog", "get", "com.jetbrains.desktop.JBRFileDialog")
.proxy("com.jetbrains.JBRFileDialog", "com.jetbrains.desktop.JBRFileDialog")
.service("com.jetbrains.CustomWindowDecoration", "java.awt.Window$CustomWindowDecoration")
.service("com.jetbrains.RoundedCornersManager")
.withStatic("setRoundedCorners", "setRoundedCorners", "sun.lwawt.macosx.CPlatformWindow",
"sun.awt.windows.WWindowPeer")
.service("com.jetbrains.DesktopActions")
.withStatic("setHandler", "setDesktopActionsHandler", "java.awt.Desktop")
.clientProxy("java.awt.Desktop$DesktopActionsHandler", "com.jetbrains.DesktopActions$Handler");

View File

@@ -62,6 +62,9 @@ import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import javax.swing.JRootPane;
import javax.swing.RootPaneContainer;
import sun.awt.AWTAccessor;
import sun.awt.AppContext;
import sun.awt.DisplayChangedListener;
@@ -84,6 +87,8 @@ public class WWindowPeer extends WPanelPeer implements WindowPeer,
private static final PlatformLogger log = PlatformLogger.getLogger("sun.awt.windows.WWindowPeer");
private static final PlatformLogger screenLog = PlatformLogger.getLogger("sun.awt.windows.screen.WWindowPeer");
public static final String WINDOW_CORNER_RADIUS = "apple.awt.windowCornerRadius";
// we can't use WDialogPeer as blocker may be an instance of WPrintDialogPeer that
// extends WWindowPeer, not WDialogPeer
private WWindowPeer modalBlocker = null;
@@ -254,6 +259,13 @@ public class WWindowPeer extends WPanelPeer implements WindowPeer,
this.isOpaque = true;
setOpaque(((Window)target).isOpaque());
}
if (target instanceof RootPaneContainer) {
JRootPane rootpane = ((RootPaneContainer)target).getRootPane();
if (rootpane != null) {
setRoundedCornersImpl(rootpane.getClientProperty(WINDOW_CORNER_RADIUS));
}
}
}
native void createAwtWindow(WComponentPeer parent);
@@ -821,6 +833,35 @@ public class WWindowPeer extends WPanelPeer implements WindowPeer,
}
}
// JBR API internals
private static void setRoundedCorners(Window window, Object params) {
Object peer = AWTAccessor.getComponentAccessor().getPeer(window);
if (peer instanceof WWindowPeer) {
((WWindowPeer)peer).setRoundedCornersImpl(params);
} else if (window instanceof RootPaneContainer) {
JRootPane rootpane = ((RootPaneContainer)window).getRootPane();
if (rootpane != null) {
rootpane.putClientProperty(WINDOW_CORNER_RADIUS, params);
}
}
}
private void setRoundedCornersImpl(Object params) {
if (params instanceof String) {
int type = 0; // default
if ("none".equals(params)) {
type = 1;
} else if ("full".equals(params)) {
type = 2;
} else if ("small".equals(params)) {
type = 3;
}
setRoundedCorners(type);
}
}
private native void setRoundedCorners(int type);
native void updateWindowImpl(int[] data, int width, int height);
@Override

View File

@@ -45,11 +45,16 @@
#include "sun_awt_windows_WCanvasPeer.h"
#include <windowsx.h>
#include <dwmapi.h>
#include <math.h>
#if !defined(__int3264)
typedef __int32 LONG_PTR;
#endif // __int3264
// Define these to be able to build with older SDKs
#define DWM_WINDOW_CORNER_PREFERENCE int
#define DWMWA_WINDOW_CORNER_PREFERENCE 33
// Used for Swing's Menu/Tooltip animation Support
const int UNSPECIFIED = 0;
const int TOOLTIP = 1;
@@ -125,6 +130,11 @@ struct OpaqueStruct {
jobject window;
jboolean isOpaque;
};
// struct for _SetRoundedCorners() method
struct RoundedCornersStruct {
jobject window;
DWM_WINDOW_CORNER_PREFERENCE type;
};
// struct for _UpdateWindow() method
struct UpdateWindowStruct {
jobject window;
@@ -3341,6 +3351,23 @@ void AwtWindow::_SetOpaque(void* param)
delete os;
}
void AwtWindow::_SetRoundedCorners(void *param) {
JNIEnv *env = (JNIEnv *)JNU_GetEnv(jvm, JNI_VERSION_1_2);
RoundedCornersStruct *rcs = (RoundedCornersStruct *)param;
jobject self = rcs->window;
PDATA pData;
JNI_CHECK_PEER_GOTO(self, ret);
AwtWindow *window = (AwtWindow *)pData;
DwmSetWindowAttribute(window->GetHWnd(), DWMWA_WINDOW_CORNER_PREFERENCE, &rcs->type, sizeof(DWM_WINDOW_CORNER_PREFERENCE));
ret:
env->DeleteGlobalRef(self);
delete rcs;
}
void AwtWindow::_UpdateWindow(void* param)
{
JNIEnv *env = (JNIEnv *)JNU_GetEnv(jvm, JNI_VERSION_1_2);
@@ -4101,6 +4128,26 @@ Java_sun_awt_windows_WWindowPeer_repositionSecurityWarning(JNIEnv *env,
CATCH_BAD_ALLOC;
}
/*
* Class: sun_awt_windows_WWindowPeer
* Method: setRoundedCorners
* Signature: (I)V
*/
JNIEXPORT void JNICALL
Java_sun_awt_windows_WWindowPeer_setRoundedCorners(JNIEnv *env, jobject self, jint type)
{
TRY;
RoundedCornersStruct *rcs = new RoundedCornersStruct;
rcs->window = env->NewGlobalRef(self);
rcs->type = (DWM_WINDOW_CORNER_PREFERENCE)type;
AwtToolkit::GetInstance().SyncCall(AwtWindow::_SetRoundedCorners, rcs);
// global refs and rcs are deleted in _SetRoundedCorners
CATCH_BAD_ALLOC;
}
/*
* Class: sun_awt_windows_WLightweightFramePeer
* Method: overrideNativeHandle

View File

@@ -249,6 +249,7 @@ public:
static void _SetFullScreenExclusiveModeState(void* param);
static void _GetNativeWindowSize(void* param);
static void _OverrideHandle(void *param);
static void _SetRoundedCorners(void* param);
inline static BOOL IsResizing() {
return sm_resizing;

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2000-2022 JetBrains s.r.o.
*
* 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
*
* http://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.jetbrains;
import java.awt.Window;
/**
* This manager allows decorate awt Window with rounded corners.
* Appearance depends from operating system.
*/
public interface RoundedCornersManager {
/**
* @param params for macOS is Float object with radius.
*
* @param params for Windows 11 is String with values:
* "default" - let the system decide whether or not to round window corners,
* "none" - never round window corners,
* "full" - round the corners if appropriate,
* "small" - round the corners if appropriate, with a small radius.
*/
void setRoundedCorners(Window window, Object params);
}

View File

@@ -6,9 +6,9 @@
# 2. When only new API is added, or some existing API was @Deprecated - increment MINOR, reset PATCH to 0
# 3. For major backwards incompatible API changes - increment MAJOR, reset MINOR and PATCH to 0
VERSION = 0.0.4
VERSION = 0.0.5
# Hash is used to track changes to jetbrains.api, so you would not forget to update version when needed.
# When you make any changes, "make jbr-api" will fail and ask you to update hash and version number here.
HASH = 619850297D6FDD55F28939833FE25CC3
HASH = 7AA8AF5DDF8293EF33786DF3B91F3B0

View File

@@ -50,5 +50,8 @@ public class JBRApiTest {
private static void testServices() {
Objects.requireNonNull(JBR.getExtendedGlyphCache().getSubpixelResolution());
Objects.requireNonNull(JBRFileDialog.get(new FileDialog((Frame) null)));
if (!System.getProperty("os.name").toLowerCase().contains("linux")) {
Objects.requireNonNull(JBR.getRoundedCornersManager());
}
}
}

View File

@@ -0,0 +1,175 @@
/*
* Copyright 2022 JetBrains s.r.o.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
import com.jetbrains.JBR;
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.lang.reflect.InvocationTargetException;
/**
* @test
* @summary JBR-4563 Rounded corners for native Window on Mac OS
* @summary JBR-4787 Rounded corners for native Window on Windows 11
* @author Alexander Lobas
* @requires (os.family == "mac" || os.family == "windows")
* @run shell build.sh
* @run main/manual TestSetRoundedCorners
*/
public class TestSetRoundedCorners {
private static final int TD_RED = 50;
private static final int TD = 0;
private static final int DELAY = 1000;
private static TestSetRoundedCorners theTest;
private final Robot robot;
private JFrame frame;
private JFrame testFrame;
public TestSetRoundedCorners() {
try {
robot = new Robot();
} catch (AWTException ex) {
throw new RuntimeException(ex);
}
}
public void performTest(Object roundParams) {
if (!JBR.isRoundedCornersManagerSupported()) {
throw new RuntimeException("JBR Rounded API is not available");
}
runSwing(() -> {
frame = new JFrame("");
frame.setUndecorated(true);
JPanel panel = new JPanel();
panel.setOpaque(true);
panel.setBackground(Color.RED);
frame.setContentPane(panel);
frame.setBounds(100, 100, 300, 300);
frame.setVisible(true);
testFrame = new JFrame("");
testFrame.setUndecorated(true);
JPanel testPanel = new JPanel();
testPanel.setOpaque(true);
testPanel.setBackground(Color.GREEN);
testFrame.setContentPane(testPanel);
testFrame.setBounds(150, 150, 50, 50);
testFrame.setVisible(true);
});
robot.delay(DELAY);
// check that window without rounded corners
validateColor(51, 51, Color.GREEN, "0");
validateColor(51, 99, Color.GREEN, "0");
validateColor(99, 51, Color.GREEN, "0");
validateColor(99, 99, Color.GREEN, "0");
runSwing(() -> testFrame.setVisible(false));
robot.delay(DELAY);
runSwing(() -> {
JBR.getRoundedCornersManager().setRoundedCorners(testFrame, roundParams);
testFrame.setVisible(true);
});
robot.delay(DELAY);
// check that window with rounded corners
validateColor(51, 51, Color.RED, "1");
validateColor(51, 99, Color.RED, "1");
validateColor(99, 51, Color.RED, "1");
validateColor(99, 99, Color.RED, "1");
runSwing(() -> dispose());
}
private Color getTestPixel(int x, int y) {
Rectangle bounds = frame.getBounds();
BufferedImage screenImage = robot.createScreenCapture(bounds);
int rgb = screenImage.getRGB(x, y);
int red = (rgb >> 16) & 0xFF;
int green = (rgb >> 8) & 0xFF;
int blue = rgb & 0xFF;
return new Color(red, green, blue);
}
private static boolean validateColor(Color actual, Color expected) {
return Math.abs(actual.getRed() - expected.getRed()) <= TD_RED &&
Math.abs(actual.getGreen() - expected.getGreen()) <= TD &&
Math.abs(actual.getBlue() - expected.getBlue()) <= TD;
}
private void validateColor(int x, int y, Color expected, String place) {
Color actual = getTestPixel(x, y);
if (!validateColor(actual, expected)) {
throw new RuntimeException(
"Test failed [" + place + "]. Incorrect color " + actual + " instead " + expected + " at (" + x + "," + y + ")");
}
}
private static void runSwing(Runnable r) {
try {
SwingUtilities.invokeAndWait(r);
} catch (InterruptedException e) {
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
}
}
public void dispose() {
if (frame != null) {
frame.dispose();
frame = null;
}
if (testFrame != null) {
testFrame.dispose();
testFrame = null;
}
}
public static void runTest(Object roundParams) {
try {
runSwing(() -> theTest = new TestSetRoundedCorners());
theTest.performTest(roundParams);
} finally {
if (theTest != null) {
runSwing(() -> theTest.dispose());
}
}
}
public static void main(String[] args) {
String osName = System.getProperty("os.name");
if (osName.contains("Windows 11")) {
runTest("full");
} else if (osName.contains("OS X")) {
runTest(10F);
} else {
System.out.println("This test is for MacOS or Windows 11 only. Automatically passed on other platforms.");
}
}
}

View File

@@ -1,161 +0,0 @@
/*
* Copyright 2022 JetBrains s.r.o.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.lang.reflect.InvocationTargetException;
/**
* @test
* @key headful
* @summary JBR-4563 Rounded corners for native Window on Mac OS
* @author Alexander Lobas
* @run main MacNativeSetRoundedCorners
* @requires (os.family == "mac")
*/
public class MacNativeSetRoundedCorners {
private static final int TD = 30;
private static final int DELAY = 1000;
private static MacNativeSetRoundedCorners theTest;
private final Robot robot;
private JFrame frame;
private JFrame testFrame;
public MacNativeSetRoundedCorners() {
try {
robot = new Robot();
} catch (AWTException ex) {
throw new RuntimeException(ex);
}
}
public void performTest() {
runSwing(() -> {
frame = new JFrame("");
frame.setUndecorated(true);
JPanel panel = new JPanel();
panel.setOpaque(true);
panel.setBackground(Color.RED);
frame.setContentPane(panel);
frame.setBounds(100, 100, 300, 300);
frame.setVisible(true);
testFrame = new JFrame("");
testFrame.setUndecorated(true);
JPanel testPanel = new JPanel();
testPanel.setOpaque(true);
testPanel.setBackground(Color.GREEN);
testFrame.setContentPane(testPanel);
testFrame.setBounds(150, 150, 50, 50);
testFrame.setVisible(true);
});
robot.delay(DELAY);
// check that window without rounded corners
validateColor(51, 51, Color.GREEN, "0");
validateColor(51, 99, Color.GREEN, "0");
validateColor(99, 51, Color.GREEN, "0");
validateColor(99, 99, Color.GREEN, "0");
runSwing(() -> testFrame.setVisible(false));
robot.delay(DELAY);
runSwing(() -> {
testFrame.getRootPane().putClientProperty("apple.awt.windowCornerRadius", Float.valueOf(10));
testFrame.setVisible(true);
});
robot.delay(DELAY);
// check that window with rounded corners
validateColor(51, 51, Color.RED, "1");
validateColor(51, 99, Color.RED, "1");
validateColor(99, 51, Color.RED, "1");
validateColor(99, 99, Color.RED, "1");
runSwing(() -> dispose());
}
private Color getTestPixel(int x, int y) {
Rectangle bounds = frame.getBounds();
BufferedImage screenImage = robot.createScreenCapture(bounds);
int rgb = screenImage.getRGB(x, y);
int red = (rgb >> 16) & 0xFF;
int green = (rgb >> 8) & 0xFF;
int blue = rgb & 0xFF;
return new Color(red, green, blue);
}
private static boolean validateColor(Color actual, Color expected) {
return Math.abs(actual.getRed() - expected.getRed()) <= TD &&
Math.abs(actual.getGreen() - expected.getGreen()) <= TD &&
Math.abs(actual.getBlue() - expected.getBlue()) <= TD;
}
private void validateColor(int x, int y, Color expected, String place) {
Color actual = getTestPixel(x, y);
if (!validateColor(actual, expected)) {
throw new RuntimeException(
"Test failed [" + place + "]. Incorrect color " + actual + " instead " + expected + " at (" + x + "," + y + ")");
}
}
private static void runSwing(Runnable r) {
try {
SwingUtilities.invokeAndWait(r);
} catch (InterruptedException e) {
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
}
}
public void dispose() {
if (frame != null) {
frame.dispose();
frame = null;
}
if (testFrame != null) {
testFrame.dispose();
testFrame = null;
}
}
public static void main(String[] args) {
if (!System.getProperty("os.name").contains("OS X")) {
System.out.println("This test is for MacOS only. Automatically passed on other platforms.");
return;
}
try {
runSwing(() -> theTest = new MacNativeSetRoundedCorners());
theTest.performTest();
} finally {
if (theTest != null) {
runSwing(() -> theTest.dispose());
}
}
}
}