JBR API v0.0.3

JBR-4228 report HTMINBUTTON, HTMAXBUTTON, HTCLOSE, HTMENU, HTCAPTION targets via JBR API & handle corresponding non-client mouse events for windows with custom decoration
This commit is contained in:
Nikita Gubarkov
2022-02-14 23:04:34 +03:00
committed by jbrbot
parent be32154f9f
commit 21a66fec26
10 changed files with 300 additions and 35 deletions

View File

@@ -39,6 +39,7 @@ public class JBRApiModule {
.withStatic("getSubpixelResolution", "sun.font.FontUtilities")
.service("com.jetbrains.JBRFileDialogService", null)
.withStatic("getFileDialog", "com.jetbrains.desktop.JBRFileDialog", "get")
.proxy("com.jetbrains.JBRFileDialog", "com.jetbrains.desktop.JBRFileDialog");
.proxy("com.jetbrains.JBRFileDialog", "com.jetbrains.desktop.JBRFileDialog")
.service("com.jetbrains.CustomWindowDecoration", "java.awt.Window$CustomWindowDecoration");
}
}

View File

@@ -46,13 +46,17 @@ import java.io.ObjectOutputStream;
import java.io.OptionalDataException;
import java.io.Serial;
import java.io.Serializable;
import java.lang.annotation.Native;
import java.lang.invoke.MethodHandles;
import java.lang.ref.WeakReference;
import java.lang.reflect.InvocationTargetException;
import java.security.AccessController;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EventListener;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.Vector;
@@ -64,6 +68,7 @@ import javax.accessibility.AccessibleRole;
import javax.accessibility.AccessibleState;
import javax.accessibility.AccessibleStateSet;
import com.jetbrains.internal.JBRApi;
import sun.awt.AWTAccessor;
import sun.awt.AWTPermissions;
import sun.awt.AppContext;
@@ -4005,8 +4010,63 @@ public class Window extends Container implements Accessible {
}
}
private volatile boolean hasCustomDecoration;
private transient volatile boolean hasCustomDecoration;
private transient volatile List<Map.Entry<Shape, Integer>> customDecorHitTestSpots;
private transient volatile int customDecorTitleBarHeight = -1; // 0 can be a legal value when no title bar is expected
// called from native
private int hitTestCustomDecoration(int x, int y) {
var spots = customDecorHitTestSpots;
if (spots == null) return CustomWindowDecoration.NO_HIT_SPOT;
for (var spot : spots) {
if (spot.getKey().contains(x, y)) return spot.getValue();
}
return CustomWindowDecoration.NO_HIT_SPOT;
}
private static class CustomWindowDecoration {
@Native public static final int
NO_HIT_SPOT = 0,
OTHER_HIT_SPOT = 1,
MINIMIZE_BUTTON = 2,
MAXIMIZE_BUTTON = 3,
CLOSE_BUTTON = 4,
MENU_BAR = 5,
DRAGGABLE_AREA = 6;
void setCustomDecorationEnabled(Window window, boolean enabled) {
window.hasCustomDecoration = enabled;
if (Win.INSTANCE != null) {
Win.INSTANCE.updateCustomDecoration(window.peer);
}
}
boolean isCustomDecorationEnabled(Window window) {
return window.hasCustomDecoration;
}
void setCustomDecorationHitTestSpots(Window window, List<Map.Entry<Shape, Integer>> spots) {
window.customDecorHitTestSpots = List.copyOf(spots);
}
List<Map.Entry<Shape, Integer>> getCustomDecorationHitTestSpots(Window window) {
return window.customDecorHitTestSpots;
}
void setCustomDecorationTitleBarHeight(Window window, int height) {
if (height >= 0) window.customDecorTitleBarHeight = height;
}
int getCustomDecorationTitleBarHeight(Window window) {
return window.customDecorTitleBarHeight;
}
private interface Win {
Win INSTANCE = (Win) JBRApi.internalServiceBuilder(MethodHandles.lookup(), null)
.withStatic("updateCustomDecoration", "sun.awt.windows.WFramePeer").build();
void updateCustomDecoration(ComponentPeer peer);
}
}
@Deprecated
boolean hasCustomDecoration() {
return hasCustomDecoration;
}
@@ -4014,6 +4074,7 @@ public class Window extends Container implements Accessible {
/**
* Set via reflection (JB JdkEx API).
*/
@Deprecated
void setHasCustomDecoration() {
hasCustomDecoration = true;
}

View File

@@ -31,6 +31,7 @@ import java.awt.Frame;
import java.awt.GraphicsConfiguration;
import java.awt.MenuBar;
import java.awt.Rectangle;
import java.awt.peer.ComponentPeer;
import java.awt.peer.FramePeer;
import java.security.AccessController;
@@ -253,4 +254,10 @@ class WFramePeer extends WWindowPeer implements FramePeer {
}
private native void synthesizeWmActivate(boolean activate);
// JBR API internals
private static void updateCustomDecoration(ComponentPeer peer) {
if (peer instanceof WFramePeer) ((WFramePeer) peer).updateCustomDecoration();
}
private native void updateCustomDecoration();
}

View File

@@ -55,8 +55,11 @@ import java.awt.image.DataBufferInt;
import java.awt.peer.WindowPeer;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import sun.awt.AWTAccessor;
@@ -995,31 +998,29 @@ public class WWindowPeer extends WPanelPeer implements WindowPeer,
return err;
}
private volatile List<Rectangle> customDecorHitTestSpots;
private volatile int customDecorTitleBarHeight = -1; // 0 can be a legal value when no title bar is expected
// called from client via reflection
@Deprecated
private void setCustomDecorationHitTestSpots(List<Rectangle> hitTestSpots) {
this.customDecorHitTestSpots = new CopyOnWriteArrayList<>(hitTestSpots);
List<Map.Entry<Shape, Integer>> spots = new ArrayList<>();
for (Rectangle spot : hitTestSpots) spots.add(Map.entry(spot, 1));
try {
Field f = Window.class.getDeclaredField("customDecorHitTestSpots");
f.setAccessible(true);
f.set(target, spots);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new Error(e);
}
}
// called from client via reflection
@Deprecated
private void setCustomDecorationTitleBarHeight(int height) {
if (height >= 0) customDecorTitleBarHeight = height;
}
// called from native
private boolean hitTestCustomDecoration(int x, int y) {
List<Rectangle> spots = customDecorHitTestSpots;
if (spots == null) return false;
for (Rectangle spot : spots) {
if (spot.contains(x, y)) return true;
try {
Field f = Window.class.getDeclaredField("customDecorTitleBarHeight");
f.setAccessible(true);
f.set(target, height);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new Error(e);
}
return false;
}
// called from native
private int getCustomDecorationTitleBarHeight() {
return customDecorTitleBarHeight;
}
}

View File

@@ -1646,6 +1646,9 @@ LRESULT AwtComponent::WindowProc(UINT message, WPARAM wParam, LPARAM lParam)
case WM_NCRBUTTONDOWN:
mr = WmNcMouseDown(wParam, GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam), RIGHT_BUTTON);
break;
case WM_NCMOUSEMOVE:
mr = WmNcMouseMove(wParam, GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
break;
case WM_LBUTTONUP:
if (ignoreNextLBTNUP) {
ignoreNextLBTNUP = FALSE;
@@ -2338,6 +2341,9 @@ MsgRouting AwtComponent::WmNcMouseDown(WPARAM hitTest, int x, int y, int button)
MsgRouting AwtComponent::WmNcMouseUp(WPARAM hitTest, int x, int y, int button) {
return mrDoDefault;
}
MsgRouting AwtComponent::WmNcMouseMove(WPARAM hitTest, int x, int y) {
return mrDoDefault;
}
MsgRouting AwtComponent::WmWindowPosChanging(LPARAM windowPos) {
return mrDoDefault;

View File

@@ -525,6 +525,7 @@ public:
int wheelRotation, BOOL isHorizontal);
virtual MsgRouting WmNcMouseDown(WPARAM hitTest, int x, int y, int button);
virtual MsgRouting WmNcMouseUp(WPARAM hitTest, int x, int y, int button);
virtual MsgRouting WmNcMouseMove(WPARAM hitTest, int x, int y);
virtual MsgRouting WmWindowPosChanging(LPARAM windowPos);
virtual MsgRouting WmWindowPosChanged(LPARAM windowPos);
virtual void WmTouch(WPARAM wParam, LPARAM lParam);

View File

@@ -37,6 +37,7 @@
#include <dwmapi.h>
#include <java_lang_Integer.h>
#include <java_awt_Window_CustomWindowDecoration.h>
#include <sun_awt_windows_WEmbeddedFrame.h>
#include <sun_awt_windows_WEmbeddedFramePeer.h>
@@ -514,7 +515,15 @@ MsgRouting AwtFrame::WmMouseMove(UINT flags, int x, int y) {
* If this Frame is non-focusable then we should implement move and size operation for it by
* ourselfves because we don't dispatch appropriate mouse messages to default window procedure.
*/
if (!IsFocusableWindow() && isInManualMoveOrSize) {
if (isInManualMoveOrSize) {
if (grabbedHitTest == HTCAPTION) {
WINDOWPLACEMENT placement;
::GetWindowPlacement(GetHWnd(), &placement);
if (placement.showCmd == SW_SHOWMAXIMIZED) {
placement.showCmd = SW_SHOWNORMAL;
::SetWindowPlacement(GetHWnd(), &placement);
}
}
DWORD curPos = ::GetMessagePos();
x = GET_X_LPARAM(curPos);
y = GET_Y_LPARAM(curPos);
@@ -622,6 +631,44 @@ MsgRouting AwtFrame::WmNcMouseDown(WPARAM hitTest, int x, int y, int button) {
if (m_grabbedWindow != NULL/* && !m_grabbedWindow->IsOneOfOwnersOf(this)*/) {
m_grabbedWindow->Ungrab();
}
// For windows with custom decorations, handle caption-related mouse events
// Do not handle events from caption itself to preserve native drag behavior
if (HasCustomDecoration()) {
switch (hitTest) {
case HTCAPTION:
case HTMINBUTTON:
case HTMAXBUTTON:
case HTCLOSE:
case HTMENU:
RECT rcWindow;
GetWindowRect(GetHWnd(), &rcWindow);
if (hitTest == HTCAPTION) {
JNIEnv *env = (JNIEnv *) JNU_GetEnv(jvm, JNI_VERSION_1_2);
jint customSpot = JNU_CallMethodByName(env, NULL, GetTarget(env),
"hitTestCustomDecoration", "(II)I",
ScaleDownX(x - rcWindow.left),
ScaleDownY(y - rcWindow.top)).i;
if (customSpot == java_awt_Window_CustomWindowDecoration_DRAGGABLE_AREA) {
if (button & LEFT_BUTTON) {
savedMousePos.x = x;
savedMousePos.y = y;
::SetCapture(GetHWnd());
isInManualMoveOrSize = TRUE;
grabbedHitTest = hitTest;
}
} else break;
}
POINT myPos;
myPos.x = x;
myPos.y = y;
::ScreenToClient(GetHWnd(), &myPos);
WmMouseDown(GetButtonMK(button),
myPos.x,
myPos.y,
button);
return mrConsume;
}
}
if (!IsFocusableWindow() && (button & LEFT_BUTTON)) {
switch (hitTest) {
case HTTOP:
@@ -653,6 +700,26 @@ MsgRouting AwtFrame::WmNcMouseDown(WPARAM hitTest, int x, int y, int button) {
return AwtWindow::WmNcMouseDown(hitTest, x, y, button);
}
MsgRouting AwtFrame::WmNcMouseMove(WPARAM hitTest, int x, int y) {
// For windows with custom decorations, handle caption-related mouse events
if (HasCustomDecoration()) {
switch (hitTest) {
case HTMINBUTTON:
case HTMAXBUTTON:
case HTCLOSE:
case HTMENU:
case HTCAPTION:
POINT myPos;
myPos.x = x;
myPos.y = y;
::ScreenToClient(GetHWnd(), &myPos);
WmMouseMove(0, myPos.x, myPos.y);
if (hitTest != HTCAPTION) return mrConsume; // Preserve default window drag for HTCAPTION
}
}
return AwtWindow::WmNcMouseMove(hitTest, x, y);
}
// Override AwtWindow::Reshape() to handle minimized/maximized
// frames (see 6525850, 4065534)
void AwtFrame::Reshape(int x, int y, int w, int h)
@@ -1670,11 +1737,26 @@ BOOL AwtFrame::HasCustomDecoration()
if (!m_pHasCustomDecoration) {
m_pHasCustomDecoration = new BOOL;
JNIEnv *env = (JNIEnv *) JNU_GetEnv(jvm, JNI_VERSION_1_2);
*m_pHasCustomDecoration = JNU_CallMethodByName(env, NULL, GetTarget(env), "hasCustomDecoration", "()Z").z;
*m_pHasCustomDecoration = JNU_GetFieldByName(env, NULL, GetTarget(env), "hasCustomDecoration", "Z").z;
}
return *m_pHasCustomDecoration;
}
void _UpdateCustomDecoration(void* p) {
JNIEnv *env = (JNIEnv *)JNU_GetEnv(jvm, JNI_VERSION_1_2);
jobject self = reinterpret_cast<jobject>(p);
PDATA pData;
JNI_CHECK_PEER_GOTO(self, ret);
AwtFrame* frame = (AwtFrame*)pData;
if (!frame->m_pHasCustomDecoration) frame->m_pHasCustomDecoration = new BOOL;
*frame->m_pHasCustomDecoration = JNU_GetFieldByName(env, NULL, frame->GetTarget(env), "hasCustomDecoration", "Z").z;
frame->RedrawNonClient();
ret:
env->DeleteGlobalRef(self);
}
void GetSysInsets(RECT* insets, AwtFrame* pFrame) {
if (pFrame->IsUndecorated()) {
::SetRectEmpty(insets);
@@ -1712,10 +1794,8 @@ LRESULT HitTestNCA(AwtFrame* frame, int x, int y) {
AdjustWindowRectEx(&rcFrame, WS_OVERLAPPEDWINDOW & ~WS_CAPTION, FALSE, NULL);
JNIEnv *env = (JNIEnv *) JNU_GetEnv(jvm, JNI_VERSION_1_2);
int titleHeight = (int)JNU_CallMethodByName(env, NULL, frame->GetPeer(env),
"getCustomDecorationTitleBarHeight", "()I",
frame->ScaleDownX(x - rcWindow.left),
frame->ScaleDownY(y - rcWindow.top)).i;
int titleHeight = (int)JNU_GetFieldByName(env, NULL, frame->GetTarget(env),
"customDecorTitleBarHeight", "I").i;
if (titleHeight >= 0) {
titleHeight = frame->ScaleUpY(titleHeight);
insets.top = titleHeight; // otherwise leave default
@@ -1728,12 +1808,24 @@ LRESULT HitTestNCA(AwtFrame* frame, int x, int y) {
if (y >= rcWindow.top &&
y < rcWindow.top + insets.top)
{
if (JNU_CallMethodByName(env, NULL, frame->GetPeer(env),
"hitTestCustomDecoration", "(II)Z",
frame->ScaleDownX(x - rcWindow.left),
frame->ScaleDownY(y - rcWindow.top)).z)
{
return HTNOWHERE;
jint customSpot = JNU_CallMethodByName(env, NULL, frame->GetTarget(env),
"hitTestCustomDecoration", "(II)I",
frame->ScaleDownX(x - rcWindow.left),
frame->ScaleDownY(y - rcWindow.top)).i;
switch (customSpot) {
case java_awt_Window_CustomWindowDecoration_NO_HIT_SPOT:
case java_awt_Window_CustomWindowDecoration_DRAGGABLE_AREA:
break; // Nothing
case java_awt_Window_CustomWindowDecoration_MINIMIZE_BUTTON:
return HTMINBUTTON;
case java_awt_Window_CustomWindowDecoration_MAXIMIZE_BUTTON:
return HTMAXBUTTON;
case java_awt_Window_CustomWindowDecoration_CLOSE_BUTTON:
return HTCLOSE;
case java_awt_Window_CustomWindowDecoration_MENU_BAR:
return HTMENU;
default:
return HTNOWHERE;
}
fOnResizeBorder = (y < (rcWindow.top - rcFrame.top));
uRow = 0;
@@ -2160,6 +2252,17 @@ Java_sun_awt_windows_WFramePeer_updateIcon(JNIEnv *env, jobject self)
CATCH_BAD_ALLOC;
}
JNIEXPORT void JNICALL
Java_sun_awt_windows_WFramePeer_updateCustomDecoration(JNIEnv *env, jobject self)
{
TRY;
AwtToolkit::GetInstance().InvokeFunction(_UpdateCustomDecoration, env->NewGlobalRef(self));
// global ref is deleted in _UpdateCustomDecoration()
CATCH_BAD_ALLOC;
}
} /* extern "C" */
static bool SetFocusToPluginControl(HWND hwndPlugin)

View File

@@ -111,6 +111,7 @@ public:
MsgRouting WmMouseMove(UINT flags, int x, int y);
MsgRouting WmNcMouseDown(WPARAM hitTest, int x, int y, int button);
MsgRouting WmNcMouseUp(WPARAM hitTest, int x, int y, int button);
MsgRouting WmNcMouseMove(WPARAM hitTest, int x, int y);
MsgRouting WmGetIcon(WPARAM iconType, LRESULT& retVal);
MsgRouting WmShowWindow(BOOL show, UINT status);
MsgRouting WmWindowPosChanging(LPARAM windowPos);

View File

@@ -0,0 +1,84 @@
/*
* Copyright 2000-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.
*/
package com.jetbrains;
import java.awt.*;
import java.util.List;
import java.util.Map;
/**
* Custom window decoration allows merging of window content with native title bar,
* which is usually done by treating title bar as part of client area, but with some
* special behavior like dragging or maximizing on double click.
* @implNote Behavior is platform-dependent, only macOS and Windows are supported.
*/
public interface CustomWindowDecoration {
/*CONST java.awt.Window.*_HIT_SPOT*/
/*CONST java.awt.Window.*_BUTTON*/
/*CONST java.awt.Window.MENU_BAR*/
/*CONST java.awt.Window.DRAGGABLE_AREA*/
/**
* Turn custom decoration on or off, {@link #setCustomDecorationTitleBarHeight(Window, int)}
* must be called to configure title bar height.
*/
void setCustomDecorationEnabled(Window window, boolean enabled);
/**
* @see #setCustomDecorationEnabled(Window, boolean)
*/
boolean isCustomDecorationEnabled(Window window);
/**
* Set list of hit test spots for a custom decoration.
* Hit test spot is a special area inside custom-decorated title bar.
* Each hit spot type has its own (probably platform-specific) behavior:
* <ul>
* <li>{@link #NO_HIT_SPOT} - default title bar area, can be dragged to move a window, maximizes on double-click</li>
* <li>{@link #OTHER_HIT_SPOT} - generic hit spot, window cannot be moved with drag or maximized on double-click</li>
* <li>{@link #MINIMIZE_BUTTON} - like {@link #OTHER_HIT_SPOT} but displays tooltip on Windows when hovered</li>
* <li>{@link #MAXIMIZE_BUTTON} - like {@link #OTHER_HIT_SPOT} but displays tooltip / snap layout menu on Windows when hovered</li>
* <li>{@link #CLOSE_BUTTON} - like {@link #OTHER_HIT_SPOT} but displays tooltip on Windows when hovered</li>
* <li>{@link #MENU_BAR} - currently no different from {@link #OTHER_HIT_SPOT}</li>
* <li>{@link #DRAGGABLE_AREA} - special type, moves window when dragged, but doesn't maximize on double-click</li>
* </ul>
* @param spots pairs of hit spot shapes with corresponding types.
* @implNote Hit spots are tested in sequence, so in case of overlapping areas, first found wins.
*/
void setCustomDecorationHitTestSpots(Window window, List<Map.Entry<Shape, Integer>> spots);
/**
* @see #setCustomDecorationHitTestSpots(Window, List)
*/
List<Map.Entry<Shape, Integer>> getCustomDecorationHitTestSpots(Window window);
/**
* Set height of custom window decoration, won't take effect until custom decoration
* is enabled via {@link #setCustomDecorationEnabled(Window, boolean)}.
*/
void setCustomDecorationTitleBarHeight(Window window, int height);
/**
* @see #setCustomDecorationTitleBarHeight(Window, int)
*/
int getCustomDecorationTitleBarHeight(Window window);
}

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.2
VERSION = 0.0.3
# 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 = 814F29888E5EB6C23DF27B53636E9F94
HASH = 414128B7903002F2EF6234BC2CFCE3