From 3d19b872f5ffaec6b326c11f91d7302af4a3bc0b Mon Sep 17 00:00:00 2001 From: mstr2 <43553916+mstr2@users.noreply.github.com> Date: Sat, 19 Oct 2024 21:33:32 +0200 Subject: [PATCH 01/35] WIP --- buildSrc/win.gradle | 4 +- .../test/java/test/util/ReflectionUtils.java | 41 ++ .../java/com/sun/glass/events/MouseEvent.java | 33 +- .../java/com/sun/glass/ui/Application.java | 6 + .../com/sun/glass/ui/NonClientHandler.java | 43 ++ .../src/main/java/com/sun/glass/ui/View.java | 34 +- .../main/java/com/sun/glass/ui/Window.java | 313 ++-------- .../sun/glass/ui/WindowControlsOverlay.java | 540 ++++++++++++++++++ .../sun/glass/ui/WindowOverlayMetrics.java | 45 ++ .../com/sun/glass/ui/gtk/GtkApplication.java | 5 + .../java/com/sun/glass/ui/gtk/GtkWindow.java | 70 ++- .../glass/ui/gtk/PlatformThemeObserver.java | 64 +++ .../glass/ui/gtk/WindowDecorationTheme.java | 92 +++ .../com/sun/glass/ui/gtk/WindowManager.java | 76 +++ .../com/sun/glass/ui/ios/IosApplication.java | 5 + .../com/sun/glass/ui/mac/MacApplication.java | 5 + .../java/com/sun/glass/ui/mac/MacWindow.java | 50 +- .../glass/ui/monocle/MonocleApplication.java | 5 + .../com/sun/glass/ui/win/WinApplication.java | 5 + .../java/com/sun/glass/ui/win/WinWindow.java | 104 +++- .../java/com/sun/javafx/scene/NodeHelper.java | 11 + .../main/java/com/sun/javafx/tk/TKScene.java | 8 +- .../com/sun/javafx/tk/TKSceneListener.java | 11 +- .../tk/quantum/GlassViewEventHandler.java | 14 +- .../sun/javafx/tk/quantum/OverlayWarning.java | 46 +- .../tk/quantum/OverlayWarningHelper.java | 79 --- .../sun/javafx/tk/quantum/QuantumToolkit.java | 2 + .../com/sun/javafx/tk/quantum/ViewScene.java | 74 ++- .../javafx/tk/quantum/ViewSceneOverlay.java | 110 ++++ .../sun/javafx/tk/quantum/WindowStage.java | 46 +- .../main/java/com/sun/javafx/util/Utils.java | 30 +- .../application/ConditionalFeature.java | 11 +- .../src/main/java/javafx/scene/Node.java | 10 + .../src/main/java/javafx/scene/Scene.java | 39 ++ .../java/javafx/scene/layout/HeaderBar.java | 483 ++++++++++++++++ .../javafx/scene/layout/HeaderBarBase.java | 191 +++++++ .../main/java/javafx/stage/StageStyle.java | 45 +- .../native-glass/gtk/GlassApplication.cpp | 3 +- .../src/main/native-glass/gtk/GlassWindow.cpp | 5 +- .../main/native-glass/gtk/glass_general.cpp | 6 +- .../src/main/native-glass/gtk/glass_general.h | 5 +- .../main/native-glass/gtk/glass_window.cpp | 183 +++++- .../src/main/native-glass/gtk/glass_window.h | 13 +- .../src/main/native-glass/ios/GlassWindow.m | 2 +- .../main/native-glass/mac/GlassViewDelegate.h | 4 +- .../main/native-glass/mac/GlassViewDelegate.m | 5 + .../src/main/native-glass/mac/GlassWindow.m | 60 +- .../src/main/native-glass/win/GlassWindow.cpp | 178 +++++- .../src/main/native-glass/win/GlassWindow.h | 9 +- .../src/main/native-glass/win/Utils.h | 3 + .../main/native-glass/win/ViewContainer.cpp | 124 +++- .../src/main/native-glass/win/ViewContainer.h | 1 + .../src/main/native-glass/win/common.h | 4 +- .../glass/ui/gtk/WindowDecorationGnome.css | 126 ++++ .../sun/glass/ui/gtk/WindowDecorationKDE.css | 111 ++++ .../com/sun/glass/ui/win/WindowDecoration.css | 112 ++++ modules/javafx.graphics/src/test/addExports | 2 + .../glass/ui/WindowControlsOverlayTest.java | 349 +++++++++++ .../test/com/sun/javafx/util/UtilsTest.java | 48 ++ .../javafx/scene/layout/HeaderBarTest.java | 438 ++++++++++++++ 60 files changed, 4055 insertions(+), 486 deletions(-) create mode 100644 modules/javafx.graphics/src/main/java/com/sun/glass/ui/NonClientHandler.java create mode 100644 modules/javafx.graphics/src/main/java/com/sun/glass/ui/WindowControlsOverlay.java create mode 100644 modules/javafx.graphics/src/main/java/com/sun/glass/ui/WindowOverlayMetrics.java create mode 100644 modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/PlatformThemeObserver.java create mode 100644 modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/WindowDecorationTheme.java create mode 100644 modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/WindowManager.java delete mode 100644 modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/OverlayWarningHelper.java create mode 100644 modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/ViewSceneOverlay.java create mode 100644 modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBar.java create mode 100644 modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBarBase.java create mode 100644 modules/javafx.graphics/src/main/resources/com/sun/glass/ui/gtk/WindowDecorationGnome.css create mode 100644 modules/javafx.graphics/src/main/resources/com/sun/glass/ui/gtk/WindowDecorationKDE.css create mode 100644 modules/javafx.graphics/src/main/resources/com/sun/glass/ui/win/WindowDecoration.css create mode 100644 modules/javafx.graphics/src/test/java/test/com/sun/glass/ui/WindowControlsOverlayTest.java create mode 100644 modules/javafx.graphics/src/test/java/test/javafx/scene/layout/HeaderBarTest.java diff --git a/buildSrc/win.gradle b/buildSrc/win.gradle index 83a97645a27..db34da5ad61 100644 --- a/buildSrc/win.gradle +++ b/buildSrc/win.gradle @@ -324,9 +324,9 @@ WIN.glass.rcFlags = [ WIN.glass.ccFlags = [ccFlags].flatten() WIN.glass.linker = linker WIN.glass.linkFlags = (IS_STATIC_BUILD ? [linkFlags] : [linkFlags, "delayimp.lib", "gdi32.lib", "urlmon.lib", "Comdlg32.lib", - "winmm.lib", "imm32.lib", "shell32.lib", "Uiautomationcore.lib", "dwmapi.lib", + "winmm.lib", "imm32.lib", "shell32.lib", "Uiautomationcore.lib", "dwmapi.lib", "uxtheme.lib", "/DELAYLOAD:user32.dll", "/DELAYLOAD:urlmon.dll", "/DELAYLOAD:winmm.dll", "/DELAYLOAD:shell32.dll", - "/DELAYLOAD:Uiautomationcore.dll", "/DELAYLOAD:dwmapi.dll"]).flatten() + "/DELAYLOAD:Uiautomationcore.dll", "/DELAYLOAD:dwmapi.dll", "/DELAYLOAD:uxtheme.dll"]).flatten() WIN.glass.lib = "glass" WIN.decora = [:] diff --git a/modules/javafx.base/src/test/java/test/util/ReflectionUtils.java b/modules/javafx.base/src/test/java/test/util/ReflectionUtils.java index 0e4793c5983..838fb8de323 100644 --- a/modules/javafx.base/src/test/java/test/util/ReflectionUtils.java +++ b/modules/javafx.base/src/test/java/test/util/ReflectionUtils.java @@ -26,10 +26,14 @@ package test.util; import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.function.Function; public final class ReflectionUtils { + private ReflectionUtils() {} + /** * Returns the value of a potentially private field of the specified object. * The field can be declared on any of the object's inherited classes. @@ -61,4 +65,41 @@ public static Object getFieldValue(Object object, String fieldName) { throw new AssertionError("Field not found: " + fieldName); } + + /** + * Invokes the specified method on the object, and returns a value. + * The method can be declared on any of the object's inherited classes. + * + * @param object the object on which the method will be invoked + * @param methodName the method name + * @param args the arguments + * @return the return value + */ + public static Object invokeMethod(Object object, String methodName, Class[] parameterTypes, Object... args) { + Function, Method> getMethod = cls -> { + try { + var method = cls.getDeclaredMethod(methodName, parameterTypes); + method.setAccessible(true); + return method; + } catch (NoSuchMethodException e) { + return null; + } + }; + + Class cls = object.getClass(); + while (cls != null) { + Method method = getMethod.apply(cls); + if (method != null) { + try { + return method.invoke(object, args); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new AssertionError(e); + } + } + + cls = cls.getSuperclass(); + } + + throw new AssertionError("Method not found: " + methodName); + } } diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/events/MouseEvent.java b/modules/javafx.graphics/src/main/java/com/sun/glass/events/MouseEvent.java index d1cfe4370e1..b9a5651db14 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/events/MouseEvent.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/events/MouseEvent.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -24,6 +24,8 @@ */ package com.sun.glass.events; +import com.sun.glass.ui.WindowControlsOverlay; +import javafx.stage.StageStyle; import java.lang.annotation.Native; public class MouseEvent { @@ -49,4 +51,33 @@ public class MouseEvent { * This identifier is required for internal purposes. */ @Native final static public int WHEEL = 228; + + /** + * Non-client events are only natively produced on the Windows platform as a result of + * handling the {@code WM_NCHITTEST} message for an {@link StageStyle#EXTENDED} window. + *

+ * They are never sent to applications, but are processed by {@link WindowControlsOverlay}. + */ + @Native final static public int NC_DOWN = 230; + @Native final static public int NC_UP = 231; + @Native final static public int NC_DRAG = 232; + @Native final static public int NC_MOVE = 233; + @Native final static public int NC_ENTER = 234; + @Native final static public int NC_EXIT = 235; + + public static boolean isNonClientEvent(int event) { + return event >= NC_DOWN && event <= NC_EXIT; + } + + public static int toNonClientEvent(int event) { + return switch (event) { + case DOWN -> NC_DOWN; + case UP -> NC_UP; + case DRAG -> NC_DRAG; + case MOVE -> NC_MOVE; + case ENTER -> NC_ENTER; + case EXIT -> NC_EXIT; + default -> event; + }; + } } diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/Application.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/Application.java index 8d5f981030c..d0703da38ec 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/Application.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/Application.java @@ -714,6 +714,12 @@ public final boolean supportsUnifiedWindows() { return _supportsUnifiedWindows(); } + protected abstract boolean _supportsExtendedWindows(); + public final boolean supportsExtendedWindows() { + checkEventThread(); + return _supportsExtendedWindows(); + } + protected boolean _supportsSystemMenu() { // Overridden in subclasses return false; diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/NonClientHandler.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/NonClientHandler.java new file mode 100644 index 00000000000..d0a1dbf7da5 --- /dev/null +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/NonClientHandler.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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.sun.glass.ui; + +import javafx.stage.StageStyle; + +/** + * A non-client handler is used in some implementations of windows with the {@link StageStyle#EXTENDED} style. + * It can inspect a mouse event before it is sent to FX, and decide to consume it if it affects + * a non-client part of the window (for example, minimize/maximize/close buttons). + */ +public interface NonClientHandler { + + /** + * Handles the event. + * + * @return {@code true} if the event was handled, {@code false} otherwise + */ + boolean handleMouseEvent(int type, int button, int x, int y, int xAbs, int yAbs, int clickCount); +} diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/View.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/View.java index 5ee833e479b..97846bc8fa6 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/View.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/View.java @@ -365,6 +365,10 @@ public void handleSwipeGestureEvent(View view, long time, int type, int yAbs) { } + public boolean handleDragAreaHitTestEvent(double x, double y) { + return false; + } + public Accessible getSceneAccessible() { return null; } @@ -395,6 +399,7 @@ protected void _finishInputMethodComposition(long ptr) { */ private volatile long ptr; // Native handle (NSView*, or internal structure pointer) private Window window; // parent window + private NonClientHandler nonClientHandler; private EventHandler eventHandler; private int width = -1; // not set @@ -494,6 +499,12 @@ void setWindow(Window window) { this.window = window; _setParent(this.ptr, window == null ? 0L : window.getNativeHandle()); this.isValid = this.ptr != 0 && window != null; + + if (this.isValid && window.isExtendedWindow()) { + this.nonClientHandler = window.getNonClientHandler(); + } else { + this.nonClientHandler = null; + } } // package private @@ -913,15 +924,6 @@ protected void notifyMenu(int x, int y, int xAbs, int yAbs, boolean isKeyboardTr protected void notifyMouse(int type, int button, int x, int y, int xAbs, int yAbs, int modifiers, boolean isPopupTrigger, boolean isSynthesized) { - // gznote: optimize - only call for undecorated Windows! - if (this.window != null) { - // handled by window (programmatical move/resize) - if (this.window.handleMouseEvent(type, button, x, y, xAbs, yAbs)) { - // The evnet has been processed by Glass - return; - } - } - long now = System.nanoTime(); if (type == MouseEvent.DOWN) { View lastClickedView = View.lastClickedView == null ? null : View.lastClickedView.get(); @@ -945,6 +947,20 @@ protected void notifyMouse(int type, int button, int x, int y, int xAbs, lastClickedTime = now; } + // If we have a non-client handler, we give it the first chance to handle the event. + // Note that a full-screen window has no non-client area, and thus the non-client handler + // is not notified. + if (!inFullscreen + && nonClientHandler != null + && nonClientHandler.handleMouseEvent(type, button, x, y, xAbs, yAbs, clickCount)) { + return; + } + + // We never send non-client events to the application. + if (MouseEvent.isNonClientEvent(type)) { + return; + } + handleMouseEvent(now, type, button, x, y, xAbs, yAbs, modifiers, isPopupTrigger, isSynthesized); diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/Window.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/Window.java index 20e28d47109..bbbcd2389eb 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/Window.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/Window.java @@ -24,9 +24,10 @@ */ package com.sun.glass.ui; -import com.sun.glass.events.MouseEvent; import com.sun.glass.events.WindowEvent; import com.sun.prism.impl.PrismSettings; +import javafx.beans.value.ObservableValue; +import javafx.scene.Parent; import java.lang.annotation.Native; @@ -116,6 +117,7 @@ static protected void remove(Window window) { public static final int UNTITLED = 0; public static final int TITLED = 1 << 0; public static final int TRANSPARENT = 1 << 1; + public static final int EXTENDED = 1 << 2; // functional type: mutually exclusive /** @@ -130,7 +132,7 @@ static protected void remove(Window window) { * Often used for floating toolbars. It has smaller than usual decorations * and doesn't display a taskbar button. */ - @Native public static final int UTILITY = 1 << 2; + @Native public static final int UTILITY = 1 << 3; /** * A popup window. * @@ -138,18 +140,18 @@ static protected void remove(Window window) { * default it may display a task-bar button. To hide it the window must be * owned. */ - @Native public static final int POPUP = 1 << 3; + @Native public static final int POPUP = 1 << 4; // These affect window decorations as well as system menu actions, // so applicable to both decorated and undecorated windows - @Native public static final int CLOSABLE = 1 << 4; - @Native public static final int MINIMIZABLE = 1 << 5; - @Native public static final int MAXIMIZABLE = 1 << 6; + @Native public static final int CLOSABLE = 1 << 5; + @Native public static final int MINIMIZABLE = 1 << 6; + @Native public static final int MAXIMIZABLE = 1 << 7; /** * Indicates that the window trim will draw from right to left. */ - @Native public static final int RIGHT_TO_LEFT = 1 << 7; + @Native public static final int RIGHT_TO_LEFT = 1 << 8; /** * Indicates that a window will have a client area textured the same way as the platform decorations @@ -157,12 +159,12 @@ static protected void remove(Window window) { * This is supported not on all platforms, the client should check if the feature is supported by using * {@link com.sun.glass.ui.Application#supportsUnifiedWindows()} */ - @Native public static final int UNIFIED = 1 << 8; + @Native public static final int UNIFIED = 1 << 9; /** * Indicates that the window is modal which affects whether the window is minimizable. */ - @Native public static final int MODAL = 1 << 9; + @Native public static final int MODAL = 1 << 10; final static public class State { @Native public static final int NORMAL = 1; @@ -197,13 +199,11 @@ public static final class Level { private final int styleMask; private final boolean isDecorated; private final boolean isPopup; - private boolean shouldStartUndecoratedMove = false; protected View view = null; protected Screen screen = null; private MenuBar menubar = null; private String title = ""; - private UndecoratedMoveResizeHelper helper = null; private int state = State.NORMAL; private int level = Level.NORMAL; @@ -237,13 +237,14 @@ public static final class Level { protected abstract long _createWindow(long ownerPtr, long screenPtr, int mask); protected Window(Window owner, Screen screen, int styleMask) { Application.checkEventThread(); - switch (styleMask & (TITLED | TRANSPARENT)) { + switch (styleMask & (TITLED | TRANSPARENT | EXTENDED)) { case UNTITLED: case TITLED: case TRANSPARENT: + case EXTENDED: break; default: - throw new RuntimeException("The visual kind should be UNTITLED, TITLED, or TRANSPARENT, but not a combination of these"); + throw new RuntimeException("The visual kind should be UNTITLED, TITLED, TRANSPARENT, or EXTENDED, but not a combination of these"); } switch (styleMask & (POPUP | UTILITY)) { case NORMAL: @@ -254,6 +255,10 @@ protected Window(Window owner, Screen screen, int styleMask) { throw new RuntimeException("The functional type should be NORMAL, POPUP, or UTILITY, but not a combination of these"); } + if ((styleMask & UNIFIED) != 0 && (styleMask & EXTENDED) != 0) { + throw new RuntimeException("UNIFIED and EXTENDED cannot be combined"); + } + if (((styleMask & UNIFIED) != 0) && !Application.GetApplication().supportsUnifiedWindows()) { styleMask &= ~UNIFIED; @@ -264,7 +269,6 @@ protected Window(Window owner, Screen screen, int styleMask) { styleMask &= ~TRANSPARENT; } - this.owner = owner; this.styleMask = styleMask; this.isDecorated = (this.styleMask & Window.TITLED) != 0; @@ -375,9 +379,6 @@ public void setView(final View view) { // after we call view.setWindow(this); otherwise with UI scaling different than // 100% some platforms might display scenes wrong after Window was shown. _updateViewSize(this.ptr); - if (this.isDecorated == false) { - this.helper = new UndecoratedMoveResizeHelper(); - } } else { _setView(this.ptr, null); this.view = null; @@ -422,6 +423,33 @@ public void setMenuBar(final MenuBar menubar) { } } + /** + * Returns metrics of the window-provided overlay controls. + * + * @return the overlay metrics, or {@code null} if the window type does not support metrics + */ + public ObservableValue windowOverlayMetrics() { + return null; + } + + /** + * Returns the window-provided overlay controls, which are rendered above all application content. + * + * @return the overlay, or {@code null} if the window type does not provide overlay controls + */ + public Parent getWindowOverlay() { + return null; + } + + /** + * Returns the window-provided non-client event handler. + * + * @return the non-client event handler, or {@code null} + */ + public NonClientHandler getNonClientHandler() { + return null; + } + public boolean isDecorated() { Application.checkEventThread(); return this.isDecorated; @@ -674,6 +702,10 @@ public boolean isUnifiedWindow() { return (this.styleMask & Window.UNIFIED) != 0; } + public boolean isExtendedWindow() { + return (this.styleMask & Window.EXTENDED) != 0; + } + public boolean isTransparentWindow() { //The TRANSPARENT flag is set only if it is supported return (this.styleMask & Window.TRANSPARENT) != 0; @@ -1148,15 +1180,6 @@ public void setEventHandler(EventHandler eventHandler) { this.eventHandler = eventHandler; } - /** - * Enables unconditional start of window move operation when - * mouse is dragged in the client area. - */ - public void setShouldStartUndecoratedMove(boolean v) { - Application.checkEventThread(); - this.shouldStartUndecoratedMove = v; - } - // ***************************************************** // notification callbacks // ***************************************************** @@ -1214,11 +1237,6 @@ protected void notifyResize(final int type, final int width, final int height) { } this.width = width; this.height = height; - - // update moveRect/resizeRect - if (this.helper != null){ - this.helper.updateRectangles(); - } } handleWindowEvent(System.nanoTime(), type); @@ -1269,102 +1287,6 @@ protected void handleWindowEvent(long time, int type) { } } - // ***************************************************** - // programmatical move/resize - // ***************************************************** - /** Sets "programmatical move" rectangle. - * The rectangle is measured from top of the View: - * width is View.width, height is size. - * - * throws RuntimeException for decorated window. - */ - public void setUndecoratedMoveRectangle(int size) { - Application.checkEventThread(); - if (this.isDecorated == true) { - //throw new RuntimeException("setUndecoratedMoveRectangle is only valid for Undecorated Window"); - System.err.println("Glass Window.setUndecoratedMoveRectangle is only valid for Undecorated Window. In the future this will be hard error."); - Thread.dumpStack(); - return; - } - - if (this.helper != null) { - this.helper.setMoveRectangle(size); - } - } - /** The method called only for undecorated windows - * x, y: mouse coordinates (in View space). - * - * throws RuntimeException for decorated window. - */ - public boolean shouldStartUndecoratedMove(final int x, final int y) { - Application.checkEventThread(); - if (this.shouldStartUndecoratedMove == true) { - return true; - } - if (this.isDecorated == true) { - return false; - } - - if (this.helper != null) { - return this.helper.shouldStartMove(x, y); - } else { - return false; - } - } - - /** Sets "programmatical resize" rectangle. - * The rectangle is measured from top of the View: - * width is View.width, height is size. - * - * throws RuntimeException for decorated window. - */ - public void setUndecoratedResizeRectangle(int size) { - Application.checkEventThread(); - if ((this.isDecorated == true) || (this.isResizable == false)) { - //throw new RuntimeException("setUndecoratedMoveRectangle is only valid for Undecorated Resizable Window"); - System.err.println("Glass Window.setUndecoratedResizeRectangle is only valid for Undecorated Resizable Window. In the future this will be hard error."); - Thread.dumpStack(); - return; - } - - if (this.helper != null) { - this.helper.setResizeRectangle(size); - } - } - - /** The method called only for undecorated windows - * x, y: mouse coordinates (in View space). - * - * throws RuntimeException for decorated window. - */ - public boolean shouldStartUndecoratedResize(final int x, final int y) { - Application.checkEventThread(); - if ((this.isDecorated == true) || (this.isResizable == false)) { - return false; - } - - if (this.helper != null) { - return this.helper.shouldStartResize(x, y); - } else { - return false; - } - } - - /** Mouse event handler for processing programmatical resize/move - * (for undecorated windows only). - * Must be called by View. - * x & y are View coordinates. - * NOTE: it's package private! - * @return true if the event is processed by the window, - * false if it has to be delivered to the app - */ - boolean handleMouseEvent(int type, int button, int x, int y, int xAbs, int yAbs) { - if (this.isDecorated == false) { - return this.helper.handleMouseEvent(type, button, x, y, xAbs, yAbs); - } - return false; - } - @Override public String toString() { Application.checkEventThread(); @@ -1400,143 +1322,6 @@ protected void notifyLevelChanged(int level) { } } - private class UndecoratedMoveResizeHelper { - TrackingRectangle moveRect = null; - TrackingRectangle resizeRect = null; - - boolean inMove = false; // we are in "move" mode - boolean inResize = false; // we are in "resize" mode - - int startMouseX, startMouseY; // start mouse coords - int startX, startY; // start window location (for move) - int startWidth, startHeight; // start window size (for resize) - - UndecoratedMoveResizeHelper() { - this.moveRect = new TrackingRectangle(); - this.resizeRect = new TrackingRectangle(); - } - - void setMoveRectangle(final int size) { - this.moveRect.size = size; - - this.moveRect.x = 0; - this.moveRect.y = 0; - this.moveRect.width = getWidth(); - this.moveRect.height = this.moveRect.size; - } - - boolean shouldStartMove(final int x, final int y) { - return this.moveRect.contains(x, y); - } - - boolean inMove() { - return this.inMove; - } - - void startMove(final int x, final int y) { - this.inMove = true; - - this.startMouseX = x; - this.startMouseY = y; - - this.startX = getX(); - this.startY = getY(); - } - - void deltaMove(final int x, final int y) { - int deltaX = x - this.startMouseX; - int deltaY = y - this.startMouseY; - - setPosition(this.startX + deltaX, this.startY + deltaY); - } - - void stopMove() { - this.inMove = false; - } - - void setResizeRectangle(final int size) { - this.resizeRect.size = size; - - // set the rect (bottom right corner of the Window) - this.resizeRect.x = getWidth() - this.resizeRect.size; - this.resizeRect.y = getHeight() - this.resizeRect.size; - this.resizeRect.width = this.resizeRect.size; - this.resizeRect.height = this.resizeRect.size; - } - - boolean shouldStartResize(final int x, final int y) { - return this.resizeRect.contains(x, y); - } - - boolean inResize() { - return this.inResize; - } - - void startResize(final int x, final int y) { - this.inResize = true; - - this.startMouseX = x; - this.startMouseY = y; - - this.startWidth = getWidth(); - this.startHeight = getHeight(); - } - - void deltaResize(final int x, final int y) { - int deltaX = x - this.startMouseX; - int deltaY = y - this.startMouseY; - - setSize(this.startWidth + deltaX, this.startHeight + deltaY); - } - - protected void stopResize() { - this.inResize = false; - } - - void updateRectangles() { - if (this.moveRect.size > 0) { - setMoveRectangle(this.moveRect.size); - } - if (this.resizeRect.size > 0) { - setResizeRectangle(this.resizeRect.size); - } - } - - boolean handleMouseEvent(final int type, final int button, final int x, final int y, final int xAbs, final int yAbs) { - switch (type) { - case MouseEvent.DOWN: - if (button == MouseEvent.BUTTON_LEFT) { - if (shouldStartUndecoratedMove(x, y) == true) { - startMove(xAbs, yAbs); - return true; - } else if (shouldStartUndecoratedResize(x, y) == true) { - startResize(xAbs, yAbs); - return true; - } - } - break; - - case MouseEvent.MOVE: - case MouseEvent.DRAG: - if (inMove() == true) { - deltaMove(xAbs, yAbs); - return true; - } else if (inResize() == true) { - deltaResize(xAbs, yAbs); - return true; - } - break; - - case MouseEvent.UP: - boolean wasProcessed = inMove() || inResize(); - stopResize(); - stopMove(); - return wasProcessed; - } - return false; - } - } - /** * Requests text input in form of native keyboard for text component * contained by this Window. Native text input component is drawn on the place diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/WindowControlsOverlay.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/WindowControlsOverlay.java new file mode 100644 index 00000000000..ab476f16424 --- /dev/null +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/WindowControlsOverlay.java @@ -0,0 +1,540 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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.sun.glass.ui; + +import com.sun.glass.events.MouseEvent; +import com.sun.javafx.util.Utils; +import javafx.application.Platform; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.value.ObservableValue; +import javafx.css.CssMetaData; +import javafx.css.PseudoClass; +import javafx.css.SimpleStyleableBooleanProperty; +import javafx.css.SimpleStyleableIntegerProperty; +import javafx.css.SimpleStyleableObjectProperty; +import javafx.css.StyleConverter; +import javafx.css.Styleable; +import javafx.css.StyleableBooleanProperty; +import javafx.css.StyleableIntegerProperty; +import javafx.css.StyleableObjectProperty; +import javafx.css.StyleableProperty; +import javafx.geometry.Dimension2D; +import javafx.geometry.HPos; +import javafx.geometry.HorizontalDirection; +import javafx.geometry.NodeOrientation; +import javafx.geometry.VPos; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.layout.Region; +import javafx.scene.paint.Paint; +import javafx.stage.Stage; +import javafx.stage.StageStyle; +import javafx.util.Subscription; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Stream; + +/** + * Contains the visuals and behaviors for the minimize/maximize/close buttons on an {@link StageStyle#EXTENDED} + * window for platforms that use client-side decorations (Windows and Linux/GTK). This control supports + * left-to-right and right-to-left orientations, as well as a customizable layout order of buttons. + * + *

Substructure

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
CSS properties of {@code window-button-container}
CSS propertyValuesDefaultComment
-fx-button-placement[ left | right ]right + * Specifies the placement of the window buttons on the left or the right side of the window. + *
-fx-allow-rtl<boolean>true + * Specifies whether the minimize/maximize/close buttons support right-to-left orientations. + *
+ * + * + * + * + * + * + * + * + * + * + *
CSS properties of {@code minimize-button}, {@code maximize-button}, {@code close-button}
CSS propertyValuesDefaultComment
-fx-button-order<integer>0/1/2 + * Specifies the layout order of a button relative to the other buttons. + * Lower values are laid out before higher values.
+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Conditional style classes of window buttons
Style classApplies toComment
.darkall buttons + * This style class will be present if the brightness of {@link Scene#fillProperty()} + * as determined by {@link Utils#calculateAverageBrightness(Paint)} is less than 0.5 + *
.restore{@code maximize-button} + * This style class will be present if {@link Stage#isMaximized()} is {@code true}
+ */ +public final class WindowControlsOverlay extends Region { + + private static final CssMetaData BUTTON_PLACEMENT_METADATA = + new CssMetaData<>("-fx-button-placement", + StyleConverter.getEnumConverter(HorizontalDirection.class), + HorizontalDirection.RIGHT) { + @Override + public boolean isSettable(WindowControlsOverlay overlay) { + return true; + } + + @Override + public StyleableProperty getStyleableProperty(WindowControlsOverlay overlay) { + return overlay.buttonPlacement; + } + }; + + private static final CssMetaData ALLOW_RTL_METADATA = + new CssMetaData<>("-fx-allow-rtl", StyleConverter.getBooleanConverter(), true) { + @Override + public boolean isSettable(WindowControlsOverlay overlay) { + return true; + } + + @Override + public StyleableProperty getStyleableProperty(WindowControlsOverlay overlay) { + return overlay.allowRtl; + } + }; + + private static final List> METADATA = + Stream.concat(getClassCssMetaData().stream(), + Stream.of(BUTTON_PLACEMENT_METADATA, ALLOW_RTL_METADATA)).toList(); + + private static final PseudoClass HOVER_PSEUDOCLASS = PseudoClass.getPseudoClass("hover"); + private static final PseudoClass PRESSED_PSEUDOCLASS = PseudoClass.getPseudoClass("pressed"); + private static final PseudoClass ACTIVE_PSEUDOCLASS = PseudoClass.getPseudoClass("active"); + private static final String DARK_STYLE_CLASS = "dark"; + private static final String RESTORE_STYLE_CLASS = "restore"; + + /** + * The metrics (placement and size) of the window buttons. + */ + private final ObjectProperty metrics = new SimpleObjectProperty<>( + this, "metrics", new WindowOverlayMetrics(HorizontalDirection.RIGHT, new Dimension2D(0, 0))); + + /** + * Specifies the placement of the window buttons on the left or the right side of the window. + *

+ * This property corresponds to the {@code -fx-button-placement} CSS property. + */ + private final StyleableObjectProperty buttonPlacement = + new SimpleStyleableObjectProperty<>( + BUTTON_PLACEMENT_METADATA, this, "buttonPlacement", HorizontalDirection.RIGHT) { + @Override + protected void invalidated() { + requestLayout(); + } + }; + + /** + * Specifies whether the minimize/maximize/close buttons support right-to-left orientations. + *

+ * If this property is {@code true} and the effective node orientation is right-to-left, the + * window buttons are mirrored to the other side of the window. + *

+ * This property corresponds to the {@code -fx-allow-rtl} CSS property. + */ + private final StyleableBooleanProperty allowRtl = + new SimpleStyleableBooleanProperty(ALLOW_RTL_METADATA, this, "allowRtl", true) { + @Override + protected void invalidated() { + requestLayout(); + } + }; + + /** + * Contains the buttons in the order as they will appear on the window. + * This list is automatically updated by the implementation of {@link ButtonRegion#buttonOrder}. + */ + private final List orderedButtons = new ArrayList<>(3); + private final ButtonRegion minimizeButton = new ButtonRegion(ButtonType.MINIMIZE, "minimize-button", 0); + private final ButtonRegion maximizeButton = new ButtonRegion(ButtonType.MAXIMIZE, "maximize-button", 1); + private final ButtonRegion closeButton = new ButtonRegion(ButtonType.CLOSE, "close-button", 2); + private final Subscription subscriptions; + + private Node buttonAtMouseDown; + + public WindowControlsOverlay(ObservableValue stylesheet) { + var stage = sceneProperty() + .flatMap(Scene::windowProperty) + .map(w -> w instanceof Stage ? (Stage)w : null); + + var focusedSubscription = stage + .flatMap(Stage::focusedProperty) + .orElse(true) + .subscribe(this::onFocusedChanged); + + var resizableSubscription = stage + .flatMap(Stage::resizableProperty) + .orElse(true) + .subscribe(this::onResizableChanged); + + var maximizedSubscription = stage + .flatMap(Stage::maximizedProperty) + .orElse(false) + .subscribe(this::onMaximizedChanged); + + var updateStylesheetSubscription = sceneProperty() + .flatMap(Scene::fillProperty) + .map(this::isDarkBackground) + .orElse(false) + .subscribe(x -> updateStyleClass()); // use a value subscriber, not an invalidation subscriber + + subscriptions = Subscription.combine( + focusedSubscription, + resizableSubscription, + maximizedSubscription, + updateStylesheetSubscription, + stylesheet.subscribe(this::updateStylesheet)); + + getStyleClass().setAll("window-button-container"); + getChildren().addAll(minimizeButton, maximizeButton, closeButton); + } + + public void dispose() { + subscriptions.unsubscribe(); + } + + public ReadOnlyObjectProperty metricsProperty() { + return metrics; + } + + /** + * Classifies and returns the button type at the specified coordinate, or returns + * {@code null} if the specified coordinate does not intersect a button. + * + * @param x the X coordinate, in pixels relative to the window + * @param y the Y coordinate, in pixels relative to the window + * @return the {@code ButtonType} or {@code null} + */ + public ButtonType buttonAt(double x, double y) { + for (var button : orderedButtons) { + if (button.isVisible() && button.getBoundsInParent().contains(x, y)) { + return button.getButtonType(); + } + } + + return null; + } + + /** + * Handles the specified mouse event. + * + * @param type the event type + * @param button the button type + * @param x the X coordinate, in pixels relative to the window + * @param y the Y coordinate, in pixels relative to the window + * @return {@code true} if the event was handled, {@code false} otherwise + */ + public boolean handleMouseEvent(int type, int button, double x, double y) { + ButtonType buttonType = buttonAt(x, y); + Node node = buttonType != null ? switch (buttonType) { + case MINIMIZE -> minimizeButton; + case MAXIMIZE -> maximizeButton; + case CLOSE -> closeButton; + } : null; + + if (type == MouseEvent.NC_ENTER || type == MouseEvent.NC_MOVE || type == MouseEvent.NC_DRAG) { + handleMouseOver(node); + } else if (type == MouseEvent.NC_EXIT) { + handleMouseExit(); + } else if (type == MouseEvent.NC_UP && button == MouseEvent.BUTTON_LEFT) { + handleMouseUp(node, buttonType); + } else if (node != null && type == MouseEvent.NC_DOWN && button == MouseEvent.BUTTON_LEFT) { + handleMouseDown(node); + } + + return node != null || buttonAtMouseDown != null; + } + + private void handleMouseOver(Node button) { + minimizeButton.pseudoClassStateChanged(HOVER_PSEUDOCLASS, button == minimizeButton); + maximizeButton.pseudoClassStateChanged(HOVER_PSEUDOCLASS, button == maximizeButton); + closeButton.pseudoClassStateChanged(HOVER_PSEUDOCLASS, button == closeButton); + + if (buttonAtMouseDown != null && buttonAtMouseDown != button) { + buttonAtMouseDown.pseudoClassStateChanged(PRESSED_PSEUDOCLASS, false); + } + } + + private void handleMouseExit() { + buttonAtMouseDown = null; + + for (var node : new Node[] {minimizeButton, maximizeButton, closeButton}) { + node.pseudoClassStateChanged(HOVER_PSEUDOCLASS, false); + node.pseudoClassStateChanged(PRESSED_PSEUDOCLASS, false); + } + } + + private void handleMouseDown(Node node) { + buttonAtMouseDown = node; + + if (!node.isDisabled()) { + node.pseudoClassStateChanged(PRESSED_PSEUDOCLASS, true); + } + } + + private void handleMouseUp(Node node, ButtonType buttonType) { + boolean releasedOnButton = buttonAtMouseDown == node; + buttonAtMouseDown = null; + Scene scene = getScene(); + + if (node == null || node.isDisabled() + || scene == null || !(scene.getWindow() instanceof Stage stage)) { + return; + } + + node.pseudoClassStateChanged(PRESSED_PSEUDOCLASS, false); + + if (releasedOnButton) { + switch (buttonType) { + case MINIMIZE -> stage.setIconified(true); + case MAXIMIZE -> stage.setMaximized(!stage.isMaximized()); + case CLOSE -> stage.close(); + } + } + } + + private void onFocusedChanged(boolean focused) { + for (var node : new Node[] {minimizeButton, maximizeButton, closeButton}) { + node.pseudoClassStateChanged(ACTIVE_PSEUDOCLASS, focused); + } + } + + private void onResizableChanged(boolean resizable) { + maximizeButton.setDisable(!resizable); + } + + private void onMaximizedChanged(boolean maximized) { + toggleStyleClass(maximizeButton, RESTORE_STYLE_CLASS, maximized); + } + + private void updateStyleClass() { + boolean darkScene = isDarkBackground(getScene() != null ? getScene().getFill() : null); + toggleStyleClass(minimizeButton, DARK_STYLE_CLASS, darkScene); + toggleStyleClass(maximizeButton, DARK_STYLE_CLASS, darkScene); + toggleStyleClass(closeButton, DARK_STYLE_CLASS, darkScene); + } + + private void updateStylesheet(String stylesheet) { + getStylesheets().setAll(stylesheet); + } + + private void toggleStyleClass(Node node, String styleClass, boolean enabled) { + if (enabled && !node.getStyleClass().contains(styleClass)) { + node.getStyleClass().add(styleClass); + } else if (!enabled) { + node.getStyleClass().remove(styleClass); + } + } + + private boolean isDarkBackground(Paint paint) { + return paint != null && Utils.calculateAverageBrightness(paint) < 0.5; + } + + @Override + protected void layoutChildren() { + boolean left; + Node button1, button2, button3; + + if (allowRtl.get() && getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT) { + button1 = orderedButtons.get(2); + button2 = orderedButtons.get(1); + button3 = orderedButtons.get(0); + left = buttonPlacement.get() != HorizontalDirection.LEFT; + } else { + button1 = orderedButtons.get(0); + button2 = orderedButtons.get(1); + button3 = orderedButtons.get(2); + left = buttonPlacement.get() == HorizontalDirection.LEFT; + } + + double width = getWidth(); + double button1Width = boundedWidth(button1); + double button2Width = boundedWidth(button2); + double button3Width = boundedWidth(button3); + double button1Height = boundedHeight(button1); + double button2Height = boundedHeight(button2); + double button3Height = boundedHeight(button3); + double button1X = left ? 0 : width - button1Width - button2Width - button3Width; + double button2X = left ? button1Width : width - button3Width - button2Width; + double button3X = left ? button1Width + button2Width : width - button3Width; + double totalWidth = button1Width + button2Width + button3Width; + double totalHeight = Math.max(button1Height, Math.max(button2Height, button3Height)); + Dimension2D currentSize = metrics.get().size(); + + if (currentSize.getWidth() != totalWidth || currentSize.getHeight() != totalHeight) { + var newMetrics = new WindowOverlayMetrics( + left ? HorizontalDirection.LEFT : HorizontalDirection.RIGHT, + new Dimension2D(totalWidth, totalHeight)); + + // Don't change the metrics during layout, since we don't know who might be listening. + Platform.runLater(() -> metrics.set(newMetrics)); + } + + layoutInArea(button1, button1X, 0, button1Width, button1Height, + BASELINE_OFFSET_SAME_AS_HEIGHT, HPos.LEFT, VPos.TOP); + + layoutInArea(button2, button2X, 0, button2Width, button2Height, + BASELINE_OFFSET_SAME_AS_HEIGHT, HPos.LEFT, VPos.TOP); + + layoutInArea(button3, button3X, 0, button3Width, button3Height, + BASELINE_OFFSET_SAME_AS_HEIGHT, HPos.LEFT, VPos.TOP); + } + + @Override + public boolean usesMirroring() { + return false; + } + + @Override + public List> getCssMetaData() { + return METADATA; + } + + private static double boundedWidth(Node node) { + return node.isManaged() ? boundedSize(node.minWidth(-1), node.prefWidth(-1), node.maxWidth(-1)) : 0; + } + + private static double boundedHeight(Node node) { + return node.isManaged() ? boundedSize(node.minHeight(-1), node.prefHeight(-1), node.maxHeight(-1)) : 0; + } + + private static double boundedSize(double min, double pref, double max) { + return Math.min(Math.max(pref, min), Math.max(min, max)); + } + + public enum ButtonType { + MINIMIZE, + MAXIMIZE, + CLOSE + } + + private class ButtonRegion extends Region { + + private static final CssMetaData BUTTON_ORDER_METADATA = + new CssMetaData<>("-fx-button-order", StyleConverter.getSizeConverter()) { + @Override + public boolean isSettable(ButtonRegion node) { + return true; + } + + @Override + public StyleableProperty getStyleableProperty(ButtonRegion region) { + return region.buttonOrder; + } + }; + + private static final List> METADATA = + Stream.concat(getClassCssMetaData().stream(), Stream.of(BUTTON_ORDER_METADATA)).toList(); + + /** + * Specifies the layout order of this button relative to the other buttons. + * Buttons with a lower value are laid out before buttons with a higher value. + *

+ * This property corresponds to the {@code -fx-button-order} CSS property. + */ + private final StyleableIntegerProperty buttonOrder = + new SimpleStyleableIntegerProperty(BUTTON_ORDER_METADATA, this, "buttonOrder") { + @Override + protected void invalidated() { + requestParentLayout(); + + WindowControlsOverlay.this.orderedButtons.sort( + Comparator.comparing(ButtonRegion::getButtonOrder)); + } + }; + + private final Region glyph = new Region(); + private final ButtonType type; + + ButtonRegion(ButtonType type, String styleClass, int order) { + this.type = type; + orderedButtons.add(this); + buttonOrder.set(order); + glyph.getStyleClass().setAll("glyph"); + getChildren().add(glyph); + getStyleClass().setAll("window-button", styleClass); + } + + public ButtonType getButtonType() { + return type; + } + + public int getButtonOrder() { + return buttonOrder.get(); + } + + @Override + protected void layoutChildren() { + layoutInArea(glyph, 0, 0, getWidth(), getHeight(), 0, HPos.LEFT, VPos.TOP); + } + + @Override + public List> getCssMetaData() { + return METADATA; + } + } +} diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/WindowOverlayMetrics.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/WindowOverlayMetrics.java new file mode 100644 index 00000000000..15614172e27 --- /dev/null +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/WindowOverlayMetrics.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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.sun.glass.ui; + +import javafx.geometry.Dimension2D; +import javafx.geometry.HorizontalDirection; +import javafx.stage.StageStyle; +import java.util.Objects; + +/** + * Provides metrics about the window buttons of {@link StageStyle#EXTENDED} windows. + * + * @param placement the placement of the window buttons + * @param size the size of the window buttons + */ +public record WindowOverlayMetrics(HorizontalDirection placement, Dimension2D size) { + + public WindowOverlayMetrics { + Objects.requireNonNull(placement, "placement cannot be null"); + Objects.requireNonNull(size, "size cannot be null"); + } +} diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkApplication.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkApplication.java index fb1a1f8797d..8f0da0f8c71 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkApplication.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkApplication.java @@ -462,6 +462,11 @@ protected boolean _supportsInputMethods() { return false; } + @Override + protected boolean _supportsExtendedWindows() { + return true; + } + @Override protected native int _getKeyCodeForChar(char c, int hint); diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkWindow.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkWindow.java index 002d698398f..d02c4ffe390 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkWindow.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkWindow.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -24,12 +24,17 @@ */ package com.sun.glass.ui.gtk; +import com.sun.glass.events.MouseEvent; import com.sun.glass.ui.Cursor; import com.sun.glass.events.WindowEvent; +import com.sun.glass.ui.NonClientHandler; import com.sun.glass.ui.Pixels; import com.sun.glass.ui.Screen; import com.sun.glass.ui.View; import com.sun.glass.ui.Window; +import com.sun.glass.ui.WindowControlsOverlay; +import com.sun.glass.ui.WindowOverlayMetrics; +import javafx.beans.value.ObservableValue; class GtkWindow extends Window { @@ -198,4 +203,67 @@ public long getRawHandle() { long ptr = super.getRawHandle(); return ptr == 0L ? 0L : _getNativeWindowImpl(ptr); } + + private WindowControlsOverlay windowControlsOverlay; + + @Override + public ObservableValue windowOverlayMetrics() { + var overlay = getWindowOverlay(); + return overlay != null ? overlay.metricsProperty() : null; + } + + @Override + public WindowControlsOverlay getWindowOverlay() { + if (windowControlsOverlay == null && isExtendedWindow()) { + windowControlsOverlay = new WindowControlsOverlay( + PlatformThemeObserver.getInstance().stylesheetProperty()); + } + + return windowControlsOverlay; + } + + @Override + public NonClientHandler getNonClientHandler() { + var overlay = getWindowOverlay(); + if (overlay == null) { + return null; + } + + return (type, button, x, y, xAbs, yAbs, clickCount) -> { + // In contrast to Windows, GTK doesn't produce non-client events. We convert regular + // mouse events to non-client events since that's what WindowControlsOverlay expects. + return overlay.handleMouseEvent( + MouseEvent.toNonClientEvent(type), button, x / platformScaleX, y / platformScaleY); + }; + } + + /** + * Returns whether the window is draggable at the specified coordinate. + *

+ * This method is called from native code. + * + * @param x the X coordinate in physical pixels + * @param y the Y coordinate in physical pixels + */ + @SuppressWarnings("unused") + private boolean dragAreaHitTest(int x, int y) { + // A full-screen window has no draggable area. + if (view == null || view.isInFullscreen() || !isExtendedWindow()) { + return false; + } + + double wx = x / platformScaleX; + double wy = y / platformScaleY; + + if (windowControlsOverlay != null && windowControlsOverlay.buttonAt(wx, wy) != null) { + return false; + } + + View.EventHandler eventHandler = view.getEventHandler(); + if (eventHandler == null) { + return false; + } + + return eventHandler.handleDragAreaHitTestEvent(wx, wy); + } } diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/PlatformThemeObserver.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/PlatformThemeObserver.java new file mode 100644 index 00000000000..583a6653985 --- /dev/null +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/PlatformThemeObserver.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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.sun.glass.ui.gtk; + +import com.sun.javafx.application.PlatformImpl; +import javafx.beans.property.ReadOnlyStringProperty; +import javafx.beans.property.ReadOnlyStringWrapper; +import javafx.collections.MapChangeListener; + +final class PlatformThemeObserver { + + private static final String THEME_NAME_KEY = "GTK.theme_name"; + + private final ReadOnlyStringWrapper stylesheet = new ReadOnlyStringWrapper(this, "stylesheet"); + + private PlatformThemeObserver() { + PlatformImpl.getPlatformPreferences().addListener((MapChangeListener) change -> { + if (THEME_NAME_KEY.equals(change.getKey())) { + updateThemeStylesheets(); + } + }); + + updateThemeStylesheets(); + } + + public static PlatformThemeObserver getInstance() { + class Holder { + static final PlatformThemeObserver instance = new PlatformThemeObserver(); + } + + return Holder.instance; + } + + public ReadOnlyStringProperty stylesheetProperty() { + return stylesheet.getReadOnlyProperty(); + } + + private void updateThemeStylesheets() { + stylesheet.set(WindowDecorationTheme.findBestTheme().getStylesheet()); + } +} diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/WindowDecorationTheme.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/WindowDecorationTheme.java new file mode 100644 index 00000000000..007a2e22864 --- /dev/null +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/WindowDecorationTheme.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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.sun.glass.ui.gtk; + +import com.sun.javafx.application.PlatformImpl; +import javafx.stage.StageStyle; +import java.util.Locale; +import java.util.Map; + +/** + * The client-side window decoration theme used for {@link StageStyle#EXTENDED} windows. + */ +enum WindowDecorationTheme { + + GNOME("WindowDecorationGnome.css"), + KDE("WindowDecorationKDE.css"); + + WindowDecorationTheme(String stylesheet) { + this.stylesheet = stylesheet; + } + + private static final String THEME_NAME_KEY = "GTK.theme_name"; + + /** + * A mapping of platform theme names to the most similar window decoration theme. + */ + private static final Map SIMILAR_THEMES = Map.of( + "adwaita", WindowDecorationTheme.GNOME, + "yaru", WindowDecorationTheme.GNOME, + "breeze", WindowDecorationTheme.KDE + ); + + private final String stylesheet; + + /** + * Determines the best window decoration theme for the current window manager theme. + *

+ * Since we can't ship decorations for all possible window manager themes, we need to choose the + * theme most similar to the native window manager theme. If we can't choose a theme by name, we + * fall back to choosing a theme by determining the current window manager. + */ + public static WindowDecorationTheme findBestTheme() { + return PlatformImpl.getPlatformPreferences() + .getString(THEME_NAME_KEY) + .map(name -> { + for (Map.Entry entry : SIMILAR_THEMES.entrySet()) { + if (name.toLowerCase(Locale.ROOT).startsWith(entry.getKey())) { + return entry.getValue(); + } + } + + return null; + }) + .orElse(switch (WindowManager.current()) { + case GNOME -> WindowDecorationTheme.GNOME; + case KDE -> WindowDecorationTheme.KDE; + default -> WindowDecorationTheme.GNOME; + }); + } + + public String getStylesheet() { + var url = getClass().getResource(stylesheet); + if (url == null) { + throw new RuntimeException("Resource not found: " + stylesheet); + } + + return url.toExternalForm(); + } +} diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/WindowManager.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/WindowManager.java new file mode 100644 index 00000000000..43f9ca04b44 --- /dev/null +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/WindowManager.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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.sun.glass.ui.gtk; + +import java.util.Locale; + +/** + * The window manager of the current desktop environment. + */ +enum WindowManager { + UNKNOWN, + GNOME, + KDE; + + /** + * Returns the window manager of the current desktop environment. + */ + public static WindowManager current() { + var result = parse(System.getenv("XDG_CURRENT_DESKTOP")); + if (result != UNKNOWN) { + return result; + } + + result = parse(System.getenv("GDMSESSION")); + if (result != UNKNOWN) { + return result; + } + + if (System.getenv("KDE_FULL_SESSION") != null) { + return KDE; + } + + return UNKNOWN; + } + + private static WindowManager parse(String value) { + if (value == null) { + return UNKNOWN; + } + + String v = value.toLowerCase(Locale.ROOT); + + if (v.contains("gnome")) { + return GNOME; + } + + if (v.contains("kde")) { + return KDE; + } + + return UNKNOWN; + } +} diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/ios/IosApplication.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/ios/IosApplication.java index 27e32634776..877566ad971 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/ios/IosApplication.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/ios/IosApplication.java @@ -228,6 +228,11 @@ protected boolean _supportsTransparentWindows() { return false; } + @Override + protected boolean _supportsExtendedWindows() { + return false; + } + /** * Hides / Shows iOS status bar. * @param hidden diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/mac/MacApplication.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/mac/MacApplication.java index fedd40bef09..d8efc945157 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/mac/MacApplication.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/mac/MacApplication.java @@ -407,6 +407,11 @@ protected boolean _supportsTransparentWindows() { return true; } + @Override + protected boolean _supportsExtendedWindows() { + return true; + } + @Override native protected boolean _supportsSystemMenu(); // NOTE: this will not return a valid result until the native _runloop diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/mac/MacWindow.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/mac/MacWindow.java index 01c1a402012..501991e4d1a 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/mac/MacWindow.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/mac/MacWindow.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -24,12 +24,19 @@ */ package com.sun.glass.ui.mac; +import com.sun.glass.events.MouseEvent; import com.sun.glass.events.WindowEvent; import com.sun.glass.ui.Cursor; +import com.sun.glass.ui.NonClientHandler; import com.sun.glass.ui.Pixels; import com.sun.glass.ui.Screen; import com.sun.glass.ui.View; import com.sun.glass.ui.Window; +import com.sun.glass.ui.WindowOverlayMetrics; +import com.sun.javafx.binding.ObjectConstant; +import javafx.beans.value.ObservableValue; +import javafx.geometry.Dimension2D; +import javafx.geometry.HorizontalDirection; import java.nio.ByteBuffer; /** @@ -150,5 +157,46 @@ protected void _requestInput(long ptr, String text, int type, double width, doub protected void _releaseInput(long ptr) { throw new UnsupportedOperationException("Not supported yet."); } + + private native void _performWindowDrag(long ptr); + + @Override + public NonClientHandler getNonClientHandler() { + return (type, button, x, y, xAbs, yAbs, clickCount) -> { + if (type == MouseEvent.DOWN) { + double wx = x / platformScaleX; + double wy = y / platformScaleY; + + View.EventHandler eventHandler = view != null ? view.getEventHandler() : null; + if (eventHandler != null && eventHandler.handleDragAreaHitTestEvent(wx, wy)) { + if (clickCount == 2) { + maximize(!isMaximized()); + } else if (clickCount == 1) { + _performWindowDrag(getRawHandle()); + } + } + } + + return false; + }; + } + + private native boolean _isRightToLeftLayoutDirection(); + + private ObservableValue windowOverlayMetrics; + + @Override + public ObservableValue windowOverlayMetrics() { + if (windowOverlayMetrics == null) { + HorizontalDirection direction = _isRightToLeftLayoutDirection() + ? HorizontalDirection.RIGHT + : HorizontalDirection.LEFT; + + windowOverlayMetrics = ObjectConstant.valueOf( + new WindowOverlayMetrics(direction, new Dimension2D(78, 38))); + } + + return windowOverlayMetrics; + } } diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/monocle/MonocleApplication.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/monocle/MonocleApplication.java index 3b5618d5195..fbbfefe6cb2 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/monocle/MonocleApplication.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/monocle/MonocleApplication.java @@ -306,6 +306,11 @@ protected boolean _supportsUnifiedWindows() { return false; } + @Override + protected boolean _supportsExtendedWindows() { + return false; + } + @Override public boolean hasTwoLevelFocus() { return deviceFlags[DEVICE_PC_KEYBOARD] == 0 && deviceFlags[DEVICE_5WAY] > 0; diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/win/WinApplication.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/win/WinApplication.java index 2c7b3ff7c2c..7686671437a 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/win/WinApplication.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/win/WinApplication.java @@ -352,6 +352,11 @@ protected boolean _supportsTransparentWindows() { @Override native protected boolean _supportsUnifiedWindows(); + @Override + protected boolean _supportsExtendedWindows() { + return true; + } + @Override public String getDataDirectory() { checkEventThread(); diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/win/WinWindow.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/win/WinWindow.java index 4e1bd82093b..52c35f88dc6 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/win/WinWindow.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/win/WinWindow.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,10 +25,15 @@ package com.sun.glass.ui.win; import com.sun.glass.ui.Cursor; +import com.sun.glass.ui.WindowControlsOverlay; +import com.sun.glass.ui.NonClientHandler; import com.sun.glass.ui.Pixels; import com.sun.glass.ui.Screen; import com.sun.glass.ui.View; import com.sun.glass.ui.Window; +import com.sun.glass.ui.WindowOverlayMetrics; +import com.sun.javafx.binding.StringConstant; +import javafx.beans.value.ObservableValue; /** * MS Windows platform implementation class for Window. @@ -40,10 +45,12 @@ class WinWindow extends Window { public static final long ANCHOR_NO_CAPTURE = (1L << 63); - float fxReqWidth; - float fxReqHeight; - int pfReqWidth; - int pfReqHeight; + private static final String WINDOW_DECORATION_STYLESHEET = "WindowDecoration.css"; + + private float fxReqWidth; + private float fxReqHeight; + private int pfReqWidth; + private int pfReqHeight; private native static void _initIDs(); static { @@ -115,6 +122,10 @@ public void setBounds(float x, float y, boolean xSet, boolean ySet, } fxReqHeight = fx_ch; + int maxW = getMaximumWidth(), maxH = getMaximumHeight(); + pw = Math.max(Math.min(pw, maxW > 0 ? maxW : Integer.MAX_VALUE), getMinimumWidth()); + ph = Math.max(Math.min(ph, maxH > 0 ? maxH : Integer.MAX_VALUE), getMinimumHeight()); + long anchor = _getAnchor(getRawHandle()); int resizeMode = (anchor == ANCHOR_NO_CAPTURE) ? RESIZE_TO_FX_ORIGIN @@ -315,10 +326,93 @@ void setDeferredClosing(boolean dc) { @Override public void close() { if (!deferredClosing) { + if (windowControlsOverlay != null) { + windowControlsOverlay.dispose(); + } + super.close(); } else { closingRequested = true; setVisible(false); } } + + private WindowControlsOverlay windowControlsOverlay; + + @Override + public ObservableValue windowOverlayMetrics() { + var overlay = getWindowOverlay(); + return overlay != null ? overlay.metricsProperty() : null; + } + + @Override + public WindowControlsOverlay getWindowOverlay() { + if (windowControlsOverlay == null && isExtendedWindow()) { + var url = getClass().getResource(WINDOW_DECORATION_STYLESHEET); + if (url == null) { + throw new RuntimeException("Resource not found: " + WINDOW_DECORATION_STYLESHEET); + } + + windowControlsOverlay = new WindowControlsOverlay(StringConstant.valueOf(url.toExternalForm())); + } + + return windowControlsOverlay; + } + + @Override + public NonClientHandler getNonClientHandler() { + var overlay = getWindowOverlay(); + if (overlay == null) { + return null; + } + + return (type, button, x, y, xAbs, yAbs, clickCount) -> { + double wx = x / platformScaleX; + double wy = y / platformScaleY; + return overlay.handleMouseEvent(type, button, wx, wy); + }; + } + + /** + * Classifies the window region at the specified physical coordinate. + *

+ * This method is called from native code. + * + * @param x the X coordinate in physical pixels + * @param y the Y coordinate in physical pixels + */ + @SuppressWarnings("unused") + private int nonClientHitTest(int x, int y) { + // https://learn.microsoft.com/en-us/windows/win32/inputdev/wm-nchittest + enum HT { + CLIENT(1), CAPTION(2), MINBUTTON(8), MAXBUTTON(9), CLOSE(20); + HT(int value) { this.value = value; } + final int value; + } + + // A full-screen window has no non-client area. + if (view == null || view.isInFullscreen() || !isExtendedWindow()) { + return HT.CLIENT.value; + } + + double wx = x / platformScaleX; + double wy = y / platformScaleY; + + // If the cursor is over one of the window buttons (minimize, maximize, close), we need to + // report the value of HTMINBUTTON, HTMAXBUTTON, or HTCLOSE back to the native layer. + switch (windowControlsOverlay != null ? windowControlsOverlay.buttonAt(wx, wy) : null) { + case MINIMIZE: return HT.MINBUTTON.value; + case MAXIMIZE: return HT.MAXBUTTON.value; + case CLOSE: return HT.CLOSE.value; + case null: break; + } + + // Otherwise, test if the cursor is over a draggable area and return HTCAPTION. + View.EventHandler eventHandler = view.getEventHandler(); + if (eventHandler != null && eventHandler.handleDragAreaHitTestEvent(wx, wy)) { + return HT.CAPTION.value; + } + + return HT.CLIENT.value; + } } diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/scene/NodeHelper.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/scene/NodeHelper.java index 41b1fd62244..94372a6bd12 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/scene/NodeHelper.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/scene/NodeHelper.java @@ -46,6 +46,7 @@ import javafx.css.StyleableProperty; import javafx.geometry.Bounds; import javafx.scene.Node; +import javafx.scene.Scene; import javafx.scene.SubScene; import javafx.scene.shape.Shape; import javafx.scene.shape.Shape3D; @@ -208,6 +209,14 @@ public static boolean isDirtyEmpty(Node node) { return nodeAccessor.isDirtyEmpty(node); } + public static void setScenes(Node node, Scene newScene, SubScene newSubScene) { + nodeAccessor.setScenes(node, newScene, newSubScene); + } + + public static void updateBounds(Node node) { + nodeAccessor.updateBounds(node); + } + public static void syncPeer(Node node) { nodeAccessor.syncPeer(node); } @@ -372,6 +381,8 @@ boolean doComputeIntersects(Node node, PickRay pickRay, void doProcessCSS(Node node); boolean isDirty(Node node, DirtyBits dirtyBit); boolean isDirtyEmpty(Node node); + void setScenes(Node node, Scene newScene, SubScene newSubScene); + void updateBounds(Node node); void syncPeer(Node node);

P getPeer(Node node); void layoutBoundsChanged(Node node); diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/TKScene.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/TKScene.java index 3d3358c8446..9cfe483499b 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/TKScene.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/TKScene.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -89,4 +89,10 @@ public interface TKScene { @SuppressWarnings("removal") public AccessControlContext getAccessControlContext(); + + default void processOverlayCSS() {} + + default void layoutOverlay() {} + + default void synchronizeOverlay() {} } diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/TKSceneListener.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/TKSceneListener.java index 116bff3d590..7a5cdfe8ac1 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/TKSceneListener.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/TKSceneListener.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -120,4 +120,13 @@ public void touchEventNext( public void touchEventEnd(); public Accessible getSceneAccessible(); + + /** + * Tests whether the specified coordinate identifies a draggable area. + * + * @param x the X coordinate relative to the scene + * @param y the Y coordinate relative to the scene + * @return {@code true} if the area is draggable, {@code false} otherwise + */ + public boolean dragAreaHitTest(double x, double y); } diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/GlassViewEventHandler.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/GlassViewEventHandler.java index a263853789e..7d5f8b90ce8 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/GlassViewEventHandler.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/GlassViewEventHandler.java @@ -892,8 +892,7 @@ public Void run() { final Window w = view.getWindow(); float pScaleX = (w == null) ? 1.0f : w.getPlatformScaleX(); float pScaleY = (w == null) ? 1.0f : w.getPlatformScaleY(); - scene.sceneListener.changedSize(view.getWidth() / pScaleX, - view.getHeight() / pScaleY); + scene.setViewSize(view.getWidth() / pScaleX, view.getHeight() / pScaleY); scene.entireSceneNeedsRepaint(); QuantumToolkit.runWithRenderLock(() -> { scene.updateSceneState(); @@ -1407,4 +1406,15 @@ public Accessible getSceneAccessible() { } return null; } + + @Override + public boolean handleDragAreaHitTestEvent(double x, double y) { + return QuantumToolkit.runWithoutRenderLock(() -> { + if (scene.sceneListener != null) { + return scene.sceneListener.dragAreaHitTest(x, y); + } + + return false; + }); + } } diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/OverlayWarning.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/OverlayWarning.java index c4fb8f70f58..445c6b7a8e8 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/OverlayWarning.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/OverlayWarning.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,15 +25,12 @@ package com.sun.javafx.tk.quantum; -import com.sun.javafx.scene.DirtyBits; -import com.sun.javafx.scene.NodeHelper; import javafx.animation.Animation.Status; import javafx.animation.FadeTransition; import javafx.animation.PauseTransition; import javafx.animation.SequentialTransition; import javafx.geometry.Rectangle2D; import javafx.scene.Group; -import javafx.scene.Node; import javafx.scene.paint.Color; import javafx.scene.shape.Rectangle; import javafx.scene.text.Font; @@ -42,22 +39,6 @@ import javafx.util.Duration; public class OverlayWarning extends Group { - static { - // This is used by classes in different packages to get access to - // private and package private methods. - OverlayWarningHelper.setOverlayWarningAccessor( - new OverlayWarningHelper.OverlayWarningAccessor() { - @Override - public void doUpdatePeer(Node node) { - ((OverlayWarning) node).doUpdatePeer(); - } - - @Override - public void doMarkDirty(Node node, DirtyBits dirtyBit) { - ((OverlayWarning) node).doMarkDirty(dirtyBit); - } - }); - } private static final float PAD = 40f; private static final float RECTW = 600f; @@ -69,11 +50,6 @@ public void doMarkDirty(Node node, DirtyBits dirtyBit) { private SequentialTransition overlayTransition; private boolean warningTransition; - { - // To initialize the class helper at the begining each constructor of this class - OverlayWarningHelper.initHelper(this); - } - public OverlayWarning(final ViewScene vs) { view = vs; @@ -170,24 +146,4 @@ private Rectangle createBackground(Text text, Rectangle2D screen) { return rectangle; } - - /* - * Note: This method MUST only be called via its accessor method. - */ - private void doUpdatePeer() { - NodeHelper.updatePeer(text); - NodeHelper.updatePeer(background); - } - - @Override - protected void updateBounds() { - super.updateBounds(); - } - - /* - * Note: This method MUST only be called via its accessor method. - */ - private void doMarkDirty(DirtyBits dirtyBit) { - view.synchroniseOverlayWarning(); - } } diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/OverlayWarningHelper.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/OverlayWarningHelper.java deleted file mode 100644 index d6f317eca5e..00000000000 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/OverlayWarningHelper.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (c) 2016, 2022, Oracle and/or its affiliates. All rights reserved. - * 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. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * 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.sun.javafx.tk.quantum; - -import com.sun.javafx.scene.DirtyBits; -import com.sun.javafx.scene.GroupHelper; -import com.sun.javafx.util.Utils; -import javafx.scene.Node; - -/** - * Used to access internal methods of OverlayWarning. - */ -public class OverlayWarningHelper extends GroupHelper { - - private static final OverlayWarningHelper theInstance; - private static OverlayWarningAccessor overlayWarningAccessor; - - static { - theInstance = new OverlayWarningHelper(); - Utils.forceInit(OverlayWarning.class); - } - - private static OverlayWarningHelper getInstance() { - return theInstance; - } - - public static void initHelper(OverlayWarning overlayWarning) { - setHelper(overlayWarning, getInstance()); - } - - @Override - protected void updatePeerImpl(Node node) { - overlayWarningAccessor.doUpdatePeer(node); - super.updatePeerImpl(node); - } - - @Override - protected void markDirtyImpl(Node node, DirtyBits dirtyBit) { - super.markDirtyImpl(node, dirtyBit); - overlayWarningAccessor.doMarkDirty(node, dirtyBit); - } - - public static void setOverlayWarningAccessor(final OverlayWarningAccessor newAccessor) { - if (overlayWarningAccessor != null) { - throw new IllegalStateException(); - } - - overlayWarningAccessor = newAccessor; - } - - public interface OverlayWarningAccessor { - void doMarkDirty(Node node, DirtyBits dirtyBit); - void doUpdatePeer(Node node); - } - -} diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/QuantumToolkit.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/QuantumToolkit.java index 96f00d09a75..031f80bba41 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/QuantumToolkit.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/QuantumToolkit.java @@ -1256,6 +1256,8 @@ public boolean isSupported(ConditionalFeature feature) { return Application.GetApplication().supportsTransparentWindows(); case UNIFIED_WINDOW: return Application.GetApplication().supportsUnifiedWindows(); + case EXTENDED_WINDOW: + return Application.GetApplication().supportsExtendedWindows(); case TWO_LEVEL_FOCUS: return Application.GetApplication().hasTwoLevelFocus(); case VIRTUAL_KEYBOARD: diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/ViewScene.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/ViewScene.java index e781830f8d6..83f0a62f058 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/ViewScene.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/ViewScene.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2008, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2008, 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -32,24 +32,27 @@ import com.sun.glass.ui.View; import com.sun.glass.ui.Window; import com.sun.javafx.cursor.CursorFrame; -import com.sun.javafx.scene.NodeHelper; import com.sun.javafx.sg.prism.NGNode; import com.sun.javafx.tk.Toolkit; import com.sun.prism.GraphicsPipeline; +import javafx.scene.Parent; class ViewScene extends GlassScene { private static final String UNSUPPORTED_FORMAT = "Transparent windows only supported for BYTE_BGRA_PRE format on LITTLE_ENDIAN machines"; + private final javafx.scene.Scene fxScene; private View platformView; private ViewPainter painter; - private PaintRenderJob paintRenderJob; + private ViewSceneOverlay viewSceneOverlay; + private Parent overlayRoot; - public ViewScene(boolean depthBuffer, boolean msaa) { + public ViewScene(javafx.scene.Scene fxScene, boolean depthBuffer, boolean msaa) { super(depthBuffer, msaa); + this.fxScene = fxScene; this.platformView = Application.GetApplication().createView(); this.platformView.setEventHandler(new GlassViewEventHandler(this)); } @@ -81,6 +84,14 @@ public void setStage(GlassStage stage) { } else { painter = new PresentingPainter(this); } + + if (fxScene != null) { + viewSceneOverlay = new ViewSceneOverlay(fxScene, painter); + viewSceneOverlay.setRoot(overlayRoot); + } else { + viewSceneOverlay = null; + } + painter.setRoot(getRoot()); paintRenderJob = new PaintRenderJob(this, PaintCollector.getInstance().getRendered(), painter); } @@ -101,6 +112,7 @@ public void dispose() { updateSceneState(); painter = null; paintRenderJob = null; + viewSceneOverlay = null; return null; }); } @@ -152,26 +164,46 @@ public void finishInputMethodComposition() { platformView.finishInputMethodComposition(); } - @Override public String toString() { - View view = getPlatformView(); - return (" scene: " + hashCode() + " @ (" + view.getWidth() + "," + view.getHeight() + ")"); + @Override + public void processOverlayCSS() { + if (viewSceneOverlay != null) { + viewSceneOverlay.processCSS(); + } } - void synchroniseOverlayWarning() { - try { - waitForSynchronization(); - OverlayWarning warning = getWindowStage().getWarning(); - if (warning == null) { - painter.setOverlayRoot(null); - } else { - painter.setOverlayRoot(NodeHelper.getPeer(warning)); - warning.updateBounds(); - NodeHelper.updatePeer(warning); - } - } finally { - releaseSynchronization(true); - entireSceneNeedsRepaint(); + @Override + public void layoutOverlay() { + if (viewSceneOverlay != null) { + viewSceneOverlay.layout(); + } + } + + @Override + public void synchronizeOverlay() { + if (viewSceneOverlay != null) { + viewSceneOverlay.synchronize(); + } + } + + public void setViewSize(float width, float height) { + sceneListener.changedSize(width, height); + + if (viewSceneOverlay != null) { + viewSceneOverlay.resize(width, height); } } + + public void setOverlay(Parent root) { + overlayRoot = root; + + if (viewSceneOverlay != null) { + viewSceneOverlay.setRoot(root); + } + } + + @Override public String toString() { + View view = getPlatformView(); + return (" scene: " + hashCode() + " @ (" + view.getWidth() + "," + view.getHeight() + ")"); + } } diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/ViewSceneOverlay.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/ViewSceneOverlay.java new file mode 100644 index 00000000000..fe9d18540b2 --- /dev/null +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/ViewSceneOverlay.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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.sun.javafx.tk.quantum; + +import com.sun.javafx.scene.NodeHelper; +import javafx.scene.Node; +import javafx.scene.Parent; +import javafx.scene.SubScene; + +/** + * Shows an overlay over a {@link javafx.scene.Scene}. + * The overlay is not part of the scene graph, and not accessible by applications. + */ +final class ViewSceneOverlay { + + private final javafx.scene.Scene fxScene; + private final ViewPainter painter; + private Parent root; + private double width, height; + + ViewSceneOverlay(javafx.scene.Scene fxScene, ViewPainter painter) { + this.fxScene = fxScene; + this.painter = painter; + } + + public void processCSS() { + if (root != null) { + NodeHelper.processCSS(root); + } + } + + public void resize(double width, double height) { + this.width = width; + this.height = height; + } + + public void layout() { + if (fxScene == null) { + return; + } + + var window = fxScene.getWindow(); + + if (root != null && window != null) { + root.resize(width, height); + root.layout(); + NodeHelper.updateBounds(root); + } + } + + public void setRoot(Parent root) { + if (this.root == root) { + return; + } + + this.root = root; + + if (root != null) { + NodeHelper.setScenes(root, fxScene, null); + painter.setOverlayRoot(NodeHelper.getPeer(root)); + } else { + painter.setOverlayRoot(null); + } + } + + public void synchronize() { + if (root != null && !NodeHelper.isDirtyEmpty(root)) { + syncPeer(root); + } + } + + private void syncPeer(Node node) { + NodeHelper.syncPeer(node); + + if (node instanceof Parent parent) { + for (Node child : parent.getChildrenUnmodifiable()) { + syncPeer(child); + } + } else if (node instanceof SubScene subScene) { + syncPeer(subScene.getRoot()); + } + + if (node.getClip() != null) { + syncPeer(node.getClip()); + } + } +} diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/WindowStage.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/WindowStage.java index eef1ce1742b..5c76cde4d77 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/WindowStage.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/WindowStage.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2008, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2008, 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -154,20 +154,25 @@ private void initPlatformWindow() { } focusable = false; } else { + // Downgrade conditional stage styles if not supported + if (style == StageStyle.UNIFIED && !app.supportsUnifiedWindows()) { + style = StageStyle.DECORATED; + } else if (style == StageStyle.EXTENDED && !app.supportsExtendedWindows()) { + style = StageStyle.DECORATED; + } + switch (style) { case UNIFIED: - if (app.supportsUnifiedWindows()) { - windowMask |= Window.UNIFIED; - } + windowMask |= Window.UNIFIED; // fall through case DECORATED: windowMask |= Window.TITLED | Window.CLOSABLE | Window.MINIMIZABLE | Window.MAXIMIZABLE; - if (ownerWindow != null || modality != Modality.NONE) { - windowMask &= - ~(Window.MINIMIZABLE | Window.MAXIMIZABLE); - } + resizable = true; + break; + case EXTENDED: + windowMask |= Window.EXTENDED | Window.CLOSABLE | Window.MINIMIZABLE | Window.MAXIMIZABLE; resizable = true; break; case UTILITY: @@ -178,6 +183,10 @@ private void initPlatformWindow() { (transparent ? Window.TRANSPARENT : Window.UNTITLED) | Window.CLOSABLE; break; } + + if (ownerWindow != null || modality != Modality.NONE) { + windowMask &= ~(Window.MINIMIZABLE | Window.MAXIMIZABLE); + } } if (modality != Modality.NONE) { windowMask |= Window.MODAL; @@ -244,8 +253,14 @@ StageStyle getStyle() { } @Override public TKScene createTKScene(boolean depthBuffer, boolean msaa, @SuppressWarnings("removal") AccessControlContext acc) { - ViewScene scene = new ViewScene(depthBuffer, msaa); + ViewScene scene = new ViewScene(fxStage != null ? fxStage.getScene() : null, depthBuffer, msaa); scene.setSecurityContext(acc); + + // The window-provided overlay is not visible in full-screen mode. + if (!isInFullScreen) { + scene.setOverlay(platformWindow.getWindowOverlay()); + } + return scene; } @@ -696,8 +711,9 @@ private void applyFullScreen() { } else { if (warning != null) { warning.cancel(); - setWarning(null); } + + setWarning(null); v.exitFullscreen(false); } // Reset flag once we are done process fullscreen @@ -711,11 +727,11 @@ private void applyFullScreen() { void setWarning(OverlayWarning newWarning) { this.warning = newWarning; - getViewScene().synchroniseOverlayWarning(); - } - - OverlayWarning getWarning() { - return warning; + if (newWarning != null) { + getViewScene().setOverlay(newWarning); + } else if (!isInFullScreen) { + getViewScene().setOverlay(platformWindow.getWindowOverlay()); + } } @Override public void setFullScreen(boolean fullScreen) { diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/util/Utils.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/util/Utils.java index 495c0aac45f..02c35b79632 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/util/Utils.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/util/Utils.java @@ -38,6 +38,9 @@ import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.paint.Color; +import javafx.scene.paint.LinearGradient; +import javafx.scene.paint.Paint; +import javafx.scene.paint.RadialGradient; import javafx.scene.paint.Stop; import javafx.stage.Screen; import javafx.stage.Stage; @@ -219,12 +222,37 @@ public static boolean contains(String src, String s) { **************************************************************************/ /** - * Calculates a perceptual brightness for a color between 0.0 black and 1.0 while + * Calculates a perceptual brightness for a color between 0.0 (black) and 1.0 (white). */ public static double calculateBrightness(Color color) { return (0.3*color.getRed()) + (0.59*color.getGreen()) + (0.11*color.getBlue()); } + /** + * Calculates an average perceptual brightness for a paint between 0.0 (black) and 1.0 (white). + *

+ * The average brightness of gradient paints only takes into account the colors of gradient stops, + * but not the distribution of the gradient stops across the paint area. + *

+ * The brightness of {@code ImagePattern} paints is 1.0 by convention. + */ + public static double calculateAverageBrightness(Paint paint) { + return switch (paint) { + case Color color -> calculateBrightness(color); + case LinearGradient gradient -> calculateAverageGradientBrightness(gradient.getStops()); + case RadialGradient gradient -> calculateAverageGradientBrightness(gradient.getStops()); + default -> 1.0; + }; + } + + private static double calculateAverageGradientBrightness(List stops) { + return stops.stream() + .map(Stop::getColor) + .mapToDouble(Utils::calculateBrightness) + .average() + .orElse(1.0); + } + /** * Derives a lighter or darker of a given color. * diff --git a/modules/javafx.graphics/src/main/java/javafx/application/ConditionalFeature.java b/modules/javafx.graphics/src/main/java/javafx/application/ConditionalFeature.java index 61aadb3d662..ef60e66740f 100644 --- a/modules/javafx.graphics/src/main/java/javafx/application/ConditionalFeature.java +++ b/modules/javafx.graphics/src/main/java/javafx/application/ConditionalFeature.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2008, 2016, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2008, 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -159,6 +159,15 @@ public enum ConditionalFeature { */ UNIFIED_WINDOW, + /** + * Indicates that a system supports {@link javafx.stage.StageStyle#EXTENDED}. + *

+ * This feature is currently supported on Windows, Linux, and macOS. + * + * @since 24 + */ + EXTENDED_WINDOW, + /** * Indicates whether or not controls should use two-level focus. Two-level * focus is when separate operations are needed in some controls to first diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/Node.java b/modules/javafx.graphics/src/main/java/javafx/scene/Node.java index 00064b6a6fe..2935a3dfed5 100644 --- a/modules/javafx.graphics/src/main/java/javafx/scene/Node.java +++ b/modules/javafx.graphics/src/main/java/javafx/scene/Node.java @@ -498,6 +498,16 @@ public boolean isDirtyEmpty(Node node) { return node.isDirtyEmpty(); } + @Override + public void setScenes(Node node, Scene newScene, SubScene newSubScene) { + node.setScenes(newScene, newSubScene); + } + + @Override + public void updateBounds(Node node) { + node.updateBounds(); + } + @Override public void syncPeer(Node node) { node.syncPeer(); diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/Scene.java b/modules/javafx.graphics/src/main/java/javafx/scene/Scene.java index d9f2a0acd9a..02a367697e0 100644 --- a/modules/javafx.graphics/src/main/java/javafx/scene/Scene.java +++ b/modules/javafx.graphics/src/main/java/javafx/scene/Scene.java @@ -76,6 +76,7 @@ import javafx.geometry.*; import javafx.scene.image.WritableImage; import javafx.scene.input.*; +import javafx.scene.layout.HeaderBarBase; import javafx.scene.paint.Color; import javafx.scene.paint.Paint; import javafx.stage.PopupWindow; @@ -566,6 +567,10 @@ void addToDirtyList(Node n) { } private void doCSSPass() { + if (peer != null) { + peer.processOverlayCSS(); + } + final Parent sceneRoot = getRoot(); // // RT-17547: when the tree is synchronized, the dirty bits are @@ -592,6 +597,10 @@ private void doCSSPass() { } void doLayoutPass() { + if (peer != null) { + peer.layoutOverlay(); + } + final Parent r = getRoot(); if (r != null) { r.layout(); @@ -2630,6 +2639,7 @@ public void pulse() { if (PULSE_LOGGING_ENABLED) { PulseLogger.newPhase("Copy state to render graph"); } + peer.synchronizeOverlay(); syncLights(); synchronizeSceneProperties(); // Run the synchronizer @@ -2989,6 +2999,35 @@ public void touchEventEnd() { } } + private final PickRay pickRay = new PickRay(); + + @Override + public boolean dragAreaHitTest(double x, double y) { + Node root = Scene.this.getRoot(); + if (root != null) { + pickRay.set(x, y, 1, 0, Double.POSITIVE_INFINITY); + var pickResultChooser = new PickResultChooser(); + root.pickNode(pickRay, pickResultChooser); + var intersectedNode = pickResultChooser.getIntersectedNode(); + + if (intersectedNode instanceof HeaderBarBase) { + return true; + } + + while (intersectedNode != null) { + if (HeaderBarBase.isDraggable(intersectedNode) instanceof Boolean value) { + return value; + } + + intersectedNode = intersectedNode.getParent(); + } + + return false; + } + + return false; + } + @Override public Accessible getSceneAccessible() { return getAccessible(); diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBar.java b/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBar.java new file mode 100644 index 00000000000..bca236a68db --- /dev/null +++ b/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBar.java @@ -0,0 +1,483 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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 javafx.scene.layout; + +import com.sun.javafx.geom.Vec2d; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.ObjectPropertyBase; +import javafx.geometry.HPos; +import javafx.geometry.Insets; +import javafx.geometry.NodeOrientation; +import javafx.geometry.Orientation; +import javafx.geometry.Pos; +import javafx.geometry.VPos; +import javafx.scene.Node; +import javafx.stage.StageStyle; + +/** + * A client-area header bar that is used as a replacement for the system-provided header bar in stages + * with the {@link StageStyle#EXTENDED} style. This class enables the drag to move and + * double-click to maximize behaviors that are usually afforded by system-provided header bars. + * The entire {@code HeaderBar} background is draggable by default, but its content is not. Applications + * can specify draggable content nodes of the {@code HeaderBar} with the {@link #setDraggable} method. + *

+ * {@code HeaderBar} is a layout container that allows applications to place scene graph nodes + * in three areas: {@link #leadingProperty() leading}, {@link #centerProperty() center}, and + * {@link #trailingProperty() trailing}. {@code HeaderBar} ensures that the leading and trailing areas + * account for the default window buttons (minimize, maximize, close). If a child is configured to be + * centered in the {@code center} area, it is laid out with respect to the stage, and not with respect + * to the {@code center} area. This ensures that the child will appear centered in the stage regardless + * of leading or trailing children or the platform-specific placement of default window buttons. + *

+ * All children will be resized to their preferred widths and extend the height of the {@code HeaderBar}. + * {@code HeaderBar} honors the minimum, preferred, and maximum sizes of its children. If the child's + * resizable range prevents it from be resized to fit within its position, it will be vertically centered + * relative to the available space; this alignment can be customized with a layout constraint. + * + *

Layout constraints

+ * An application may set constraints on individual children to customize their layout. + * For each constraint, {@code HeaderBar} provides static getter and setter methods. + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Layout constraints of {@code HeaderBar}
ConstraintTypeDescription
alignment{@link Pos}The alignment of the child within its area of the {@code HeaderBar}.
margin{@link Insets}Margin space around the outside of the child.
+ * + *

Example

+ *
{@code
+ *     var button = new Button("My button");
+ *     HeaderBar.setAlignment(button, Pos.CENTER_LEFT);
+ *     HeaderBar.setMargin(button, new Insets(5));
+ *     myHeaderBar.setCenter(button);
+ * }
+ * + * @see HeaderBarBase + * @since 24 + */ +public class HeaderBar extends HeaderBarBase { + + private static final String ALIGNMENT = "headerbar-alignment"; + private static final String MARGIN = "headerbar-margin"; + + /** + * Sets the alignment for the child when contained in a {@code HeaderBar}. + * If set, will override the header bar's default alignment for the child's position. + * Setting the value to {@code null} will remove the constraint. + * + * @param child the child node + * @param value the alignment position + */ + public static void setAlignment(Node child, Pos value) { + Pane.setConstraint(child, ALIGNMENT, value); + } + + /** + * Returns the child's alignment in the {@code HeaderBar}. + * + * @param child the child node + * @return the alignment position for the child, or {@code null} if no alignment was set + */ + public static Pos getAlignment(Node child) { + return (Pos)Pane.getConstraint(child, ALIGNMENT); + } + + /** + * Sets the margin for the child when contained in a {@code HeaderBar}. + * If set, the header bar will lay it out with the margin space around it. + * Setting the value to {@code null} will remove the constraint. + * + * @param child the child node + * @param value the margin of space around the child + */ + public static void setMargin(Node child, Insets value) { + Pane.setConstraint(child, MARGIN, value); + } + + /** + * Returns the child's margin. + * + * @param child the child node + * @return the margin for the child, or {@code null} if no margin was set + */ + public static Insets getMargin(Node child) { + return (Insets)Pane.getConstraint(child, MARGIN); + } + + private static Insets getNodeMargin(Node child) { + Insets margin = getMargin(child); + return margin != null ? margin : Insets.EMPTY; + } + + /** + * Creates a new {@code HeaderBar}. + */ + public HeaderBar() { + } + + /** + * Creates a new {@code HeaderBar} with the specified children. + * + * @param leading the leading node + * @param center the center node + * @param trailing the trailing node + */ + public HeaderBar(Node leading, Node center, Node trailing) { + setLeading(leading); + setCenter(center); + setTrailing(trailing); + } + + /** + * The leading area of the {@code HeaderBar}. + *

+ * Usually, this corresponds to the left side of the header bar; with right-to-left orientation, + * this corresponds to the right side of the header bar. + */ + private final ObjectProperty leading = new NodeProperty() { + @Override + public String getName() { + return "leading"; + } + }; + + public final ObjectProperty leadingProperty() { + return leading; + } + + public final Node getLeading() { + return leading.get(); + } + + public final void setLeading(Node value) { + leading.set(value); + } + + /** + * The center area of the {@code HeaderBar}. + */ + private final ObjectProperty center = new NodeProperty() { + @Override + public String getName() { + return "center"; + } + }; + + public final ObjectProperty centerProperty() { + return center; + } + + public final Node getCenter() { + return center.get(); + } + + public final void setCenter(Node value) { + center.set(value); + } + + /** + * The trailing area of the {@code HeaderBar}. + *

+ * Usually, this corresponds to the right side of the header bar; with right-to-left orientation, + * this corresponds to the left side of the header bar. + */ + private final ObjectProperty trailing = new NodeProperty() { + @Override + public String getName() { + return "trailing"; + } + }; + + public final ObjectProperty trailingProperty() { + return trailing; + } + + public final Node getTrailing() { + return trailing.get(); + } + + public final void setTrailing(Node value) { + trailing.set(value); + } + + @Override + protected double computeMinWidth(double height) { + Node leading = getLeading(), center = getCenter(), trailing = getTrailing(); + Insets insets = getInsets(); + double leftPrefWidth; + double rightPrefWidth; + double centerMinWidth; + + if (height != -1 + && (childHasContentBias(leading, Orientation.VERTICAL) || + childHasContentBias(trailing, Orientation.VERTICAL) || + childHasContentBias(center, Orientation.VERTICAL))) { + double areaHeight = Math.max(0, height); + leftPrefWidth = getAreaWidth(leading, areaHeight, false); + rightPrefWidth = getAreaWidth(trailing, areaHeight, false); + centerMinWidth = getAreaWidth(center, areaHeight, true); + } else { + leftPrefWidth = getAreaWidth(leading, -1, false); + rightPrefWidth = getAreaWidth(trailing, -1, false); + centerMinWidth = getAreaWidth(center, -1, true); + } + + return insets.getLeft() + + leftPrefWidth + + centerMinWidth + + rightPrefWidth + + insets.getRight() + + getLeftSystemInset().getWidth() + + getRightSystemInset().getWidth(); + } + + @Override + protected double computeMinHeight(double width) { + Node leading = getLeading(), center = getCenter(), trailing = getTrailing(); + Insets insets = getInsets(); + double leadingMinHeight = getAreaHeight(leading, -1, true); + double trailingMinHeight = getAreaHeight(trailing, -1, true); + double centerMinHeight; + + if (width != -1 && childHasContentBias(center, Orientation.HORIZONTAL)) { + double leadingPrefWidth = getAreaWidth(leading, -1, false); + double trailingPrefWidth = getAreaWidth(trailing, -1, false); + centerMinHeight = getAreaHeight(center, Math.max(0, width - leadingPrefWidth - trailingPrefWidth), true); + } else { + centerMinHeight = getAreaHeight(center, -1, true); + } + + return insets.getTop() + + insets.getBottom() + + Math.max(centerMinHeight, Math.max(trailingMinHeight, leadingMinHeight)); + } + + @Override + protected double computePrefHeight(double width) { + Node leading = getLeading(), center = getCenter(), trailing = getTrailing(); + Insets insets = getInsets(); + double leadingPrefHeight = getAreaHeight(leading, -1, false); + double trailingPrefHeight = getAreaHeight(trailing, -1, false); + double centerPrefHeight; + + if (width != -1 && childHasContentBias(center, Orientation.HORIZONTAL)) { + double leadingPrefWidth = getAreaWidth(leading, -1, false); + double trailingPrefWidth = getAreaWidth(trailing, -1, false); + centerPrefHeight = getAreaHeight(center, Math.max(0, width - leadingPrefWidth - trailingPrefWidth), false); + } else { + centerPrefHeight = getAreaHeight(center, -1, false); + } + + return insets.getTop() + + insets.getBottom() + + Math.max(centerPrefHeight, Math.max(trailingPrefHeight, leadingPrefHeight)); + } + + @Override + public boolean usesMirroring() { + return false; + } + + @Override + protected void layoutChildren() { + Node center = getCenter(); + Node left, right; + Insets insets = getInsets(); + boolean rtl = getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT; + double width = Math.max(getWidth(), minWidth(-1)); + double height = Math.max(getHeight(), minHeight(-1)); + double leftSystemInset = getLeftSystemInset().getWidth(); + double rightSystemInset = getRightSystemInset().getWidth(); + double leftWidth = 0; + double rightWidth = 0; + double insideY = insets.getTop(); + double insideHeight = height - insideY - insets.getBottom(); + double insideX, insideWidth; + + if (rtl) { + left = getTrailing(); + right = getLeading(); + insideX = insets.getRight() + leftSystemInset; + insideWidth = width - insideX - insets.getLeft() - rightSystemInset; + } else { + left = getLeading(); + right = getTrailing(); + insideX = insets.getLeft() + leftSystemInset; + insideWidth = width - insideX - insets.getRight() - rightSystemInset; + } + + if (left != null && left.isManaged()) { + Insets leftMargin = adjustMarginForRTL(getNodeMargin(left), rtl); + double adjustedWidth = adjustWidthByMargin(insideWidth, leftMargin); + Vec2d childSize = resizeChild(left, adjustedWidth, insideHeight, leftMargin); + leftWidth = snapSpaceX(leftMargin.getLeft()) + childSize.x + snapSpaceX(leftMargin.getRight()); + Pos alignment = getAlignment(left); + + positionInArea( + left, insideX, insideY, + leftWidth, insideHeight, 0, + leftMargin, + alignment != null ? alignment.getHpos() : HPos.CENTER, + alignment != null ? alignment.getVpos() : VPos.CENTER, + isSnapToPixel()); + } + + if (right != null && right.isManaged()) { + Insets rightMargin = adjustMarginForRTL(getNodeMargin(right), rtl); + double adjustedWidth = adjustWidthByMargin(insideWidth - leftWidth, rightMargin); + Vec2d childSize = resizeChild(right, adjustedWidth, insideHeight, rightMargin); + rightWidth = snapSpaceX(rightMargin.getLeft()) + childSize.x + snapSpaceX(rightMargin.getRight()); + Pos alignment = getAlignment(right); + + positionInArea( + right, insideX + insideWidth - rightWidth, insideY, + rightWidth, insideHeight, 0, + rightMargin, + alignment != null ? alignment.getHpos() : HPos.CENTER, + alignment != null ? alignment.getVpos() : VPos.CENTER, + isSnapToPixel()); + } + + if (center != null && center.isManaged()) { + Insets centerMargin = adjustMarginForRTL(getNodeMargin(center), rtl); + double adjustedWidth = adjustWidthByMargin(insideWidth - leftWidth - rightWidth, centerMargin); + Vec2d childSize = resizeChild(center, adjustedWidth, insideHeight, centerMargin); + double centerWidth = childSize.x; + Pos alignment = getAlignment(center); + + if (alignment == null || alignment.getHpos() == HPos.CENTER) { + double idealX = width / 2 - centerWidth / 2; + double minX = insideX + leftWidth + centerMargin.getLeft(); + double maxX = insideX + insideWidth - rightWidth - centerMargin.getRight(); + double adjustedX; + + if (idealX < minX) { + adjustedX = minX; + } else if (idealX + centerWidth > maxX) { + adjustedX = maxX - centerWidth; + } else { + adjustedX = idealX; + } + + positionInArea( + center, + adjustedX, insideY, + centerWidth, insideHeight, 0, + new Insets(centerMargin.getTop(), 0, centerMargin.getBottom(), 0), + HPos.LEFT, alignment != null ? alignment.getVpos() : VPos.CENTER, + isSnapToPixel()); + } else { + positionInArea( + center, + insideX + leftWidth, insideY, + insideWidth - leftWidth - rightWidth, insideHeight, 0, + centerMargin, + alignment.getHpos(), alignment.getVpos(), + isSnapToPixel()); + } + } + } + + private Insets adjustMarginForRTL(Insets margin, boolean rtl) { + if (margin == null) { + return null; + } + + return rtl + ? new Insets(margin.getTop(), margin.getLeft(), margin.getBottom(), margin.getRight()) + : margin; + } + + private boolean childHasContentBias(Node child, Orientation orientation) { + if (child != null && child.isManaged()) { + return child.getContentBias() == orientation; + } + + return false; + } + + private Vec2d resizeChild(Node child, double adjustedWidth, double insideHeight, Insets margin) { + double adjustedHeight = adjustHeightByMargin(insideHeight, margin); + double childWidth = Math.min(snapSizeX(child.prefWidth(adjustedHeight)), adjustedWidth); + Vec2d size = boundedNodeSizeWithBias(child, childWidth, adjustedHeight, false, true, TEMP_VEC2D); + size.x = snapSizeX(size.x); + size.y = snapSizeX(size.y); + child.resize(size.x, size.y); + return size; + } + + private double getAreaWidth(Node child, double height, boolean minimum) { + if (child != null && child.isManaged()) { + Insets margin = getNodeMargin(child); + return minimum + ? computeChildMinAreaWidth(child, -1, margin, height, false) + : computeChildPrefAreaWidth(child, -1, margin, height, false); + } + + return 0; + } + + private double getAreaHeight(Node child, double width, boolean minimum) { + if (child != null && child.isManaged()) { + Insets margin = getNodeMargin(child); + return minimum + ? computeChildMinAreaHeight(child, -1, margin, width) + : computeChildPrefAreaHeight(child, -1, margin, width); + } + + return 0; + } + + private abstract class NodeProperty extends ObjectPropertyBase { + private Node value; + + @Override + public Object getBean() { + return HeaderBar.this; + } + + @Override + protected void invalidated() { + if (value != null) { + getChildren().remove(value); + } + + value = get(); + + if (value != null) { + getChildren().add(value); + } + } + } +} diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBarBase.java b/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBarBase.java new file mode 100644 index 00000000000..f5932eb65d1 --- /dev/null +++ b/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBarBase.java @@ -0,0 +1,191 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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 javafx.scene.layout; + +import com.sun.glass.ui.WindowOverlayMetrics; +import com.sun.javafx.stage.StageHelper; +import com.sun.javafx.tk.quantum.WindowStage; +import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.beans.value.ObservableValue; +import javafx.geometry.Dimension2D; +import javafx.geometry.HorizontalDirection; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.stage.Stage; +import javafx.stage.StageStyle; +import javafx.stage.Window; +import javafx.util.Subscription; + +/** + * Base class for a client-area header bar that is used as a replacement for the system-provided header bar + * in stages with the {@link StageStyle#EXTENDED} style. This class is intended for application developers + * to use as a starting point for custom header bar implementations, and it enables the drag to move + * and double-click to maximize behaviors that are usually afforded by system-provided header bars. + * The entire {@code HeaderBarBase} background is draggable by default, but its content is not. Applications + * can specify draggable content nodes of the {@code HeaderBarBase} with the {@link #setDraggable} method. + *

+ * Most application developers should use the {@link HeaderBar} implementation instead of + * creating a custom header bar. + * + * @see HeaderBar + * @since 24 + */ +public abstract class HeaderBarBase extends Region { + + private static final String DRAGGABLE = "headerbar-draggable"; + + /** + * Specifies whether the child and its subtree is a draggable part of the {@code HeaderBar}. + *

+ * If set to a non-null value, the value will apply for the entire subtree of the child unless + * another node in the subtree specifies a different value. Setting the value to {@code null} + * will remove the flag. + * + * @param child the child node + * @param value a {@code Boolean} value indicating whether the child and its subtree is draggable, + * or {@code null} to remove the flag + */ + public static void setDraggable(Node child, Boolean value) { + Pane.setConstraint(child, DRAGGABLE, value); + } + + /** + * Returns whether the child and its subtree is a draggable part of the {@code HeaderBar}. + * + * @param child the child node + * @return a {@code Boolean} value indicating whether the child and its subtree is draggable, + * or {@code null} if not set + */ + public static Boolean isDraggable(Node child) { + return (Boolean)Pane.getConstraint(child, DRAGGABLE); + } + + private Subscription metricsSubscription; + private WindowOverlayMetrics currentMetrics; + private boolean currentFullScreen; + + /** + * Constructor called by subclasses. + */ + protected HeaderBarBase() { + var stage = sceneProperty() + .flatMap(Scene::windowProperty) + .map(w -> w instanceof Stage s ? s : null); + + stage.flatMap(Window::showingProperty) + .orElse(false) + .subscribe(this::onShowingChanged); + + stage.flatMap(Stage::fullScreenProperty) + .orElse(false) + .subscribe(this::onFullScreenChanged); + } + + private void onShowingChanged(boolean showing) { + if (!showing) { + if (metricsSubscription != null) { + metricsSubscription.unsubscribe(); + metricsSubscription = null; + } + } else if (getScene().getWindow() instanceof Stage stage + && StageHelper.getPeer(stage) instanceof WindowStage windowStage) { + ObservableValue metrics = + windowStage.getPlatformWindow().windowOverlayMetrics(); + + if (metrics != null) { + metricsSubscription = metrics.subscribe(this::onMetricsChanged); + } + } + } + + private void onMetricsChanged(WindowOverlayMetrics metrics) { + currentMetrics = metrics; + updateInsets(); + } + + private void onFullScreenChanged(boolean fullScreen) { + currentFullScreen = fullScreen; + updateInsets(); + } + + private void updateInsets() { + if (currentFullScreen || currentMetrics == null) { + leftSystemInset.set(new Dimension2D(0, 0)); + rightSystemInset.set(new Dimension2D(0, 0)); + } else if (currentMetrics.placement() == HorizontalDirection.LEFT) { + leftSystemInset.set(currentMetrics.size()); + rightSystemInset.set(new Dimension2D(0, 0)); + } else if (currentMetrics.placement() == HorizontalDirection.RIGHT) { + leftSystemInset.set(new Dimension2D(0, 0)); + rightSystemInset.set(currentMetrics.size()); + } else { + leftSystemInset.set(new Dimension2D(0, 0)); + rightSystemInset.set(new Dimension2D(0, 0)); + } + } + + /** + * Describes the size of the left system inset, which is an area reserved for the + * minimize, maximize, and close window buttons. If there are no window buttons on + * the left side of the window, the returned area is empty. + */ + private final ReadOnlyObjectWrapper leftSystemInset = + new ReadOnlyObjectWrapper<>(this, "leftInset", new Dimension2D(0, 0)) { + @Override + protected void invalidated() { + requestLayout(); + } + }; + + public final ReadOnlyObjectWrapper leftSystemInsetProperty() { + return leftSystemInset; + } + + public final Dimension2D getLeftSystemInset() { + return leftSystemInset.get(); + } + + /** + * Describes the size of the right system inset, which is an area reserved for the + * minimize, maximize, and close window buttons. If there are no window buttons on + * the right side of the window, the returned area is empty. + */ + private final ReadOnlyObjectWrapper rightSystemInset = + new ReadOnlyObjectWrapper<>(this, "rightInset", new Dimension2D(0, 0)) { + @Override + protected void invalidated() { + requestLayout(); + } + }; + + public final ReadOnlyObjectWrapper rightSystemInsetProperty() { + return rightSystemInset; + } + + public final Dimension2D getRightSystemInset() { + return rightSystemInset.get(); + } +} diff --git a/modules/javafx.graphics/src/main/java/javafx/stage/StageStyle.java b/modules/javafx.graphics/src/main/java/javafx/stage/StageStyle.java index baed126e9e4..26a040f77a2 100644 --- a/modules/javafx.graphics/src/main/java/javafx/stage/StageStyle.java +++ b/modules/javafx.graphics/src/main/java/javafx/stage/StageStyle.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2008, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2008, 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,6 +25,12 @@ package javafx.stage; +import javafx.application.ConditionalFeature; +import javafx.application.Platform; +import javafx.scene.Scene; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HeaderBar; + /** * This enum defines the possible styles for a {@code Stage}. * @since JavaFX 2.0 @@ -66,5 +72,40 @@ public enum StageStyle { * NOTE: To see the effect, the {@code Scene} covering the {@code Stage} should have {@code Color.TRANSPARENT} * @since JavaFX 8.0 */ - UNIFIED + UNIFIED, + + /** + * Defines a {@code Stage} style in which the client area is extended into the header bar area, removing + * the separation between the two areas and allowing applications to place scene graph nodes in the header + * bar area of the stage. + *

+ * An extended window has the default window buttons (minimize, maximize, close), but no system-provided + * draggable header bar. Applications need to provide their own header bar by placing the {@link HeaderBar} + * control in the scene graph. Usually, this is combined with a {@link BorderPane} root container: + *

{@code
+     * public class MyApp extends Application {
+     *     @Override
+     *     public void start(Stage stage) {
+     *         var headerBar = new HeaderBar();
+     *         var root = new BorderPane();
+     *         root.setTop(headerBar);
+     *
+     *         stage.setScene(new Scene(root));
+     *         stage.initStyle(StageStyle.EXTENDED);
+     *         stage.show();
+     *     }
+     * }
+     * }
+ *

+ * The color scheme of the default window buttons is adjusted to the {@link Scene#fillProperty() fill} + * of the {@code Scene} to remain easily recognizable. Applications should set the scene fill to a color + * that matches the brightness of the user interface, even if the scene fill is not visible because it + * is obscured by other controls. + *

+ * This is a conditional feature, to check if it is supported see {@link Platform#isSupported(ConditionalFeature)}. + * If the feature is not supported by the platform, this style downgrades to {@link StageStyle#DECORATED}. + * + * @since 24 + */ + EXTENDED } diff --git a/modules/javafx.graphics/src/main/native-glass/gtk/GlassApplication.cpp b/modules/javafx.graphics/src/main/native-glass/gtk/GlassApplication.cpp index 8413aeaf474..66e807aa537 100644 --- a/modules/javafx.graphics/src/main/native-glass/gtk/GlassApplication.cpp +++ b/modules/javafx.graphics/src/main/native-glass/gtk/GlassApplication.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -520,6 +520,7 @@ static void process_events(GdkEvent* event, gpointer data) gtk_main_do_event(event); break; case GDK_BUTTON_PRESS: + case GDK_2BUTTON_PRESS: case GDK_BUTTON_RELEASE: ctx->process_mouse_button(&event->button); break; diff --git a/modules/javafx.graphics/src/main/native-glass/gtk/GlassWindow.cpp b/modules/javafx.graphics/src/main/native-glass/gtk/GlassWindow.cpp index 3bd5bd38c1c..aa66a878fdd 100644 --- a/modules/javafx.graphics/src/main/native-glass/gtk/GlassWindow.cpp +++ b/modules/javafx.graphics/src/main/native-glass/gtk/GlassWindow.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -41,6 +41,9 @@ static WindowFrameType glass_mask_to_window_frame_type(jint mask) { if (mask & com_sun_glass_ui_gtk_GtkWindow_TITLED) { return TITLED; } + if (mask & com_sun_glass_ui_gtk_GtkWindow_EXTENDED) { + return EXTENDED; + } return UNTITLED; } diff --git a/modules/javafx.graphics/src/main/native-glass/gtk/glass_general.cpp b/modules/javafx.graphics/src/main/native-glass/gtk/glass_general.cpp index b75b6fde6ed..d9df201be24 100644 --- a/modules/javafx.graphics/src/main/native-glass/gtk/glass_general.cpp +++ b/modules/javafx.graphics/src/main/native-glass/gtk/glass_general.cpp @@ -83,6 +83,7 @@ jfieldID jWindowPtr; jfieldID jCursorPtr; jmethodID jGtkWindowNotifyStateChanged; +jmethodID jGtkWindowDragAreaHitTest; jmethodID jClipboardContentChanged; @@ -274,8 +275,9 @@ JNI_OnLoad(JavaVM *jvm, void *reserved) clazz = env->FindClass("com/sun/glass/ui/gtk/GtkWindow"); if (env->ExceptionCheck()) return JNI_ERR; - jGtkWindowNotifyStateChanged = - env->GetMethodID(clazz, "notifyStateChanged", "(I)V"); + jGtkWindowNotifyStateChanged = env->GetMethodID(clazz, "notifyStateChanged", "(I)V"); + if (env->ExceptionCheck()) return JNI_ERR; + jGtkWindowDragAreaHitTest = env->GetMethodID(clazz, "dragAreaHitTest", "(II)Z"); if (env->ExceptionCheck()) return JNI_ERR; clazz = env->FindClass("com/sun/glass/ui/Clipboard"); diff --git a/modules/javafx.graphics/src/main/native-glass/gtk/glass_general.h b/modules/javafx.graphics/src/main/native-glass/gtk/glass_general.h index e406550cb8c..f47e32fc952 100644 --- a/modules/javafx.graphics/src/main/native-glass/gtk/glass_general.h +++ b/modules/javafx.graphics/src/main/native-glass/gtk/glass_general.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -195,7 +195,8 @@ struct jni_exception: public std::exception { extern jfieldID jWindowPtr; // com.sun.glass.ui.Window#ptr extern jfieldID jCursorPtr; // com.sun.glass.ui.Cursor#ptr - extern jmethodID jGtkWindowNotifyStateChanged; // com.sun.glass.ui.GtkWindow#notifyStateChanged (I)V + extern jmethodID jGtkWindowNotifyStateChanged; // com.sun.glass.ui.gtk.GtkWindow#notifyStateChanged (I)V + extern jmethodID jGtkWindowDragAreaHitTest; //com.sun.glass.ui.gtk.GtkWindow#dragAreaHitTest (II)Z extern jmethodID jClipboardContentChanged; // com.sun.glass.ui.Clipboard#contentChanged ()V diff --git a/modules/javafx.graphics/src/main/native-glass/gtk/glass_window.cpp b/modules/javafx.graphics/src/main/native-glass/gtk/glass_window.cpp index e11fc79991a..8058e52dcf5 100644 --- a/modules/javafx.graphics/src/main/native-glass/gtk/glass_window.cpp +++ b/modules/javafx.graphics/src/main/native-glass/gtk/glass_window.cpp @@ -51,6 +51,9 @@ #define MOUSE_BACK_BTN 8 #define MOUSE_FORWARD_BTN 9 +// Resize border width of EXTENDED windows +#define RESIZE_BORDER_WIDTH 5 + WindowContext * WindowContextBase::sm_grab_window = NULL; WindowContext * WindowContextBase::sm_mouse_drag_window = NULL; @@ -260,6 +263,11 @@ static inline jint gtk_button_number_to_mouse_button(guint button) { } void WindowContextBase::process_mouse_button(GdkEventButton* event) { + // We only handle single press/release events here. + if (event->type != GDK_BUTTON_PRESS && event->type != GDK_BUTTON_RELEASE) { + return; + } + bool press = event->type == GDK_BUTTON_PRESS; guint state = event->state; guint mask = 0; @@ -648,7 +656,26 @@ void WindowContextBase::set_cursor(GdkCursor* cursor) { WindowContextBase::sm_grab_window->get_gdk_window(), cursor, TRUE); } } - gdk_window_set_cursor(gdk_window, cursor); + + gdk_cursor = cursor; + + if (gdk_cursor_override == NULL) { + gdk_window_set_cursor(gdk_window, cursor); + } +} + +void WindowContextBase::set_cursor_override(GdkCursor* cursor) { + if (gdk_cursor_override == cursor) { + return; + } + + gdk_cursor_override = cursor; + + if (cursor != NULL) { + gdk_window_set_cursor(gdk_window, cursor); + } else { + gdk_window_set_cursor(gdk_window, gdk_cursor); + } } void WindowContextBase::set_background(float r, float g, float b) { @@ -656,6 +683,10 @@ void WindowContextBase::set_background(float r, float g, float b) { gtk_widget_override_background_color(gtk_widget, GTK_STATE_FLAG_NORMAL, &rgba); } +bool WindowContextBase::get_window_edge(int x, int y, GdkWindowEdge* window_edge) { + return false; +} + WindowContextBase::~WindowContextBase() { if (xim.ic) { XDestroyIC(xim.ic); @@ -1366,6 +1397,156 @@ void WindowContextTop::notify_window_move() { } } +/* + * Handles mouse button events of EXTENDED windows and adds the window behaviors for non-client + * regions that are usually provided by the window manager. Note that a full-screen window has + * no non-client regions. + */ +void WindowContextTop::process_mouse_button(GdkEventButton* event) { + // Non-EXTENDED or full-screen windows don't have additional behaviors, so we delegate + // directly to the base implementation. + if (is_fullscreen || frame_type != EXTENDED || jwindow == NULL) { + WindowContextBase::process_mouse_button(event); + return; + } + + // Double-clicking on the drag area maximizes the window (or restores its size). + if (event->type == GDK_2BUTTON_PRESS) { + jboolean dragArea = mainEnv->CallBooleanMethod( + jwindow, jGtkWindowDragAreaHitTest, (jint)event->x, (jint)event->y); + CHECK_JNI_EXCEPTION(mainEnv); + + if (dragArea) { + set_maximized(!is_maximized); + } + + // We don't process the GDK_2BUTTON_PRESS event in the base implementation. + return; + } + + if (event->button == 1 && event->type == GDK_BUTTON_PRESS) { + GdkWindowEdge edge; + bool shouldStartResizeDrag = !is_maximized && get_window_edge(event->x, event->y, &edge); + + // Clicking on a window edge starts a move-resize operation. + if (shouldStartResizeDrag) { + // Send a synthetic PRESS + RELEASE to FX. This allows FX to do things that need to be done + // prior to resizing the window, like closing a popup menu. We do this because we won't be + // sending events to FX once the resize operation has started. + WindowContextBase::process_mouse_button(event); + event->type = GDK_BUTTON_RELEASE; + WindowContextBase::process_mouse_button(event); + + gint rx = 0, ry = 0; + gdk_window_get_root_coords(get_gdk_window(), event->x, event->y, &rx, &ry); + gtk_window_begin_resize_drag(get_gtk_window(), edge, 1, rx, ry, event->time); + return; + } + + bool shouldStartMoveDrag = mainEnv->CallBooleanMethod( + jwindow, jGtkWindowDragAreaHitTest, (jint)event->x, (jint)event->y); + CHECK_JNI_EXCEPTION(mainEnv); + + // Clicking on a draggable area starts a move-drag operation. + if (shouldStartMoveDrag) { + // Send a synthetic PRESS + RELEASE to FX. + WindowContextBase::process_mouse_button(event); + event->type = GDK_BUTTON_RELEASE; + WindowContextBase::process_mouse_button(event); + + gint rx = 0, ry = 0; + gdk_window_get_root_coords(get_gdk_window(), event->x, event->y, &rx, &ry); + gtk_window_begin_move_drag(get_gtk_window(), 1, rx, ry, event->time); + return; + } + } + + // Call the base implementation for client area events. + WindowContextBase::process_mouse_button(event); +} + +/* + * Handles mouse motion events of EXTENDED windows and changes the cursor when it is on top + * of the internal resize border. Note that a full-screen window or maximized window has no + * resize border. + */ +void WindowContextTop::process_mouse_motion(GdkEventMotion* event) { + GdkWindowEdge edge; + + // Call the base implementation for client area events. + if (is_fullscreen + || is_maximized + || frame_type != EXTENDED + || !get_window_edge(event->x, event->y, &edge)) { + set_cursor_override(NULL); + WindowContextBase::process_mouse_motion(event); + return; + } + + static const struct Cursors { + GdkCursor* NORTH = gdk_cursor_new(GDK_TOP_SIDE); + GdkCursor* NORTH_EAST = gdk_cursor_new(GDK_TOP_RIGHT_CORNER); + GdkCursor* EAST = gdk_cursor_new(GDK_RIGHT_SIDE); + GdkCursor* SOUTH_EAST = gdk_cursor_new(GDK_BOTTOM_RIGHT_CORNER); + GdkCursor* SOUTH = gdk_cursor_new(GDK_BOTTOM_SIDE); + GdkCursor* SOUTH_WEST = gdk_cursor_new(GDK_BOTTOM_LEFT_CORNER); + GdkCursor* WEST = gdk_cursor_new(GDK_LEFT_SIDE); + GdkCursor* NORTH_WEST = gdk_cursor_new(GDK_TOP_LEFT_CORNER); + } cursors; + + GdkCursor* cursor = NULL; + + switch (edge) { + case GDK_WINDOW_EDGE_NORTH: cursor = cursors.NORTH; break; + case GDK_WINDOW_EDGE_NORTH_EAST: cursor = cursors.NORTH_EAST; break; + case GDK_WINDOW_EDGE_EAST: cursor = cursors.EAST; break; + case GDK_WINDOW_EDGE_SOUTH_EAST: cursor = cursors.SOUTH_EAST; break; + case GDK_WINDOW_EDGE_SOUTH: cursor = cursors.SOUTH; break; + case GDK_WINDOW_EDGE_SOUTH_WEST: cursor = cursors.SOUTH_WEST; break; + case GDK_WINDOW_EDGE_WEST: cursor = cursors.WEST; break; + case GDK_WINDOW_EDGE_NORTH_WEST: cursor = cursors.NORTH_WEST; break; + } + + set_cursor_override(cursor); + + // If the cursor is not on a resize border, call the base handler. + if (cursor == NULL) { + WindowContextBase::process_mouse_motion(event); + } +} + +/* + * Determines the GdkWindowEdge at the specified coordinate; returns true if the coordinate + * identifies a window edge, false otherwise. + */ +bool WindowContextTop::get_window_edge(int x, int y, GdkWindowEdge* window_edge) { + GdkWindowEdge edge; + gint width, height; + gtk_window_get_size(get_gtk_window(), &width, &height); + + if (x <= RESIZE_BORDER_WIDTH) { + if (y <= 2 * RESIZE_BORDER_WIDTH) edge = GDK_WINDOW_EDGE_NORTH_WEST; + else if (y >= height - 2 * RESIZE_BORDER_WIDTH) edge = GDK_WINDOW_EDGE_SOUTH_WEST; + else edge = GDK_WINDOW_EDGE_WEST; + } else if (x >= width - RESIZE_BORDER_WIDTH) { + if (y <= 2 * RESIZE_BORDER_WIDTH) edge = GDK_WINDOW_EDGE_NORTH_EAST; + else if (y >= height - 2 * RESIZE_BORDER_WIDTH) edge = GDK_WINDOW_EDGE_SOUTH_EAST; + else edge = GDK_WINDOW_EDGE_EAST; + } else if (y <= RESIZE_BORDER_WIDTH) { + edge = GDK_WINDOW_EDGE_NORTH; + } else if (y >= height - RESIZE_BORDER_WIDTH) { + edge = GDK_WINDOW_EDGE_SOUTH; + } else { + return false; + } + + if (window_edge != NULL) { + *window_edge = edge; + } + + return true; +} + void WindowContextTop::process_destroy() { if (owner) { owner->remove_child(this); diff --git a/modules/javafx.graphics/src/main/native-glass/gtk/glass_window.h b/modules/javafx.graphics/src/main/native-glass/gtk/glass_window.h index a7094d98eec..9e2ee1cfe50 100644 --- a/modules/javafx.graphics/src/main/native-glass/gtk/glass_window.h +++ b/modules/javafx.graphics/src/main/native-glass/gtk/glass_window.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -44,7 +44,8 @@ enum WindowManager { enum WindowFrameType { TITLED, UNTITLED, - TRANSPARENT + TRANSPARENT, + EXTENDED }; enum WindowType { @@ -162,6 +163,7 @@ class WindowContext : public DeletedMemDebug<0xCC> { virtual void increment_events_counter() = 0; virtual void decrement_events_counter() = 0; virtual size_t get_events_count() = 0; + virtual bool get_window_edge(int x, int y, GdkWindowEdge*) = 0; virtual bool is_dead() = 0; virtual ~WindowContext() {} }; @@ -182,6 +184,8 @@ class WindowContextBase: public WindowContext { jobject jview; GtkWidget* gtk_widget; GdkWindow* gdk_window = NULL; + GdkCursor* gdk_cursor = NULL; + GdkCursor* gdk_cursor_override = NULL; GdkWMFunction gdk_windowManagerFunctions; bool is_iconified; @@ -228,6 +232,7 @@ class WindowContextBase: public WindowContext { void ungrab_focus(); void ungrab_mouse_drag_focus(); void set_cursor(GdkCursor*); + void set_cursor_override(GdkCursor*); void set_level(int) {} void set_background(float, float, float); @@ -247,6 +252,7 @@ class WindowContextBase: public WindowContext { void increment_events_counter(); void decrement_events_counter(); size_t get_events_count(); + bool get_window_edge(int x, int y, GdkWindowEdge*); bool is_dead(); ~WindowContextBase(); @@ -284,6 +290,8 @@ class WindowContextTop: public WindowContextBase { void process_state(GdkEventWindowState*); void process_configure(GdkEventConfigure*); void process_destroy(); + void process_mouse_motion(GdkEventMotion*); + void process_mouse_button(GdkEventButton*); void work_around_compiz_state(); WindowFrameExtents get_frame_extents(); @@ -331,6 +339,7 @@ class WindowContextTop: public WindowContextBase { bool effective_on_top(); void notify_window_move(); void notify_window_resize(); + bool get_window_edge(int x, int y, GdkWindowEdge*); WindowContextTop(WindowContextTop&); WindowContextTop& operator= (const WindowContextTop&); }; diff --git a/modules/javafx.graphics/src/main/native-glass/ios/GlassWindow.m b/modules/javafx.graphics/src/main/native-glass/ios/GlassWindow.m index 5b37d31e210..b7425c75d36 100644 --- a/modules/javafx.graphics/src/main/native-glass/ios/GlassWindow.m +++ b/modules/javafx.graphics/src/main/native-glass/ios/GlassWindow.m @@ -816,7 +816,7 @@ jlong _1createWindow(JNIEnv *env, jobject jWindow, jlong jOwnerPtr, jlong jScree BOOL hidden = YES; if (jOwnerPtr == 0L) { // no owner means it is the primary stage; Decorated primary stage shows status bar by default - hidden = ((jStyleMask & com_sun_glass_ui_Window_TITLED) == 0); + hidden = ((jStyleMask & com_sun_glass_ui_Window_DECORATED) == 0); NSObject * values = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIStatusBarHidden"]; //we prefer explicit settings from .plist diff --git a/modules/javafx.graphics/src/main/native-glass/mac/GlassViewDelegate.h b/modules/javafx.graphics/src/main/native-glass/mac/GlassViewDelegate.h index e8cbfc2be57..191cc55cc93 100644 --- a/modules/javafx.graphics/src/main/native-glass/mac/GlassViewDelegate.h +++ b/modules/javafx.graphics/src/main/native-glass/mac/GlassViewDelegate.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -115,4 +115,6 @@ typedef enum GestureMaskType { - (GlassAccessible*)getAccessible; +- (NSEvent*)lastEvent; + @end diff --git a/modules/javafx.graphics/src/main/native-glass/mac/GlassViewDelegate.m b/modules/javafx.graphics/src/main/native-glass/mac/GlassViewDelegate.m index 1b84e04fdba..1b443d7bbd0 100644 --- a/modules/javafx.graphics/src/main/native-glass/mac/GlassViewDelegate.m +++ b/modules/javafx.graphics/src/main/native-glass/mac/GlassViewDelegate.m @@ -1333,4 +1333,9 @@ - (GlassAccessible*)getAccessible return (GlassAccessible*)jlong_to_ptr(accessible); } +- (NSEvent*)lastEvent +{ + return lastEvent; +} + @end diff --git a/modules/javafx.graphics/src/main/native-glass/mac/GlassWindow.m b/modules/javafx.graphics/src/main/native-glass/mac/GlassWindow.m index a054ac12c69..dda2b3241bd 100644 --- a/modules/javafx.graphics/src/main/native-glass/mac/GlassWindow.m +++ b/modules/javafx.graphics/src/main/native-glass/mac/GlassWindow.m @@ -429,6 +429,10 @@ static jlong _createWindowCommonDo(JNIEnv *env, jobject jWindow, jlong jOwnerPtr styleMask = styleMask|NSWindowStyleMaskMiniaturizable; } + if ((jStyleMask&com_sun_glass_ui_Window_EXTENDED) != 0) { + styleMask = styleMask | NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView; + } + if ((jStyleMask&com_sun_glass_ui_Window_UNIFIED) != 0) { styleMask = styleMask|NSWindowStyleMaskTexturedBackground; } @@ -454,6 +458,12 @@ static jlong _createWindowCommonDo(JNIEnv *env, jobject jWindow, jlong jOwnerPtr NSScreen *screen = (NSScreen*)jlong_to_ptr(jScreenPtr); window = [[GlassWindow alloc] _initWithContentRect:NSMakeRect(x, y, w, h) styleMask:styleMask screen:screen jwindow:jWindow]; + if ((jStyleMask & com_sun_glass_ui_Window_EXTENDED) != 0) { + [window->nsWindow setTitlebarAppearsTransparent:YES]; + [window->nsWindow setToolbar:[NSToolbar new]]; + [window->nsWindow setToolbarStyle:NSWindowToolbarStyleUnifiedCompact]; + } + if ((jStyleMask & com_sun_glass_ui_Window_UNIFIED) != 0) { //Prevent the textured effect from disappearing on border thickness recalculation [window->nsWindow setAutorecalculatesContentBorderThickness:NO forEdge:NSMaxYEdge]; @@ -472,7 +482,8 @@ static jlong _createWindowCommonDo(JNIEnv *env, jobject jWindow, jlong jOwnerPtr window->owner = getGlassWindow(env, jOwnerPtr)->nsWindow; // not retained (use weak reference?) } window->isResizable = NO; - window->isDecorated = (jStyleMask&com_sun_glass_ui_Window_TITLED) != 0; + window->isDecorated = (jStyleMask&com_sun_glass_ui_Window_TITLED) != 0 || + (jStyleMask&com_sun_glass_ui_Window_EXTENDED) != 0; /* 10.7 full screen window support */ if ([NSWindow instancesRespondToSelector:@selector(toggleFullScreen:)]) { NSWindowCollectionBehavior behavior = [window->nsWindow collectionBehavior]; @@ -1468,3 +1479,50 @@ static jlong _createWindowCommonDo(JNIEnv *env, jobject jWindow, jlong jOwnerPtr GLASS_POOL_EXIT; GLASS_CHECK_EXCEPTION(env); } + +/* + * Class: com_sun_glass_ui_mac_MacWindow + * Method: _performWindowDrag + * Signature: (J)V + */ +JNIEXPORT void JNICALL Java_com_sun_glass_ui_mac_MacWindow__1performWindowDrag +(JNIEnv *env, jobject jWindow, jlong jPtr) +{ + LOG("Java_com_sun_glass_ui_mac_MacWindow__1performWindowDrag"); + if (!jPtr) return; + + GLASS_ASSERT_MAIN_JAVA_THREAD(env); + GLASS_POOL_ENTER; + { + GlassWindow *window = getGlassWindow(env, jPtr); + GlassViewDelegate* delegate = [window->view delegate]; + [window->nsWindow performWindowDragWithEvent:[delegate lastEvent]]; + } + GLASS_POOL_EXIT; + GLASS_CHECK_EXCEPTION(env); +} + +/* + * Class: com_sun_glass_ui_mac_MacWindow + * Method: _isRightToLeftLayoutDirection + * Signature: ()Z; + */ +JNIEXPORT jboolean JNICALL Java_com_sun_glass_ui_mac_MacWindow__1isRightToLeftLayoutDirection +(JNIEnv *env, jobject self) +{ + LOG("Java_com_sun_glass_ui_mac_MacWindow__1isRightToLeftLayoutDirection"); + jboolean result = false; + + GLASS_ASSERT_MAIN_JAVA_THREAD(env); + GLASS_POOL_ENTER; + { + NSString* preferredLanguage = [[NSLocale preferredLanguages] objectAtIndex:0]; + NSLocale* locale = [NSLocale localeWithLocaleIdentifier:preferredLanguage]; + NSString* languageCode = [locale objectForKey:NSLocaleLanguageCode]; + result = [NSLocale characterDirectionForLanguage:languageCode] == NSLocaleLanguageDirectionRightToLeft; + } + GLASS_POOL_EXIT; + GLASS_CHECK_EXCEPTION(env); + + return result; +} diff --git a/modules/javafx.graphics/src/main/native-glass/win/GlassWindow.cpp b/modules/javafx.graphics/src/main/native-glass/win/GlassWindow.cpp index 99b2db7f543..02a962c92e4 100644 --- a/modules/javafx.graphics/src/main/native-glass/win/GlassWindow.cpp +++ b/modules/javafx.graphics/src/main/native-glass/win/GlassWindow.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -41,6 +41,7 @@ #include "com_sun_glass_ui_Window_Level.h" #include "com_sun_glass_ui_win_WinWindow.h" +#define ABM_GETAUTOHIDEBAREX 0x0000000b // multimon aware autohide bars // Helper LEAVE_MAIN_THREAD for GlassWindow #define LEAVE_MAIN_THREAD_WITH_hWnd \ @@ -62,7 +63,8 @@ HHOOK GlassWindow::sm_hCBTFilter = NULL; HWND GlassWindow::sm_grabWindow = NULL; static HWND activeTouchWindow = NULL; -GlassWindow::GlassWindow(jobject jrefThis, bool isTransparent, bool isDecorated, bool isUnified, HWND parentOrOwner) +GlassWindow::GlassWindow(jobject jrefThis, bool isTransparent, bool isDecorated, bool isUnified, + bool isExtended, HWND parentOrOwner) : BaseWnd(parentOrOwner), ViewContainer(), m_winChangingReason(Unknown), @@ -74,6 +76,7 @@ GlassWindow::GlassWindow(jobject jrefThis, bool isTransparent, bool isDecorated, m_isTransparent(isTransparent), m_isDecorated(isDecorated), m_isUnified(isUnified), + m_isExtended(isExtended), m_hMenu(NULL), m_alpha(255), m_isEnabled(true), @@ -452,7 +455,18 @@ LRESULT GlassWindow::WindowProc(UINT msg, WPARAM wParam, LPARAM lParam) // p->rgrc[0].bottom++; // return WVR_VALIDRECTS; // } + + if (BOOL(wParam) && m_isExtended) { + return HandleNCCalcSizeEvent(msg, wParam, lParam); + } + break; + case WM_NCHITTEST: { + LRESULT res; + if (m_isExtended && HandleNCHitTestEvent(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam), res)) { + return res; + } break; + } case WM_PAINT: HandleViewPaintEvent(GetHWND(), msg, wParam, lParam); break; @@ -477,25 +491,11 @@ LRESULT GlassWindow::WindowProc(UINT msg, WPARAM wParam, LPARAM lParam) case WM_MOUSEHWHEEL: case WM_MOUSELEAVE: case WM_MOUSEMOVE: - if (IsEnabled()) { - if (msg == WM_MOUSELEAVE && GetDelegateWindow()) { - // Skip generating MouseEvent.EXIT when entering FullScreen - return 0; - } - BOOL handled = HandleViewMouseEvent(GetHWND(), msg, wParam, lParam); - if (handled && msg == WM_RBUTTONUP) { - // By default, DefWindowProc() sends WM_CONTEXTMENU from WM_LBUTTONUP - // Since DefWindowProc() is not called, call the mouse menu handler directly - HandleViewMenuEvent(GetHWND(), WM_CONTEXTMENU, (WPARAM) GetHWND(), ::GetMessagePos ()); - //::DefWindowProc(GetHWND(), msg, wParam, lParam); - } - if (handled) { - // Do not call the DefWindowProc() for mouse events that were handled - return 0; - } - } else { + if (!IsEnabled()) { HandleFocusDisabledEvent(); return 0; + } else if (HandleMouseEvents(msg, wParam, lParam)) { + return 0; } break; case WM_CAPTURECHANGED: @@ -546,8 +546,37 @@ LRESULT GlassWindow::WindowProc(UINT msg, WPARAM wParam, LPARAM lParam) case WM_NCXBUTTONDOWN: UngrabFocus(); // ungrab itself CheckUngrab(); // check if other owned windows hierarchy holds the grab + + if (m_isExtended) { + HandleViewNonClientMouseEvent(GetHWND(), msg, wParam, lParam); + + // We need to handle clicks on the min/max/close regions, as otherwise Windows will + // draw very ugly buttons on top of our window. + if (wParam == HTMINBUTTON || wParam == HTMAXBUTTON || wParam == HTCLOSE) { + return 0; + } + } + // Pass the event to DefWindowProc() break; + case WM_NCLBUTTONUP: + case WM_NCLBUTTONDBLCLK: + case WM_NCRBUTTONUP: + case WM_NCRBUTTONDBLCLK: + case WM_NCMBUTTONUP: + case WM_NCMBUTTONDBLCLK: + case WM_NCXBUTTONUP: + case WM_NCXBUTTONDBLCLK: + case WM_NCMOUSELEAVE: + case WM_NCMOUSEMOVE: + if (m_isExtended) { + HandleViewNonClientMouseEvent(GetHWND(), msg, wParam, lParam); + + if (wParam == HTMINBUTTON || wParam == HTMAXBUTTON || wParam == HTCLOSE) { + return 0; + } + } + break; case WM_TOUCH: if (IsEnabled()) { if (activeTouchWindow == 0 || activeTouchWindow == GetHWND()) { @@ -573,6 +602,29 @@ LRESULT GlassWindow::WindowProc(UINT msg, WPARAM wParam, LPARAM lParam) return ::DefWindowProc(GetHWND(), msg, wParam, lParam); } +bool GlassWindow::HandleMouseEvents(UINT msg, WPARAM wParam, LPARAM lParam) +{ + if (msg == WM_MOUSELEAVE && GetDelegateWindow()) { + // Skip generating MouseEvent.EXIT when entering FullScreen + return true; + } + + BOOL handled = HandleViewMouseEvent(GetHWND(), msg, wParam, lParam); + if (handled && msg == WM_RBUTTONUP) { + // By default, DefWindowProc() sends WM_CONTEXTMENU from WM_LBUTTONUP + // Since DefWindowProc() is not called, call the mouse menu handler directly + HandleViewMenuEvent(GetHWND(), WM_CONTEXTMENU, (WPARAM) GetHWND(), ::GetMessagePos ()); + //::DefWindowProc(GetHWND(), msg, wParam, lParam); + } + + if (handled) { + // Do not call the DefWindowProc() for mouse events that were handled + return true; + } + + return false; +} + void GlassWindow::HandleCloseEvent() { JNIEnv* env = GetEnv(); @@ -763,6 +815,85 @@ void GlassWindow::HandleFocusDisabledEvent() CheckAndClearException(env); } +LRESULT GlassWindow::HandleNCCalcSizeEvent(UINT msg, WPARAM wParam, LPARAM lParam) +{ + // Capture the top and size before DefWindowProc applies the default frame. + NCCALCSIZE_PARAMS *p = (NCCALCSIZE_PARAMS*)lParam; + LONG originalTop = p->rgrc[0].top; + RECT originalSize = p->rgrc[0]; + + // Apply the default window frame. + LRESULT res = DefWindowProc(GetHWND(), msg, wParam, lParam); + if (res != 0) { + return res; + } + + // Restore the original top, which might have been overwritten by DefWindowProc. + RECT newSize = p->rgrc[0]; + newSize.top = originalTop; + + // A maximized window extends slightly beyond the screen, so we need to account for that + // by adding the border width to the top. + bool maximized = (::GetWindowLong(GetHWND(), GWL_STYLE) & WS_MAXIMIZE) != 0; + if (maximized && !m_isInFullScreen) { + newSize.top += ::GetSystemMetrics(SM_CXPADDEDBORDER) + ::GetSystemMetrics(SM_CYSIZEFRAME); + } + + // If we have an auto-hide taskbar, we need to reduce the size of a maximized or fullscreen + // window a little bit where the taskbar is located, as otherwise the taskbar cannot be + // summoned. + HMONITOR monitor = ::MonitorFromWindow(GetHWND(), MONITOR_DEFAULTTONEAREST); + if (monitor && (maximized || m_isInFullScreen)) { + MONITORINFO monitorInfo = { 0 }; + monitorInfo.cbSize = sizeof(MONITORINFO); + ::GetMonitorInfo(monitor, &monitorInfo); + + APPBARDATA data = { 0 }; + data.cbSize = sizeof(data); + + if ((::SHAppBarMessage(ABM_GETSTATE, &data) & ABS_AUTOHIDE) == ABS_AUTOHIDE) { + data.rc = monitorInfo.rcMonitor; + DWORD appBarMsg = ::IsWindows8OrGreater() ? ABM_GETAUTOHIDEBAREX : ABM_GETAUTOHIDEBAR; + + // Reduce the window size by one pixel on the taskbar side. + if ((data.uEdge = ABE_TOP), ::SHAppBarMessage(appBarMsg, &data) != NULL) { + newSize.top += 1; + } else if ((data.uEdge = ABE_BOTTOM), ::SHAppBarMessage(appBarMsg, &data) != NULL) { + newSize.bottom -= 1; + } else if ((data.uEdge = ABE_LEFT), ::SHAppBarMessage(appBarMsg, &data) != NULL) { + newSize.left += 1; + } else if ((data.uEdge = ABE_RIGHT), ::SHAppBarMessage(appBarMsg, &data) != NULL) { + newSize.right -= 1; + } + } + } + + p->rgrc[0] = newSize; + return 0; +} + +// Handling this message tells Windows which parts of the window are non-client regions. +// This enables window behaviors like dragging or snap layouts. +BOOL GlassWindow::HandleNCHitTestEvent(SHORT x, SHORT y, LRESULT& result) +{ + if (::DefWindowProc(GetHWND(), WM_NCHITTEST, 0, MAKELONG(x, y)) != HTCLIENT) { + return FALSE; + } + + POINT pt = { x, y }; + + if (!::ScreenToClient(GetHWND(), &pt)) { + return FALSE; + } + + JNIEnv* env = GetEnv(); + jint res = env->CallIntMethod(m_grefThis, javaIDs.WinWindow.nonClientHitTest, pt.x, pt.y); + CheckAndClearException(env); + result = LRESULT(res); + + return TRUE; +} + bool GlassWindow::HandleCommand(WORD cmdID) { return HandleMenuCommand(GetHWND(), cmdID); } @@ -1145,6 +1276,10 @@ JNIEXPORT void JNICALL Java_com_sun_glass_ui_win_WinWindow__1initIDs javaIDs.Window.notifyDelegatePtr = env->GetMethodID(cls, "notifyDelegatePtr", "(J)V"); ASSERT(javaIDs.Window.notifyDelegatePtr); if (env->ExceptionCheck()) return; + + javaIDs.WinWindow.nonClientHitTest = env->GetMethodID(cls, "nonClientHitTest", "(II)I"); + ASSERT(javaIDs.WinWindow.nonClientHitTest); + if (env->ExceptionCheck()) return; } /* @@ -1164,6 +1299,10 @@ JNIEXPORT jlong JNICALL Java_com_sun_glass_ui_win_WinWindow__1createWindow dwStyle = WS_CLIPCHILDREN | WS_SYSMENU; closeable = (mask & com_sun_glass_ui_Window_CLOSABLE) != 0; + if (mask & com_sun_glass_ui_Window_EXTENDED) { + mask |= com_sun_glass_ui_Window_TITLED; + } + if (mask & com_sun_glass_ui_Window_TITLED) { dwExStyle = WS_EX_WINDOWEDGE; dwStyle |= WS_CAPTION; @@ -1206,6 +1345,7 @@ JNIEXPORT jlong JNICALL Java_com_sun_glass_ui_win_WinWindow__1createWindow (mask & com_sun_glass_ui_Window_TRANSPARENT) != 0, (mask & com_sun_glass_ui_Window_TITLED) != 0, (mask & com_sun_glass_ui_Window_UNIFIED) != 0, + (mask & com_sun_glass_ui_Window_EXTENDED) != 0, owner); HWND hWnd = pWindow->Create(dwStyle, dwExStyle, hMonitor, owner); diff --git a/modules/javafx.graphics/src/main/native-glass/win/GlassWindow.h b/modules/javafx.graphics/src/main/native-glass/win/GlassWindow.h index a56b060f340..72c986ccc9a 100644 --- a/modules/javafx.graphics/src/main/native-glass/win/GlassWindow.h +++ b/modules/javafx.graphics/src/main/native-glass/win/GlassWindow.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -32,7 +32,8 @@ class GlassWindow : public BaseWnd, public ViewContainer { public: - GlassWindow(jobject jrefThis, bool isTransparent, bool isDecorated, bool isUnified, HWND parentOrOwner); + GlassWindow(jobject jrefThis, bool isTransparent, bool isDecorated, bool isUnified, + bool isExtended, HWND parentOrOwner); virtual ~GlassWindow(); static GlassWindow* FromHandle(HWND hWnd) { @@ -144,6 +145,7 @@ class GlassWindow : public BaseWnd, public ViewContainer { const bool m_isTransparent; const bool m_isDecorated; const bool m_isUnified; + const bool m_isExtended; bool m_isResizable; @@ -184,6 +186,9 @@ class GlassWindow : public BaseWnd, public ViewContainer { void HandleDPIEvent(WPARAM wParam, LPARAM lParam); bool HandleCommand(WORD cmdID); void HandleFocusDisabledEvent(); + bool HandleMouseEvents(UINT msg, WPARAM wParam, LPARAM lParam); + LRESULT HandleNCCalcSizeEvent(UINT msg, WPARAM wParam, LPARAM lParam); + BOOL HandleNCHitTestEvent(SHORT, SHORT, LRESULT&); }; diff --git a/modules/javafx.graphics/src/main/native-glass/win/Utils.h b/modules/javafx.graphics/src/main/native-glass/win/Utils.h index 3ff428aabae..1c757c739cb 100644 --- a/modules/javafx.graphics/src/main/native-glass/win/Utils.h +++ b/modules/javafx.graphics/src/main/native-glass/win/Utils.h @@ -490,6 +490,9 @@ typedef struct _tagJavaIDs { jmethodID notifyDestroy; jmethodID notifyDelegatePtr; } Window; + struct { + jmethodID nonClientHitTest; + } WinWindow; struct { jmethodID notifyResize; jmethodID notifyRepaint; diff --git a/modules/javafx.graphics/src/main/native-glass/win/ViewContainer.cpp b/modules/javafx.graphics/src/main/native-glass/win/ViewContainer.cpp index 97fdce3cfec..73e36b6893a 100644 --- a/modules/javafx.graphics/src/main/native-glass/win/ViewContainer.cpp +++ b/modules/javafx.graphics/src/main/native-glass/win/ViewContainer.cpp @@ -934,6 +934,128 @@ BOOL ViewContainer::HandleViewMouseEvent(HWND hwnd, UINT msg, WPARAM wParam, LPA return TRUE; } +void ViewContainer::HandleViewNonClientMouseEvent(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) +{ + if (!GetGlassView()) { + return; + } + + int type = 0; + int button = com_sun_glass_events_MouseEvent_BUTTON_NONE; + POINT pt; // client coords + + if (msg == WM_NCMOUSELEAVE) { + type = com_sun_glass_events_MouseEvent_NC_EXIT; + pt.x = GET_X_LPARAM(lParam); + pt.y = GET_Y_LPARAM(lParam); + ::MapWindowPoints(NULL, hwnd, &pt, 1); + m_bTrackingMouse = FALSE; + m_lastMouseMovePosition = -1; + } else if (msg >= WM_NCMOUSEMOVE && msg <= WM_NCXBUTTONDBLCLK) { + pt.x = GET_X_LPARAM(lParam); + pt.y = GET_Y_LPARAM(lParam); + ::MapWindowPoints(NULL, hwnd, &pt, 1); + + switch (msg) { + case WM_NCMOUSEMOVE: + if (lParam == m_lastMouseMovePosition) { + // Avoid sending synthetic NC_MOVE events if + // the pointer hasn't moved actually. + // Just consume the messages. + return; + } + + m_lastMouseMovePosition = lParam; + type = com_sun_glass_events_MouseEvent_NC_MOVE; + break; + case WM_NCLBUTTONDOWN: + type = com_sun_glass_events_MouseEvent_NC_DOWN; + button = com_sun_glass_events_MouseEvent_BUTTON_LEFT; + break; + case WM_NCLBUTTONUP: + type = com_sun_glass_events_MouseEvent_NC_UP; + button = com_sun_glass_events_MouseEvent_BUTTON_LEFT; + break; + case WM_NCRBUTTONDOWN: + type = com_sun_glass_events_MouseEvent_NC_DOWN; + button = com_sun_glass_events_MouseEvent_BUTTON_RIGHT; + break; + case WM_NCRBUTTONUP: + type = com_sun_glass_events_MouseEvent_NC_UP; + button = com_sun_glass_events_MouseEvent_BUTTON_RIGHT; + break; + case WM_NCMBUTTONDOWN: + type = com_sun_glass_events_MouseEvent_NC_DOWN; + button = com_sun_glass_events_MouseEvent_BUTTON_OTHER; + break; + case WM_NCMBUTTONUP: + type = com_sun_glass_events_MouseEvent_NC_UP; + button = com_sun_glass_events_MouseEvent_BUTTON_OTHER; + break; + case WM_NCXBUTTONDOWN: + type = com_sun_glass_events_MouseEvent_NC_DOWN; + button = GET_XBUTTON_WPARAM(wParam) == XBUTTON1 + ? com_sun_glass_events_MouseEvent_BUTTON_BACK + : com_sun_glass_events_MouseEvent_BUTTON_FORWARD; + break; + case WM_NCXBUTTONUP: + type = com_sun_glass_events_MouseEvent_NC_UP; + button = GET_XBUTTON_WPARAM(wParam) == XBUTTON1 + ? com_sun_glass_events_MouseEvent_BUTTON_BACK + : com_sun_glass_events_MouseEvent_BUTTON_FORWARD; + break; + } + } + + // Event was not handled + if (type == 0) { + return; + } + + // get screen coords + POINT ptAbs = pt; + ::ClientToScreen(hwnd, &ptAbs); + + // unmirror the x coordinate + LONG style = ::GetWindowLong(hwnd, GWL_EXSTYLE); + if (style & WS_EX_LAYOUTRTL) { + RECT rect = {0}; + ::GetClientRect(hwnd, &rect); + pt.x = max(0, rect.right - rect.left) - pt.x; + } + + jint jModifiers = GetModifiers(); + jboolean isSynthesized = jboolean(IsTouchEvent()); + JNIEnv* env = GetEnv(); + + if (!m_bTrackingMouse && type != com_sun_glass_events_MouseEvent_NC_EXIT) { + TRACKMOUSEEVENT trackData; + trackData.cbSize = sizeof(trackData); + trackData.dwFlags = TME_LEAVE | TME_NONCLIENT; + trackData.hwndTrack = hwnd; + trackData.dwHoverTime = HOVER_DEFAULT; + + if (::TrackMouseEvent(&trackData)) { + // Mouse tracking will be canceled automatically upon receiving WM_NCMOUSELEAVE + m_bTrackingMouse = TRUE; + } + + env->CallVoidMethod(GetView(), javaIDs.View.notifyMouse, + com_sun_glass_events_MouseEvent_NC_ENTER, + com_sun_glass_events_MouseEvent_BUTTON_NONE, + pt.x, pt.y, ptAbs.x, ptAbs.y, + jModifiers, JNI_FALSE, isSynthesized); + CheckAndClearException(env); + } + + env->CallVoidMethod(GetView(), javaIDs.View.notifyMouse, + type, button, pt.x, pt.y, ptAbs.x, ptAbs.y, + jModifiers, + type == com_sun_glass_events_MouseEvent_NC_UP && button == com_sun_glass_events_MouseEvent_BUTTON_RIGHT, + isSynthesized); + CheckAndClearException(env); +} + void ViewContainer::NotifyCaptureChanged(HWND hwnd, HWND to) { m_mouseButtonDownCounter = 0; @@ -948,7 +1070,7 @@ void ViewContainer::ResetMouseTracking(HWND hwnd) // We don't expect WM_MOUSELEAVE anymore, so we cancel mouse tracking manually TRACKMOUSEEVENT trackData; trackData.cbSize = sizeof(trackData); - trackData.dwFlags = TME_LEAVE | TME_CANCEL; + trackData.dwFlags = TME_LEAVE | TME_NONCLIENT | TME_CANCEL; trackData.hwndTrack = hwnd; trackData.dwHoverTime = HOVER_DEFAULT; ::TrackMouseEvent(&trackData); diff --git a/modules/javafx.graphics/src/main/native-glass/win/ViewContainer.h b/modules/javafx.graphics/src/main/native-glass/win/ViewContainer.h index 5d9de1436c1..0eea989d0c0 100644 --- a/modules/javafx.graphics/src/main/native-glass/win/ViewContainer.h +++ b/modules/javafx.graphics/src/main/native-glass/win/ViewContainer.h @@ -70,6 +70,7 @@ class ViewContainer { void HandleViewDeadKeyEvent(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam); void HandleViewTypedEvent(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam); BOOL HandleViewMouseEvent(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam); + void HandleViewNonClientMouseEvent(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam); BOOL HandleViewInputMethodEvent(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam); LRESULT HandleViewGetAccessible(HWND hwnd, WPARAM wParam, LPARAM lParam); diff --git a/modules/javafx.graphics/src/main/native-glass/win/common.h b/modules/javafx.graphics/src/main/native-glass/win/common.h index 6b237da61de..67ca2bbc49d 100644 --- a/modules/javafx.graphics/src/main/native-glass/win/common.h +++ b/modules/javafx.graphics/src/main/native-glass/win/common.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2016, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -59,6 +59,8 @@ #include #include #include +#include +#include #include "Utils.h" #include "OleUtils.h" diff --git a/modules/javafx.graphics/src/main/resources/com/sun/glass/ui/gtk/WindowDecorationGnome.css b/modules/javafx.graphics/src/main/resources/com/sun/glass/ui/gtk/WindowDecorationGnome.css new file mode 100644 index 00000000000..8573e492ea0 --- /dev/null +++ b/modules/javafx.graphics/src/main/resources/com/sun/glass/ui/gtk/WindowDecorationGnome.css @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +.window-button-container { + -fx-button-placement: right; + -fx-allow-rtl: true; +} + +.minimize-button, +.maximize-button, +.close-button { + -fx-background-color: #00000009; + -fx-background-radius: 15; + -fx-background-insets: 6; + -fx-pref-width: 36; + -fx-pref-height: 36; +} + +.minimize-button.dark, +.maximize-button.dark, +.close-button.dark { + -fx-background-color: #ffffff09; +} + +.minimize-button:active, +.maximize-button:active, +.close-button:active { + -fx-background-color: #00000015; +} + +.minimize-button.dark:active, +.maximize-button.dark:active, +.close-button.dark:active { + -fx-background-color: #ffffff15 +} + +.minimize-button:active:hover, +.maximize-button:active:hover, +.close-button:active:hover { + -fx-background-color: #00000025; +} + +.minimize-button.dark:active:hover, +.maximize-button.dark:active:hover, +.close-button.dark:active:hover { + -fx-background-color: #ffffff25; +} + +.minimize-button:pressed, +.maximize-button:pressed, +.close-button:pressed { + -fx-background-color: #00000035 !important; +} + +.minimize-button.dark:pressed, +.maximize-button.dark:pressed, +.close-button.dark:pressed { + -fx-background-color: #ffffff35 !important; +} + +.minimize-button > .glyph, +.maximize-button > .glyph, +.close-button > .glyph { + -fx-background-color: #777; + -fx-background-insets: 6 -6 -6 6; + -fx-scale-shape: false; + -fx-position-shape: false; +} + +.minimize-button:active > .glyph, +.maximize-button:active > .glyph, +.close-button:active > .glyph { + -fx-background-color: #333; +} + +.minimize-button.dark:active > .glyph, +.maximize-button.dark:active > .glyph, +.close-button.dark:active > .glyph { + -fx-background-color: white; +} + +.maximize-button:disabled { + -fx-background-color: transparent; +} + +.maximize-button:disabled > .glyph { + -fx-background-color: #777; +} + +.minimize-button > .glyph { + -fx-shape: "m 8,13 v 1 h 8 v -1 z"; +} + +.maximize-button > .glyph { + -fx-shape: "M 15,8.934 V 15 H 9 V 8.934 Z M 8,8 v 8 h 8 V 8 Z"; +} + +.maximize-button.restore > .glyph { + -fx-shape: "M 10 7 L 10 8 L 16 8 L 16 14 L 17 14 L 17 7 L 10 7 z M 8 9 L 8 16 L 15 16 L 15 9 L 8 9 z M 9 9.9238281 L 14 9.9238281 L 14 15 L 9 15 L 9 9.9238281 z"; +} + +.close-button > .glyph { + -fx-shape: "m 8.1464844,8.1464844 a 0.5,0.5 0 0 0 0,0.7070312 L 11.292969,12 8.1464844,15.146484 a 0.5,0.5 0 0 0 0,0.707032 0.5,0.5 0 0 0 0.7070312,0 L 12,12.707031 l 3.146484,3.146485 a 0.5,0.5 0 0 0 0.707032,0 0.5,0.5 0 0 0 0,-0.707032 L 12.707031,12 15.853516,8.8535156 a 0.5,0.5 0 0 0 0,-0.7070312 0.5,0.5 0 0 0 -0.707032,0 L 12,11.292969 8.8535156,8.1464844 a 0.5,0.5 0 0 0 -0.7070312,0 z"; +} diff --git a/modules/javafx.graphics/src/main/resources/com/sun/glass/ui/gtk/WindowDecorationKDE.css b/modules/javafx.graphics/src/main/resources/com/sun/glass/ui/gtk/WindowDecorationKDE.css new file mode 100644 index 00000000000..c9caacc6fd8 --- /dev/null +++ b/modules/javafx.graphics/src/main/resources/com/sun/glass/ui/gtk/WindowDecorationKDE.css @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +.window-button-container { + -fx-button-placement: right; + -fx-allow-rtl: true; +} + +.minimize-button, +.maximize-button, +.close-button { + -fx-background-color: transparent; + -fx-background-radius: 13; + -fx-background-insets: 8; + -fx-pref-width: 36; + -fx-pref-height: 36; +} + +.minimize-button > .glyph, +.maximize-button > .glyph, +.close-button > .glyph { + -fx-background-color: #1d1d1e; + -fx-background-insets: 8 -8 -8 8; + -fx-scale-shape: false; + -fx-position-shape: false; +} + +.minimize-button.dark > .glyph, +.maximize-button.dark > .glyph, +.close-button.dark > .glyph { + -fx-background-color: #ced0d6; +} + +.minimize-button:hover, +.maximize-button:hover, +.close-button:hover { + -fx-background-color: #27292d; +} + +.minimize-button.dark:hover, +.maximize-button.dark:hover, +.close-button.dark:hover { + -fx-background-color: #dfe1e6; +} + +.minimize-button:hover > .glyph, +.maximize-button:hover > .glyph, +.close-button:hover > .glyph { + -fx-background-color: #ced0d6; +} + +.minimize-button.dark:hover > .glyph, +.maximize-button.dark:hover > .glyph, +.close-button.dark:hover > .glyph { + -fx-background-color: #393b40; +} + +.minimize-button:pressed, +.maximize-button:pressed, +.close-button:pressed { + -fx-background-color: #474b52 !important; +} + +.minimize-button.dark:pressed, +.maximize-button.dark:pressed, +.close-button.dark:pressed { + -fx-background-color: #6c7076 !important; +} + +.maximize-button:disabled { + -fx-managed: false; + visibility: hidden; +} + +.minimize-button > .glyph { + -fx-shape: "m 5,7.5 a 0.5,0.5 0 0 0 -0.3535156,0.1464844 0.5,0.5 0 0 0 0,0.7070312 l 5,5.0000004 a 0.50005,0.50005 0 0 0 0.7070316,0 l 5,-5.0000004 a 0.5,0.5 0 0 0 0,-0.7070312 0.5,0.5 0 0 0 -0.707032,0 L 10,12.292969 5.3535156,7.6464844 A 0.5,0.5 0 0 0 5,7.5 Z"; +} + +.maximize-button > .glyph { + -fx-shape: "m 9.6464844,6.6464844 -5,4.9999996 a 0.5,0.5 0 0 0 0,0.707032 0.5,0.5 0 0 0 0.7070312,0 L 10,7.7070312 14.646484,12.353516 a 0.5,0.5 0 0 0 0.707032,0 0.5,0.5 0 0 0 0,-0.707032 l -5,-4.9999996 a 0.50005,0.50005 0 0 0 -0.7070316,0 z"; +} + +.maximize-button.restore > .glyph { + -fx-shape: "m 9.6464844,5.1464844 -4.5,4.5 a 0.50005,0.50005 0 0 0 0,0.7070316 l 4.5,4.5 a 0.50005,0.50005 0 0 0 0.7070316,0 l 4.5,-4.5 a 0.50005,0.50005 0 0 0 0,-0.7070316 l -4.5,-4.5 a 0.50005,0.50005 0 0 0 -0.7070316,0 z M 10,6.2070312 13.792969,10 10,13.792969 6.2070312,10 Z"; +} + +.close-button > .glyph { + -fx-shape: "m 6,5.5 a 0.5,0.5 0 0 0 -0.3535156,0.1464844 0.5,0.5 0 0 0 0,0.7070312 L 9.2929688,10 5.6464844,13.646484 a 0.5,0.5 0 0 0 0,0.707032 0.5,0.5 0 0 0 0.7070312,0 L 10,10.707031 l 3.646484,3.646485 a 0.5,0.5 0 0 0 0.707032,0 0.5,0.5 0 0 0 0,-0.707032 L 10.707031,10 14.353516,6.3535156 a 0.5,0.5 0 0 0 0,-0.7070312 0.5,0.5 0 0 0 -0.707032,0 L 10,9.2929688 6.3535156,5.6464844 A 0.5,0.5 0 0 0 6,5.5 Z"; +} diff --git a/modules/javafx.graphics/src/main/resources/com/sun/glass/ui/win/WindowDecoration.css b/modules/javafx.graphics/src/main/resources/com/sun/glass/ui/win/WindowDecoration.css new file mode 100644 index 00000000000..15b918f803e --- /dev/null +++ b/modules/javafx.graphics/src/main/resources/com/sun/glass/ui/win/WindowDecoration.css @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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. + */ + +.window-button-container { + -fx-button-placement: right; + -fx-allow-rtl: true; +} + +.minimize-button, +.maximize-button, +.close-button { + -fx-background-color: transparent; + -fx-pref-width: 46; + -fx-pref-height: 29; +} + +.minimize-button:hover, +.maximize-button:hover { + -fx-background-color: #00000015; +} + +.minimize-button.dark:hover, +.maximize-button.dark:hover { + -fx-background-color: #ffffff15; +} + +.close-button:hover { + -fx-background-color: #c42b1c; +} + +.minimize-button:pressed, +.maximize-button:pressed, +.close-button:pressed { + -fx-opacity: 0.8; +} + +.minimize-button > .glyph, +.maximize-button > .glyph, +.close-button > .glyph { + -fx-background-color: #777; + -fx-scale-shape: false; +} + +.minimize-button:active > .glyph, +.maximize-button:active > .glyph, +.close-button:active > .glyph { + -fx-background-color: black; +} + +.minimize-button.dark:active > .glyph, +.maximize-button.dark:active > .glyph, +.close-button.dark:active > .glyph { + -fx-background-color: white; +} + +.close-button:hover > .glyph { + -fx-background-color: white; +} + +.minimize-button.dark:pressed > .glyph { + -fx-background-color: white; +} + +.maximize-button:disabled { + -fx-background-color: transparent !important; +} + +.maximize-button:disabled > .glyph { + -fx-background-color: #00000020 !important; +} + +.maximize-button.dark:disabled > .glyph { + -fx-background-color: #ffffff20 !important; +} + +.minimize-button > .glyph { + -fx-shape: "m 0.42342443,4.6755 q -0.0871756,0 -0.16397319,-0.0332 Q 0.18265374,4.6091 0.1245366,4.551 0.06641952,4.4929 0.03320975,4.41608 0,4.33925 0,4.25208 0,4.16488 0.03320975,4.0881 0.06641952,4.0113 0.1245366,3.95112 0.1826537,3.89092 0.25945124,3.85772 0.33624881,3.8245 0.42342443,3.8245 H 8.078274 q 0.087175,0 0.1639731,0.0332 0.076797,0.0332 0.1349147,0.0934 0.058117,0.0602 0.091327,0.13698 0.033209,0.0768 0.033209,0.16397 0,0.0872 -0.03321,0.16397 -0.03321,0.0768 -0.091327,0.13492 -0.058117,0.0581 -0.1349147,0.0913 -0.076797,0.0332 -0.1639731,0.0332 z"; +} + +.maximize-button > .glyph { + -fx-shape: "M 1.253418,8.5 Q 1.0043945,8.5 0.77612305,8.3983154 0.54785156,8.2966309 0.37561035,8.1243896 0.20336914,7.9521484 0.10168457,7.723877 0,7.4956055 0,7.246582 V 1.253418 Q 0,1.0043945 0.10168457,0.77612305 0.20336914,0.54785156 0.37561035,0.37561035 0.54785156,0.20336914 0.77612305,0.10168457 1.0043945,0 1.253418,0 H 7.246582 Q 7.4956055,0 7.723877,0.10168457 7.9521484,0.20336914 8.1243896,0.37561035 8.2966309,0.54785156 8.3983154,0.77612305 8.5,1.0043945 8.5,1.253418 V 7.246582 Q 8.5,7.4956055 8.3983154,7.723877 8.2966309,7.9521484 8.1243896,8.1243896 7.9521484,8.2966309 7.723877,8.3983154 7.4956055,8.5 7.246582,8.5 Z M 7.2258301,7.6491699 q 0.087158,0 0.1639404,-0.033203 0.076782,-0.033203 0.1348877,-0.091309 0.058105,-0.058105 0.091309,-0.1348877 0.033203,-0.076782 0.033203,-0.1639404 V 1.2741699 q 0,-0.087158 -0.033203,-0.1639404 Q 7.5827637,1.0334473 7.5246582,0.9753418 7.4665527,0.91723633 7.3897705,0.8840332 7.3129883,0.85083008 7.2258301,0.85083008 H 1.2741699 q -0.087158,0 -0.1639404,0.0332031 -0.076782,0.0332031 -0.1348877,0.0913086 Q 0.9172363,1.0334473 0.8840332,1.1102295 0.8508301,1.1870115 0.8508301,1.2741699 v 5.9516602 q 0,0.087158 0.0332031,0.1639404 0.0332031,0.076782 0.0913086,0.1348877 0.0581055,0.058105 0.1348877,0.091309 0.076782,0.033203 0.1639404,0.033203 z"; +} + +.maximize-button.restore > .glyph { + -fx-shape: "m 7.6491699,2.5192871 q 0,-0.3444824 -0.1369629,-0.6495361 Q 7.3752441,1.5646973 7.1407471,1.338501 6.90625,1.1123047 6.5970459,0.98156738 6.2878418,0.85083008 5.9475098,0.85083008 H 1.7722168 Q 1.838623,0.65991211 1.9589844,0.50219727 2.0793457,0.34448242 2.2370605,0.23242188 2.3947754,0.12036133 2.5836182,0.06018066 2.7724609,0 2.9758301,0 H 5.9475098 Q 6.4746094,0 6.9394531,0.20129395 7.4042969,0.40258789 7.7508545,0.74707031 8.0974121,1.0915527 8.2987061,1.5563965 8.5,2.0212402 8.5,2.5483398 V 5.5241699 Q 8.5,5.7275391 8.4398193,5.9163818 8.3796387,6.1052246 8.2675781,6.2629395 8.1555176,6.4206543 7.9978027,6.5410156 7.8400879,6.661377 7.6491699,6.7277832 Z M 1.253418,8.5 Q 1.0043945,8.5 0.77612305,8.3983154 0.54785156,8.2966309 0.37561035,8.1243896 0.20336914,7.9521484 0.10168457,7.723877 0,7.4956055 0,7.246582 V 2.9550781 Q 0,2.7019043 0.10168457,2.475708 0.20336914,2.2495117 0.37561035,2.0772705 0.54785156,1.9050293 0.77404785,1.8033447 1.0002441,1.7016602 1.253418,1.7016602 h 4.2915039 q 0.2531738,0 0.4814453,0.1016845 0.2282715,0.1016846 0.3984375,0.2718506 0.170166,0.170166 0.2718506,0.3984375 0.1016845,0.2282715 0.1016845,0.4814453 V 7.246582 q 0,0.2531739 -0.1016845,0.4793701 Q 6.5949707,7.9521484 6.4227295,8.1243896 6.2504883,8.2966309 6.024292,8.3983154 5.7980957,8.5 5.5449219,8.5 Z M 5.5241699,7.6491699 q 0.087158,0 0.1639405,-0.033203 0.076782,-0.033203 0.1369628,-0.091309 0.060181,-0.058105 0.093384,-0.1348877 0.033203,-0.076782 0.033203,-0.1639404 v -4.25 q 0,-0.087158 -0.033203,-0.1660156 Q 5.8852539,2.730957 5.8271484,2.6728516 5.769043,2.6147461 5.6901855,2.581543 5.6113281,2.5483398 5.5241699,2.5483398 h -4.25 q -0.087158,0 -0.1639404,0.033203 -0.076782,0.033203 -0.1348877,0.093384 -0.0581055,0.060181 -0.0913086,0.1369628 -0.0332031,0.076782 -0.0332031,0.1639405 v 4.25 q 0,0.087158 0.0332031,0.1639404 0.0332031,0.076782 0.0913086,0.1348877 0.0581055,0.058105 0.1348877,0.091309 0.076782,0.033203 0.1639404,0.033203 z"; +} + +.close-button > .glyph { + -fx-shape: "M 4.25,4.8518066 0.7263184,8.3754883 Q 0.6018066,8.5 0.4274902,8.5 0.2448731,8.5 0.1224365,8.3775635 0,8.255127 0,8.0725098 0,7.8981934 0.1245117,7.7736816 L 3.6481934,4.25 0.1245117,0.72631836 Q 0,0.60180664 0,0.42333984 0,0.33618164 0.033203,0.25732422 0.066406,0.17846682 0.124512,0.12243652 0.182617,0.06640625 0.2614749,0.03320312 0.340332,0 0.4274902,0 0.6018066,0 0.7263184,0.12451172 L 4.25,3.6481934 7.7736816,0.12451172 Q 7.8981934,0 8.0766602,0 q 0.087158,0 0.1639404,0.03320312 0.076782,0.03320313 0.1348877,0.0913086 0.058105,0.0581055 0.091309,0.13488769 Q 8.5,0.33618164 8.5,0.42333984 8.5,0.60180664 8.3754883,0.72631836 L 4.8518066,4.25 8.3754883,7.7736816 Q 8.5,7.8981934 8.5,8.0725098 8.5,8.1596678 8.466797,8.2385254 8.433594,8.3173824 8.377564,8.3754883 8.321534,8.4335933 8.2426763,8.4667973 8.1638193,8.5 8.0766607,8.5 7.8981939,8.5 7.7736821,8.3754883 Z"; +} diff --git a/modules/javafx.graphics/src/test/addExports b/modules/javafx.graphics/src/test/addExports index b34406f8919..4f2137a9e60 100644 --- a/modules/javafx.graphics/src/test/addExports +++ b/modules/javafx.graphics/src/test/addExports @@ -1,6 +1,7 @@ --add-exports javafx.base/com.sun.javafx.collections=ALL-UNNAMED --add-exports javafx.base/com.sun.javafx.property=ALL-UNNAMED --add-exports javafx.base/com.sun.javafx=ALL-UNNAMED +--add-exports javafx.base/com.sun.javafx.binding=ALL-UNNAMED --add-exports javafx.base/com.sun.javafx.event=ALL-UNNAMED --add-exports javafx.base/com.sun.javafx.logging=ALL-UNNAMED # @@ -53,6 +54,7 @@ --add-opens javafx.graphics/javafx.scene.robot=ALL-UNNAMED --add-opens javafx.graphics/javafx.scene.layout=ALL-UNNAMED --add-opens javafx.graphics/javafx.scene.paint=ALL-UNNAMED +--add-opens javafx.graphics/javafx.stage=ALL-UNNAMED # # compile time additions --add-exports=javafx.base/com.sun.javafx.runtime=ALL-UNNAMED diff --git a/modules/javafx.graphics/src/test/java/test/com/sun/glass/ui/WindowControlsOverlayTest.java b/modules/javafx.graphics/src/test/java/test/com/sun/glass/ui/WindowControlsOverlayTest.java new file mode 100644 index 00000000000..a03afc65ca3 --- /dev/null +++ b/modules/javafx.graphics/src/test/java/test/com/sun/glass/ui/WindowControlsOverlayTest.java @@ -0,0 +1,349 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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 test.com.sun.glass.ui; + +import com.sun.glass.ui.WindowControlsOverlay; +import com.sun.javafx.binding.ObjectConstant; +import javafx.beans.value.ObservableValue; +import javafx.geometry.Dimension2D; +import javafx.geometry.HorizontalDirection; +import javafx.geometry.NodeOrientation; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.paint.Color; +import javafx.stage.Stage; +import org.junit.jupiter.api.Test; +import test.util.ReflectionUtils; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +import static org.junit.jupiter.api.Assertions.*; + +public class WindowControlsOverlayTest { + + /** + * Asserts that the buttons are laid out on the right side of the control (left-to-right orientation). + */ + @Test + void rightPlacement() { + var overlay = new WindowControlsOverlay(getStylesheet(""" + .window-button-container { -fx-button-placement: right; } + .window-button { -fx-pref-width: 20; -fx-pref-height: 10; } + """)); + + var unused = new Scene(overlay); + var children = overlay.getChildrenUnmodifiable(); + overlay.resize(200, 100); + overlay.applyCss(); + overlay.layout(); + + assertSize(overlay, 200, 100); + assertLayoutBounds(children.get(0), 140, 0, 20, 10); + assertLayoutBounds(children.get(1), 160, 0, 20, 10); + assertLayoutBounds(children.get(2), 180, 0, 20, 10); + assertEquals(HorizontalDirection.RIGHT, overlay.metricsProperty().get().placement()); + assertEquals(new Dimension2D(60, 10), overlay.metricsProperty().get().size()); + } + + /** + * Asserts that the buttons are laid out on the left side of the control (right-to-left orientation). + */ + @Test + void rightPlacement_rightToLeft() { + var overlay = new WindowControlsOverlay(getStylesheet(""" + .window-button-container { -fx-button-placement: right; } + .window-button { -fx-pref-width: 20; -fx-pref-height: 10; } + """)); + + var unused = new Scene(overlay); + var children = overlay.getChildrenUnmodifiable(); + overlay.setNodeOrientation(NodeOrientation.RIGHT_TO_LEFT); + overlay.resize(200, 100); + overlay.applyCss(); + overlay.layout(); + + assertSize(overlay, 200, 100); + assertLayoutBounds(children.get(0), 40, 0, 20, 10); + assertLayoutBounds(children.get(1), 20, 0, 20, 10); + assertLayoutBounds(children.get(2), 0, 0, 20, 10); + assertEquals(HorizontalDirection.LEFT, overlay.metricsProperty().get().placement()); + assertEquals(new Dimension2D(60, 10), overlay.metricsProperty().get().size()); + } + + /** + * Asserts that the buttons are laid out on the left side of the control (left-to-right orientation). + */ + @Test + void leftPlacement() { + var overlay = new WindowControlsOverlay(getStylesheet(""" + .window-button-container { -fx-button-placement: left; } + .window-button { -fx-pref-width: 20; -fx-pref-height: 10; } + """)); + + var unused = new Scene(overlay); + var children = overlay.getChildrenUnmodifiable(); + overlay.resize(200, 100); + overlay.applyCss(); + overlay.layout(); + + assertSize(overlay, 200, 100); + assertLayoutBounds(children.get(0), 0, 0, 20, 10); + assertLayoutBounds(children.get(1), 20, 0, 20, 10); + assertLayoutBounds(children.get(2), 40, 0, 20, 10); + assertEquals(HorizontalDirection.LEFT, overlay.metricsProperty().get().placement()); + assertEquals(new Dimension2D(60, 10), overlay.metricsProperty().get().size()); + } + + /** + * Asserts that the buttons are laid out on the right side of the control (right-to-left orientation). + */ + @Test + void leftPlacement_rightToLeft() { + var overlay = new WindowControlsOverlay(getStylesheet(""" + .window-button-container { -fx-button-placement: left; } + .window-button { -fx-pref-width: 20; -fx-pref-height: 10; } + """)); + + var unused = new Scene(overlay); + var children = overlay.getChildrenUnmodifiable(); + overlay.setNodeOrientation(NodeOrientation.RIGHT_TO_LEFT); + overlay.resize(200, 100); + overlay.applyCss(); + overlay.layout(); + + assertSize(overlay, 200, 100); + assertLayoutBounds(children.get(0), 180, 0, 20, 10); + assertLayoutBounds(children.get(1), 160, 0, 20, 10); + assertLayoutBounds(children.get(2), 140, 0, 20, 10); + assertEquals(HorizontalDirection.RIGHT, overlay.metricsProperty().get().placement()); + assertEquals(new Dimension2D(60, 10), overlay.metricsProperty().get().size()); + } + + /** + * Asserts that the buttons are laid out in a custom order (left-to-right orientation). + */ + @Test + void customButtonOrder() { + var overlay = new WindowControlsOverlay(getStylesheet(""" + .window-button { -fx-pref-width: 20; -fx-pref-height: 10; } + .minimize-button { -fx-button-order: 5; } + .maximize-button { -fx-button-order: 1; } + .close-button { -fx-button-order: 3; } + """)); + + var unused = new Scene(overlay); + var children = overlay.getChildrenUnmodifiable(); + overlay.resize(200, 100); + overlay.applyCss(); + overlay.layout(); + + assertTrue(children.get(0).getStyleClass().contains("minimize-button")); + assertLayoutBounds(children.get(0), 180, 0, 20, 10); + assertTrue(children.get(1).getStyleClass().contains("maximize-button")); + assertLayoutBounds(children.get(1), 140, 0, 20, 10); + assertTrue(children.get(2).getStyleClass().contains("close-button")); + assertLayoutBounds(children.get(2), 160, 0, 20, 10); + } + + /** + * Asserts that the buttons are laid out in a custom order (right-to-left orientation). + */ + @Test + void customButtonOrder_rightToLeft() { + var overlay = new WindowControlsOverlay(getStylesheet(""" + .window-button { -fx-pref-width: 20; -fx-pref-height: 10; } + .minimize-button { -fx-button-order: 5; } + .maximize-button { -fx-button-order: 1; } + .close-button { -fx-button-order: 3; } + """)); + + var unused = new Scene(overlay); + var children = overlay.getChildrenUnmodifiable(); + overlay.setNodeOrientation(NodeOrientation.RIGHT_TO_LEFT); + overlay.resize(200, 100); + overlay.applyCss(); + overlay.layout(); + + assertTrue(children.get(0).getStyleClass().contains("minimize-button")); + assertLayoutBounds(children.get(0), 0, 0, 20, 10); + assertTrue(children.get(1).getStyleClass().contains("maximize-button")); + assertLayoutBounds(children.get(1), 40, 0, 20, 10); + assertTrue(children.get(2).getStyleClass().contains("close-button")); + assertLayoutBounds(children.get(2), 20, 0, 20, 10); + } + + /** + * Asserts that the buttons are laid out on the right, even though the node orientation is right-to-left. + */ + @Test + void disallowRightToLeft() { + var overlay = new WindowControlsOverlay(getStylesheet(""" + .window-button-container { -fx-button-placement: right; -fx-allow-rtl: false; } + .window-button { -fx-pref-width: 20; -fx-pref-height: 10; } + """)); + + var unused = new Scene(overlay); + var children = overlay.getChildrenUnmodifiable(); + overlay.setNodeOrientation(NodeOrientation.RIGHT_TO_LEFT); + overlay.resize(200, 100); + overlay.applyCss(); + overlay.layout(); + + assertLayoutBounds(children.get(0), 140, 0, 20, 10); + assertLayoutBounds(children.get(1), 160, 0, 20, 10); + assertLayoutBounds(children.get(2), 180, 0, 20, 10); + assertEquals(HorizontalDirection.RIGHT, overlay.metricsProperty().get().placement()); + assertEquals(new Dimension2D(60, 10), overlay.metricsProperty().get().size()); + } + + @Test + void activePseudoClassCorrespondsToStageFocusedProperty() { + var overlay = new WindowControlsOverlay(getStylesheet(""" + .window-button-container { -fx-button-placement: right; } + .window-button { -fx-pref-width: 20; -fx-pref-height: 10; } + """)); + + var scene = new Scene(overlay); + var stage = new Stage(); + stage.setScene(scene); + stage.show(); + + assertTrue(stage.isFocused()); + assertTrue(overlay.getChildrenUnmodifiable().get(0).getPseudoClassStates().stream().anyMatch( + pc -> pc.getPseudoClassName().equals("active"))); + + ReflectionUtils.invokeMethod(stage, "setFocused", new Class[] { boolean.class }, false); + + assertFalse(stage.isFocused()); + assertTrue(overlay.getChildrenUnmodifiable().get(0).getPseudoClassStates().stream().noneMatch( + pc -> pc.getPseudoClassName().equals("active"))); + } + + /** + * Asserts that the maximize button is disabled when the stage is not resizable. + */ + @Test + void maximizeButtonIsDisabledWhenStageIsNotResizable() { + var overlay = new WindowControlsOverlay(getStylesheet(""" + .window-button-container { -fx-button-placement: right; } + .window-button { -fx-pref-width: 20; -fx-pref-height: 10; } + """)); + + var scene = new Scene(overlay); + var stage = new Stage(); + stage.setScene(scene); + stage.show(); + + var maxButton = overlay.getChildrenUnmodifiable().get(1); + assertTrue(maxButton.getStyleClass().contains("maximize-button")); + assertTrue(stage.isResizable()); + assertFalse(maxButton.isDisabled()); + + stage.setResizable(false); + assertTrue(maxButton.isDisabled()); + } + + /** + * Asserts that the .restore style class is added to the maximize button when the stage is maximized. + */ + @Test + void restoreStyleClassIsPresentWhenStageIsMaximized() { + var overlay = new WindowControlsOverlay(getStylesheet(""" + .window-button-container { -fx-button-placement: right; } + .window-button { -fx-pref-width: 20; -fx-pref-height: 10; } + """)); + + var scene = new Scene(overlay); + var stage = new Stage(); + stage.setScene(scene); + stage.show(); + + var maxButton = overlay.getChildrenUnmodifiable().get(1); + assertTrue(maxButton.getStyleClass().contains("maximize-button")); + assertFalse(maxButton.getStyleClass().contains("restore")); + + stage.setMaximized(true); + assertTrue(maxButton.getStyleClass().contains("restore")); + } + + /** + * Asserts that the .dark style class is added to all buttons when {@link Scene#getFill()} is dark. + */ + @Test + void darkStyleClassIsPresentWhenSceneFillIsDark() { + var overlay = new WindowControlsOverlay(getStylesheet(""" + .window-button-container { -fx-button-placement: right; } + .window-button { -fx-pref-width: 20; -fx-pref-height: 10; } + """)); + + var scene = new Scene(overlay); + + scene.setFill(Color.WHITE); + assertTrue(overlay.getChildrenUnmodifiable().stream().noneMatch(b -> b.getStyleClass().contains("dark"))); + + scene.setFill(Color.BLACK); + assertTrue(overlay.getChildrenUnmodifiable().stream().allMatch(b -> b.getStyleClass().contains("dark"))); + } + + /** + * Tests button picking using {@link WindowControlsOverlay#buttonAt(double, double)}. + */ + @Test + void pickButtonAtCoordinates() { + var overlay = new WindowControlsOverlay(getStylesheet(""" + .window-button-container { -fx-button-placement: right; } + .window-button { -fx-pref-width: 20; -fx-pref-height: 10; } + """)); + + var unused = new Scene(overlay); + overlay.resize(200, 100); + overlay.applyCss(); + overlay.layout(); + + assertNull(overlay.buttonAt(139, 5)); + assertEquals(WindowControlsOverlay.ButtonType.MINIMIZE, overlay.buttonAt(140, 0)); + assertEquals(WindowControlsOverlay.ButtonType.MAXIMIZE, overlay.buttonAt(165, 5)); + assertEquals(WindowControlsOverlay.ButtonType.CLOSE, overlay.buttonAt(181, 10)); + } + + private static ObservableValue getStylesheet(String text) { + String stylesheet = "data:text/css;base64," + + Base64.getEncoder().encodeToString(text.getBytes(StandardCharsets.UTF_8)); + + return ObjectConstant.valueOf(stylesheet); + } + + private static void assertLayoutBounds(Node node, double x, double y, double width, double height) { + assertEquals(x, node.getLayoutX()); + assertEquals(y, node.getLayoutY()); + assertSize(node, width, height); + } + + private static void assertSize(Node node, double width, double height) { + assertEquals(width, node.getLayoutBounds().getWidth()); + assertEquals(height, node.getLayoutBounds().getHeight()); + } +} diff --git a/modules/javafx.graphics/src/test/java/test/com/sun/javafx/util/UtilsTest.java b/modules/javafx.graphics/src/test/java/test/com/sun/javafx/util/UtilsTest.java index ca19162733c..d4f0d740332 100644 --- a/modules/javafx.graphics/src/test/java/test/com/sun/javafx/util/UtilsTest.java +++ b/modules/javafx.graphics/src/test/java/test/com/sun/javafx/util/UtilsTest.java @@ -35,7 +35,14 @@ import javafx.scene.Group; import javafx.scene.Scene; import javafx.scene.SubScene; +import javafx.scene.image.Image; import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; +import javafx.scene.paint.CycleMethod; +import javafx.scene.paint.ImagePattern; +import javafx.scene.paint.LinearGradient; +import javafx.scene.paint.RadialGradient; +import javafx.scene.paint.Stop; import javafx.scene.shape.Rectangle; import javafx.stage.Stage; @@ -181,4 +188,45 @@ public void testPointRelativeTo_InSubScene() { assertEquals(70, res.getY(), 1e-1); } + + @Test + void testAveragePerceptualBrightness_LinearGradient() { + var gradient = new LinearGradient( + 0, 0, 1, 1, true, CycleMethod.NO_CYCLE, + new Stop(0, Color.RED), new Stop(0.5, Color.GREEN), new Stop(1, Color.BLUE)); + + double actual = Utils.calculateAverageBrightness(gradient); + double expect = (Utils.calculateBrightness(Color.RED) + + Utils.calculateBrightness(Color.GREEN) + + Utils.calculateBrightness(Color.BLUE)) / 3; + + assertEquals(expect, actual); + } + + @Test + void testAveragePerceptualBrightness_RadialGradient() { + var gradient = new RadialGradient( + 0, 0, 0, 0, 1, true, CycleMethod.NO_CYCLE, + new Stop(0, Color.RED), new Stop(0.5, Color.GREEN), new Stop(1, Color.BLUE)); + + double actual = Utils.calculateAverageBrightness(gradient); + double expect = (Utils.calculateBrightness(Color.RED) + + Utils.calculateBrightness(Color.GREEN) + + Utils.calculateBrightness(Color.BLUE)) / 3; + + assertEquals(expect, actual); + } + + @Test + void testAveragePerceptualBrightness_ImagePattern() { + var pattern = new ImagePattern(new Image("test")); + assertEquals(1, Utils.calculateAverageBrightness(pattern)); + } + + @Test + void testAveragePerceptualBrightness_Color() { + var actual = Utils.calculateAverageBrightness(Color.RED); + var expect = Utils.calculateBrightness(Color.RED); + assertEquals(expect, actual); + } } diff --git a/modules/javafx.graphics/src/test/java/test/javafx/scene/layout/HeaderBarTest.java b/modules/javafx.graphics/src/test/java/test/javafx/scene/layout/HeaderBarTest.java new file mode 100644 index 00000000000..839f72c428c --- /dev/null +++ b/modules/javafx.graphics/src/test/java/test/javafx/scene/layout/HeaderBarTest.java @@ -0,0 +1,438 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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 test.javafx.scene.layout; + +import javafx.beans.property.ObjectProperty; +import javafx.geometry.Dimension2D; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.layout.HeaderBar; +import javafx.scene.shape.Rectangle; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import test.util.ReflectionUtils; + +import static org.junit.jupiter.api.Assertions.*; + +public class HeaderBarTest { + + HeaderBar headerBar; + + @BeforeEach + void setup() { + headerBar = new HeaderBar(); + } + + @Test + void emptyHeaderBar() { + assertNull(headerBar.getLeading()); + assertNull(headerBar.getCenter()); + assertNull(headerBar.getTrailing()); + } + + @ParameterizedTest + @ValueSource(strings = { + "TOP_LEFT, 10, 10, 100, 80", + "TOP_CENTER, 10, 10, 100, 80", + "TOP_RIGHT, 10, 10, 100, 80", + "CENTER_LEFT, 10, 10, 100, 80", + "CENTER, 10, 10, 100, 80", + "CENTER_RIGHT, 10, 10, 100, 80", + "BOTTOM_LEFT, 10, 10, 100, 80", + "BOTTOM_CENTER, 10, 10, 100, 80", + "BOTTOM_RIGHT, 10, 10, 100, 80" + }) + void alignmentOfLeadingChildOnly_resizable(String arg) { + String[] args = arg.split(","); + var content = new MockResizable(100, 50); + HeaderBar.setAlignment(content, Pos.valueOf(args[0])); + HeaderBar.setMargin(content, new Insets(10)); + headerBar.setLeading(content); + headerBar.resize(1000, 100); + headerBar.layout(); + + assertBounds( + Double.parseDouble(args[1]), + Double.parseDouble(args[2]), + Double.parseDouble(args[3]), + Double.parseDouble(args[4]), + content); + } + + @ParameterizedTest + @ValueSource(strings = { + "TOP_LEFT, 10, 10, 100, 50", + "TOP_CENTER, 10, 10, 100, 50", + "TOP_RIGHT, 10, 10, 100, 50", + "CENTER_LEFT, 10, 25, 100, 50", + "CENTER, 10, 25, 100, 50", + "CENTER_RIGHT, 10, 25, 100, 50", + "BOTTOM_LEFT, 10, 40, 100, 50", + "BOTTOM_CENTER, 10, 40, 100, 50", + "BOTTOM_RIGHT, 10, 40, 100, 50" + }) + void alignmentOfLeadingChildOnly_notResizable(String arg) { + String[] args = arg.split(","); + var content = new Rectangle(100, 50); + HeaderBar.setAlignment(content, Pos.valueOf(args[0])); + HeaderBar.setMargin(content, new Insets(10)); + headerBar.setLeading(content); + headerBar.resize(1000, 100); + headerBar.layout(); + + assertBounds( + Double.parseDouble(args[1]), + Double.parseDouble(args[2]), + Double.parseDouble(args[3]), + Double.parseDouble(args[4]), + content); + } + + @ParameterizedTest + @ValueSource(strings = { + "TOP_LEFT, 890, 10, 100, 80", + "TOP_CENTER, 890, 10, 100, 80", + "TOP_RIGHT, 890, 10, 100, 80", + "CENTER_LEFT, 890, 10, 100, 80", + "CENTER, 890, 10, 100, 80", + "CENTER_RIGHT, 890, 10, 100, 80", + "BOTTOM_LEFT, 890, 10, 100, 80", + "BOTTOM_CENTER, 890, 10, 100, 80", + "BOTTOM_RIGHT, 890, 10, 100, 80" + }) + void alignmentOfTrailingChildOnly_resizable(String arg) { + String[] args = arg.split(","); + var content = new MockResizable(100, 50); + HeaderBar.setAlignment(content, Pos.valueOf(args[0])); + HeaderBar.setMargin(content, new Insets(10)); + headerBar.setTrailing(content); + headerBar.resize(1000, 100); + headerBar.layout(); + + assertBounds( + Double.parseDouble(args[1]), + Double.parseDouble(args[2]), + Double.parseDouble(args[3]), + Double.parseDouble(args[4]), + content); + } + + @ParameterizedTest + @ValueSource(strings = { + "TOP_LEFT, 890, 10, 100, 50", + "TOP_CENTER, 890, 10, 100, 50", + "TOP_RIGHT, 890, 10, 100, 50", + "CENTER_LEFT, 890, 25, 100, 50", + "CENTER, 890, 25, 100, 50", + "CENTER_RIGHT, 890, 25, 100, 50", + "BOTTOM_LEFT, 890, 40, 100, 50", + "BOTTOM_CENTER, 890, 40, 100, 50", + "BOTTOM_RIGHT, 890, 40, 100, 50" + }) + void alignmentOfTrailingChildOnly_notResizable(String arg) { + String[] args = arg.split(","); + var content = new Rectangle(100, 50); + HeaderBar.setAlignment(content, Pos.valueOf(args[0])); + HeaderBar.setMargin(content, new Insets(10)); + headerBar.setTrailing(content); + headerBar.resize(1000, 100); + headerBar.layout(); + + assertBounds( + Double.parseDouble(args[1]), + Double.parseDouble(args[2]), + Double.parseDouble(args[3]), + Double.parseDouble(args[4]), + content); + } + + @ParameterizedTest + @ValueSource(strings = { + "TOP_LEFT, 10, 10, 100, 80", + "TOP_CENTER, 450, 10, 100, 80", + "TOP_RIGHT, 890, 10, 100, 80", + "CENTER_LEFT, 10, 10, 100, 80", + "CENTER, 450, 10, 100, 80", + "CENTER_RIGHT, 890, 10, 100, 80", + "BOTTOM_LEFT, 10, 10, 100, 80", + "BOTTOM_CENTER, 450, 10, 100, 80", + "BOTTOM_RIGHT, 890, 10, 100, 80" + }) + void alignmentOfCenterChildOnly_resizable(String arg) { + String[] args = arg.split(","); + var content = new MockResizable(100, 50); + HeaderBar.setAlignment(content, Pos.valueOf(args[0])); + HeaderBar.setMargin(content, new Insets(10)); + headerBar.setCenter(content); + headerBar.resize(1000, 100); + headerBar.layout(); + + assertBounds( + Double.parseDouble(args[1]), + Double.parseDouble(args[2]), + Double.parseDouble(args[3]), + Double.parseDouble(args[4]), + content); + } + + @ParameterizedTest + @ValueSource(strings = { + "TOP_LEFT, 10, 10, 100, 50", + "TOP_CENTER, 450, 10, 100, 50", + "TOP_RIGHT, 890, 10, 100, 50", + "CENTER_LEFT, 10, 25, 100, 50", + "CENTER, 450, 25, 100, 50", + "CENTER_RIGHT, 890, 25, 100, 50", + "BOTTOM_LEFT, 10, 40, 100, 50", + "BOTTOM_CENTER, 450, 40, 100, 50", + "BOTTOM_RIGHT, 890, 40, 100, 50" + }) + void alignmentOfCenterChildOnly_notResizable(String arg) { + String[] args = arg.split(","); + var content = new Rectangle(100, 50); + HeaderBar.setAlignment(content, Pos.valueOf(args[0])); + HeaderBar.setMargin(content, new Insets(10)); + headerBar.setCenter(content); + headerBar.resize(1000, 100); + headerBar.layout(); + + assertBounds( + Double.parseDouble(args[1]), + Double.parseDouble(args[2]), + Double.parseDouble(args[3]), + Double.parseDouble(args[4]), + content); + } + + @ParameterizedTest + @ValueSource(strings = { + "TOP_LEFT, 60, 10, 100, 80", + "TOP_CENTER, 450, 10, 100, 80", + "TOP_RIGHT, 740, 10, 100, 80", + "CENTER_LEFT, 60, 10, 100, 80", + "CENTER, 450, 10, 100, 80", + "CENTER_RIGHT, 740, 10, 100, 80", + "BOTTOM_LEFT, 60, 10, 100, 80", + "BOTTOM_CENTER, 450, 10, 100, 80", + "BOTTOM_RIGHT, 740, 10, 100, 80" + }) + void alignmentOfCenterChild_resizable_withNonEmptyLeadingAndTrailingChild(String arg) { + String[] args = arg.split(","); + var leading = new MockResizable(50, 50); + var center = new MockResizable(100, 50); + var trailing = new MockResizable(150, 50); + HeaderBar.setAlignment(center, Pos.valueOf(args[0])); + HeaderBar.setMargin(center, new Insets(10)); + headerBar.setLeading(leading); + headerBar.setCenter(center); + headerBar.setTrailing(trailing); + headerBar.resize(1000, 100); + headerBar.layout(); + + assertBounds( + Double.parseDouble(args[1]), + Double.parseDouble(args[2]), + Double.parseDouble(args[3]), + Double.parseDouble(args[4]), + center); + } + + @ParameterizedTest + @ValueSource(strings = { + "TOP_LEFT, 60, 10, 100, 50", + "TOP_CENTER, 450, 10, 100, 50", + "TOP_RIGHT, 740, 10, 100, 50", + "CENTER_LEFT, 60, 25, 100, 50", + "CENTER, 450, 25, 100, 50", + "CENTER_RIGHT, 740, 25, 100, 50", + "BOTTOM_LEFT, 60, 40, 100, 50", + "BOTTOM_CENTER, 450, 40, 100, 50", + "BOTTOM_RIGHT, 740, 40, 100, 50" + }) + void alignmentOfCenterChild_notResizable_withNonEmptyLeadingAndTrailingChild(String arg) { + String[] args = arg.split(","); + var leading = new Rectangle(50, 50); + var center = new Rectangle(100, 50); + var trailing = new Rectangle(150, 50); + HeaderBar.setAlignment(center, Pos.valueOf(args[0])); + HeaderBar.setMargin(center, new Insets(10)); + headerBar.setLeading(leading); + headerBar.setCenter(center); + headerBar.setTrailing(trailing); + headerBar.resize(1000, 100); + headerBar.layout(); + + assertBounds( + Double.parseDouble(args[1]), + Double.parseDouble(args[2]), + Double.parseDouble(args[3]), + Double.parseDouble(args[4]), + center); + } + + @ParameterizedTest + @SuppressWarnings("unchecked") + @ValueSource(strings = { + "TOP_LEFT, 160, 10, 100, 80", + "TOP_CENTER, 450, 10, 100, 80", + "TOP_RIGHT, 740, 10, 100, 80", + "CENTER_LEFT, 160, 10, 100, 80", + "CENTER, 450, 10, 100, 80", + "CENTER_RIGHT, 740, 10, 100, 80", + "BOTTOM_LEFT, 160, 10, 100, 80", + "BOTTOM_CENTER, 450, 10, 100, 80", + "BOTTOM_RIGHT, 740, 10, 100, 80" + }) + void alignmentOfCenterChild_withLeftSystemInset(String arg) { + String[] args = arg.split(","); + var leftSystemInset = (ObjectProperty)ReflectionUtils.getFieldValue(headerBar, "leftSystemInset"); + leftSystemInset.set(new Dimension2D(100, 100)); + var leading = new MockResizable(50, 50); + var center = new MockResizable(100, 50); + var trailing = new MockResizable(150, 50); + HeaderBar.setAlignment(center, Pos.valueOf(args[0])); + HeaderBar.setMargin(center, new Insets(10)); + headerBar.setLeading(leading); + headerBar.setCenter(center); + headerBar.setTrailing(trailing); + headerBar.resize(1000, 100); + headerBar.layout(); + + assertBounds( + Double.parseDouble(args[1]), + Double.parseDouble(args[2]), + Double.parseDouble(args[3]), + Double.parseDouble(args[4]), + center); + } + + @ParameterizedTest + @SuppressWarnings("unchecked") + @ValueSource(strings = { + "TOP_LEFT, 60, 10, 100, 80", + "TOP_CENTER, 450, 10, 100, 80", + "TOP_RIGHT, 640, 10, 100, 80", + "CENTER_LEFT, 60, 10, 100, 80", + "CENTER, 450, 10, 100, 80", + "CENTER_RIGHT, 640, 10, 100, 80", + "BOTTOM_LEFT, 60, 10, 100, 80", + "BOTTOM_CENTER, 450, 10, 100, 80", + "BOTTOM_RIGHT, 640, 10, 100, 80" + }) + void alignmentOfCenterChild_withRightSystemInset(String arg) { + String[] args = arg.split(","); + var rightSystemInset = (ObjectProperty)ReflectionUtils.getFieldValue(headerBar, "rightSystemInset"); + rightSystemInset.set(new Dimension2D(100, 100)); + var leading = new MockResizable(50, 50); + var center = new MockResizable(100, 50); + var trailing = new MockResizable(150, 50); + HeaderBar.setAlignment(center, Pos.valueOf(args[0])); + HeaderBar.setMargin(center, new Insets(10)); + headerBar.setLeading(leading); + headerBar.setCenter(center); + headerBar.setTrailing(trailing); + headerBar.resize(1000, 100); + headerBar.layout(); + + assertBounds( + Double.parseDouble(args[1]), + Double.parseDouble(args[2]), + Double.parseDouble(args[3]), + Double.parseDouble(args[4]), + center); + } + + @ParameterizedTest + @SuppressWarnings("unchecked") + @ValueSource(strings = { + "TOP_CENTER, 260, 10, 80, 80", + "CENTER, 260, 10, 80, 80", + "BOTTOM_CENTER, 260, 10, 80, 80" + }) + void alignmentOfCenterChild_withLeftSystemInset_andOffsetCausedByInsufficientHorizontalSpace(String arg) { + String[] args = arg.split(","); + var leftSystemInset = (ObjectProperty)ReflectionUtils.getFieldValue(headerBar, "leftSystemInset"); + leftSystemInset.set(new Dimension2D(200, 100)); + var leading = new MockResizable(50, 50); + var center = new MockResizable(100, 50); + var trailing = new MockResizable(150, 50); + HeaderBar.setAlignment(center, Pos.valueOf(args[0])); + HeaderBar.setMargin(center, new Insets(10)); + headerBar.setLeading(leading); + headerBar.setCenter(center); + headerBar.setTrailing(trailing); + headerBar.resize(500, 100); + headerBar.layout(); + + assertBounds( + Double.parseDouble(args[1]), + Double.parseDouble(args[2]), + Double.parseDouble(args[3]), + Double.parseDouble(args[4]), + center); + } + + @ParameterizedTest + @SuppressWarnings("unchecked") + @ValueSource(strings = { + "TOP_CENTER, 60, 10, 80, 80", + "CENTER, 60, 10, 80, 80", + "BOTTOM_CENTER, 60, 10, 80, 80" + }) + void alignmentOfCenterChild_withRightSystemInset_andOffsetCausedByInsufficientHorizontalSpace(String arg) { + String[] args = arg.split(","); + var rightSystemInset = (ObjectProperty)ReflectionUtils.getFieldValue(headerBar, "rightSystemInset"); + rightSystemInset.set(new Dimension2D(200, 100)); + var leading = new MockResizable(50, 50); + var center = new MockResizable(100, 50); + var trailing = new MockResizable(150, 50); + HeaderBar.setAlignment(center, Pos.valueOf(args[0])); + HeaderBar.setMargin(center, new Insets(10)); + headerBar.setLeading(leading); + headerBar.setCenter(center); + headerBar.setTrailing(trailing); + headerBar.resize(500, 100); + headerBar.layout(); + + assertBounds( + Double.parseDouble(args[1]), + Double.parseDouble(args[2]), + Double.parseDouble(args[3]), + Double.parseDouble(args[4]), + center); + } + + private void assertBounds(double x, double y, double width, double height, Node node) { + var bounds = node.getLayoutBounds(); + assertEquals(x, node.getLayoutX()); + assertEquals(y, node.getLayoutY()); + assertEquals(width, bounds.getWidth()); + assertEquals(height, bounds.getHeight()); + } +} From 0ddd63d47a0a0f632c377f9656bac0c74b684c7a Mon Sep 17 00:00:00 2001 From: mstr2 <43553916+mstr2@users.noreply.github.com> Date: Sun, 20 Oct 2024 02:53:11 +0200 Subject: [PATCH 02/35] doc change --- .../src/main/java/javafx/scene/layout/HeaderBar.java | 2 +- .../src/main/java/javafx/scene/layout/HeaderBarBase.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBar.java b/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBar.java index bca236a68db..d566101f914 100644 --- a/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBar.java +++ b/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBar.java @@ -39,7 +39,7 @@ /** * A client-area header bar that is used as a replacement for the system-provided header bar in stages - * with the {@link StageStyle#EXTENDED} style. This class enables the drag to move and + * with the {@link StageStyle#EXTENDED} style. This class enables the click-and-drag and * double-click to maximize behaviors that are usually afforded by system-provided header bars. * The entire {@code HeaderBar} background is draggable by default, but its content is not. Applications * can specify draggable content nodes of the {@code HeaderBar} with the {@link #setDraggable} method. diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBarBase.java b/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBarBase.java index f5932eb65d1..6b99d463615 100644 --- a/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBarBase.java +++ b/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBarBase.java @@ -42,7 +42,7 @@ /** * Base class for a client-area header bar that is used as a replacement for the system-provided header bar * in stages with the {@link StageStyle#EXTENDED} style. This class is intended for application developers - * to use as a starting point for custom header bar implementations, and it enables the drag to move + * to use as a starting point for custom header bar implementations, and it enables the click-and-drag * and double-click to maximize behaviors that are usually afforded by system-provided header bars. * The entire {@code HeaderBarBase} background is draggable by default, but its content is not. Applications * can specify draggable content nodes of the {@code HeaderBarBase} with the {@link #setDraggable} method. From 79696225c7dc4db985f2cc84e949a93bd30a79e1 Mon Sep 17 00:00:00 2001 From: mstr2 <43553916+mstr2@users.noreply.github.com> Date: Sun, 20 Oct 2024 03:19:58 +0200 Subject: [PATCH 03/35] revert unintended change --- modules/javafx.graphics/src/main/native-glass/ios/GlassWindow.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/javafx.graphics/src/main/native-glass/ios/GlassWindow.m b/modules/javafx.graphics/src/main/native-glass/ios/GlassWindow.m index b7425c75d36..5b37d31e210 100644 --- a/modules/javafx.graphics/src/main/native-glass/ios/GlassWindow.m +++ b/modules/javafx.graphics/src/main/native-glass/ios/GlassWindow.m @@ -816,7 +816,7 @@ jlong _1createWindow(JNIEnv *env, jobject jWindow, jlong jOwnerPtr, jlong jScree BOOL hidden = YES; if (jOwnerPtr == 0L) { // no owner means it is the primary stage; Decorated primary stage shows status bar by default - hidden = ((jStyleMask & com_sun_glass_ui_Window_DECORATED) == 0); + hidden = ((jStyleMask & com_sun_glass_ui_Window_TITLED) == 0); NSObject * values = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIStatusBarHidden"]; //we prefer explicit settings from .plist From ba02e8f71e4d177e4cee029ba4dfff21199325a6 Mon Sep 17 00:00:00 2001 From: mstr2 <43553916+mstr2@users.noreply.github.com> Date: Thu, 24 Oct 2024 22:00:38 +0200 Subject: [PATCH 04/35] Improve HeaderBar documentation --- .../java/javafx/scene/layout/HeaderBar.java | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBar.java b/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBar.java index d566101f914..e5bac22da28 100644 --- a/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBar.java +++ b/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBar.java @@ -76,13 +76,35 @@ * * *

Example

+ * Usually, {@code HeaderBar} is placed in a root container like {@code BorderPane} to align it + * with the top of the scene: *
{@code
- *     var button = new Button("My button");
- *     HeaderBar.setAlignment(button, Pos.CENTER_LEFT);
- *     HeaderBar.setMargin(button, new Insets(5));
- *     myHeaderBar.setCenter(button);
+ * public class MyApp extends Application {
+ *     @Override
+ *     public void start(Stage stage) {
+ *         var button = new Button("My button");
+ *         HeaderBar.setAlignment(button, Pos.CENTER_LEFT);
+ *         HeaderBar.setMargin(button, new Insets(5));
+ *
+ *         var headerBar = new HeaderBar();
+ *         headerBar.setCenter(button);
+ *
+ *         var root = new BorderPane();
+ *         root.setTop(headerBar);
+ *
+ *         stage.setScene(new Scene(root));
+ *         stage.initStyle(StageStyle.EXTENDED);
+ *         stage.show();
+ *     }
+ * }
  * }
* + * @apiNote An application should only add a single {@code HeaderBar} to the scene graph, and it should + * be located at the top of the scene. While it is technically possible to add multiple header + * bars to the scene graph, or place a header bar in another area of the scene, the resulting + * user experience is not what users typically expect from JavaFX applications and should + * therefore be avoided. + * * @see HeaderBarBase * @since 24 */ From f973e8c55a8d81d2e9ba5c9d2f61d3f11f072005 Mon Sep 17 00:00:00 2001 From: mstr2 <43553916+mstr2@users.noreply.github.com> Date: Fri, 25 Oct 2024 18:36:31 +0200 Subject: [PATCH 05/35] improve documentation --- .../java/javafx/scene/layout/HeaderBarBase.java | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBarBase.java b/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBarBase.java index 6b99d463615..af15f431042 100644 --- a/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBarBase.java +++ b/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBarBase.java @@ -46,10 +46,9 @@ * and double-click to maximize behaviors that are usually afforded by system-provided header bars. * The entire {@code HeaderBarBase} background is draggable by default, but its content is not. Applications * can specify draggable content nodes of the {@code HeaderBarBase} with the {@link #setDraggable} method. - *

- * Most application developers should use the {@link HeaderBar} implementation instead of - * creating a custom header bar. * + * @apiNote Most application developers should use the {@link HeaderBar} implementation instead of + * creating a custom header bar. * @see HeaderBar * @since 24 */ @@ -150,7 +149,10 @@ private void updateInsets() { /** * Describes the size of the left system inset, which is an area reserved for the * minimize, maximize, and close window buttons. If there are no window buttons on - * the left side of the window, the returned area is empty. + * the left side of the window, the returned area is an empty {@code Dimension2D}. + *

+ * Note that the left system inset refers to the physical left side of the window, + * independent of layout orientation. */ private final ReadOnlyObjectWrapper leftSystemInset = new ReadOnlyObjectWrapper<>(this, "leftInset", new Dimension2D(0, 0)) { @@ -171,7 +173,10 @@ public final Dimension2D getLeftSystemInset() { /** * Describes the size of the right system inset, which is an area reserved for the * minimize, maximize, and close window buttons. If there are no window buttons on - * the right side of the window, the returned area is empty. + * the right side of the window, the returned area is an empty {@code Dimension2D}. + *

+ * Note that the right system inset refers to the physical right side of the window, + * independent of layout orientation. */ private final ReadOnlyObjectWrapper rightSystemInset = new ReadOnlyObjectWrapper<>(this, "rightInset", new Dimension2D(0, 0)) { From f02e7e9034caa66ba373495f148ba3d78ef982e3 Mon Sep 17 00:00:00 2001 From: mstr2 <43553916+mstr2@users.noreply.github.com> Date: Sat, 26 Oct 2024 00:44:48 +0200 Subject: [PATCH 06/35] Windows: add system menu --- .../java/com/sun/glass/ui/win/WinWindow.java | 14 +++- .../src/main/native-glass/win/GlassWindow.cpp | 65 +++++++++++++++++++ .../src/main/native-glass/win/GlassWindow.h | 1 + 3 files changed, 79 insertions(+), 1 deletion(-) diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/win/WinWindow.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/win/WinWindow.java index 52c35f88dc6..d4495c09861 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/win/WinWindow.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/win/WinWindow.java @@ -24,6 +24,7 @@ */ package com.sun.glass.ui.win; +import com.sun.glass.events.MouseEvent; import com.sun.glass.ui.Cursor; import com.sun.glass.ui.WindowControlsOverlay; import com.sun.glass.ui.NonClientHandler; @@ -268,6 +269,7 @@ protected boolean _setBackground(long ptr, float r, float g, float b) { native private long _getInsets(long ptr); native private long _getAnchor(long ptr); + native private void _showSystemMenu(long ptr, int x, int y); @Override native protected long _createWindow(long ownerPtr, long screenPtr, int mask); @Override native protected boolean _close(long ptr); @Override native protected boolean _setView(long ptr, View view); @@ -369,7 +371,17 @@ public NonClientHandler getNonClientHandler() { return (type, button, x, y, xAbs, yAbs, clickCount) -> { double wx = x / platformScaleX; double wy = y / platformScaleY; - return overlay.handleMouseEvent(type, button, wx, wy); + boolean handled = overlay.handleMouseEvent(type, button, wx, wy); + + // A right click on a non-client header bar area opens the system menu. + if (!handled && type == MouseEvent.NC_UP && button == MouseEvent.BUTTON_RIGHT) { + View.EventHandler eventHandler = view != null ? view.getEventHandler() : null; + if (eventHandler != null && eventHandler.handleDragAreaHitTestEvent(wx, wy)) { + _showSystemMenu(getRawHandle(), x, y); + } + } + + return handled; }; } diff --git a/modules/javafx.graphics/src/main/native-glass/win/GlassWindow.cpp b/modules/javafx.graphics/src/main/native-glass/win/GlassWindow.cpp index 02a962c92e4..1eeaa6318f3 100644 --- a/modules/javafx.graphics/src/main/native-glass/win/GlassWindow.cpp +++ b/modules/javafx.graphics/src/main/native-glass/win/GlassWindow.cpp @@ -1218,6 +1218,48 @@ void GlassWindow::SetIcon(HICON hIcon) m_hIcon = hIcon; } +void GlassWindow::ShowSystemMenu(int x, int y) +{ + WINDOWPLACEMENT placement; + if (!::GetWindowPlacement(GetHWND(), &placement)) { + return; + } + + HMENU systemMenu = GetSystemMenu(GetHWND(), FALSE); + bool maximized = placement.showCmd == SW_SHOWMAXIMIZED; + + MENUITEMINFO menuItemInfo { sizeof(MENUITEMINFO) }; + menuItemInfo.fMask = MIIM_STATE; + menuItemInfo.fType = MFT_STRING; + + menuItemInfo.fState = maximized ? MF_ENABLED : MF_DISABLED; + SetMenuItemInfo(systemMenu, SC_RESTORE, FALSE, &menuItemInfo); + + menuItemInfo.fState = maximized ? MF_DISABLED : MF_ENABLED; + SetMenuItemInfo(systemMenu, SC_MOVE, FALSE, &menuItemInfo); + + menuItemInfo.fState = maximized ? MF_DISABLED : MF_ENABLED; + SetMenuItemInfo(systemMenu, SC_SIZE, FALSE, &menuItemInfo); + + menuItemInfo.fState = MF_ENABLED; + SetMenuItemInfo(systemMenu, SC_MINIMIZE, FALSE, &menuItemInfo); + + menuItemInfo.fState = maximized ? MF_DISABLED : MF_ENABLED; + SetMenuItemInfo(systemMenu, SC_MAXIMIZE, FALSE, &menuItemInfo); + + menuItemInfo.fState = MF_ENABLED; + SetMenuItemInfo(systemMenu, SC_CLOSE, FALSE, &menuItemInfo); + SetMenuDefaultItem(systemMenu, UINT_MAX, FALSE); + + POINT ptAbs = { x, y }; + ::ClientToScreen(GetHWND(), &ptAbs); + + BOOL menuItem = TrackPopupMenu(systemMenu, TPM_RETURNCMD, ptAbs.x, ptAbs.y, 0, GetHWND(), NULL); + if (menuItem != 0) { + PostMessage(GetHWND(), WM_SYSCOMMAND, menuItem, 0); + } +} + /* * JNI methods section * @@ -2048,4 +2090,27 @@ JNIEXPORT void JNICALL Java_com_sun_glass_ui_win_WinWindow__1setCursor PERFORM(); } +/* + * Class: com_sun_glass_ui_win_WinWindow + * Method: _showSystemMenu + * Signature: (JII)V + */ +JNIEXPORT void JNICALL Java_com_sun_glass_ui_win_WinWindow__1showSystemMenu + (JNIEnv *env, jobject jThis, jlong ptr, jint x, jint y) +{ + ENTER_MAIN_THREAD() + { + GlassWindow *pWindow = GlassWindow::FromHandle(hWnd); + if (pWindow) { + pWindow->ShowSystemMenu(x, y); + } + } + jint x, y; + LEAVE_MAIN_THREAD_WITH_hWnd; + + ARG(x) = x; + ARG(y) = y; + PERFORM(); +} + } // extern "C" diff --git a/modules/javafx.graphics/src/main/native-glass/win/GlassWindow.h b/modules/javafx.graphics/src/main/native-glass/win/GlassWindow.h index 72c986ccc9a..8d766e30ab6 100644 --- a/modules/javafx.graphics/src/main/native-glass/win/GlassWindow.h +++ b/modules/javafx.graphics/src/main/native-glass/win/GlassWindow.h @@ -103,6 +103,7 @@ class GlassWindow : public BaseWnd, public ViewContainer { void SetIcon(HICON hIcon); void HandleWindowPosChangedEvent(); + void ShowSystemMenu(int x, int y); protected: virtual LRESULT WindowProc(UINT msg, WPARAM wParam, LPARAM lParam); From fef8cfc0b03a41902bd90a7ad01bf589c758600e Mon Sep 17 00:00:00 2001 From: mstr2 <43553916+mstr2@users.noreply.github.com> Date: Sat, 26 Oct 2024 05:42:42 +0200 Subject: [PATCH 07/35] Windows: custom context menu overrides system menu --- .../src/main/java/com/sun/glass/ui/View.java | 31 ++++++---- .../java/com/sun/glass/ui/gtk/GtkWindow.java | 2 +- .../java/com/sun/glass/ui/mac/MacWindow.java | 2 +- .../java/com/sun/glass/ui/win/WinView.java | 24 +++++++- .../java/com/sun/glass/ui/win/WinWindow.java | 25 ++++---- .../com/sun/javafx/tk/TKSceneListener.java | 10 +-- .../tk/quantum/GlassViewEventHandler.java | 61 ++++++++++--------- .../src/main/java/javafx/scene/Scene.java | 43 +++++++------ .../src/main/native-glass/win/GlassWindow.cpp | 18 +++++- .../src/main/native-glass/win/GlassWindow.h | 1 + 10 files changed, 136 insertions(+), 81 deletions(-) diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/View.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/View.java index 97846bc8fa6..e2fbb338b2a 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/View.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/View.java @@ -26,7 +26,7 @@ import com.sun.glass.events.MouseEvent; import com.sun.glass.events.ViewEvent; - +import javafx.scene.Node; import java.lang.annotation.Native; import java.lang.ref.WeakReference; import java.security.AccessController; @@ -69,8 +69,9 @@ public boolean handleKeyEvent(View view, long time, int action, int keyCode, char[] keyChars, int modifiers) { return false; } - public void handleMenuEvent(View view, int x, int y, int xAbs, + public boolean handleMenuEvent(View view, int x, int y, int xAbs, int yAbs, boolean isKeyboardTrigger) { + return false; } public void handleMouseEvent(View view, long time, int type, int button, int x, int y, int xAbs, int yAbs, @@ -365,8 +366,16 @@ public void handleSwipeGestureEvent(View view, long time, int type, int yAbs) { } - public boolean handleDragAreaHitTestEvent(double x, double y) { - return false; + /** + * Returns the draggable area node at the specified coordinates, or {@code null} + * if the specified coordinates do not intersect with a draggable area. + * + * @param x the X coordinate + * @param y the Y coordinate + * @return the draggable area node, or {@code null} + */ + public Node pickDragAreaNode(double x, double y) { + return null; } public Accessible getSceneAccessible() { @@ -576,10 +585,11 @@ private void handleMouseEvent(long time, int type, int button, int x, int y, } } - private void handleMenuEvent(int x, int y, int xAbs, int yAbs, boolean isKeyboardTrigger) { + protected boolean handleMenuEvent(int x, int y, int xAbs, int yAbs, boolean isKeyboardTrigger) { if (shouldHandleEvent()) { - this.eventHandler.handleMenuEvent(this, x, y, xAbs, yAbs, isKeyboardTrigger); + return this.eventHandler.handleMenuEvent(this, x, y, xAbs, yAbs, isKeyboardTrigger); } + return false; } public void handleBeginTouchEvent(View view, long time, int modifiers, @@ -950,14 +960,11 @@ protected void notifyMouse(int type, int button, int x, int y, int xAbs, // If we have a non-client handler, we give it the first chance to handle the event. // Note that a full-screen window has no non-client area, and thus the non-client handler // is not notified. - if (!inFullscreen - && nonClientHandler != null - && nonClientHandler.handleMouseEvent(type, button, x, y, xAbs, yAbs, clickCount)) { - return; - } + boolean handled = !inFullscreen && nonClientHandler != null + && nonClientHandler.handleMouseEvent(type, button, x, y, xAbs, yAbs, clickCount); // We never send non-client events to the application. - if (MouseEvent.isNonClientEvent(type)) { + if (handled || MouseEvent.isNonClientEvent(type)) { return; } diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkWindow.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkWindow.java index d02c4ffe390..cd3ef7701c4 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkWindow.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkWindow.java @@ -264,6 +264,6 @@ private boolean dragAreaHitTest(int x, int y) { return false; } - return eventHandler.handleDragAreaHitTestEvent(wx, wy); + return eventHandler.pickDragAreaNode(wx, wy) != null; } } diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/mac/MacWindow.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/mac/MacWindow.java index 501991e4d1a..9f05f9744e6 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/mac/MacWindow.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/mac/MacWindow.java @@ -168,7 +168,7 @@ public NonClientHandler getNonClientHandler() { double wy = y / platformScaleY; View.EventHandler eventHandler = view != null ? view.getEventHandler() : null; - if (eventHandler != null && eventHandler.handleDragAreaHitTestEvent(wx, wy)) { + if (eventHandler != null && eventHandler.pickDragAreaNode(wx, wy) != null) { if (clickCount == 2) { maximize(!isMaximized()); } else if (clickCount == 1) { diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/win/WinView.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/win/WinView.java index ddf233c67be..c8b7af84cb8 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/win/WinView.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/win/WinView.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -95,5 +95,27 @@ protected void notifyResize(int width, int height) { // to be recalculated. updateLocation(); } + + @Override + protected void notifyMenu(int x, int y, int xAbs, int yAbs, boolean isKeyboardTrigger) { + // If all of the following conditions are satisfied, we open a system menu at the specified coordinates: + // 1. The application didn't consume the menu event. + // 2. The window is an EXTENDED window. + // 3. The menu event occurred on a draggable area. + if (!handleMenuEvent(x, y, xAbs, yAbs, isKeyboardTrigger)) { + var window = (WinWindow)getWindow(); + if (!window.isExtendedWindow()) { + return; + } + + double wx = x / window.getPlatformScaleX(); + double wy = y / window.getPlatformScaleY(); + + EventHandler eventHandler = getEventHandler(); + if (eventHandler != null && eventHandler.pickDragAreaNode(wx, wy) != null) { + window.showSystemMenu(x, y); + } + } + } } diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/win/WinWindow.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/win/WinWindow.java index d4495c09861..6895c5a0500 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/win/WinWindow.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/win/WinWindow.java @@ -24,7 +24,6 @@ */ package com.sun.glass.ui.win; -import com.sun.glass.events.MouseEvent; import com.sun.glass.ui.Cursor; import com.sun.glass.ui.WindowControlsOverlay; import com.sun.glass.ui.NonClientHandler; @@ -371,20 +370,22 @@ public NonClientHandler getNonClientHandler() { return (type, button, x, y, xAbs, yAbs, clickCount) -> { double wx = x / platformScaleX; double wy = y / platformScaleY; - boolean handled = overlay.handleMouseEvent(type, button, wx, wy); - // A right click on a non-client header bar area opens the system menu. - if (!handled && type == MouseEvent.NC_UP && button == MouseEvent.BUTTON_RIGHT) { - View.EventHandler eventHandler = view != null ? view.getEventHandler() : null; - if (eventHandler != null && eventHandler.handleDragAreaHitTestEvent(wx, wy)) { - _showSystemMenu(getRawHandle(), x, y); - } - } - - return handled; + // Give the window button overlay the first chance to handle the event. + return overlay.handleMouseEvent(type, button, wx, wy); }; } + /** + * Opens a system menu at the specified coordinates. + * + * @param x the X coordinate in physical pixels + * @param y the Y coordinate in physical pixels + */ + public void showSystemMenu(int x, int y) { + _showSystemMenu(getRawHandle(), x, y); + } + /** * Classifies the window region at the specified physical coordinate. *

@@ -421,7 +422,7 @@ enum HT { // Otherwise, test if the cursor is over a draggable area and return HTCAPTION. View.EventHandler eventHandler = view.getEventHandler(); - if (eventHandler != null && eventHandler.handleDragAreaHitTestEvent(wx, wy)) { + if (eventHandler != null && eventHandler.pickDragAreaNode(wx, wy) != null) { return HT.CAPTION.value; } diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/TKSceneListener.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/TKSceneListener.java index 7a5cdfe8ac1..8593f08c220 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/TKSceneListener.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/TKSceneListener.java @@ -28,6 +28,7 @@ import com.sun.glass.ui.Accessible; import javafx.collections.ObservableList; import javafx.event.EventType; +import javafx.scene.Node; import javafx.scene.input.*; /** @@ -84,7 +85,7 @@ public void scrollEvent( boolean _altDown, boolean _metaDown, boolean _direct, boolean _inertia); - public void menuEvent(double x, double y, double xAbs, double yAbs, + public boolean menuEvent(double x, double y, double xAbs, double yAbs, boolean isKeyboardTrigger); public void zoomEvent( @@ -122,11 +123,12 @@ public void touchEventNext( public Accessible getSceneAccessible(); /** - * Tests whether the specified coordinate identifies a draggable area. + * Returns the draggable area node at the specified coordinates, or {@code null} + * if the specified coordinates do not intersect with a draggable area. * * @param x the X coordinate relative to the scene * @param y the Y coordinate relative to the scene - * @return {@code true} if the area is draggable, {@code false} otherwise + * @return the draggable area node, or {@code null} */ - public boolean dragAreaHitTest(double x, double y); + public Node pickDragAreaNode(double x, double y); } diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/GlassViewEventHandler.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/GlassViewEventHandler.java index 7d5f8b90ce8..c53275456ec 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/GlassViewEventHandler.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/GlassViewEventHandler.java @@ -48,6 +48,7 @@ import javafx.event.EventType; import javafx.geometry.Point2D; +import javafx.scene.Node; import javafx.scene.input.InputMethodEvent; import javafx.scene.input.InputMethodHighlight; import javafx.scene.input.InputMethodTextRun; @@ -455,9 +456,9 @@ public void handleMouseEvent(View view, long time, int type, int button, } @SuppressWarnings("removal") - @Override public void handleMenuEvent(final View view, - final int x, final int y, final int xAbs, final int yAbs, - final boolean isKeyboardTrigger) + @Override public boolean handleMenuEvent(final View view, + final int x, final int y, final int xAbs, final int yAbs, + final boolean isKeyboardTrigger) { if (PULSE_LOGGING_ENABLED) { PulseLogger.newInput("MENU_EVENT"); @@ -467,33 +468,35 @@ public void handleMouseEvent(View view, long time, int type, int button, if (stage != null) { stage.setInAllowedEventHandler(true); } - QuantumToolkit.runWithoutRenderLock(() -> { - return AccessController.doPrivileged((PrivilegedAction) () -> { - if (scene.sceneListener != null) { - double pScaleX, pScaleY, spx, spy, sx, sy; - final Window w = view.getWindow(); - if (w != null) { - pScaleX = w.getPlatformScaleX(); - pScaleY = w.getPlatformScaleY(); - Screen scr = w.getScreen(); - if (scr != null) { - spx = scr.getPlatformX(); - spy = scr.getPlatformY(); - sx = scr.getX(); - sy = scr.getY(); - } else { - spx = spy = sx = sy = 0.0; - } + return QuantumToolkit.runWithoutRenderLock(() -> { + return AccessController.doPrivileged((PrivilegedAction) () -> { + if (scene.sceneListener == null) { + return false; + } + + double pScaleX, pScaleY, spx, spy, sx, sy; + final Window w = view.getWindow(); + if (w != null) { + pScaleX = w.getPlatformScaleX(); + pScaleY = w.getPlatformScaleY(); + Screen scr = w.getScreen(); + if (scr != null) { + spx = scr.getPlatformX(); + spy = scr.getPlatformY(); + sx = scr.getX(); + sy = scr.getY(); } else { - pScaleX = pScaleY = 1.0; spx = spy = sx = sy = 0.0; } - scene.sceneListener.menuEvent(x / pScaleX, y / pScaleY, - sx + (xAbs - spx) / pScaleX, - sy + (yAbs - spy) / pScaleY, - isKeyboardTrigger); + } else { + pScaleX = pScaleY = 1.0; + spx = spy = sx = sy = 0.0; } - return null; + + return scene.sceneListener.menuEvent(x / pScaleX, y / pScaleY, + sx + (xAbs - spx) / pScaleX, + sy + (yAbs - spy) / pScaleY, + isKeyboardTrigger); }, scene.getAccessControlContext()); }); } finally { @@ -1408,13 +1411,13 @@ public Accessible getSceneAccessible() { } @Override - public boolean handleDragAreaHitTestEvent(double x, double y) { + public Node pickDragAreaNode(double x, double y) { return QuantumToolkit.runWithoutRenderLock(() -> { if (scene.sceneListener != null) { - return scene.sceneListener.dragAreaHitTest(x, y); + return scene.sceneListener.pickDragAreaNode(x, y); } - return false; + return null; }); } } diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/Scene.java b/modules/javafx.graphics/src/main/java/javafx/scene/Scene.java index 02a367697e0..c69e674f392 100644 --- a/modules/javafx.graphics/src/main/java/javafx/scene/Scene.java +++ b/modules/javafx.graphics/src/main/java/javafx/scene/Scene.java @@ -1903,7 +1903,7 @@ void processMouseEvent(MouseEvent e) { mouseHandler.process(e, false); } - private void processMenuEvent(double x2, double y2, double xAbs, double yAbs, boolean isKeyboardTrigger) { + private boolean processMenuEvent(double x2, double y2, double xAbs, double yAbs, boolean isKeyboardTrigger) { EventTarget eventTarget = null; Scene.inMousePick = true; if (isKeyboardTrigger) { @@ -1937,12 +1937,16 @@ private void processMenuEvent(double x2, double y2, double xAbs, double yAbs, bo } } + boolean handled = false; + if (eventTarget != null) { ContextMenuEvent context = new ContextMenuEvent(ContextMenuEvent.CONTEXT_MENU_REQUESTED, x2, y2, xAbs, yAbs, isKeyboardTrigger, res); - Event.fireEvent(eventTarget, context); + handled = EventUtil.fireEvent(eventTarget, context) == null; } Scene.inMousePick = false; + + return handled; } private void processGestureEvent(GestureEvent e, TouchGesture gesture) { @@ -2742,9 +2746,9 @@ public void inputMethodEvent(EventType type, } @Override - public void menuEvent(double x, double y, double xAbs, double yAbs, + public boolean menuEvent(double x, double y, double xAbs, double yAbs, boolean isKeyboardTrigger) { - Scene.this.processMenuEvent(x, y, xAbs,yAbs, isKeyboardTrigger); + return Scene.this.processMenuEvent(x, y, xAbs,yAbs, isKeyboardTrigger); } @Override @@ -3002,30 +3006,31 @@ public void touchEventEnd() { private final PickRay pickRay = new PickRay(); @Override - public boolean dragAreaHitTest(double x, double y) { + public Node pickDragAreaNode(double x, double y) { Node root = Scene.this.getRoot(); - if (root != null) { - pickRay.set(x, y, 1, 0, Double.POSITIVE_INFINITY); - var pickResultChooser = new PickResultChooser(); - root.pickNode(pickRay, pickResultChooser); - var intersectedNode = pickResultChooser.getIntersectedNode(); + if (root == null) { + return null; + } + + pickRay.set(x, y, 1, 0, Double.POSITIVE_INFINITY); + var pickResultChooser = new PickResultChooser(); + root.pickNode(pickRay, pickResultChooser); + Node intersectedNode = pickResultChooser.getIntersectedNode(); + Boolean draggable = intersectedNode instanceof HeaderBarBase ? true : null; + while (intersectedNode != null) { if (intersectedNode instanceof HeaderBarBase) { - return true; + return draggable == Boolean.TRUE ? intersectedNode : null; } - while (intersectedNode != null) { - if (HeaderBarBase.isDraggable(intersectedNode) instanceof Boolean value) { - return value; - } - - intersectedNode = intersectedNode.getParent(); + if (draggable == null && HeaderBarBase.isDraggable(intersectedNode) instanceof Boolean value) { + draggable = value; } - return false; + intersectedNode = intersectedNode.getParent(); } - return false; + return null; } @Override diff --git a/modules/javafx.graphics/src/main/native-glass/win/GlassWindow.cpp b/modules/javafx.graphics/src/main/native-glass/win/GlassWindow.cpp index 1eeaa6318f3..1f9707aaeaf 100644 --- a/modules/javafx.graphics/src/main/native-glass/win/GlassWindow.cpp +++ b/modules/javafx.graphics/src/main/native-glass/win/GlassWindow.cpp @@ -548,7 +548,7 @@ LRESULT GlassWindow::WindowProc(UINT msg, WPARAM wParam, LPARAM lParam) CheckUngrab(); // check if other owned windows hierarchy holds the grab if (m_isExtended) { - HandleViewNonClientMouseEvent(GetHWND(), msg, wParam, lParam); + HandleNonClientMouseEvents(msg, wParam, lParam); // We need to handle clicks on the min/max/close regions, as otherwise Windows will // draw very ugly buttons on top of our window. @@ -570,7 +570,7 @@ LRESULT GlassWindow::WindowProc(UINT msg, WPARAM wParam, LPARAM lParam) case WM_NCMOUSELEAVE: case WM_NCMOUSEMOVE: if (m_isExtended) { - HandleViewNonClientMouseEvent(GetHWND(), msg, wParam, lParam); + HandleNonClientMouseEvents(msg, wParam, lParam); if (wParam == HTMINBUTTON || wParam == HTMAXBUTTON || wParam == HTCLOSE) { return 0; @@ -625,6 +625,20 @@ bool GlassWindow::HandleMouseEvents(UINT msg, WPARAM wParam, LPARAM lParam) return false; } +void GlassWindow::HandleNonClientMouseEvents(UINT msg, WPARAM wParam, LPARAM lParam) +{ + HandleViewNonClientMouseEvent(GetHWND(), msg, wParam, lParam); + LRESULT result; + + // If the right mouse button was released on a HTCAPTION area, we synthesize a WM_CONTEXTMENU event. + // This allows JavaFX applications to respond to context menu events in the non-client header bar area. + if (msg == WM_NCRBUTTONUP + && HandleNCHitTestEvent(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam), result) + && result == HTCAPTION) { + HandleViewMenuEvent(GetHWND(), WM_CONTEXTMENU, (WPARAM)GetHWND(), ::GetMessagePos()); + } +} + void GlassWindow::HandleCloseEvent() { JNIEnv* env = GetEnv(); diff --git a/modules/javafx.graphics/src/main/native-glass/win/GlassWindow.h b/modules/javafx.graphics/src/main/native-glass/win/GlassWindow.h index 8d766e30ab6..7cad2254c9d 100644 --- a/modules/javafx.graphics/src/main/native-glass/win/GlassWindow.h +++ b/modules/javafx.graphics/src/main/native-glass/win/GlassWindow.h @@ -188,6 +188,7 @@ class GlassWindow : public BaseWnd, public ViewContainer { bool HandleCommand(WORD cmdID); void HandleFocusDisabledEvent(); bool HandleMouseEvents(UINT msg, WPARAM wParam, LPARAM lParam); + void HandleNonClientMouseEvents(UINT msg, WPARAM wParam, LPARAM lParam); LRESULT HandleNCCalcSizeEvent(UINT msg, WPARAM wParam, LPARAM lParam); BOOL HandleNCHitTestEvent(SHORT, SHORT, LRESULT&); }; From 778e6c1f22dd531d4a064b09636286265c19277c Mon Sep 17 00:00:00 2001 From: mstr2 <43553916+mstr2@users.noreply.github.com> Date: Sat, 26 Oct 2024 22:21:23 +0200 Subject: [PATCH 08/35] GTK: prevent resizing below window button size, fix crash --- .../src/main/java/com/sun/glass/ui/View.java | 7 ++++- .../java/com/sun/glass/ui/gtk/GtkWindow.java | 10 +++++++ .../src/main/native-glass/gtk/GlassWindow.cpp | 17 +++++++++++ .../main/native-glass/gtk/glass_window.cpp | 29 ++++++++++++------- .../src/main/native-glass/gtk/glass_window.h | 11 ++++--- 5 files changed, 58 insertions(+), 16 deletions(-) diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/View.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/View.java index e2fbb338b2a..cb62a4df94d 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/View.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/View.java @@ -960,7 +960,12 @@ protected void notifyMouse(int type, int button, int x, int y, int xAbs, // If we have a non-client handler, we give it the first chance to handle the event. // Note that a full-screen window has no non-client area, and thus the non-client handler // is not notified. - boolean handled = !inFullscreen && nonClientHandler != null + // Some implementations (like GTK) can fire synthesized events when they receive a mouse + // button event on the resize border. These events, even though happening on non-client + // regions, must not be processed by the non-client handler. For example, if a mouse click + // happens on the resize border that straddles the window close button, we don't want the + // close button to act on this click, because we just started a resize-drag operation. + boolean handled = !isSynthesized && !inFullscreen && nonClientHandler != null && nonClientHandler.handleMouseEvent(type, button, x, y, xAbs, yAbs, clickCount); // We never send non-client events to the application. diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkWindow.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkWindow.java index cd3ef7701c4..1fb0aff1d72 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkWindow.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkWindow.java @@ -96,6 +96,8 @@ protected boolean _setMenubar(long ptr, long menubarPtr) { @Override protected native void _setEnabled(long ptr, boolean enabled); + private native boolean _setSystemMinimumSize(long ptr, int width, int height); + @Override protected native boolean _setMinimumSize(long ptr, int width, int height); @@ -217,6 +219,14 @@ public WindowControlsOverlay getWindowOverlay() { if (windowControlsOverlay == null && isExtendedWindow()) { windowControlsOverlay = new WindowControlsOverlay( PlatformThemeObserver.getInstance().stylesheetProperty()); + + // Set the system-defined absolute minimum size to the size of the window buttons area, + // regardless of whether the application has specified a smaller minimum size. + windowControlsOverlay.metricsProperty().addListener((_, _, metrics) -> { + int width = (int)(metrics.size().getWidth() * platformScaleX); + int height = (int)(metrics.size().getHeight() * platformScaleY); + _setSystemMinimumSize(super.getRawHandle(), width, height); + }); } return windowControlsOverlay; diff --git a/modules/javafx.graphics/src/main/native-glass/gtk/GlassWindow.cpp b/modules/javafx.graphics/src/main/native-glass/gtk/GlassWindow.cpp index aa66a878fdd..885554e01f0 100644 --- a/modules/javafx.graphics/src/main/native-glass/gtk/GlassWindow.cpp +++ b/modules/javafx.graphics/src/main/native-glass/gtk/GlassWindow.cpp @@ -378,6 +378,23 @@ JNIEXPORT jboolean JNICALL Java_com_sun_glass_ui_gtk_GtkWindow__1setMinimumSize return JNI_TRUE; } +/* + * Class: com_sun_glass_ui_gtk_GtkWindow + * Method: _setSystemMinimumSize + * Signature: (JII)Z + */ +JNIEXPORT jboolean JNICALL Java_com_sun_glass_ui_gtk_GtkWindow__1setSystemMinimumSize + (JNIEnv * env, jobject obj, jlong ptr, jint w, jint h) +{ + (void)env; + (void)obj; + + WindowContext* ctx = JLONG_TO_WINDOW_CTX(ptr); + if (w < 0 || h < 0) return JNI_FALSE; + ctx->set_system_minimum_size(w, h); + return JNI_TRUE; +} + /* * Class: com_sun_glass_ui_gtk_GtkWindow * Method: _setMaximumSize diff --git a/modules/javafx.graphics/src/main/native-glass/gtk/glass_window.cpp b/modules/javafx.graphics/src/main/native-glass/gtk/glass_window.cpp index 8058e52dcf5..736747b599c 100644 --- a/modules/javafx.graphics/src/main/native-glass/gtk/glass_window.cpp +++ b/modules/javafx.graphics/src/main/native-glass/gtk/glass_window.cpp @@ -262,7 +262,7 @@ static inline jint gtk_button_number_to_mouse_button(guint button) { } } -void WindowContextBase::process_mouse_button(GdkEventButton* event) { +void WindowContextBase::process_mouse_button(GdkEventButton* event, bool synthesized) { // We only handle single press/release events here. if (event->type != GDK_BUTTON_PRESS && event->type != GDK_BUTTON_RELEASE) { return; @@ -332,7 +332,7 @@ void WindowContextBase::process_mouse_button(GdkEventButton* event) { (jint) event->x_root, (jint) event->y_root, gdk_modifier_mask_to_glass(state), (event->button == 3 && press) ? JNI_TRUE : JNI_FALSE, - JNI_FALSE); + synthesized); CHECK_JNI_EXCEPTION(mainEnv) if (jview && event->button == 3 && press) { @@ -1068,10 +1068,11 @@ void WindowContextTop::update_window_constraints() { GdkGeometry hints; if (resizable.value && !is_disabled) { - int min_w = (resizable.minw == -1) ? 1 - : resizable.minw - geometry.extents.left - geometry.extents.right; - int min_h = (resizable.minh == -1) ? 1 - : resizable.minh - geometry.extents.top - geometry.extents.bottom; + int w = std::max(resizable.sysminw, resizable.minw); + int h = std::max(resizable.sysminh, resizable.minh); + + int min_w = (w == -1) ? 1 : w - geometry.extents.left - geometry.extents.right; + int min_h = (h == -1) ? 1 : h - geometry.extents.top - geometry.extents.bottom; hints.min_width = (min_w < 1) ? 1 : min_w; hints.min_height = (min_h < 1) ? 1 : min_h; @@ -1247,6 +1248,12 @@ void WindowContextTop::set_enabled(bool enabled) { update_window_constraints(); } +void WindowContextTop::set_system_minimum_size(int w, int h) { + resizable.sysminw = w; + resizable.sysminh = h; + update_window_constraints(); +} + void WindowContextTop::set_minimum_size(int w, int h) { resizable.minw = (w <= 0) ? 1 : w; resizable.minh = (h <= 0) ? 1 : h; @@ -1402,7 +1409,7 @@ void WindowContextTop::notify_window_move() { * regions that are usually provided by the window manager. Note that a full-screen window has * no non-client regions. */ -void WindowContextTop::process_mouse_button(GdkEventButton* event) { +void WindowContextTop::process_mouse_button(GdkEventButton* event, bool synthesized) { // Non-EXTENDED or full-screen windows don't have additional behaviors, so we delegate // directly to the base implementation. if (is_fullscreen || frame_type != EXTENDED || jwindow == NULL) { @@ -1433,9 +1440,9 @@ void WindowContextTop::process_mouse_button(GdkEventButton* event) { // Send a synthetic PRESS + RELEASE to FX. This allows FX to do things that need to be done // prior to resizing the window, like closing a popup menu. We do this because we won't be // sending events to FX once the resize operation has started. - WindowContextBase::process_mouse_button(event); + WindowContextBase::process_mouse_button(event, true); event->type = GDK_BUTTON_RELEASE; - WindowContextBase::process_mouse_button(event); + WindowContextBase::process_mouse_button(event, true); gint rx = 0, ry = 0; gdk_window_get_root_coords(get_gdk_window(), event->x, event->y, &rx, &ry); @@ -1450,9 +1457,9 @@ void WindowContextTop::process_mouse_button(GdkEventButton* event) { // Clicking on a draggable area starts a move-drag operation. if (shouldStartMoveDrag) { // Send a synthetic PRESS + RELEASE to FX. - WindowContextBase::process_mouse_button(event); + WindowContextBase::process_mouse_button(event, true); event->type = GDK_BUTTON_RELEASE; - WindowContextBase::process_mouse_button(event); + WindowContextBase::process_mouse_button(event, true); gint rx = 0, ry = 0; gdk_window_get_root_coords(get_gdk_window(), event->x, event->y, &rx, &ry); diff --git a/modules/javafx.graphics/src/main/native-glass/gtk/glass_window.h b/modules/javafx.graphics/src/main/native-glass/gtk/glass_window.h index 9e2ee1cfe50..56b3c08d77c 100644 --- a/modules/javafx.graphics/src/main/native-glass/gtk/glass_window.h +++ b/modules/javafx.graphics/src/main/native-glass/gtk/glass_window.h @@ -120,6 +120,7 @@ class WindowContext : public DeletedMemDebug<0xCC> { virtual void set_title(const char*) = 0; virtual void set_alpha(double) = 0; virtual void set_enabled(bool) = 0; + virtual void set_system_minimum_size(int, int) = 0; virtual void set_minimum_size(int, int) = 0; virtual void set_maximum_size(int, int) = 0; virtual void set_minimized(bool) = 0; @@ -139,7 +140,7 @@ class WindowContext : public DeletedMemDebug<0xCC> { virtual void process_destroy() = 0; virtual void process_delete() = 0; virtual void process_expose(GdkEventExpose*) = 0; - virtual void process_mouse_button(GdkEventButton*) = 0; + virtual void process_mouse_button(GdkEventButton*, bool synthesized = false) = 0; virtual void process_mouse_motion(GdkEventMotion*) = 0; virtual void process_mouse_scroll(GdkEventScroll*) = 0; virtual void process_mouse_cross(GdkEventCrossing*) = 0; @@ -240,7 +241,7 @@ class WindowContextBase: public WindowContext { void process_destroy(); void process_delete(); void process_expose(GdkEventExpose*); - void process_mouse_button(GdkEventButton*); + void process_mouse_button(GdkEventButton*, bool synthesized = false); void process_mouse_motion(GdkEventMotion*); void process_mouse_scroll(GdkEventScroll*); void process_mouse_cross(GdkEventCrossing*); @@ -270,9 +271,10 @@ class WindowContextTop: public WindowContextBase { WindowGeometry geometry; struct _Resizable {// we can't use set/get gtk_window_resizable function _Resizable(): value(true), - minw(-1), minh(-1), maxw(-1), maxh(-1) {} + minw(-1), minh(-1), maxw(-1), maxh(-1), sysminw(-1), sysminh(-1) {} bool value; //actual value of resizable for a window int minw, minh, maxw, maxh; //minimum and maximum window width/height; + int sysminw, sysminh; // size of window button area of EXTENDED windows } resizable; bool on_top; @@ -291,7 +293,7 @@ class WindowContextTop: public WindowContextBase { void process_configure(GdkEventConfigure*); void process_destroy(); void process_mouse_motion(GdkEventMotion*); - void process_mouse_button(GdkEventButton*); + void process_mouse_button(GdkEventButton*, bool synthesized = false); void work_around_compiz_state(); WindowFrameExtents get_frame_extents(); @@ -305,6 +307,7 @@ class WindowContextTop: public WindowContextBase { void set_title(const char*); void set_alpha(double); void set_enabled(bool); + void set_system_minimum_size(int, int); void set_minimum_size(int, int); void set_maximum_size(int, int); void set_icon(GdkPixbuf*); From 3b468fe556365444a81fa84ef1831d29a089d0ff Mon Sep 17 00:00:00 2001 From: mstr2 <43553916+mstr2@users.noreply.github.com> Date: Sat, 26 Oct 2024 23:29:16 +0200 Subject: [PATCH 09/35] GTK: add system menu --- .../java/com/sun/glass/ui/gtk/GtkView.java | 22 ++++++++++++++++ .../java/com/sun/glass/ui/gtk/GtkWindow.java | 12 +++++++++ .../src/main/native-glass/gtk/GlassWindow.cpp | 15 +++++++++++ .../main/native-glass/gtk/glass_window.cpp | 26 +++++++++++++++++++ .../src/main/native-glass/gtk/glass_window.h | 2 ++ 5 files changed, 77 insertions(+) diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkView.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkView.java index 2100216bf7d..12ff513efc0 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkView.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkView.java @@ -215,4 +215,26 @@ protected void notifyInputMethodCaret(int pos, int direction, int style) { } notifyInputMethod(preedit.toString(), null, null, null, 0, lastCaret, 0); } + + @Override + protected void notifyMenu(int x, int y, int xAbs, int yAbs, boolean isKeyboardTrigger) { + // If all of the following conditions are satisfied, we open a system menu at the specified coordinates: + // 1. The application didn't consume the menu event. + // 2. The window is an EXTENDED window. + // 3. The menu event occurred on a draggable area. + if (!handleMenuEvent(x, y, xAbs, yAbs, isKeyboardTrigger)) { + var window = (GtkWindow)getWindow(); + if (!window.isExtendedWindow()) { + return; + } + + double wx = x / window.getPlatformScaleX(); + double wy = y / window.getPlatformScaleY(); + + EventHandler eventHandler = getEventHandler(); + if (eventHandler != null && eventHandler.pickDragAreaNode(wx, wy) != null) { + window.showSystemMenu(x, y); + } + } + } } diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkWindow.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkWindow.java index 1fb0aff1d72..c690c1b22a1 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkWindow.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkWindow.java @@ -124,6 +124,8 @@ protected boolean _setMenubar(long ptr, long menubarPtr) { protected native long _getNativeWindowImpl(long ptr); + private native void _showSystemMenu(long ptr, int x, int y); + private native boolean isVisible(long ptr); @Override @@ -247,6 +249,16 @@ public NonClientHandler getNonClientHandler() { }; } + /** + * Opens a system menu at the specified coordinates. + * + * @param x the X coordinate in physical pixels + * @param y the Y coordinate in physical pixels + */ + public void showSystemMenu(int x, int y) { + _showSystemMenu(super.getRawHandle(), x, y); + } + /** * Returns whether the window is draggable at the specified coordinate. *

diff --git a/modules/javafx.graphics/src/main/native-glass/gtk/GlassWindow.cpp b/modules/javafx.graphics/src/main/native-glass/gtk/GlassWindow.cpp index 885554e01f0..f0f6e39cbe6 100644 --- a/modules/javafx.graphics/src/main/native-glass/gtk/GlassWindow.cpp +++ b/modules/javafx.graphics/src/main/native-glass/gtk/GlassWindow.cpp @@ -544,6 +544,21 @@ JNIEXPORT void JNICALL Java_com_sun_glass_ui_gtk_GtkWindow__1setCustomCursor ctx->set_cursor(cursor); } +/* + * Class: com_sun_glass_ui_gtk_GtkWindow + * Method: _showSystemMenu + * Signature: (JII)V + */ +JNIEXPORT void JNICALL Java_com_sun_glass_ui_gtk_GtkWindow__1showSystemMenu + (JNIEnv * env, jobject obj, jlong ptr, jint x, jint y) +{ + (void)env; + (void)obj; + + WindowContext* ctx = JLONG_TO_WINDOW_CTX(ptr); + ctx->show_system_menu(x, y); +} + /* * Class: com_sun_glass_ui_gtk_GtkWindow * Method: isVisible diff --git a/modules/javafx.graphics/src/main/native-glass/gtk/glass_window.cpp b/modules/javafx.graphics/src/main/native-glass/gtk/glass_window.cpp index 736747b599c..c4fd26fcbcc 100644 --- a/modules/javafx.graphics/src/main/native-glass/gtk/glass_window.cpp +++ b/modules/javafx.graphics/src/main/native-glass/gtk/glass_window.cpp @@ -1404,6 +1404,32 @@ void WindowContextTop::notify_window_move() { } } +void WindowContextTop::show_system_menu(int x, int y) { + GdkDisplay* display = gdk_display_get_default(); + if (!display) { + return; + } + + GdkSeat* seat = gdk_display_get_default_seat(display); + GdkDevice* device = gdk_seat_get_pointer(seat); + if (!device) { + return; + } + + gint rx = 0, ry = 0; + gdk_window_get_root_coords(gdk_window, x, y, &rx, &ry); + + GdkEvent* event = (GdkEvent*)gdk_event_new(GDK_BUTTON_PRESS); + GdkEventButton* buttonEvent = (GdkEventButton*)event; + buttonEvent->x_root = rx; + buttonEvent->y_root = ry; + buttonEvent->window = g_object_ref(gdk_window); + buttonEvent->device = g_object_ref(device); + + gdk_window_show_window_menu(gdk_window, event); + gdk_event_free(event); +} + /* * Handles mouse button events of EXTENDED windows and adds the window behaviors for non-client * regions that are usually provided by the window manager. Note that a full-screen window has diff --git a/modules/javafx.graphics/src/main/native-glass/gtk/glass_window.h b/modules/javafx.graphics/src/main/native-glass/gtk/glass_window.h index 56b3c08d77c..413e90fcdcf 100644 --- a/modules/javafx.graphics/src/main/native-glass/gtk/glass_window.h +++ b/modules/javafx.graphics/src/main/native-glass/gtk/glass_window.h @@ -105,6 +105,7 @@ class WindowContext : public DeletedMemDebug<0xCC> { virtual void paint(void* data, jint width, jint height) = 0; virtual WindowFrameExtents get_frame_extents() = 0; + virtual void show_system_menu(int x, int y) = 0; virtual void enter_fullscreen() = 0; virtual void exit_fullscreen() = 0; virtual void set_visible(bool) = 0; @@ -320,6 +321,7 @@ class WindowContextTop: public WindowContextBase { void update_view_size(); void notify_view_resize(); + void show_system_menu(int x, int y); void enter_fullscreen(); void exit_fullscreen(); From 0526edbae806510c44da32227e5119f816d3ad22 Mon Sep 17 00:00:00 2001 From: mstr2 <43553916+mstr2@users.noreply.github.com> Date: Mon, 28 Oct 2024 06:32:31 +0100 Subject: [PATCH 10/35] small code changes --- .../main/java/com/sun/glass/ui/Window.java | 20 +++++++++++++------ .../java/com/sun/glass/ui/gtk/GtkWindow.java | 10 ++-------- .../java/com/sun/glass/ui/mac/MacWindow.java | 18 ++++++----------- .../java/com/sun/glass/ui/win/WinWindow.java | 9 +-------- .../javafx/scene/layout/HeaderBarBase.java | 19 ++++++++---------- 5 files changed, 31 insertions(+), 45 deletions(-) diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/Window.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/Window.java index bbbcd2389eb..f0a5db0ed38 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/Window.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/Window.java @@ -25,12 +25,16 @@ package com.sun.glass.ui; import com.sun.glass.events.WindowEvent; +import com.sun.javafx.binding.ObjectConstant; import com.sun.prism.impl.PrismSettings; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ObservableValue; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; import javafx.scene.Parent; - +import javafx.scene.layout.Region; import java.lang.annotation.Native; - import java.util.Collections; import java.util.LinkedList; import java.util.List; @@ -233,6 +237,10 @@ public static final class Level { private int maximumWidth = Integer.MAX_VALUE, maximumHeight = Integer.MAX_VALUE; private EventHandler eventHandler; + private ObservableList nonClientAreas; + + protected final ObjectProperty windowOverlayMetrics = + new SimpleObjectProperty<>(this, "windowOverlayMetrics"); protected abstract long _createWindow(long ownerPtr, long screenPtr, int mask); protected Window(Window owner, Screen screen, int styleMask) { @@ -426,16 +434,16 @@ public void setMenuBar(final MenuBar menubar) { /** * Returns metrics of the window-provided overlay controls. * - * @return the overlay metrics, or {@code null} if the window type does not support metrics + * @return the overlay metrics */ - public ObservableValue windowOverlayMetrics() { - return null; + public ObservableValue getWindowOverlayMetrics() { + return windowOverlayMetrics; } /** * Returns the window-provided overlay controls, which are rendered above all application content. * - * @return the overlay, or {@code null} if the window type does not provide overlay controls + * @return the overlay, or {@code null} if the window does not provide an overlay */ public Parent getWindowOverlay() { return null; diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkWindow.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkWindow.java index c690c1b22a1..61732aa9569 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkWindow.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkWindow.java @@ -33,8 +33,6 @@ import com.sun.glass.ui.View; import com.sun.glass.ui.Window; import com.sun.glass.ui.WindowControlsOverlay; -import com.sun.glass.ui.WindowOverlayMetrics; -import javafx.beans.value.ObservableValue; class GtkWindow extends Window { @@ -210,12 +208,6 @@ public long getRawHandle() { private WindowControlsOverlay windowControlsOverlay; - @Override - public ObservableValue windowOverlayMetrics() { - var overlay = getWindowOverlay(); - return overlay != null ? overlay.metricsProperty() : null; - } - @Override public WindowControlsOverlay getWindowOverlay() { if (windowControlsOverlay == null && isExtendedWindow()) { @@ -229,6 +221,8 @@ public WindowControlsOverlay getWindowOverlay() { int height = (int)(metrics.size().getHeight() * platformScaleY); _setSystemMinimumSize(super.getRawHandle(), width, height); }); + + windowOverlayMetrics.bind(windowControlsOverlay.metricsProperty()); } return windowControlsOverlay; diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/mac/MacWindow.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/mac/MacWindow.java index 9f05f9744e6..9e0962ebc1e 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/mac/MacWindow.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/mac/MacWindow.java @@ -33,7 +33,6 @@ import com.sun.glass.ui.View; import com.sun.glass.ui.Window; import com.sun.glass.ui.WindowOverlayMetrics; -import com.sun.javafx.binding.ObjectConstant; import javafx.beans.value.ObservableValue; import javafx.geometry.Dimension2D; import javafx.geometry.HorizontalDirection; @@ -183,18 +182,13 @@ public NonClientHandler getNonClientHandler() { private native boolean _isRightToLeftLayoutDirection(); - private ObservableValue windowOverlayMetrics; - @Override - public ObservableValue windowOverlayMetrics() { - if (windowOverlayMetrics == null) { - HorizontalDirection direction = _isRightToLeftLayoutDirection() - ? HorizontalDirection.RIGHT - : HorizontalDirection.LEFT; - - windowOverlayMetrics = ObjectConstant.valueOf( - new WindowOverlayMetrics(direction, new Dimension2D(78, 38))); - } + public ObservableValue getWindowOverlayMetrics() { + HorizontalDirection direction = _isRightToLeftLayoutDirection() + ? HorizontalDirection.RIGHT + : HorizontalDirection.LEFT; + + windowOverlayMetrics.set(new WindowOverlayMetrics(direction, new Dimension2D(78, 38))); return windowOverlayMetrics; } diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/win/WinWindow.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/win/WinWindow.java index 6895c5a0500..d669846bd97 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/win/WinWindow.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/win/WinWindow.java @@ -31,9 +31,7 @@ import com.sun.glass.ui.Screen; import com.sun.glass.ui.View; import com.sun.glass.ui.Window; -import com.sun.glass.ui.WindowOverlayMetrics; import com.sun.javafx.binding.StringConstant; -import javafx.beans.value.ObservableValue; /** * MS Windows platform implementation class for Window. @@ -340,12 +338,6 @@ void setDeferredClosing(boolean dc) { private WindowControlsOverlay windowControlsOverlay; - @Override - public ObservableValue windowOverlayMetrics() { - var overlay = getWindowOverlay(); - return overlay != null ? overlay.metricsProperty() : null; - } - @Override public WindowControlsOverlay getWindowOverlay() { if (windowControlsOverlay == null && isExtendedWindow()) { @@ -355,6 +347,7 @@ public WindowControlsOverlay getWindowOverlay() { } windowControlsOverlay = new WindowControlsOverlay(StringConstant.valueOf(url.toExternalForm())); + windowOverlayMetrics.bind(windowControlsOverlay.metricsProperty()); } return windowControlsOverlay; diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBarBase.java b/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBarBase.java index af15f431042..8a0049d00fb 100644 --- a/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBarBase.java +++ b/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBarBase.java @@ -29,7 +29,6 @@ import com.sun.javafx.stage.StageHelper; import com.sun.javafx.tk.quantum.WindowStage; import javafx.beans.property.ReadOnlyObjectWrapper; -import javafx.beans.value.ObservableValue; import javafx.geometry.Dimension2D; import javafx.geometry.HorizontalDirection; import javafx.scene.Node; @@ -82,7 +81,7 @@ public static Boolean isDraggable(Node child) { return (Boolean)Pane.getConstraint(child, DRAGGABLE); } - private Subscription metricsSubscription; + private Subscription subscription; private WindowOverlayMetrics currentMetrics; private boolean currentFullScreen; @@ -105,18 +104,16 @@ protected HeaderBarBase() { private void onShowingChanged(boolean showing) { if (!showing) { - if (metricsSubscription != null) { - metricsSubscription.unsubscribe(); - metricsSubscription = null; + if (subscription != null) { + subscription.unsubscribe(); + subscription = null; } } else if (getScene().getWindow() instanceof Stage stage && StageHelper.getPeer(stage) instanceof WindowStage windowStage) { - ObservableValue metrics = - windowStage.getPlatformWindow().windowOverlayMetrics(); - - if (metrics != null) { - metricsSubscription = metrics.subscribe(this::onMetricsChanged); - } + subscription = windowStage + .getPlatformWindow() + .getWindowOverlayMetrics() + .subscribe(this::onMetricsChanged); } } From 95736df772d33bda0ec0f116a5d3404df58b6ef1 Mon Sep 17 00:00:00 2001 From: mstr2 <43553916+mstr2@users.noreply.github.com> Date: Mon, 28 Oct 2024 06:34:36 +0100 Subject: [PATCH 11/35] remove unused code --- .../javafx.graphics/src/main/java/com/sun/glass/ui/Window.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/Window.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/Window.java index f0a5db0ed38..57e44b8ac89 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/Window.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/Window.java @@ -25,7 +25,6 @@ package com.sun.glass.ui; import com.sun.glass.events.WindowEvent; -import com.sun.javafx.binding.ObjectConstant; import com.sun.prism.impl.PrismSettings; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; @@ -33,7 +32,6 @@ import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.scene.Parent; -import javafx.scene.layout.Region; import java.lang.annotation.Native; import java.util.Collections; import java.util.LinkedList; @@ -237,7 +235,6 @@ public static final class Level { private int maximumWidth = Integer.MAX_VALUE, maximumHeight = Integer.MAX_VALUE; private EventHandler eventHandler; - private ObservableList nonClientAreas; protected final ObjectProperty windowOverlayMetrics = new SimpleObjectProperty<>(this, "windowOverlayMetrics"); From c0b588f80ca76565735117424b81bfd65d692d77 Mon Sep 17 00:00:00 2001 From: mstr2 <43553916+mstr2@users.noreply.github.com> Date: Mon, 28 Oct 2024 20:26:38 +0100 Subject: [PATCH 12/35] set minHeight to native height of title bar --- .../java/javafx/scene/layout/HeaderBar.java | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBar.java b/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBar.java index e5bac22da28..bb0003ce349 100644 --- a/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBar.java +++ b/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBar.java @@ -28,6 +28,7 @@ import com.sun.javafx.geom.Vec2d; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectPropertyBase; +import javafx.css.StyleableDoubleProperty; import javafx.geometry.HPos; import javafx.geometry.Insets; import javafx.geometry.NodeOrientation; @@ -157,15 +158,16 @@ public static Insets getMargin(Node child) { return (Insets)Pane.getConstraint(child, MARGIN); } - private static Insets getNodeMargin(Node child) { - Insets margin = getMargin(child); - return margin != null ? margin : Insets.EMPTY; - } - /** * Creates a new {@code HeaderBar}. */ public HeaderBar() { + // Inflate the minHeight property. This is important so that we can track whether a stylesheet or + // user code changes the property value before we set it to the height of the native title bar. + minHeightProperty(); + + leftSystemInsetProperty().subscribe(this::updateDefaultMinHeight); + rightSystemInsetProperty().subscribe(this::updateDefaultMinHeight); } /** @@ -176,6 +178,7 @@ public HeaderBar() { * @param trailing the trailing node */ public HeaderBar(Node leading, Node center, Node trailing) { + this(); setLeading(leading); setCenter(center); setTrailing(trailing); @@ -481,6 +484,21 @@ private double getAreaHeight(Node child, double width, boolean minimum) { return 0; } + private Insets getNodeMargin(Node child) { + Insets margin = getMargin(child); + return margin != null ? margin : Insets.EMPTY; + } + + private void updateDefaultMinHeight() { + var minHeight = (StyleableDoubleProperty)minHeightProperty(); + + // Only change the default minHeight if it was not set by a stylesheet or application code. + if (minHeight.getStyleOrigin() == null) { + double height = Math.max(getLeftSystemInset().getHeight(), getRightSystemInset().getHeight()); + ((StyleableDoubleProperty)minHeightProperty()).applyStyle(null, height); + } + } + private abstract class NodeProperty extends ObjectPropertyBase { private Node value; From d7f88c3312d861c824f86b10777c8d0feb40db0d Mon Sep 17 00:00:00 2001 From: mstr2 <43553916+mstr2@users.noreply.github.com> Date: Mon, 28 Oct 2024 20:38:02 +0100 Subject: [PATCH 13/35] better documentation --- .../src/main/java/javafx/scene/layout/HeaderBar.java | 3 +++ .../src/main/java/javafx/stage/StageStyle.java | 7 +++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBar.java b/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBar.java index bb0003ce349..277f1f52af4 100644 --- a/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBar.java +++ b/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBar.java @@ -57,6 +57,9 @@ * {@code HeaderBar} honors the minimum, preferred, and maximum sizes of its children. If the child's * resizable range prevents it from be resized to fit within its position, it will be vertically centered * relative to the available space; this alignment can be customized with a layout constraint. + *

+ * The default {@link #minHeightProperty() minHeight} of the {@code HeaderBar} is set to match the height + * of the platform-specific default window buttons. * *

Layout constraints

* An application may set constraints on individual children to customize their layout. diff --git a/modules/javafx.graphics/src/main/java/javafx/stage/StageStyle.java b/modules/javafx.graphics/src/main/java/javafx/stage/StageStyle.java index 26a040f77a2..2e216ee2615 100644 --- a/modules/javafx.graphics/src/main/java/javafx/stage/StageStyle.java +++ b/modules/javafx.graphics/src/main/java/javafx/stage/StageStyle.java @@ -80,8 +80,11 @@ public enum StageStyle { * bar area of the stage. *

* An extended window has the default window buttons (minimize, maximize, close), but no system-provided - * draggable header bar. Applications need to provide their own header bar by placing the {@link HeaderBar} - * control in the scene graph. Usually, this is combined with a {@link BorderPane} root container: + * draggable header bar. Applications need to provide their own header bar by placing a single {@link HeaderBar} + * control in the scene graph. The {@code HeaderBar} control should be positioned at the top of the window + * and its width should extend the entire width of the window, as otherwise the layout of the default window + * buttons and the header bar content might not be aligned. Usually, {@code HeaderBar} is combined with a + * {@link BorderPane} root container: *

{@code
      * public class MyApp extends Application {
      *     @Override

From 9de46940a7c60409c92fb43639b7e59933ddc2a3 Mon Sep 17 00:00:00 2001
From: mstr2 <43553916+mstr2@users.noreply.github.com>
Date: Mon, 28 Oct 2024 20:54:28 +0100
Subject: [PATCH 14/35] macOS: hide window title

---
 modules/javafx.graphics/src/main/native-glass/mac/GlassWindow.m | 1 +
 1 file changed, 1 insertion(+)

diff --git a/modules/javafx.graphics/src/main/native-glass/mac/GlassWindow.m b/modules/javafx.graphics/src/main/native-glass/mac/GlassWindow.m
index dda2b3241bd..ccfcca4869d 100644
--- a/modules/javafx.graphics/src/main/native-glass/mac/GlassWindow.m
+++ b/modules/javafx.graphics/src/main/native-glass/mac/GlassWindow.m
@@ -459,6 +459,7 @@ static jlong _createWindowCommonDo(JNIEnv *env, jobject jWindow, jlong jOwnerPtr
         window = [[GlassWindow alloc] _initWithContentRect:NSMakeRect(x, y, w, h) styleMask:styleMask screen:screen jwindow:jWindow];
 
         if ((jStyleMask & com_sun_glass_ui_Window_EXTENDED) != 0) {
+            [window->nsWindow setTitleVisibility:NSWindowTitleHidden];
             [window->nsWindow setTitlebarAppearsTransparent:YES];
             [window->nsWindow setToolbar:[NSToolbar new]];
             [window->nsWindow setToolbarStyle:NSWindowToolbarStyleUnifiedCompact];

From cd5d4434c70f9539f4631059d632290e501b9c9d Mon Sep 17 00:00:00 2001
From: mstr2 <43553916+mstr2@users.noreply.github.com>
Date: Mon, 28 Oct 2024 21:01:03 +0100
Subject: [PATCH 15/35] improve title text documentation

---
 .../src/main/java/javafx/stage/StageStyle.java               | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/modules/javafx.graphics/src/main/java/javafx/stage/StageStyle.java b/modules/javafx.graphics/src/main/java/javafx/stage/StageStyle.java
index 2e216ee2615..d6084661839 100644
--- a/modules/javafx.graphics/src/main/java/javafx/stage/StageStyle.java
+++ b/modules/javafx.graphics/src/main/java/javafx/stage/StageStyle.java
@@ -105,6 +105,11 @@ public enum StageStyle {
      * that matches the brightness of the user interface, even if the scene fill is not visible because it
      * is obscured by other controls.
      * 

+ * An extended stage has no title text. Applications that require title text need to provide their own + * implementation by placing a {@code Label} or a similar control in the custom header bar. + * Note that the value of {@link Stage#titleProperty()} may still be used by the platform, one example + * may be the title of miniaturized preview windows. + *

* This is a conditional feature, to check if it is supported see {@link Platform#isSupported(ConditionalFeature)}. * If the feature is not supported by the platform, this style downgrades to {@link StageStyle#DECORATED}. * From bc48ae0b9c8106220074c70ff7739abd9ad2c794 Mon Sep 17 00:00:00 2001 From: mstr2 <43553916+mstr2@users.noreply.github.com> Date: Mon, 28 Oct 2024 22:49:06 +0100 Subject: [PATCH 16/35] fix peer access outside of synchronizer --- .../com/sun/javafx/tk/quantum/ViewSceneOverlay.java | 13 ++++++++++--- .../src/main/java/javafx/scene/Scene.java | 3 ++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/ViewSceneOverlay.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/ViewSceneOverlay.java index fe9d18540b2..d35bf440951 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/ViewSceneOverlay.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/ViewSceneOverlay.java @@ -76,13 +76,14 @@ public void setRoot(Parent root) { return; } + if (this.root != null) { + NodeHelper.setScenes(this.root, null, null); + } + this.root = root; if (root != null) { NodeHelper.setScenes(root, fxScene, null); - painter.setOverlayRoot(NodeHelper.getPeer(root)); - } else { - painter.setOverlayRoot(null); } } @@ -93,6 +94,12 @@ public void synchronize() { } private void syncPeer(Node node) { + if (root != null) { + painter.setOverlayRoot(NodeHelper.getPeer(root)); + } else { + painter.setOverlayRoot(null); + } + NodeHelper.syncPeer(node); if (node instanceof Parent parent) { diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/Scene.java b/modules/javafx.graphics/src/main/java/javafx/scene/Scene.java index c69e674f392..6618bfb9882 100644 --- a/modules/javafx.graphics/src/main/java/javafx/scene/Scene.java +++ b/modules/javafx.graphics/src/main/java/javafx/scene/Scene.java @@ -2485,6 +2485,8 @@ private void synchronizeSceneNodes() { Scene.inSynchronizer = true; + peer.synchronizeOverlay(); + // if dirtyNodes is null then that means this Scene has not yet been // synchronized, and so we will simply synchronize every node in the // scene and then create the dirty nodes array list @@ -2643,7 +2645,6 @@ public void pulse() { if (PULSE_LOGGING_ENABLED) { PulseLogger.newPhase("Copy state to render graph"); } - peer.synchronizeOverlay(); syncLights(); synchronizeSceneProperties(); // Run the synchronizer From 804d0be703abb208bca4c54a8c46418958989689 Mon Sep 17 00:00:00 2001 From: mstr2 <43553916+mstr2@users.noreply.github.com> Date: Mon, 28 Oct 2024 22:56:02 +0100 Subject: [PATCH 17/35] NPE --- modules/javafx.graphics/src/main/java/javafx/scene/Scene.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/Scene.java b/modules/javafx.graphics/src/main/java/javafx/scene/Scene.java index 6618bfb9882..df1752bff08 100644 --- a/modules/javafx.graphics/src/main/java/javafx/scene/Scene.java +++ b/modules/javafx.graphics/src/main/java/javafx/scene/Scene.java @@ -2485,7 +2485,9 @@ private void synchronizeSceneNodes() { Scene.inSynchronizer = true; - peer.synchronizeOverlay(); + if (peer != null) { + peer.synchronizeOverlay(); + } // if dirtyNodes is null then that means this Scene has not yet been // synchronized, and so we will simply synchronize every node in the From f5e3121fa814b027f23981ecfb8cd3554d08bceb Mon Sep 17 00:00:00 2001 From: mstr2 <43553916+mstr2@users.noreply.github.com> Date: Tue, 29 Oct 2024 00:03:19 +0100 Subject: [PATCH 18/35] fix header bar height flicker --- .../src/main/java/com/sun/glass/ui/WindowControlsOverlay.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/WindowControlsOverlay.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/WindowControlsOverlay.java index ab476f16424..1ceb0f35860 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/WindowControlsOverlay.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/WindowControlsOverlay.java @@ -27,7 +27,6 @@ import com.sun.glass.events.MouseEvent; import com.sun.javafx.util.Utils; -import javafx.application.Platform; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.SimpleObjectProperty; @@ -430,8 +429,7 @@ protected void layoutChildren() { left ? HorizontalDirection.LEFT : HorizontalDirection.RIGHT, new Dimension2D(totalWidth, totalHeight)); - // Don't change the metrics during layout, since we don't know who might be listening. - Platform.runLater(() -> metrics.set(newMetrics)); + metrics.set(newMetrics); } layoutInArea(button1, button1X, 0, button1Width, button1Height, From 1c4ecc11b72c4a477a24dbeb6059f7a9af7c73ea Mon Sep 17 00:00:00 2001 From: mstr2 <43553916+mstr2@users.noreply.github.com> Date: Tue, 29 Oct 2024 06:13:54 +0100 Subject: [PATCH 19/35] macOS: dynamically adapt toolbar style to headerbar height --- .../main/java/com/sun/glass/ui/Window.java | 40 ++++++++++++++++- .../sun/glass/ui/WindowControlsOverlay.java | 5 ++- .../sun/glass/ui/WindowOverlayMetrics.java | 3 +- .../java/com/sun/glass/ui/mac/MacWindow.java | 45 ++++++++++++++++--- .../java/javafx/scene/layout/HeaderBar.java | 11 ++--- .../javafx/scene/layout/HeaderBarBase.java | 44 +++++++++++++++--- .../src/main/native-glass/mac/GlassWindow.m | 23 ++++++++++ 7 files changed, 147 insertions(+), 24 deletions(-) diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/Window.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/Window.java index 57e44b8ac89..605cdc68dd1 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/Window.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/Window.java @@ -29,13 +29,14 @@ import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ObservableValue; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; import javafx.scene.Parent; +import javafx.scene.layout.Region; +import javafx.util.Subscription; import java.lang.annotation.Native; import java.util.Collections; import java.util.LinkedList; import java.util.List; +import java.util.Objects; public abstract class Window { @@ -235,6 +236,7 @@ public static final class Level { private int maximumWidth = Integer.MAX_VALUE, maximumHeight = Integer.MAX_VALUE; private EventHandler eventHandler; + private Region headerBar; protected final ObjectProperty windowOverlayMetrics = new SimpleObjectProperty<>(this, "windowOverlayMetrics"); @@ -455,6 +457,40 @@ public NonClientHandler getNonClientHandler() { return null; } + /** + * Registers a user-provided header bar with this window. + *

+ * The registration will be accepted, but ignored if the window is not an extended + * window, or if another header bar is already registered. + * + * @param headerBar the header bar + * @return a {@code Subscription} to unregister + */ + public final Subscription registerHeaderBar(Region headerBar) { + Objects.requireNonNull(headerBar); + + if (!isExtendedWindow() || this.headerBar != null) { + return Subscription.EMPTY; + } + + this.headerBar = headerBar; + + Subscription subscription = headerBar.heightProperty().subscribe( + value -> onHeaderBarHeightChanged(value.doubleValue())); + + return () -> { + this.headerBar = null; + subscription.unsubscribe(); + }; + } + + /** + * Called when the height of a registered user-provided header bar has changed. + * + * @param height the height of the header bar + */ + protected void onHeaderBarHeightChanged(double height) {} + public boolean isDecorated() { Application.checkEventThread(); return this.isDecorated; diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/WindowControlsOverlay.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/WindowControlsOverlay.java index 1ceb0f35860..3289479e284 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/WindowControlsOverlay.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/WindowControlsOverlay.java @@ -171,7 +171,7 @@ public StyleableProperty getStyleableProperty(WindowControlsOverlay ove * The metrics (placement and size) of the window buttons. */ private final ObjectProperty metrics = new SimpleObjectProperty<>( - this, "metrics", new WindowOverlayMetrics(HorizontalDirection.RIGHT, new Dimension2D(0, 0))); + this, "metrics", new WindowOverlayMetrics(HorizontalDirection.RIGHT, new Dimension2D(0, 0), 0)); /** * Specifies the placement of the window buttons on the left or the right side of the window. @@ -424,10 +424,11 @@ protected void layoutChildren() { double totalHeight = Math.max(button1Height, Math.max(button2Height, button3Height)); Dimension2D currentSize = metrics.get().size(); + // Update the overlay metrics if they have changed. if (currentSize.getWidth() != totalWidth || currentSize.getHeight() != totalHeight) { var newMetrics = new WindowOverlayMetrics( left ? HorizontalDirection.LEFT : HorizontalDirection.RIGHT, - new Dimension2D(totalWidth, totalHeight)); + new Dimension2D(totalWidth, totalHeight), totalHeight); metrics.set(newMetrics); } diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/WindowOverlayMetrics.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/WindowOverlayMetrics.java index 15614172e27..95b90928476 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/WindowOverlayMetrics.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/WindowOverlayMetrics.java @@ -35,8 +35,9 @@ * * @param placement the placement of the window buttons * @param size the size of the window buttons + * @param minHeight the minimum height of the window buttons */ -public record WindowOverlayMetrics(HorizontalDirection placement, Dimension2D size) { +public record WindowOverlayMetrics(HorizontalDirection placement, Dimension2D size, double minHeight) { public WindowOverlayMetrics { Objects.requireNonNull(placement, "placement cannot be null"); diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/mac/MacWindow.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/mac/MacWindow.java index 9e0962ebc1e..0cd8bc94d82 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/mac/MacWindow.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/mac/MacWindow.java @@ -33,7 +33,6 @@ import com.sun.glass.ui.View; import com.sun.glass.ui.Window; import com.sun.glass.ui.WindowOverlayMetrics; -import javafx.beans.value.ObservableValue; import javafx.geometry.Dimension2D; import javafx.geometry.HorizontalDirection; import java.nio.ByteBuffer; @@ -50,6 +49,11 @@ final class MacWindow extends Window { protected MacWindow(Window owner, Screen screen, int styleMask) { super(owner, screen, styleMask); + + if (isExtendedWindow()) { + // The default window metrics correspond to a small toolbar style. + updateWindowOverlayMetrics(NSWindowToolbarStyle.SMALL); + } } @Override native protected long _createWindow(long ownerPtr, long screenPtr, int mask); @@ -182,15 +186,42 @@ public NonClientHandler getNonClientHandler() { private native boolean _isRightToLeftLayoutDirection(); + private native void _setToolbarStyle(long ptr, int style); + @Override - public ObservableValue getWindowOverlayMetrics() { - HorizontalDirection direction = _isRightToLeftLayoutDirection() - ? HorizontalDirection.RIGHT - : HorizontalDirection.LEFT; + protected void onHeaderBarHeightChanged(double height) { + var toolbarStyle = NSWindowToolbarStyle.ofHeight(height); + _setToolbarStyle(getRawHandle(), toolbarStyle.style); + updateWindowOverlayMetrics(toolbarStyle); + } + + private void updateWindowOverlayMetrics(NSWindowToolbarStyle toolbarStyle) { + windowOverlayMetrics.set(new WindowOverlayMetrics( + _isRightToLeftLayoutDirection() + ? HorizontalDirection.RIGHT + : HorizontalDirection.LEFT, + toolbarStyle.size, + NSWindowToolbarStyle.SMALL.size.getHeight())); + } + + private enum NSWindowToolbarStyle { + SMALL(68, 28, 1), // NSWindowToolbarStyleExpanded + MEDIUM(78, 38, 4), // NSWindowToolbarStyleUnifiedCompact + LARGE(90, 52, 3); // NSWindowToolbarStyleUnified - windowOverlayMetrics.set(new WindowOverlayMetrics(direction, new Dimension2D(78, 38))); + NSWindowToolbarStyle(double width, double height, int style) { + this.size = new Dimension2D(width, height); + this.style = style; + } - return windowOverlayMetrics; + final Dimension2D size; + final int style; + + static NSWindowToolbarStyle ofHeight(double height) { + if (height >= LARGE.size.getHeight()) return LARGE; + if (height >= MEDIUM.size.getHeight()) return MEDIUM; + return SMALL; + } } } diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBar.java b/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBar.java index 277f1f52af4..8c6220775fe 100644 --- a/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBar.java +++ b/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBar.java @@ -168,9 +168,7 @@ public HeaderBar() { // Inflate the minHeight property. This is important so that we can track whether a stylesheet or // user code changes the property value before we set it to the height of the native title bar. minHeightProperty(); - - leftSystemInsetProperty().subscribe(this::updateDefaultMinHeight); - rightSystemInsetProperty().subscribe(this::updateDefaultMinHeight); + minSystemHeightProperty().subscribe(this::updateMinHeight); } /** @@ -492,13 +490,12 @@ private Insets getNodeMargin(Node child) { return margin != null ? margin : Insets.EMPTY; } - private void updateDefaultMinHeight() { + private void updateMinHeight() { var minHeight = (StyleableDoubleProperty)minHeightProperty(); - // Only change the default minHeight if it was not set by a stylesheet or application code. + // Only change minHeight if it was not set by a stylesheet or application code. if (minHeight.getStyleOrigin() == null) { - double height = Math.max(getLeftSystemInset().getHeight(), getRightSystemInset().getHeight()); - ((StyleableDoubleProperty)minHeightProperty()).applyStyle(null, height); + ((StyleableDoubleProperty)minHeightProperty()).applyStyle(null, getMinSystemHeight()); } } diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBarBase.java b/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBarBase.java index 8a0049d00fb..81a53ddb487 100644 --- a/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBarBase.java +++ b/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBarBase.java @@ -28,6 +28,8 @@ import com.sun.glass.ui.WindowOverlayMetrics; import com.sun.javafx.stage.StageHelper; import com.sun.javafx.tk.quantum.WindowStage; +import javafx.beans.property.ReadOnlyDoubleProperty; +import javafx.beans.property.ReadOnlyDoubleWrapper; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.geometry.Dimension2D; import javafx.geometry.HorizontalDirection; @@ -110,10 +112,14 @@ private void onShowingChanged(boolean showing) { } } else if (getScene().getWindow() instanceof Stage stage && StageHelper.getPeer(stage) instanceof WindowStage windowStage) { - subscription = windowStage - .getPlatformWindow() - .getWindowOverlayMetrics() - .subscribe(this::onMetricsChanged); + subscription = Subscription.combine( + windowStage + .getPlatformWindow() + .getWindowOverlayMetrics() + .subscribe(this::onMetricsChanged), + windowStage + .getPlatformWindow() + .registerHeaderBar(this)); } } @@ -131,7 +137,11 @@ private void updateInsets() { if (currentFullScreen || currentMetrics == null) { leftSystemInset.set(new Dimension2D(0, 0)); rightSystemInset.set(new Dimension2D(0, 0)); - } else if (currentMetrics.placement() == HorizontalDirection.LEFT) { + minSystemHeight.set(0); + return; + } + + if (currentMetrics.placement() == HorizontalDirection.LEFT) { leftSystemInset.set(currentMetrics.size()); rightSystemInset.set(new Dimension2D(0, 0)); } else if (currentMetrics.placement() == HorizontalDirection.RIGHT) { @@ -141,6 +151,8 @@ private void updateInsets() { leftSystemInset.set(new Dimension2D(0, 0)); rightSystemInset.set(new Dimension2D(0, 0)); } + + minSystemHeight.set(currentMetrics.minHeight()); } /** @@ -190,4 +202,26 @@ public final ReadOnlyObjectWrapper rightSystemInsetProperty() { public final Dimension2D getRightSystemInset() { return rightSystemInset.get(); } + + /** + * The absolute minimum height of {@link #leftSystemInsetProperty() leftSystemInset} and + * {@link #rightSystemInsetProperty() rightSystemInset}. This is a platform-dependent value + * that a {@code HeaderBarBase} implementation can use to define a reasonable minimum height + * for the header bar area. + */ + private final ReadOnlyDoubleWrapper minSystemHeight = + new ReadOnlyDoubleWrapper(this, "minSystemHeight") { + @Override + protected void invalidated() { + requestLayout(); + } + }; + + public final ReadOnlyDoubleProperty minSystemHeightProperty() { + return minSystemHeight.getReadOnlyProperty(); + } + + public final double getMinSystemHeight() { + return minSystemHeight.get(); + } } diff --git a/modules/javafx.graphics/src/main/native-glass/mac/GlassWindow.m b/modules/javafx.graphics/src/main/native-glass/mac/GlassWindow.m index ccfcca4869d..d41b9c2cca0 100644 --- a/modules/javafx.graphics/src/main/native-glass/mac/GlassWindow.m +++ b/modules/javafx.graphics/src/main/native-glass/mac/GlassWindow.m @@ -1503,6 +1503,29 @@ static jlong _createWindowCommonDo(JNIEnv *env, jobject jWindow, jlong jOwnerPtr GLASS_CHECK_EXCEPTION(env); } +/* + * Class: com_sun_glass_ui_mac_MacWindow + * Method: _setToolbarStyle + * Signature: (JI)V + */ +JNIEXPORT void JNICALL Java_com_sun_glass_ui_mac_MacWindow__1setToolbarStyle +(JNIEnv *env, jobject jWindow, jlong jPtr, jint style) +{ + LOG("Java_com_sun_glass_ui_mac_MacWindow__1setToolbarStyle"); + if (!jPtr) return; + + GLASS_ASSERT_MAIN_JAVA_THREAD(env); + GLASS_POOL_ENTER; + { + GlassWindow *window = getGlassWindow(env, jPtr); + if (window && window->nsWindow) { + [window->nsWindow setToolbarStyle:style]; + } + } + GLASS_POOL_EXIT; + GLASS_CHECK_EXCEPTION(env); +} + /* * Class: com_sun_glass_ui_mac_MacWindow * Method: _isRightToLeftLayoutDirection From e7febc54e03d54ae6cf4f99d6226320e0f135373 Mon Sep 17 00:00:00 2001 From: mstr2 <43553916+mstr2@users.noreply.github.com> Date: Tue, 5 Nov 2024 01:53:49 +0100 Subject: [PATCH 20/35] fix mirroring/unmirroring of X coord in win-glass --- .../src/main/native-glass/win/GlassWindow.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/modules/javafx.graphics/src/main/native-glass/win/GlassWindow.cpp b/modules/javafx.graphics/src/main/native-glass/win/GlassWindow.cpp index 1f9707aaeaf..b2a4cd763f3 100644 --- a/modules/javafx.graphics/src/main/native-glass/win/GlassWindow.cpp +++ b/modules/javafx.graphics/src/main/native-glass/win/GlassWindow.cpp @@ -900,6 +900,14 @@ BOOL GlassWindow::HandleNCHitTestEvent(SHORT x, SHORT y, LRESULT& result) return FALSE; } + // Unmirror the X coordinate we send to JavaFX if this is a RTL window. + LONG style = ::GetWindowLong(GetHWND(), GWL_EXSTYLE); + if (style & WS_EX_LAYOUTRTL) { + RECT rect = {0}; + ::GetClientRect(GetHWND(), &rect); + pt.x = max(0, rect.right - rect.left) - pt.x; + } + JNIEnv* env = GetEnv(); jint res = env->CallIntMethod(m_grefThis, javaIDs.WinWindow.nonClientHitTest, pt.x, pt.y); CheckAndClearException(env); @@ -1239,6 +1247,14 @@ void GlassWindow::ShowSystemMenu(int x, int y) return; } + // Mirror the X coordinate we get from JavaFX if this is a RTL window. + LONG style = ::GetWindowLong(GetHWND(), GWL_EXSTYLE); + if (style & WS_EX_LAYOUTRTL) { + RECT rect = {0}; + ::GetClientRect(GetHWND(), &rect); + x = max(0, rect.right - rect.left) - x; + } + HMENU systemMenu = GetSystemMenu(GetHWND(), FALSE); bool maximized = placement.showCmd == SW_SHOWMAXIMIZED; From d1c388bffa03e11709c5a70d1486e8f8aa1ce9d5 Mon Sep 17 00:00:00 2001 From: mstr2 <43553916+mstr2@users.noreply.github.com> Date: Tue, 5 Nov 2024 02:28:04 +0100 Subject: [PATCH 21/35] stylistic changes --- .../main/java/com/sun/glass/ui/WindowControlsOverlay.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/WindowControlsOverlay.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/WindowControlsOverlay.java index 3289479e284..5cc42756b71 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/WindowControlsOverlay.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/WindowControlsOverlay.java @@ -336,7 +336,7 @@ private void handleMouseDown(Node node) { } private void handleMouseUp(Node node, ButtonType buttonType) { - boolean releasedOnButton = buttonAtMouseDown == node; + boolean releasedOnButton = (buttonAtMouseDown == node); buttonAtMouseDown = null; Scene scene = getScene(); @@ -357,9 +357,9 @@ private void handleMouseUp(Node node, ButtonType buttonType) { } private void onFocusedChanged(boolean focused) { - for (var node : new Node[] {minimizeButton, maximizeButton, closeButton}) { - node.pseudoClassStateChanged(ACTIVE_PSEUDOCLASS, focused); - } + minimizeButton.pseudoClassStateChanged(ACTIVE_PSEUDOCLASS, focused); + maximizeButton.pseudoClassStateChanged(ACTIVE_PSEUDOCLASS, focused); + closeButton.pseudoClassStateChanged(ACTIVE_PSEUDOCLASS, focused); } private void onResizableChanged(boolean resizable) { From 8c9fbbdfc00f30ec7056f68b41768d1dadd68237 Mon Sep 17 00:00:00 2001 From: mstr2 <43553916+mstr2@users.noreply.github.com> Date: Tue, 5 Nov 2024 22:00:01 +0100 Subject: [PATCH 22/35] use CsvSource in HeaderBarTest --- .../javafx/scene/layout/HeaderBarTest.java | 174 ++++++------------ 1 file changed, 53 insertions(+), 121 deletions(-) diff --git a/modules/javafx.graphics/src/test/java/test/javafx/scene/layout/HeaderBarTest.java b/modules/javafx.graphics/src/test/java/test/javafx/scene/layout/HeaderBarTest.java index 839f72c428c..43fe317ec97 100644 --- a/modules/javafx.graphics/src/test/java/test/javafx/scene/layout/HeaderBarTest.java +++ b/modules/javafx.graphics/src/test/java/test/javafx/scene/layout/HeaderBarTest.java @@ -35,7 +35,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; +import org.junit.jupiter.params.provider.CsvSource; import test.util.ReflectionUtils; import static org.junit.jupiter.api.Assertions.*; @@ -57,7 +57,7 @@ void emptyHeaderBar() { } @ParameterizedTest - @ValueSource(strings = { + @CsvSource({ "TOP_LEFT, 10, 10, 100, 80", "TOP_CENTER, 10, 10, 100, 80", "TOP_RIGHT, 10, 10, 100, 80", @@ -68,25 +68,19 @@ void emptyHeaderBar() { "BOTTOM_CENTER, 10, 10, 100, 80", "BOTTOM_RIGHT, 10, 10, 100, 80" }) - void alignmentOfLeadingChildOnly_resizable(String arg) { - String[] args = arg.split(","); + void alignmentOfLeadingChildOnly_resizable(Pos pos, double x, double y, double width, double height) { var content = new MockResizable(100, 50); - HeaderBar.setAlignment(content, Pos.valueOf(args[0])); + HeaderBar.setAlignment(content, pos); HeaderBar.setMargin(content, new Insets(10)); headerBar.setLeading(content); headerBar.resize(1000, 100); headerBar.layout(); - assertBounds( - Double.parseDouble(args[1]), - Double.parseDouble(args[2]), - Double.parseDouble(args[3]), - Double.parseDouble(args[4]), - content); + assertBounds(x, y, width, height, content); } @ParameterizedTest - @ValueSource(strings = { + @CsvSource({ "TOP_LEFT, 10, 10, 100, 50", "TOP_CENTER, 10, 10, 100, 50", "TOP_RIGHT, 10, 10, 100, 50", @@ -97,25 +91,19 @@ void alignmentOfLeadingChildOnly_resizable(String arg) { "BOTTOM_CENTER, 10, 40, 100, 50", "BOTTOM_RIGHT, 10, 40, 100, 50" }) - void alignmentOfLeadingChildOnly_notResizable(String arg) { - String[] args = arg.split(","); + void alignmentOfLeadingChildOnly_notResizable(Pos pos, double x, double y, double width, double height) { var content = new Rectangle(100, 50); - HeaderBar.setAlignment(content, Pos.valueOf(args[0])); + HeaderBar.setAlignment(content, pos); HeaderBar.setMargin(content, new Insets(10)); headerBar.setLeading(content); headerBar.resize(1000, 100); headerBar.layout(); - assertBounds( - Double.parseDouble(args[1]), - Double.parseDouble(args[2]), - Double.parseDouble(args[3]), - Double.parseDouble(args[4]), - content); + assertBounds(x, y, width, height, content); } @ParameterizedTest - @ValueSource(strings = { + @CsvSource({ "TOP_LEFT, 890, 10, 100, 80", "TOP_CENTER, 890, 10, 100, 80", "TOP_RIGHT, 890, 10, 100, 80", @@ -126,25 +114,19 @@ void alignmentOfLeadingChildOnly_notResizable(String arg) { "BOTTOM_CENTER, 890, 10, 100, 80", "BOTTOM_RIGHT, 890, 10, 100, 80" }) - void alignmentOfTrailingChildOnly_resizable(String arg) { - String[] args = arg.split(","); + void alignmentOfTrailingChildOnly_resizable(Pos pos, double x, double y, double width, double height) { var content = new MockResizable(100, 50); - HeaderBar.setAlignment(content, Pos.valueOf(args[0])); + HeaderBar.setAlignment(content, pos); HeaderBar.setMargin(content, new Insets(10)); headerBar.setTrailing(content); headerBar.resize(1000, 100); headerBar.layout(); - assertBounds( - Double.parseDouble(args[1]), - Double.parseDouble(args[2]), - Double.parseDouble(args[3]), - Double.parseDouble(args[4]), - content); + assertBounds(x, y, width, height, content); } @ParameterizedTest - @ValueSource(strings = { + @CsvSource({ "TOP_LEFT, 890, 10, 100, 50", "TOP_CENTER, 890, 10, 100, 50", "TOP_RIGHT, 890, 10, 100, 50", @@ -155,25 +137,19 @@ void alignmentOfTrailingChildOnly_resizable(String arg) { "BOTTOM_CENTER, 890, 40, 100, 50", "BOTTOM_RIGHT, 890, 40, 100, 50" }) - void alignmentOfTrailingChildOnly_notResizable(String arg) { - String[] args = arg.split(","); + void alignmentOfTrailingChildOnly_notResizable(Pos pos, double x, double y, double width, double height) { var content = new Rectangle(100, 50); - HeaderBar.setAlignment(content, Pos.valueOf(args[0])); + HeaderBar.setAlignment(content, pos); HeaderBar.setMargin(content, new Insets(10)); headerBar.setTrailing(content); headerBar.resize(1000, 100); headerBar.layout(); - assertBounds( - Double.parseDouble(args[1]), - Double.parseDouble(args[2]), - Double.parseDouble(args[3]), - Double.parseDouble(args[4]), - content); + assertBounds(x, y, width, height, content); } @ParameterizedTest - @ValueSource(strings = { + @CsvSource({ "TOP_LEFT, 10, 10, 100, 80", "TOP_CENTER, 450, 10, 100, 80", "TOP_RIGHT, 890, 10, 100, 80", @@ -184,25 +160,19 @@ void alignmentOfTrailingChildOnly_notResizable(String arg) { "BOTTOM_CENTER, 450, 10, 100, 80", "BOTTOM_RIGHT, 890, 10, 100, 80" }) - void alignmentOfCenterChildOnly_resizable(String arg) { - String[] args = arg.split(","); + void alignmentOfCenterChildOnly_resizable(Pos pos, double x, double y, double width, double height) { var content = new MockResizable(100, 50); - HeaderBar.setAlignment(content, Pos.valueOf(args[0])); + HeaderBar.setAlignment(content, pos); HeaderBar.setMargin(content, new Insets(10)); headerBar.setCenter(content); headerBar.resize(1000, 100); headerBar.layout(); - assertBounds( - Double.parseDouble(args[1]), - Double.parseDouble(args[2]), - Double.parseDouble(args[3]), - Double.parseDouble(args[4]), - content); + assertBounds(x, y, width, height, content); } @ParameterizedTest - @ValueSource(strings = { + @CsvSource({ "TOP_LEFT, 10, 10, 100, 50", "TOP_CENTER, 450, 10, 100, 50", "TOP_RIGHT, 890, 10, 100, 50", @@ -213,25 +183,19 @@ void alignmentOfCenterChildOnly_resizable(String arg) { "BOTTOM_CENTER, 450, 40, 100, 50", "BOTTOM_RIGHT, 890, 40, 100, 50" }) - void alignmentOfCenterChildOnly_notResizable(String arg) { - String[] args = arg.split(","); + void alignmentOfCenterChildOnly_notResizable(Pos pos, double x, double y, double width, double height) { var content = new Rectangle(100, 50); - HeaderBar.setAlignment(content, Pos.valueOf(args[0])); + HeaderBar.setAlignment(content, pos); HeaderBar.setMargin(content, new Insets(10)); headerBar.setCenter(content); headerBar.resize(1000, 100); headerBar.layout(); - assertBounds( - Double.parseDouble(args[1]), - Double.parseDouble(args[2]), - Double.parseDouble(args[3]), - Double.parseDouble(args[4]), - content); + assertBounds(x, y, width, height, content); } @ParameterizedTest - @ValueSource(strings = { + @CsvSource({ "TOP_LEFT, 60, 10, 100, 80", "TOP_CENTER, 450, 10, 100, 80", "TOP_RIGHT, 740, 10, 100, 80", @@ -242,12 +206,12 @@ void alignmentOfCenterChildOnly_notResizable(String arg) { "BOTTOM_CENTER, 450, 10, 100, 80", "BOTTOM_RIGHT, 740, 10, 100, 80" }) - void alignmentOfCenterChild_resizable_withNonEmptyLeadingAndTrailingChild(String arg) { - String[] args = arg.split(","); + void alignmentOfCenterChild_resizable_withNonEmptyLeadingAndTrailingChild( + Pos pos, double x, double y, double width, double height) { var leading = new MockResizable(50, 50); var center = new MockResizable(100, 50); var trailing = new MockResizable(150, 50); - HeaderBar.setAlignment(center, Pos.valueOf(args[0])); + HeaderBar.setAlignment(center, pos); HeaderBar.setMargin(center, new Insets(10)); headerBar.setLeading(leading); headerBar.setCenter(center); @@ -255,16 +219,11 @@ void alignmentOfCenterChild_resizable_withNonEmptyLeadingAndTrailingChild(String headerBar.resize(1000, 100); headerBar.layout(); - assertBounds( - Double.parseDouble(args[1]), - Double.parseDouble(args[2]), - Double.parseDouble(args[3]), - Double.parseDouble(args[4]), - center); + assertBounds(x, y, width, height, center); } @ParameterizedTest - @ValueSource(strings = { + @CsvSource({ "TOP_LEFT, 60, 10, 100, 50", "TOP_CENTER, 450, 10, 100, 50", "TOP_RIGHT, 740, 10, 100, 50", @@ -275,12 +234,12 @@ void alignmentOfCenterChild_resizable_withNonEmptyLeadingAndTrailingChild(String "BOTTOM_CENTER, 450, 40, 100, 50", "BOTTOM_RIGHT, 740, 40, 100, 50" }) - void alignmentOfCenterChild_notResizable_withNonEmptyLeadingAndTrailingChild(String arg) { - String[] args = arg.split(","); + void alignmentOfCenterChild_notResizable_withNonEmptyLeadingAndTrailingChild( + Pos pos, double x, double y, double width, double height) { var leading = new Rectangle(50, 50); var center = new Rectangle(100, 50); var trailing = new Rectangle(150, 50); - HeaderBar.setAlignment(center, Pos.valueOf(args[0])); + HeaderBar.setAlignment(center, pos); HeaderBar.setMargin(center, new Insets(10)); headerBar.setLeading(leading); headerBar.setCenter(center); @@ -288,17 +247,12 @@ void alignmentOfCenterChild_notResizable_withNonEmptyLeadingAndTrailingChild(Str headerBar.resize(1000, 100); headerBar.layout(); - assertBounds( - Double.parseDouble(args[1]), - Double.parseDouble(args[2]), - Double.parseDouble(args[3]), - Double.parseDouble(args[4]), - center); + assertBounds(x, y, width, height, center); } @ParameterizedTest @SuppressWarnings("unchecked") - @ValueSource(strings = { + @CsvSource({ "TOP_LEFT, 160, 10, 100, 80", "TOP_CENTER, 450, 10, 100, 80", "TOP_RIGHT, 740, 10, 100, 80", @@ -309,14 +263,13 @@ void alignmentOfCenterChild_notResizable_withNonEmptyLeadingAndTrailingChild(Str "BOTTOM_CENTER, 450, 10, 100, 80", "BOTTOM_RIGHT, 740, 10, 100, 80" }) - void alignmentOfCenterChild_withLeftSystemInset(String arg) { - String[] args = arg.split(","); + void alignmentOfCenterChild_withLeftSystemInset(Pos pos, double x, double y, double width, double height) { var leftSystemInset = (ObjectProperty)ReflectionUtils.getFieldValue(headerBar, "leftSystemInset"); leftSystemInset.set(new Dimension2D(100, 100)); var leading = new MockResizable(50, 50); var center = new MockResizable(100, 50); var trailing = new MockResizable(150, 50); - HeaderBar.setAlignment(center, Pos.valueOf(args[0])); + HeaderBar.setAlignment(center, pos); HeaderBar.setMargin(center, new Insets(10)); headerBar.setLeading(leading); headerBar.setCenter(center); @@ -324,17 +277,12 @@ void alignmentOfCenterChild_withLeftSystemInset(String arg) { headerBar.resize(1000, 100); headerBar.layout(); - assertBounds( - Double.parseDouble(args[1]), - Double.parseDouble(args[2]), - Double.parseDouble(args[3]), - Double.parseDouble(args[4]), - center); + assertBounds(x, y, width, height, center); } @ParameterizedTest @SuppressWarnings("unchecked") - @ValueSource(strings = { + @CsvSource({ "TOP_LEFT, 60, 10, 100, 80", "TOP_CENTER, 450, 10, 100, 80", "TOP_RIGHT, 640, 10, 100, 80", @@ -345,14 +293,13 @@ void alignmentOfCenterChild_withLeftSystemInset(String arg) { "BOTTOM_CENTER, 450, 10, 100, 80", "BOTTOM_RIGHT, 640, 10, 100, 80" }) - void alignmentOfCenterChild_withRightSystemInset(String arg) { - String[] args = arg.split(","); + void alignmentOfCenterChild_withRightSystemInset(Pos pos, double x, double y, double width, double height) { var rightSystemInset = (ObjectProperty)ReflectionUtils.getFieldValue(headerBar, "rightSystemInset"); rightSystemInset.set(new Dimension2D(100, 100)); var leading = new MockResizable(50, 50); var center = new MockResizable(100, 50); var trailing = new MockResizable(150, 50); - HeaderBar.setAlignment(center, Pos.valueOf(args[0])); + HeaderBar.setAlignment(center, pos); HeaderBar.setMargin(center, new Insets(10)); headerBar.setLeading(leading); headerBar.setCenter(center); @@ -360,29 +307,24 @@ void alignmentOfCenterChild_withRightSystemInset(String arg) { headerBar.resize(1000, 100); headerBar.layout(); - assertBounds( - Double.parseDouble(args[1]), - Double.parseDouble(args[2]), - Double.parseDouble(args[3]), - Double.parseDouble(args[4]), - center); + assertBounds(x, y, width, height, center); } @ParameterizedTest @SuppressWarnings("unchecked") - @ValueSource(strings = { + @CsvSource({ "TOP_CENTER, 260, 10, 80, 80", "CENTER, 260, 10, 80, 80", "BOTTOM_CENTER, 260, 10, 80, 80" }) - void alignmentOfCenterChild_withLeftSystemInset_andOffsetCausedByInsufficientHorizontalSpace(String arg) { - String[] args = arg.split(","); + void alignmentOfCenterChild_withLeftSystemInset_andOffsetCausedByInsufficientHorizontalSpace( + Pos pos, double x, double y, double width, double height) { var leftSystemInset = (ObjectProperty)ReflectionUtils.getFieldValue(headerBar, "leftSystemInset"); leftSystemInset.set(new Dimension2D(200, 100)); var leading = new MockResizable(50, 50); var center = new MockResizable(100, 50); var trailing = new MockResizable(150, 50); - HeaderBar.setAlignment(center, Pos.valueOf(args[0])); + HeaderBar.setAlignment(center, pos); HeaderBar.setMargin(center, new Insets(10)); headerBar.setLeading(leading); headerBar.setCenter(center); @@ -390,29 +332,24 @@ void alignmentOfCenterChild_withLeftSystemInset_andOffsetCausedByInsufficientHor headerBar.resize(500, 100); headerBar.layout(); - assertBounds( - Double.parseDouble(args[1]), - Double.parseDouble(args[2]), - Double.parseDouble(args[3]), - Double.parseDouble(args[4]), - center); + assertBounds(x, y, width, height, center); } @ParameterizedTest @SuppressWarnings("unchecked") - @ValueSource(strings = { + @CsvSource({ "TOP_CENTER, 60, 10, 80, 80", "CENTER, 60, 10, 80, 80", "BOTTOM_CENTER, 60, 10, 80, 80" }) - void alignmentOfCenterChild_withRightSystemInset_andOffsetCausedByInsufficientHorizontalSpace(String arg) { - String[] args = arg.split(","); + void alignmentOfCenterChild_withRightSystemInset_andOffsetCausedByInsufficientHorizontalSpace( + Pos pos, double x, double y, double width, double height) { var rightSystemInset = (ObjectProperty)ReflectionUtils.getFieldValue(headerBar, "rightSystemInset"); rightSystemInset.set(new Dimension2D(200, 100)); var leading = new MockResizable(50, 50); var center = new MockResizable(100, 50); var trailing = new MockResizable(150, 50); - HeaderBar.setAlignment(center, Pos.valueOf(args[0])); + HeaderBar.setAlignment(center, pos); HeaderBar.setMargin(center, new Insets(10)); headerBar.setLeading(leading); headerBar.setCenter(center); @@ -420,12 +357,7 @@ void alignmentOfCenterChild_withRightSystemInset_andOffsetCausedByInsufficientHo headerBar.resize(500, 100); headerBar.layout(); - assertBounds( - Double.parseDouble(args[1]), - Double.parseDouble(args[2]), - Double.parseDouble(args[3]), - Double.parseDouble(args[4]), - center); + assertBounds(x, y, width, height, center); } private void assertBounds(double x, double y, double width, double height, Node node) { From 3660a29325dbc08831845f7026b94be6caa381e5 Mon Sep 17 00:00:00 2001 From: mstr2 <43553916+mstr2@users.noreply.github.com> Date: Tue, 5 Nov 2024 22:02:42 +0100 Subject: [PATCH 23/35] EMPTY Dimension2D constant --- .../java/javafx/scene/layout/HeaderBarBase.java | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBarBase.java b/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBarBase.java index 81a53ddb487..37264927bb7 100644 --- a/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBarBase.java +++ b/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBarBase.java @@ -55,6 +55,7 @@ */ public abstract class HeaderBarBase extends Region { + private static final Dimension2D EMPTY = new Dimension2D(0, 0); private static final String DRAGGABLE = "headerbar-draggable"; /** @@ -135,21 +136,21 @@ private void onFullScreenChanged(boolean fullScreen) { private void updateInsets() { if (currentFullScreen || currentMetrics == null) { - leftSystemInset.set(new Dimension2D(0, 0)); - rightSystemInset.set(new Dimension2D(0, 0)); + leftSystemInset.set(EMPTY); + rightSystemInset.set(EMPTY); minSystemHeight.set(0); return; } if (currentMetrics.placement() == HorizontalDirection.LEFT) { leftSystemInset.set(currentMetrics.size()); - rightSystemInset.set(new Dimension2D(0, 0)); + rightSystemInset.set(EMPTY); } else if (currentMetrics.placement() == HorizontalDirection.RIGHT) { - leftSystemInset.set(new Dimension2D(0, 0)); + leftSystemInset.set(EMPTY); rightSystemInset.set(currentMetrics.size()); } else { - leftSystemInset.set(new Dimension2D(0, 0)); - rightSystemInset.set(new Dimension2D(0, 0)); + leftSystemInset.set(EMPTY); + rightSystemInset.set(EMPTY); } minSystemHeight.set(currentMetrics.minHeight()); @@ -188,7 +189,7 @@ public final Dimension2D getLeftSystemInset() { * independent of layout orientation. */ private final ReadOnlyObjectWrapper rightSystemInset = - new ReadOnlyObjectWrapper<>(this, "rightInset", new Dimension2D(0, 0)) { + new ReadOnlyObjectWrapper<>(this, "rightInset", EMPTY) { @Override protected void invalidated() { requestLayout(); From 8974c1413a299956f4a38f784a89f7a4491a4af8 Mon Sep 17 00:00:00 2001 From: mstr2 <43553916+mstr2@users.noreply.github.com> Date: Tue, 5 Nov 2024 22:28:59 +0100 Subject: [PATCH 24/35] HeaderBar changes --- .../java/javafx/scene/layout/HeaderBar.java | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBar.java b/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBar.java index 8c6220775fe..abaf328d59d 100644 --- a/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBar.java +++ b/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBar.java @@ -54,9 +54,10 @@ * of leading or trailing children or the platform-specific placement of default window buttons. *

* All children will be resized to their preferred widths and extend the height of the {@code HeaderBar}. - * {@code HeaderBar} honors the minimum, preferred, and maximum sizes of its children. If the child's - * resizable range prevents it from be resized to fit within its position, it will be vertically centered - * relative to the available space; this alignment can be customized with a layout constraint. + * {@code HeaderBar} honors the minimum, preferred, and maximum sizes of its children. As a consequence, + * its computed minimum width is sufficient to accommodate all of its children. If a child's resizable + * range prevents it from be resized to fit within its position, it will be vertically centered relative + * to the available space; this alignment can be customized with a layout constraint. *

* The default {@link #minHeightProperty() minHeight} of the {@code HeaderBar} is set to match the height * of the platform-specific default window buttons. @@ -259,7 +260,9 @@ public final void setTrailing(Node value) { @Override protected double computeMinWidth(double height) { - Node leading = getLeading(), center = getCenter(), trailing = getTrailing(); + Node leading = getLeading(); + Node center = getCenter(); + Node trailing = getTrailing(); Insets insets = getInsets(); double leftPrefWidth; double rightPrefWidth; @@ -290,7 +293,9 @@ protected double computeMinWidth(double height) { @Override protected double computeMinHeight(double width) { - Node leading = getLeading(), center = getCenter(), trailing = getTrailing(); + Node leading = getLeading(); + Node center = getCenter(); + Node trailing = getTrailing(); Insets insets = getInsets(); double leadingMinHeight = getAreaHeight(leading, -1, true); double trailingMinHeight = getAreaHeight(trailing, -1, true); @@ -311,7 +316,9 @@ protected double computeMinHeight(double width) { @Override protected double computePrefHeight(double width) { - Node leading = getLeading(), center = getCenter(), trailing = getTrailing(); + Node leading = getLeading(); + Node center = getCenter(); + Node trailing = getTrailing(); Insets insets = getInsets(); double leadingPrefHeight = getAreaHeight(leading, -1, false); double trailingPrefHeight = getAreaHeight(trailing, -1, false); From ca9b325ad503cf6ab8da3ed9a939ae8b4fd67638 Mon Sep 17 00:00:00 2001 From: mstr2 <43553916+mstr2@users.noreply.github.com> Date: Wed, 6 Nov 2024 19:33:39 +0100 Subject: [PATCH 25/35] refactor performWindowDrag --- .../src/main/native-glass/mac/GlassViewDelegate.h | 4 ++-- .../src/main/native-glass/mac/GlassViewDelegate.m | 12 +++++++----- .../src/main/native-glass/mac/GlassWindow.m | 3 +-- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/modules/javafx.graphics/src/main/native-glass/mac/GlassViewDelegate.h b/modules/javafx.graphics/src/main/native-glass/mac/GlassViewDelegate.h index 191cc55cc93..cc3b7387246 100644 --- a/modules/javafx.graphics/src/main/native-glass/mac/GlassViewDelegate.h +++ b/modules/javafx.graphics/src/main/native-glass/mac/GlassViewDelegate.h @@ -102,6 +102,8 @@ typedef enum GestureMaskType { - (BOOL)suppressMouseEnterExitOnMouseDown; +- (void)performWindowDrag; + - (void)enterFullscreenWithAnimate:(BOOL)animate withKeepRatio:(BOOL)keepRatio withHideCursor:(BOOL)hideCursor; - (void)exitFullscreenWithAnimate:(BOOL)animate; - (void)sendJavaFullScreenEvent:(BOOL)entered withNativeWidget:(BOOL)isNative; @@ -115,6 +117,4 @@ typedef enum GestureMaskType { - (GlassAccessible*)getAccessible; -- (NSEvent*)lastEvent; - @end diff --git a/modules/javafx.graphics/src/main/native-glass/mac/GlassViewDelegate.m b/modules/javafx.graphics/src/main/native-glass/mac/GlassViewDelegate.m index 1b443d7bbd0..244b6b70989 100644 --- a/modules/javafx.graphics/src/main/native-glass/mac/GlassViewDelegate.m +++ b/modules/javafx.graphics/src/main/native-glass/mac/GlassViewDelegate.m @@ -1143,6 +1143,13 @@ - (BOOL)suppressMouseEnterExitOnMouseDown return YES; } +- (void)performWindowDrag +{ + if (lastEvent != nil) { + [[nsView window] performWindowDragWithEvent:lastEvent]; + } +} + static jstring convertNSStringToJString(id aString, int length) { GET_MAIN_JENV; @@ -1333,9 +1340,4 @@ - (GlassAccessible*)getAccessible return (GlassAccessible*)jlong_to_ptr(accessible); } -- (NSEvent*)lastEvent -{ - return lastEvent; -} - @end diff --git a/modules/javafx.graphics/src/main/native-glass/mac/GlassWindow.m b/modules/javafx.graphics/src/main/native-glass/mac/GlassWindow.m index d41b9c2cca0..19c62bb5b8f 100644 --- a/modules/javafx.graphics/src/main/native-glass/mac/GlassWindow.m +++ b/modules/javafx.graphics/src/main/native-glass/mac/GlassWindow.m @@ -1496,8 +1496,7 @@ static jlong _createWindowCommonDo(JNIEnv *env, jobject jWindow, jlong jOwnerPtr GLASS_POOL_ENTER; { GlassWindow *window = getGlassWindow(env, jPtr); - GlassViewDelegate* delegate = [window->view delegate]; - [window->nsWindow performWindowDragWithEvent:[delegate lastEvent]]; + [[window->view delegate] performWindowDrag]; } GLASS_POOL_EXIT; GLASS_CHECK_EXCEPTION(env); From 8e77a220efd03ed9fa155dde9b98c57aa10cc787 Mon Sep 17 00:00:00 2001 From: mstr2 <43553916+mstr2@users.noreply.github.com> Date: Thu, 7 Nov 2024 04:29:20 +0100 Subject: [PATCH 26/35] HeaderBar javadoc change --- .../main/java/javafx/scene/layout/HeaderBar.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBar.java b/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBar.java index abaf328d59d..9ccdb5d15e6 100644 --- a/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBar.java +++ b/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBar.java @@ -47,15 +47,16 @@ *

* {@code HeaderBar} is a layout container that allows applications to place scene graph nodes * in three areas: {@link #leadingProperty() leading}, {@link #centerProperty() center}, and - * {@link #trailingProperty() trailing}. {@code HeaderBar} ensures that the leading and trailing areas - * account for the default window buttons (minimize, maximize, close). If a child is configured to be - * centered in the {@code center} area, it is laid out with respect to the stage, and not with respect - * to the {@code center} area. This ensures that the child will appear centered in the stage regardless - * of leading or trailing children or the platform-specific placement of default window buttons. + * {@link #trailingProperty() trailing}. All areas can be {@code null}. {@code HeaderBar} ensures that + * the leading and trailing areas account for the default window buttons (minimize, maximize, close). + * If a child is configured to be centered in the {@code center} area, it is laid out with respect to + * the stage, and not with respect to the {@code center} area. This ensures that the child will appear + * centered in the stage regardless of leading or trailing children or the platform-specific placement + * of default window buttons. *

* All children will be resized to their preferred widths and extend the height of the {@code HeaderBar}. * {@code HeaderBar} honors the minimum, preferred, and maximum sizes of its children. As a consequence, - * its computed minimum width is sufficient to accommodate all of its children. If a child's resizable + * its computed minimum size is sufficient to accommodate all of its children. If a child's resizable * range prevents it from be resized to fit within its position, it will be vertically centered relative * to the available space; this alignment can be customized with a layout constraint. *

From 4336735a457c2318b527ef77a153d9ab6c54fe9a Mon Sep 17 00:00:00 2001 From: mstr2 <43553916+mstr2@users.noreply.github.com> Date: Thu, 7 Nov 2024 04:52:54 +0100 Subject: [PATCH 27/35] WindowControlsOverlay snapping --- .../sun/glass/ui/WindowControlsOverlay.java | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/WindowControlsOverlay.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/WindowControlsOverlay.java index 5cc42756b71..93ee8e07e89 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/WindowControlsOverlay.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/WindowControlsOverlay.java @@ -45,6 +45,7 @@ import javafx.geometry.Dimension2D; import javafx.geometry.HPos; import javafx.geometry.HorizontalDirection; +import javafx.geometry.Insets; import javafx.geometry.NodeOrientation; import javafx.geometry.VPos; import javafx.scene.Node; @@ -411,17 +412,17 @@ protected void layoutChildren() { } double width = getWidth(); - double button1Width = boundedWidth(button1); - double button2Width = boundedWidth(button2); - double button3Width = boundedWidth(button3); - double button1Height = boundedHeight(button1); - double button2Height = boundedHeight(button2); - double button3Height = boundedHeight(button3); - double button1X = left ? 0 : width - button1Width - button2Width - button3Width; - double button2X = left ? button1Width : width - button3Width - button2Width; - double button3X = left ? button1Width + button2Width : width - button3Width; - double totalWidth = button1Width + button2Width + button3Width; - double totalHeight = Math.max(button1Height, Math.max(button2Height, button3Height)); + double button1Width = snapSizeX(boundedWidth(button1)); + double button2Width = snapSizeX(boundedWidth(button2)); + double button3Width = snapSizeX(boundedWidth(button3)); + double button1Height = snapSizeY(boundedHeight(button1)); + double button2Height = snapSizeY(boundedHeight(button2)); + double button3Height = snapSizeY(boundedHeight(button3)); + double button1X = snapPositionX(left ? 0 : width - button1Width - button2Width - button3Width); + double button2X = snapPositionX(left ? button1Width : width - button3Width - button2Width); + double button3X = snapPositionX(left ? button1Width + button2Width : width - button3Width); + double totalWidth = snapSizeX(button1Width + button2Width + button3Width); + double totalHeight = snapSizeY(Math.max(button1Height, Math.max(button2Height, button3Height))); Dimension2D currentSize = metrics.get().size(); // Update the overlay metrics if they have changed. @@ -433,14 +434,14 @@ protected void layoutChildren() { metrics.set(newMetrics); } - layoutInArea(button1, button1X, 0, button1Width, button1Height, - BASELINE_OFFSET_SAME_AS_HEIGHT, HPos.LEFT, VPos.TOP); + layoutInArea(button1, button1X, 0, button1Width, button1Height, BASELINE_OFFSET_SAME_AS_HEIGHT, + Insets.EMPTY, true, true, HPos.LEFT, VPos.TOP, false); - layoutInArea(button2, button2X, 0, button2Width, button2Height, - BASELINE_OFFSET_SAME_AS_HEIGHT, HPos.LEFT, VPos.TOP); + layoutInArea(button2, button2X, 0, button2Width, button2Height, BASELINE_OFFSET_SAME_AS_HEIGHT, + Insets.EMPTY, true, true, HPos.LEFT, VPos.TOP, false); - layoutInArea(button3, button3X, 0, button3Width, button3Height, - BASELINE_OFFSET_SAME_AS_HEIGHT, HPos.LEFT, VPos.TOP); + layoutInArea(button3, button3X, 0, button3Width, button3Height, BASELINE_OFFSET_SAME_AS_HEIGHT, + Insets.EMPTY, true, true, HPos.LEFT, VPos.TOP, false); } @Override From a9178b78a2d2ce0b49ee80e317095ad30e7b0cf1 Mon Sep 17 00:00:00 2001 From: mstr2 <43553916+mstr2@users.noreply.github.com> Date: Thu, 7 Nov 2024 05:08:05 +0100 Subject: [PATCH 28/35] add system menu documentation --- .../src/main/java/javafx/scene/layout/HeaderBar.java | 7 ++++++- .../main/java/javafx/scene/layout/HeaderBarBase.java | 11 ++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBar.java b/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBar.java index 9ccdb5d15e6..d3c94f2d5f7 100644 --- a/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBar.java +++ b/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBar.java @@ -36,15 +36,20 @@ import javafx.geometry.Pos; import javafx.geometry.VPos; import javafx.scene.Node; +import javafx.scene.input.ContextMenuEvent; import javafx.stage.StageStyle; /** * A client-area header bar that is used as a replacement for the system-provided header bar in stages - * with the {@link StageStyle#EXTENDED} style. This class enables the click-and-drag and + * with the {@link StageStyle#EXTENDED} style. This class enables the click-and-drag to move and * double-click to maximize behaviors that are usually afforded by system-provided header bars. * The entire {@code HeaderBar} background is draggable by default, but its content is not. Applications * can specify draggable content nodes of the {@code HeaderBar} with the {@link #setDraggable} method. *

+ * Some platforms support a system menu that can be summoned by right-clicking the draggable area. + * This platform-provided menu will only be shown if the {@link ContextMenuEvent#CONTEXT_MENU_REQUESTED} + * event that is targeted at {@code HeaderBarBase} is not consumed by the application. + *

* {@code HeaderBar} is a layout container that allows applications to place scene graph nodes * in three areas: {@link #leadingProperty() leading}, {@link #centerProperty() center}, and * {@link #trailingProperty() trailing}. All areas can be {@code null}. {@code HeaderBar} ensures that diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBarBase.java b/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBarBase.java index 37264927bb7..896dae17d9d 100644 --- a/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBarBase.java +++ b/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBarBase.java @@ -35,18 +35,23 @@ import javafx.geometry.HorizontalDirection; import javafx.scene.Node; import javafx.scene.Scene; +import javafx.scene.input.ContextMenuEvent; import javafx.stage.Stage; import javafx.stage.StageStyle; import javafx.stage.Window; import javafx.util.Subscription; /** - * Base class for a client-area header bar that is used as a replacement for the system-provided header bar - * in stages with the {@link StageStyle#EXTENDED} style. This class is intended for application developers - * to use as a starting point for custom header bar implementations, and it enables the click-and-drag + * Base class for a client-area header bar that is used as a replacement for the system-provided header bar in + * stages with the {@link StageStyle#EXTENDED} style. This class is intended for application developers to use + * as a starting point for custom header bar implementations, and it enables the click-and-drag to move * and double-click to maximize behaviors that are usually afforded by system-provided header bars. * The entire {@code HeaderBarBase} background is draggable by default, but its content is not. Applications * can specify draggable content nodes of the {@code HeaderBarBase} with the {@link #setDraggable} method. + *

+ * Some platforms support a system menu that can be summoned by right-clicking the draggable area. + * This platform-provided menu will only be shown if the {@link ContextMenuEvent#CONTEXT_MENU_REQUESTED} + * event that is targeted at {@code HeaderBarBase} is not consumed by the application. * * @apiNote Most application developers should use the {@link HeaderBar} implementation instead of * creating a custom header bar. From 26b81b858f68d9b752a0f88ad3de9bc717396d36 Mon Sep 17 00:00:00 2001 From: mstr2 <43553916+mstr2@users.noreply.github.com> Date: Thu, 14 Nov 2024 18:06:48 +0100 Subject: [PATCH 29/35] remove unneeded dll --- buildSrc/win.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/buildSrc/win.gradle b/buildSrc/win.gradle index db34da5ad61..83a97645a27 100644 --- a/buildSrc/win.gradle +++ b/buildSrc/win.gradle @@ -324,9 +324,9 @@ WIN.glass.rcFlags = [ WIN.glass.ccFlags = [ccFlags].flatten() WIN.glass.linker = linker WIN.glass.linkFlags = (IS_STATIC_BUILD ? [linkFlags] : [linkFlags, "delayimp.lib", "gdi32.lib", "urlmon.lib", "Comdlg32.lib", - "winmm.lib", "imm32.lib", "shell32.lib", "Uiautomationcore.lib", "dwmapi.lib", "uxtheme.lib", + "winmm.lib", "imm32.lib", "shell32.lib", "Uiautomationcore.lib", "dwmapi.lib", "/DELAYLOAD:user32.dll", "/DELAYLOAD:urlmon.dll", "/DELAYLOAD:winmm.dll", "/DELAYLOAD:shell32.dll", - "/DELAYLOAD:Uiautomationcore.dll", "/DELAYLOAD:dwmapi.dll", "/DELAYLOAD:uxtheme.dll"]).flatten() + "/DELAYLOAD:Uiautomationcore.dll", "/DELAYLOAD:dwmapi.dll"]).flatten() WIN.glass.lib = "glass" WIN.decora = [:] From 003e9d5628cb916a4eb1ad21b6b803aadf35ae50 Mon Sep 17 00:00:00 2001 From: mstr2 <43553916+mstr2@users.noreply.github.com> Date: Thu, 14 Nov 2024 18:46:04 +0100 Subject: [PATCH 30/35] macOS: double-click action + fullscreen toolbar --- .../java/com/sun/glass/ui/mac/MacWindow.java | 4 ++- .../main/native-glass/mac/GlassViewDelegate.m | 4 +-- .../native-glass/mac/GlassWindow+Overrides.m | 8 ++++++ .../src/main/native-glass/mac/GlassWindow.m | 27 +++++++++++++++++++ 4 files changed, 39 insertions(+), 4 deletions(-) diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/mac/MacWindow.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/mac/MacWindow.java index 0cd8bc94d82..94eaa640957 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/mac/MacWindow.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/mac/MacWindow.java @@ -163,6 +163,8 @@ protected void _releaseInput(long ptr) { private native void _performWindowDrag(long ptr); + private native void _performTitleBarDoubleClickAction(long ptr); + @Override public NonClientHandler getNonClientHandler() { return (type, button, x, y, xAbs, yAbs, clickCount) -> { @@ -173,7 +175,7 @@ public NonClientHandler getNonClientHandler() { View.EventHandler eventHandler = view != null ? view.getEventHandler() : null; if (eventHandler != null && eventHandler.pickDragAreaNode(wx, wy) != null) { if (clickCount == 2) { - maximize(!isMaximized()); + _performTitleBarDoubleClickAction(getRawHandle()); } else if (clickCount == 1) { _performWindowDrag(getRawHandle()); } diff --git a/modules/javafx.graphics/src/main/native-glass/mac/GlassViewDelegate.m b/modules/javafx.graphics/src/main/native-glass/mac/GlassViewDelegate.m index 244b6b70989..a3c466890fc 100644 --- a/modules/javafx.graphics/src/main/native-glass/mac/GlassViewDelegate.m +++ b/modules/javafx.graphics/src/main/native-glass/mac/GlassViewDelegate.m @@ -1145,9 +1145,7 @@ - (BOOL)suppressMouseEnterExitOnMouseDown - (void)performWindowDrag { - if (lastEvent != nil) { - [[nsView window] performWindowDragWithEvent:lastEvent]; - } + [[nsView window] performWindowDragWithEvent:[NSApp currentEvent]]; } static jstring convertNSStringToJString(id aString, int length) diff --git a/modules/javafx.graphics/src/main/native-glass/mac/GlassWindow+Overrides.m b/modules/javafx.graphics/src/main/native-glass/mac/GlassWindow+Overrides.m index 982a5b295da..e4b14661f44 100644 --- a/modules/javafx.graphics/src/main/native-glass/mac/GlassWindow+Overrides.m +++ b/modules/javafx.graphics/src/main/native-glass/mac/GlassWindow+Overrides.m @@ -204,6 +204,10 @@ - (void)windowWillEnterFullScreen:(NSNotification *)notification NSUInteger mask = [self->nsWindow styleMask]; self->isWindowResizable = ((mask & NSWindowStyleMaskResizable) != 0); [[self->view delegate] setResizableForFullscreen:YES]; + + if (nsWindow.toolbar != nil) { + nsWindow.toolbar.visible = NO; + } } - (void)windowDidEnterFullScreen:(NSNotification *)notification @@ -222,6 +226,10 @@ - (void)windowDidExitFullScreen:(NSNotification *)notification { //NSLog(@"windowDidExitFullScreen"); + if (nsWindow.toolbar != nil) { + nsWindow.toolbar.visible = YES; + } + GlassViewDelegate* delegate = (GlassViewDelegate*)[self->view delegate]; [delegate setResizableForFullscreen:self->isWindowResizable]; diff --git a/modules/javafx.graphics/src/main/native-glass/mac/GlassWindow.m b/modules/javafx.graphics/src/main/native-glass/mac/GlassWindow.m index 19c62bb5b8f..8daf919708b 100644 --- a/modules/javafx.graphics/src/main/native-glass/mac/GlassWindow.m +++ b/modules/javafx.graphics/src/main/native-glass/mac/GlassWindow.m @@ -1502,6 +1502,33 @@ static jlong _createWindowCommonDo(JNIEnv *env, jobject jWindow, jlong jOwnerPtr GLASS_CHECK_EXCEPTION(env); } +/* + * Class: com_sun_glass_ui_mac_MacWindow + * Method: _performTitleBarDoubleClickAction + * Signature: (J)V + */ +JNIEXPORT void JNICALL Java_com_sun_glass_ui_mac_MacWindow__1performTitleBarDoubleClickAction +(JNIEnv *env, jobject jWindow, jlong jPtr) +{ + LOG("Java_com_sun_glass_ui_mac_MacWindow__1performTitleBarDoubleClickAction"); + if (!jPtr) return; + + GLASS_ASSERT_MAIN_JAVA_THREAD(env); + GLASS_POOL_ENTER; + { + GlassWindow *window = getGlassWindow(env, jPtr); + NSString* action = [NSUserDefaults.standardUserDefaults stringForKey:@"AppleActionOnDoubleClick"]; + + if ([action isEqualToString:@"Minimize"]) { + [window->nsWindow performMiniaturize:nil]; + } else if ([action isEqualToString:@"Maximize"]) { + [window->nsWindow performZoom:nil]; + } + } + GLASS_POOL_EXIT; + GLASS_CHECK_EXCEPTION(env); +} + /* * Class: com_sun_glass_ui_mac_MacWindow * Method: _setToolbarStyle From d9b82c853a97cf70fb0bb1aa05320fccf3fd1cbd Mon Sep 17 00:00:00 2001 From: mstr2 <43553916+mstr2@users.noreply.github.com> Date: Fri, 6 Dec 2024 17:23:42 +0100 Subject: [PATCH 31/35] Add HeaderBar.overlappingSystemInset property --- .../java/javafx/scene/layout/HeaderBar.java | 168 +++++++++++++----- .../javafx/scene/layout/HeaderBarBase.java | 8 +- .../javafx/scene/layout/HeaderBarTest.java | 52 +++++- 3 files changed, 178 insertions(+), 50 deletions(-) diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBar.java b/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBar.java index d3c94f2d5f7..93935fc1c62 100644 --- a/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBar.java +++ b/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBar.java @@ -28,6 +28,10 @@ import com.sun.javafx.geom.Vec2d; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectPropertyBase; +import javafx.css.CssMetaData; +import javafx.css.StyleConverter; +import javafx.css.Styleable; +import javafx.css.StyleableBooleanProperty; import javafx.css.StyleableDoubleProperty; import javafx.geometry.HPos; import javafx.geometry.Insets; @@ -38,6 +42,8 @@ import javafx.scene.Node; import javafx.scene.input.ContextMenuEvent; import javafx.stage.StageStyle; +import java.util.List; +import java.util.stream.Stream; /** * A client-area header bar that is used as a replacement for the system-provided header bar in stages @@ -48,25 +54,24 @@ *

* Some platforms support a system menu that can be summoned by right-clicking the draggable area. * This platform-provided menu will only be shown if the {@link ContextMenuEvent#CONTEXT_MENU_REQUESTED} - * event that is targeted at {@code HeaderBarBase} is not consumed by the application. + * event that is targeted at the header bar is not consumed by the application. *

- * {@code HeaderBar} is a layout container that allows applications to place scene graph nodes - * in three areas: {@link #leadingProperty() leading}, {@link #centerProperty() center}, and - * {@link #trailingProperty() trailing}. All areas can be {@code null}. {@code HeaderBar} ensures that - * the leading and trailing areas account for the default window buttons (minimize, maximize, close). - * If a child is configured to be centered in the {@code center} area, it is laid out with respect to - * the stage, and not with respect to the {@code center} area. This ensures that the child will appear - * centered in the stage regardless of leading or trailing children or the platform-specific placement - * of default window buttons. + * {@code HeaderBar} is a layout container that allows applications to place scene graph nodes in three areas: + * {@link #leadingProperty() leading}, {@link #centerProperty() center}, and {@link #trailingProperty() trailing}. + * All areas can be {@code null}. The {@link #overlappingSystemInsetProperty() overlappingSystemInset} property + * controls whether the leading and trailing areas account for the default window buttons (minimize, maximize, + * close). If a child is configured to be centered in the {@code center} area, it is laid out with respect to the + * stage, and not with respect to the {@code center} area. This ensures that the child will appear centered in the + * stage regardless of leading or trailing children or the platform-specific placement of default window buttons. *

* All children will be resized to their preferred widths and extend the height of the {@code HeaderBar}. - * {@code HeaderBar} honors the minimum, preferred, and maximum sizes of its children. As a consequence, - * its computed minimum size is sufficient to accommodate all of its children. If a child's resizable - * range prevents it from be resized to fit within its position, it will be vertically centered relative - * to the available space; this alignment can be customized with a layout constraint. + * {@code HeaderBar} honors the minimum, preferred, and maximum sizes of its children. As a consequence, its + * computed minimum size is sufficient to accommodate all of its children. If a child's resizable range prevents + * it from be resized to fit within its position, it will be vertically centered relative to the available space; + * this alignment can be customized with a layout constraint. *

- * The default {@link #minHeightProperty() minHeight} of the {@code HeaderBar} is set to match the height - * of the platform-specific default window buttons. + * The default {@link #minHeightProperty() minHeight} of the {@code HeaderBar} is set to match the height of the + * platform-specific default window buttons. * *

Layout constraints

* An application may set constraints on individual children to customize their layout. @@ -198,12 +203,7 @@ public HeaderBar(Node leading, Node center, Node trailing) { * Usually, this corresponds to the left side of the header bar; with right-to-left orientation, * this corresponds to the right side of the header bar. */ - private final ObjectProperty leading = new NodeProperty() { - @Override - public String getName() { - return "leading"; - } - }; + private final ObjectProperty leading = new NodeProperty("leading"); public final ObjectProperty leadingProperty() { return leading; @@ -220,12 +220,7 @@ public final void setLeading(Node value) { /** * The center area of the {@code HeaderBar}. */ - private final ObjectProperty center = new NodeProperty() { - @Override - public String getName() { - return "center"; - } - }; + private final ObjectProperty center = new NodeProperty("center"); public final ObjectProperty centerProperty() { return center; @@ -245,12 +240,7 @@ public final void setCenter(Node value) { * Usually, this corresponds to the right side of the header bar; with right-to-left orientation, * this corresponds to the left side of the header bar. */ - private final ObjectProperty trailing = new NodeProperty() { - @Override - public String getName() { - return "trailing"; - } - }; + private final ObjectProperty trailing = new NodeProperty("trailing"); public final ObjectProperty trailingProperty() { return trailing; @@ -264,6 +254,44 @@ public final void setTrailing(Node value) { trailing.set(value); } + /** + * Specifies whether the system-provided window buttons overlap the content of this {@code HeaderBar}, + * or whether they are laid out next to the content of the header bar, taking up space in its layout. + */ + private final StyleableBooleanProperty overlappingSystemInset = new StyleableBooleanProperty() { + @Override + public Object getBean() { + return HeaderBar.this; + } + + @Override + public String getName() { + return "systemInset"; + } + + @Override + protected void invalidated() { + requestLayout(); + } + + @Override + public CssMetaData getCssMetaData() { + return OVERLAPPING_SYSTEM_INSET; + } + }; + + public final StyleableBooleanProperty overlappingSystemInsetProperty() { + return overlappingSystemInset; + } + + public final boolean isOverlappingSystemInset() { + return overlappingSystemInset.get(); + } + + public final void setOverlappingSystemInset(boolean value) { + overlappingSystemInset.set(value); + } + @Override protected double computeMinWidth(double height) { Node leading = getLeading(); @@ -273,6 +301,7 @@ protected double computeMinWidth(double height) { double leftPrefWidth; double rightPrefWidth; double centerMinWidth; + double systemInsetWidth; if (height != -1 && (childHasContentBias(leading, Orientation.VERTICAL) || @@ -288,13 +317,18 @@ protected double computeMinWidth(double height) { centerMinWidth = getAreaWidth(center, -1, true); } + if (isOverlappingSystemInset()) { + systemInsetWidth = 0; + } else { + systemInsetWidth = getLeftSystemInset().getWidth() + getRightSystemInset().getWidth(); + } + return insets.getLeft() + leftPrefWidth + centerMinWidth + rightPrefWidth + insets.getRight() - + getLeftSystemInset().getWidth() - + getRightSystemInset().getWidth(); + + systemInsetWidth; } @Override @@ -356,24 +390,30 @@ protected void layoutChildren() { boolean rtl = getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT; double width = Math.max(getWidth(), minWidth(-1)); double height = Math.max(getHeight(), minHeight(-1)); - double leftSystemInset = getLeftSystemInset().getWidth(); - double rightSystemInset = getRightSystemInset().getWidth(); double leftWidth = 0; double rightWidth = 0; double insideY = insets.getTop(); double insideHeight = height - insideY - insets.getBottom(); double insideX, insideWidth; + double leftSystemInsetWidth, rightSystemInsetWidth; + + if (isOverlappingSystemInset()) { + leftSystemInsetWidth = rightSystemInsetWidth = 0; + } else { + leftSystemInsetWidth = getLeftSystemInset().getWidth(); + rightSystemInsetWidth = getRightSystemInset().getWidth(); + } if (rtl) { left = getTrailing(); right = getLeading(); - insideX = insets.getRight() + leftSystemInset; - insideWidth = width - insideX - insets.getLeft() - rightSystemInset; + insideX = insets.getRight() + leftSystemInsetWidth; + insideWidth = width - insideX - insets.getLeft() - rightSystemInsetWidth; } else { left = getLeading(); right = getTrailing(); - insideX = insets.getLeft() + leftSystemInset; - insideWidth = width - insideX - insets.getRight() - rightSystemInset; + insideX = insets.getLeft() + leftSystemInsetWidth; + insideWidth = width - insideX - insets.getRight() - rightSystemInsetWidth; } if (left != null && left.isManaged()) { @@ -512,14 +552,24 @@ private void updateMinHeight() { } } - private abstract class NodeProperty extends ObjectPropertyBase { + private final class NodeProperty extends ObjectPropertyBase { + private final String name; private Node value; + NodeProperty(String name) { + this.name = name; + } + @Override public Object getBean() { return HeaderBar.this; } + @Override + public String getName() { + return name; + } + @Override protected void invalidated() { if (value != null) { @@ -533,4 +583,38 @@ protected void invalidated() { } } } + + private static final CssMetaData OVERLAPPING_SYSTEM_INSET = new CssMetaData<>( + "-fx-overlapping-system-inset", StyleConverter.getBooleanConverter(), false) { + @Override + public boolean isSettable(HeaderBar headerBar) { + return headerBar == null || !headerBar.overlappingSystemInset.isBound(); + } + + @Override + public StyleableBooleanProperty getStyleableProperty(HeaderBar headerBar) { + return headerBar.overlappingSystemInset; + } + }; + + private static final List> METADATA = + Stream.concat( + Region.getClassCssMetaData().stream(), + Stream.of(OVERLAPPING_SYSTEM_INSET)) + .toList(); + + @Override + public List> getCssMetaData() { + return METADATA; + } + + /** + * Gets the {@code CssMetaData} associated with this class, which includes the + * {@code CssMetaData} of its superclasses. + * + * @return the {@code CssMetaData} + */ + public static List> getClassCssMetaData() { + return METADATA; + } } diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBarBase.java b/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBarBase.java index 896dae17d9d..57922622ca0 100644 --- a/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBarBase.java +++ b/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBarBase.java @@ -210,10 +210,10 @@ public final Dimension2D getRightSystemInset() { } /** - * The absolute minimum height of {@link #leftSystemInsetProperty() leftSystemInset} and - * {@link #rightSystemInsetProperty() rightSystemInset}. This is a platform-dependent value - * that a {@code HeaderBarBase} implementation can use to define a reasonable minimum height - * for the header bar area. + * The system-provided reasonable minimum height of {@link #leftSystemInsetProperty() leftSystemInset} + * {@link #rightSystemInsetProperty() rightSystemInset}. This is a platform-dependent value that a + * {@code HeaderBarBase} implementation can use to define a reasonable minimum height for the header + * bar area. */ private final ReadOnlyDoubleWrapper minSystemHeight = new ReadOnlyDoubleWrapper(this, "minSystemHeight") { diff --git a/modules/javafx.graphics/src/test/java/test/javafx/scene/layout/HeaderBarTest.java b/modules/javafx.graphics/src/test/java/test/javafx/scene/layout/HeaderBarTest.java index 43fe317ec97..c17efde58e0 100644 --- a/modules/javafx.graphics/src/test/java/test/javafx/scene/layout/HeaderBarTest.java +++ b/modules/javafx.graphics/src/test/java/test/javafx/scene/layout/HeaderBarTest.java @@ -251,7 +251,6 @@ void alignmentOfCenterChild_notResizable_withNonEmptyLeadingAndTrailingChild( } @ParameterizedTest - @SuppressWarnings("unchecked") @CsvSource({ "TOP_LEFT, 160, 10, 100, 80", "TOP_CENTER, 450, 10, 100, 80", @@ -264,6 +263,7 @@ void alignmentOfCenterChild_notResizable_withNonEmptyLeadingAndTrailingChild( "BOTTOM_RIGHT, 740, 10, 100, 80" }) void alignmentOfCenterChild_withLeftSystemInset(Pos pos, double x, double y, double width, double height) { + @SuppressWarnings("unchecked") var leftSystemInset = (ObjectProperty)ReflectionUtils.getFieldValue(headerBar, "leftSystemInset"); leftSystemInset.set(new Dimension2D(100, 100)); var leading = new MockResizable(50, 50); @@ -281,7 +281,6 @@ void alignmentOfCenterChild_withLeftSystemInset(Pos pos, double x, double y, dou } @ParameterizedTest - @SuppressWarnings("unchecked") @CsvSource({ "TOP_LEFT, 60, 10, 100, 80", "TOP_CENTER, 450, 10, 100, 80", @@ -294,6 +293,7 @@ void alignmentOfCenterChild_withLeftSystemInset(Pos pos, double x, double y, dou "BOTTOM_RIGHT, 640, 10, 100, 80" }) void alignmentOfCenterChild_withRightSystemInset(Pos pos, double x, double y, double width, double height) { + @SuppressWarnings("unchecked") var rightSystemInset = (ObjectProperty)ReflectionUtils.getFieldValue(headerBar, "rightSystemInset"); rightSystemInset.set(new Dimension2D(100, 100)); var leading = new MockResizable(50, 50); @@ -311,7 +311,6 @@ void alignmentOfCenterChild_withRightSystemInset(Pos pos, double x, double y, do } @ParameterizedTest - @SuppressWarnings("unchecked") @CsvSource({ "TOP_CENTER, 260, 10, 80, 80", "CENTER, 260, 10, 80, 80", @@ -319,6 +318,7 @@ void alignmentOfCenterChild_withRightSystemInset(Pos pos, double x, double y, do }) void alignmentOfCenterChild_withLeftSystemInset_andOffsetCausedByInsufficientHorizontalSpace( Pos pos, double x, double y, double width, double height) { + @SuppressWarnings("unchecked") var leftSystemInset = (ObjectProperty)ReflectionUtils.getFieldValue(headerBar, "leftSystemInset"); leftSystemInset.set(new Dimension2D(200, 100)); var leading = new MockResizable(50, 50); @@ -336,7 +336,6 @@ void alignmentOfCenterChild_withLeftSystemInset_andOffsetCausedByInsufficientHor } @ParameterizedTest - @SuppressWarnings("unchecked") @CsvSource({ "TOP_CENTER, 60, 10, 80, 80", "CENTER, 60, 10, 80, 80", @@ -344,6 +343,7 @@ void alignmentOfCenterChild_withLeftSystemInset_andOffsetCausedByInsufficientHor }) void alignmentOfCenterChild_withRightSystemInset_andOffsetCausedByInsufficientHorizontalSpace( Pos pos, double x, double y, double width, double height) { + @SuppressWarnings("unchecked") var rightSystemInset = (ObjectProperty)ReflectionUtils.getFieldValue(headerBar, "rightSystemInset"); rightSystemInset.set(new Dimension2D(200, 100)); var leading = new MockResizable(50, 50); @@ -360,6 +360,50 @@ void alignmentOfCenterChild_withRightSystemInset_andOffsetCausedByInsufficientHo assertBounds(x, y, width, height, center); } + @ParameterizedTest + @CsvSource({ + "TOP_LEFT, 10, 10, 50, 50", + "CENTER, 10, 25, 50, 50", + "BOTTOM_LEFT, 10, 40, 50, 50" + }) + void alignmentOfLeadingChild_notResizable_withOverlappingLeftSystemInset( + Pos pos, double x, double y, double width, double height) { + @SuppressWarnings("unchecked") + var leftSystemInset = (ObjectProperty)ReflectionUtils.getFieldValue(headerBar, "leftSystemInset"); + leftSystemInset.set(new Dimension2D(100, 100)); + var leading = new Rectangle(50, 50); + HeaderBar.setAlignment(leading, pos); + HeaderBar.setMargin(leading, new Insets(10)); + headerBar.setOverlappingSystemInset(true); + headerBar.setLeading(leading); + headerBar.resize(1000, 100); + headerBar.layout(); + + assertBounds(x, y, width, height, leading); + } + + @ParameterizedTest + @CsvSource({ + "TOP_RIGHT, 940, 10, 50, 50", + "CENTER, 940, 25, 50, 50", + "BOTTOM_RIGHT, 940, 40, 50, 50" + }) + void alignmentOfTrailingChild_notResizable_withOverlappingRightSystemInset( + Pos pos, double x, double y, double width, double height) { + @SuppressWarnings("unchecked") + var rightSystemInset = (ObjectProperty)ReflectionUtils.getFieldValue(headerBar, "rightSystemInset"); + rightSystemInset.set(new Dimension2D(100, 100)); + var trailing = new Rectangle(50, 50); + HeaderBar.setAlignment(trailing, pos); + HeaderBar.setMargin(trailing, new Insets(10)); + headerBar.setOverlappingSystemInset(true); + headerBar.setTrailing(trailing); + headerBar.resize(1000, 100); + headerBar.layout(); + + assertBounds(x, y, width, height, trailing); + } + private void assertBounds(double x, double y, double width, double height, Node node) { var bounds = node.getLayoutBounds(); assertEquals(x, node.getLayoutX()); From 8649f917509a01f50b62c54afd75b28dbca4413c Mon Sep 17 00:00:00 2001 From: mstr2 <43553916+mstr2@users.noreply.github.com> Date: Fri, 6 Dec 2024 17:25:24 +0100 Subject: [PATCH 32/35] StyleableBooleanProperty -> BooleanProperty --- .../src/main/java/javafx/scene/layout/HeaderBar.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBar.java b/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBar.java index 93935fc1c62..f2d2d4ce16e 100644 --- a/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBar.java +++ b/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBar.java @@ -26,6 +26,7 @@ package javafx.scene.layout; import com.sun.javafx.geom.Vec2d; +import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectPropertyBase; import javafx.css.CssMetaData; @@ -280,7 +281,7 @@ public CssMetaData getCssMetaData() { } }; - public final StyleableBooleanProperty overlappingSystemInsetProperty() { + public final BooleanProperty overlappingSystemInsetProperty() { return overlappingSystemInset; } From 65230730eb3a8220d88a4b96bd9b2d06d77c51b0 Mon Sep 17 00:00:00 2001 From: mstr2 <43553916+mstr2@users.noreply.github.com> Date: Tue, 10 Dec 2024 19:45:15 +0100 Subject: [PATCH 33/35] fix system menu for non-resizeable extended window --- .../src/main/native-glass/win/GlassWindow.cpp | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/modules/javafx.graphics/src/main/native-glass/win/GlassWindow.cpp b/modules/javafx.graphics/src/main/native-glass/win/GlassWindow.cpp index e852562096e..cd8748fa0b3 100644 --- a/modules/javafx.graphics/src/main/native-glass/win/GlassWindow.cpp +++ b/modules/javafx.graphics/src/main/native-glass/win/GlassWindow.cpp @@ -1248,8 +1248,8 @@ void GlassWindow::ShowSystemMenu(int x, int y) } // Mirror the X coordinate we get from JavaFX if this is a RTL window. - LONG style = ::GetWindowLong(GetHWND(), GWL_EXSTYLE); - if (style & WS_EX_LAYOUTRTL) { + LONG exStyle = ::GetWindowLong(GetHWND(), GWL_EXSTYLE); + if (exStyle & WS_EX_LAYOUTRTL) { RECT rect = {0}; ::GetClientRect(GetHWND(), &rect); x = max(0, rect.right - rect.left) - x; @@ -1258,6 +1258,10 @@ void GlassWindow::ShowSystemMenu(int x, int y) HMENU systemMenu = GetSystemMenu(GetHWND(), FALSE); bool maximized = placement.showCmd == SW_SHOWMAXIMIZED; + LONG style = ::GetWindowLong(GetHWND(), GWL_STYLE); + bool canMinimize = (style & WS_MINIMIZEBOX) && !(exStyle & WS_EX_TOOLWINDOW); + bool canMaximize = (style & WS_MAXIMIZEBOX) && !maximized; + MENUITEMINFO menuItemInfo { sizeof(MENUITEMINFO) }; menuItemInfo.fMask = MIIM_STATE; menuItemInfo.fType = MFT_STRING; @@ -1268,13 +1272,13 @@ void GlassWindow::ShowSystemMenu(int x, int y) menuItemInfo.fState = maximized ? MF_DISABLED : MF_ENABLED; SetMenuItemInfo(systemMenu, SC_MOVE, FALSE, &menuItemInfo); - menuItemInfo.fState = maximized ? MF_DISABLED : MF_ENABLED; + menuItemInfo.fState = !m_isResizable || maximized ? MF_DISABLED : MF_ENABLED; SetMenuItemInfo(systemMenu, SC_SIZE, FALSE, &menuItemInfo); - menuItemInfo.fState = MF_ENABLED; + menuItemInfo.fState = canMinimize ? MF_ENABLED : MF_DISABLED; SetMenuItemInfo(systemMenu, SC_MINIMIZE, FALSE, &menuItemInfo); - menuItemInfo.fState = maximized ? MF_DISABLED : MF_ENABLED; + menuItemInfo.fState = canMaximize ? MF_ENABLED : MF_DISABLED; SetMenuItemInfo(systemMenu, SC_MAXIMIZE, FALSE, &menuItemInfo); menuItemInfo.fState = MF_ENABLED; From 8d5d7b877c1611a346a67834cb4b65faa40c48cd Mon Sep 17 00:00:00 2001 From: mstr2 <43553916+mstr2@users.noreply.github.com> Date: Tue, 10 Dec 2024 20:09:38 +0100 Subject: [PATCH 34/35] add StageStyle.EXTENDED_UTILITY --- .../main/java/com/sun/glass/ui/Window.java | 4 +++ .../sun/glass/ui/WindowControlsOverlay.java | 27 +++++++++++--- .../java/com/sun/glass/ui/gtk/GtkWindow.java | 2 +- .../java/com/sun/glass/ui/win/WinWindow.java | 4 ++- .../sun/javafx/tk/quantum/WindowStage.java | 5 +++ .../application/ConditionalFeature.java | 6 ++-- .../main/java/javafx/stage/StageStyle.java | 12 ++++++- .../com/sun/glass/ui/win/WindowDecoration.css | 4 +++ .../glass/ui/WindowControlsOverlayTest.java | 35 ++++++++++++------- 9 files changed, 77 insertions(+), 22 deletions(-) diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/Window.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/Window.java index 605cdc68dd1..519cee346ab 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/Window.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/Window.java @@ -747,6 +747,10 @@ public boolean isExtendedWindow() { return (this.styleMask & Window.EXTENDED) != 0; } + public boolean isUtilityWindow() { + return (this.styleMask & Window.UTILITY) != 0; + } + public boolean isTransparentWindow() { //The TRANSPARENT flag is set only if it is supported return (this.styleMask & Window.TRANSPARENT) != 0; diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/WindowControlsOverlay.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/WindowControlsOverlay.java index 93ee8e07e89..0ab50e2bc66 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/WindowControlsOverlay.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/WindowControlsOverlay.java @@ -26,6 +26,7 @@ package com.sun.glass.ui; import com.sun.glass.events.MouseEvent; +import com.sun.javafx.binding.ObjectConstant; import com.sun.javafx.util.Utils; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyObjectProperty; @@ -167,6 +168,7 @@ public StyleableProperty getStyleableProperty(WindowControlsOverlay ove private static final PseudoClass ACTIVE_PSEUDOCLASS = PseudoClass.getPseudoClass("active"); private static final String DARK_STYLE_CLASS = "dark"; private static final String RESTORE_STYLE_CLASS = "restore"; + private static final String UTILITY_STYLE_CLASS = "utility"; /** * The metrics (placement and size) of the window buttons. @@ -213,10 +215,13 @@ protected void invalidated() { private final ButtonRegion maximizeButton = new ButtonRegion(ButtonType.MAXIMIZE, "maximize-button", 1); private final ButtonRegion closeButton = new ButtonRegion(ButtonType.CLOSE, "close-button", 2); private final Subscription subscriptions; + private final boolean utility; private Node buttonAtMouseDown; - public WindowControlsOverlay(ObservableValue stylesheet) { + public WindowControlsOverlay(ObservableValue stylesheet, boolean utility) { + this.utility = utility; + var stage = sceneProperty() .flatMap(Scene::windowProperty) .map(w -> w instanceof Stage ? (Stage)w : null); @@ -250,7 +255,15 @@ public WindowControlsOverlay(ObservableValue stylesheet) { stylesheet.subscribe(this::updateStylesheet)); getStyleClass().setAll("window-button-container"); - getChildren().addAll(minimizeButton, maximizeButton, closeButton); + + if (utility) { + minimizeButton.managedProperty().bind(ObjectConstant.valueOf(false)); + maximizeButton.managedProperty().bind(ObjectConstant.valueOf(false)); + getChildren().add(closeButton); + getStyleClass().add(UTILITY_STYLE_CLASS); + } else { + getChildren().addAll(minimizeButton, maximizeButton, closeButton); + } } public void dispose() { @@ -270,10 +283,14 @@ public ReadOnlyObjectProperty metricsProperty() { * @return the {@code ButtonType} or {@code null} */ public ButtonType buttonAt(double x, double y) { - for (var button : orderedButtons) { - if (button.isVisible() && button.getBoundsInParent().contains(x, y)) { - return button.getButtonType(); + if (!utility) { + for (var button : orderedButtons) { + if (button.isVisible() && button.getBoundsInParent().contains(x, y)) { + return button.getButtonType(); + } } + } else if (closeButton.isVisible() && closeButton.getBoundsInParent().contains(x, y)) { + return ButtonType.CLOSE; } return null; diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkWindow.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkWindow.java index 61732aa9569..17d165d1f86 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkWindow.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkWindow.java @@ -212,7 +212,7 @@ public long getRawHandle() { public WindowControlsOverlay getWindowOverlay() { if (windowControlsOverlay == null && isExtendedWindow()) { windowControlsOverlay = new WindowControlsOverlay( - PlatformThemeObserver.getInstance().stylesheetProperty()); + PlatformThemeObserver.getInstance().stylesheetProperty(), isUtilityWindow()); // Set the system-defined absolute minimum size to the size of the window buttons area, // regardless of whether the application has specified a smaller minimum size. diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/win/WinWindow.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/win/WinWindow.java index d669846bd97..64dba9878b1 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/win/WinWindow.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/win/WinWindow.java @@ -346,7 +346,9 @@ public WindowControlsOverlay getWindowOverlay() { throw new RuntimeException("Resource not found: " + WINDOW_DECORATION_STYLESHEET); } - windowControlsOverlay = new WindowControlsOverlay(StringConstant.valueOf(url.toExternalForm())); + windowControlsOverlay = new WindowControlsOverlay( + StringConstant.valueOf(url.toExternalForm()), isUtilityWindow()); + windowOverlayMetrics.bind(windowControlsOverlay.metricsProperty()); } diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/WindowStage.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/WindowStage.java index ccae34285c7..e035d42057e 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/WindowStage.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/WindowStage.java @@ -144,6 +144,8 @@ private void initPlatformWindow() { style = StageStyle.DECORATED; } else if (style == StageStyle.EXTENDED && !app.supportsExtendedWindows()) { style = StageStyle.DECORATED; + } else if (style == StageStyle.EXTENDED_UTILITY && !app.supportsExtendedWindows()) { + style = StageStyle.UTILITY; } switch (style) { @@ -160,6 +162,9 @@ private void initPlatformWindow() { windowMask |= Window.EXTENDED | Window.CLOSABLE | Window.MINIMIZABLE | Window.MAXIMIZABLE; resizable = true; break; + case EXTENDED_UTILITY: + windowMask |= Window.EXTENDED | Window.CLOSABLE | Window.UTILITY; + break; case UTILITY: windowMask |= Window.TITLED | Window.UTILITY | Window.CLOSABLE; break; diff --git a/modules/javafx.graphics/src/main/java/javafx/application/ConditionalFeature.java b/modules/javafx.graphics/src/main/java/javafx/application/ConditionalFeature.java index 8b7e3ebf480..5028cfd0d8f 100644 --- a/modules/javafx.graphics/src/main/java/javafx/application/ConditionalFeature.java +++ b/modules/javafx.graphics/src/main/java/javafx/application/ConditionalFeature.java @@ -25,6 +25,8 @@ package javafx.application; +import javafx.stage.StageStyle; + /** * Defines a set of conditional (optional) features. These features * may not be available on all platforms. An application that wants to @@ -139,7 +141,7 @@ public enum ConditionalFeature { TRANSPARENT_WINDOW, /** - * Indicates that a system supports {@link javafx.stage.StageStyle#UNIFIED} + * Indicates that a system supports {@link StageStyle#UNIFIED} *

* NOTE: Currently, supported on: *

    @@ -151,7 +153,7 @@ public enum ConditionalFeature { UNIFIED_WINDOW, /** - * Indicates that a system supports {@link javafx.stage.StageStyle#EXTENDED}. + * Indicates that a system supports {@link StageStyle#EXTENDED} and {@link StageStyle#EXTENDED_UTILITY}. *

    * This feature is currently supported on Windows, Linux, and macOS. * diff --git a/modules/javafx.graphics/src/main/java/javafx/stage/StageStyle.java b/modules/javafx.graphics/src/main/java/javafx/stage/StageStyle.java index d6084661839..a00e3ed71f0 100644 --- a/modules/javafx.graphics/src/main/java/javafx/stage/StageStyle.java +++ b/modules/javafx.graphics/src/main/java/javafx/stage/StageStyle.java @@ -115,5 +115,15 @@ public enum StageStyle { * * @since 24 */ - EXTENDED + EXTENDED, + + /** + * Defines a {@code Stage} style with the semantics of {@link #UTILITY} and the appearance of {@link #EXTENDED}. + *

    + * This is a conditional feature, to check if it is supported see {@link Platform#isSupported(ConditionalFeature)}. + * If the feature is not supported by the platform, this style downgrades to {@link StageStyle#UTILITY}. + * + * @since 24 + */ + EXTENDED_UTILITY } diff --git a/modules/javafx.graphics/src/main/resources/com/sun/glass/ui/win/WindowDecoration.css b/modules/javafx.graphics/src/main/resources/com/sun/glass/ui/win/WindowDecoration.css index 15b918f803e..4a9f9df3ad3 100644 --- a/modules/javafx.graphics/src/main/resources/com/sun/glass/ui/win/WindowDecoration.css +++ b/modules/javafx.graphics/src/main/resources/com/sun/glass/ui/win/WindowDecoration.css @@ -36,6 +36,10 @@ -fx-pref-height: 29; } +.window-button-container.utility > .close-button { + -fx-pref-width: 29; +} + .minimize-button:hover, .maximize-button:hover { -fx-background-color: #00000015; diff --git a/modules/javafx.graphics/src/test/java/test/com/sun/glass/ui/WindowControlsOverlayTest.java b/modules/javafx.graphics/src/test/java/test/com/sun/glass/ui/WindowControlsOverlayTest.java index a03afc65ca3..533b819b986 100644 --- a/modules/javafx.graphics/src/test/java/test/com/sun/glass/ui/WindowControlsOverlayTest.java +++ b/modules/javafx.graphics/src/test/java/test/com/sun/glass/ui/WindowControlsOverlayTest.java @@ -52,7 +52,7 @@ void rightPlacement() { var overlay = new WindowControlsOverlay(getStylesheet(""" .window-button-container { -fx-button-placement: right; } .window-button { -fx-pref-width: 20; -fx-pref-height: 10; } - """)); + """), false); var unused = new Scene(overlay); var children = overlay.getChildrenUnmodifiable(); @@ -76,7 +76,7 @@ void rightPlacement_rightToLeft() { var overlay = new WindowControlsOverlay(getStylesheet(""" .window-button-container { -fx-button-placement: right; } .window-button { -fx-pref-width: 20; -fx-pref-height: 10; } - """)); + """), false); var unused = new Scene(overlay); var children = overlay.getChildrenUnmodifiable(); @@ -101,7 +101,7 @@ void leftPlacement() { var overlay = new WindowControlsOverlay(getStylesheet(""" .window-button-container { -fx-button-placement: left; } .window-button { -fx-pref-width: 20; -fx-pref-height: 10; } - """)); + """), false); var unused = new Scene(overlay); var children = overlay.getChildrenUnmodifiable(); @@ -125,7 +125,7 @@ void leftPlacement_rightToLeft() { var overlay = new WindowControlsOverlay(getStylesheet(""" .window-button-container { -fx-button-placement: left; } .window-button { -fx-pref-width: 20; -fx-pref-height: 10; } - """)); + """), false); var unused = new Scene(overlay); var children = overlay.getChildrenUnmodifiable(); @@ -152,7 +152,7 @@ void customButtonOrder() { .minimize-button { -fx-button-order: 5; } .maximize-button { -fx-button-order: 1; } .close-button { -fx-button-order: 3; } - """)); + """), false); var unused = new Scene(overlay); var children = overlay.getChildrenUnmodifiable(); @@ -178,7 +178,7 @@ void customButtonOrder_rightToLeft() { .minimize-button { -fx-button-order: 5; } .maximize-button { -fx-button-order: 1; } .close-button { -fx-button-order: 3; } - """)); + """), false); var unused = new Scene(overlay); var children = overlay.getChildrenUnmodifiable(); @@ -195,6 +195,17 @@ void customButtonOrder_rightToLeft() { assertLayoutBounds(children.get(2), 20, 0, 20, 10); } + @Test + void utilityDecorationIsOnlyCloseButton() { + var overlay = new WindowControlsOverlay(getStylesheet(""" + .window-button { -fx-pref-width: 20; -fx-pref-height: 10; } + """), true); + + var children = overlay.getChildrenUnmodifiable(); + assertEquals(1, children.size()); + assertTrue(children.getFirst().getStyleClass().contains("close-button")); + } + /** * Asserts that the buttons are laid out on the right, even though the node orientation is right-to-left. */ @@ -203,7 +214,7 @@ void disallowRightToLeft() { var overlay = new WindowControlsOverlay(getStylesheet(""" .window-button-container { -fx-button-placement: right; -fx-allow-rtl: false; } .window-button { -fx-pref-width: 20; -fx-pref-height: 10; } - """)); + """), false); var unused = new Scene(overlay); var children = overlay.getChildrenUnmodifiable(); @@ -224,7 +235,7 @@ void activePseudoClassCorrespondsToStageFocusedProperty() { var overlay = new WindowControlsOverlay(getStylesheet(""" .window-button-container { -fx-button-placement: right; } .window-button { -fx-pref-width: 20; -fx-pref-height: 10; } - """)); + """), false); var scene = new Scene(overlay); var stage = new Stage(); @@ -250,7 +261,7 @@ void maximizeButtonIsDisabledWhenStageIsNotResizable() { var overlay = new WindowControlsOverlay(getStylesheet(""" .window-button-container { -fx-button-placement: right; } .window-button { -fx-pref-width: 20; -fx-pref-height: 10; } - """)); + """), false); var scene = new Scene(overlay); var stage = new Stage(); @@ -274,7 +285,7 @@ void restoreStyleClassIsPresentWhenStageIsMaximized() { var overlay = new WindowControlsOverlay(getStylesheet(""" .window-button-container { -fx-button-placement: right; } .window-button { -fx-pref-width: 20; -fx-pref-height: 10; } - """)); + """), false); var scene = new Scene(overlay); var stage = new Stage(); @@ -297,7 +308,7 @@ void darkStyleClassIsPresentWhenSceneFillIsDark() { var overlay = new WindowControlsOverlay(getStylesheet(""" .window-button-container { -fx-button-placement: right; } .window-button { -fx-pref-width: 20; -fx-pref-height: 10; } - """)); + """), false); var scene = new Scene(overlay); @@ -316,7 +327,7 @@ void pickButtonAtCoordinates() { var overlay = new WindowControlsOverlay(getStylesheet(""" .window-button-container { -fx-button-placement: right; } .window-button { -fx-pref-width: 20; -fx-pref-height: 10; } - """)); + """), false); var unused = new Scene(overlay); overlay.resize(200, 100); From eaafd9f025c3cdde1d1466f32baee4597babca2d Mon Sep 17 00:00:00 2001 From: mstr2 <43553916+mstr2@users.noreply.github.com> Date: Tue, 10 Dec 2024 23:08:50 +0100 Subject: [PATCH 35/35] fix resizable states for GTK windows --- .../main/java/com/sun/glass/ui/Window.java | 46 +++++++++---------- .../sun/javafx/tk/quantum/WindowStage.java | 15 ++---- .../main/native-glass/gtk/glass_window.cpp | 17 +++++-- .../src/main/native-glass/gtk/glass_window.h | 3 ++ 4 files changed, 42 insertions(+), 39 deletions(-) diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/Window.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/Window.java index 519cee346ab..6dfdd402c18 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/Window.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/Window.java @@ -694,29 +694,27 @@ private void synthesizeViewMoveEvent() { protected abstract boolean _setVisible(long ptr, boolean visible); public void setVisible(final boolean visible) { Application.checkEventThread(); - if (this.isVisible != visible) { - if (!visible) { - if (getView() != null) { - getView().setVisible(visible); - } - // Avoid native call if the window has been closed already - if (this.ptr != 0L) { - this.isVisible = _setVisible(this.ptr, visible); - } else { - this.isVisible = visible; - } - remove(this); - } else { - checkNotClosed(); + if (!visible) { + if (getView() != null) { + getView().setVisible(visible); + } + // Avoid native call if the window has been closed already + if (this.ptr != 0L) { this.isVisible = _setVisible(this.ptr, visible); + } else { + this.isVisible = visible; + } + remove(this); + } else { + checkNotClosed(); + this.isVisible = _setVisible(this.ptr, visible); - if (getView() != null) { - getView().setVisible(this.isVisible); - } - add(this); - - synthesizeViewMoveEvent(); + if (getView() != null) { + getView().setVisible(this.isVisible); } + add(this); + + synthesizeViewMoveEvent(); } } @@ -724,11 +722,9 @@ public void setVisible(final boolean visible) { public boolean setResizable(final boolean resizable) { Application.checkEventThread(); checkNotClosed(); - if (this.isResizable != resizable) { - if (_setResizable(this.ptr, resizable)) { - this.isResizable = resizable; - synthesizeViewMoveEvent(); - } + if (_setResizable(this.ptr, resizable)) { + this.isResizable = resizable; + synthesizeViewMoveEvent(); } return isResizable; } diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/WindowStage.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/WindowStage.java index e035d42057e..2b6a7175f4c 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/WindowStage.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/WindowStage.java @@ -129,7 +129,7 @@ private void initPlatformWindow() { if (owner instanceof WindowStage) { ownerWindow = ((WindowStage)owner).platformWindow; } - boolean resizable = false; + boolean resizable = fxStage != null && fxStage.isResizable(); boolean focusable = true; int windowMask = rtl ? Window.RIGHT_TO_LEFT : 0; if (isPopupStage) { // TODO: make it a stage style? @@ -138,6 +138,7 @@ private void initPlatformWindow() { windowMask |= Window.TRANSPARENT; } focusable = false; + resizable = false; } else { // Downgrade conditional stage styles if not supported if (style == StageStyle.UNIFIED && !app.supportsUnifiedWindows()) { @@ -153,14 +154,10 @@ private void initPlatformWindow() { windowMask |= Window.UNIFIED; // fall through case DECORATED: - windowMask |= - Window.TITLED | Window.CLOSABLE | - Window.MINIMIZABLE | Window.MAXIMIZABLE; - resizable = true; + windowMask |= Window.TITLED | Window.CLOSABLE | Window.MINIMIZABLE | Window.MAXIMIZABLE; break; case EXTENDED: windowMask |= Window.EXTENDED | Window.CLOSABLE | Window.MINIMIZABLE | Window.MAXIMIZABLE; - resizable = true; break; case EXTENDED_UTILITY: windowMask |= Window.EXTENDED | Window.CLOSABLE | Window.UTILITY; @@ -169,8 +166,7 @@ private void initPlatformWindow() { windowMask |= Window.TITLED | Window.UTILITY | Window.CLOSABLE; break; default: - windowMask |= - (transparent ? Window.TRANSPARENT : Window.UNTITLED) | Window.CLOSABLE; + windowMask |= (transparent ? Window.TRANSPARENT : Window.UNTITLED) | Window.CLOSABLE; break; } @@ -181,8 +177,7 @@ private void initPlatformWindow() { if (modality != Modality.NONE) { windowMask |= Window.MODAL; } - platformWindow = - app.createWindow(ownerWindow, Screen.getMainScreen(), windowMask); + platformWindow = app.createWindow(ownerWindow, Screen.getMainScreen(), windowMask); platformWindow.setResizable(resizable); platformWindow.setFocusable(focusable); if (fxStage != null && fxStage.getScene() != null) { diff --git a/modules/javafx.graphics/src/main/native-glass/gtk/glass_window.cpp b/modules/javafx.graphics/src/main/native-glass/gtk/glass_window.cpp index 132109a74fe..baa9bb32cdd 100644 --- a/modules/javafx.graphics/src/main/native-glass/gtk/glass_window.cpp +++ b/modules/javafx.graphics/src/main/native-glass/gtk/glass_window.cpp @@ -585,6 +585,10 @@ bool WindowContextBase::is_visible() { return gtk_widget_get_visible(gtk_widget); } +bool WindowContextBase::is_resizable() { + return false; +} + bool WindowContextBase::set_view(jobject view) { if (jview) { mainEnv->CallVoidMethod(jview, jViewNotifyMouse, @@ -781,7 +785,7 @@ WindowContextTop::WindowContextTop(jobject _jwindow, WindowContext* _owner, long } } - if (type == UTILITY) { + if (type == UTILITY && frame_type != EXTENDED) { gtk_window_set_type_hint(GTK_WINDOW(gtk_widget), GDK_WINDOW_TYPE_HINT_UTILITY); } @@ -1067,7 +1071,7 @@ void WindowContextTop::update_window_constraints() { GdkGeometry hints; - if (resizable.value && !is_disabled) { + if (is_resizable() && !is_disabled) { int w = std::max(resizable.sysminw, resizable.minw); int h = std::max(resizable.sysminh, resizable.minh); @@ -1101,6 +1105,10 @@ void WindowContextTop::set_resizable(bool res) { update_window_constraints(); } +bool WindowContextTop::is_resizable() { + return resizable.value; +} + void WindowContextTop::set_visible(bool visible) { WindowContextBase::set_visible(visible); @@ -1449,7 +1457,7 @@ void WindowContextTop::process_mouse_button(GdkEventButton* event, bool synthesi } // Double-clicking on the drag area maximizes the window (or restores its size). - if (event->type == GDK_2BUTTON_PRESS) { + if (is_resizable() && event->type == GDK_2BUTTON_PRESS) { jboolean dragArea = mainEnv->CallBooleanMethod( jwindow, jGtkWindowDragAreaHitTest, (jint)event->x, (jint)event->y); CHECK_JNI_EXCEPTION(mainEnv); @@ -1464,7 +1472,7 @@ void WindowContextTop::process_mouse_button(GdkEventButton* event, bool synthesi if (event->button == 1 && event->type == GDK_BUTTON_PRESS) { GdkWindowEdge edge; - bool shouldStartResizeDrag = !is_maximized && get_window_edge(event->x, event->y, &edge); + bool shouldStartResizeDrag = is_resizable() && !is_maximized && get_window_edge(event->x, event->y, &edge); // Clicking on a window edge starts a move-resize operation. if (shouldStartResizeDrag) { @@ -1515,6 +1523,7 @@ void WindowContextTop::process_mouse_motion(GdkEventMotion* event) { if (is_fullscreen || is_maximized || frame_type != EXTENDED + || !is_resizable() || !get_window_edge(event->x, event->y, &edge)) { set_cursor_override(NULL); WindowContextBase::process_mouse_motion(event); diff --git a/modules/javafx.graphics/src/main/native-glass/gtk/glass_window.h b/modules/javafx.graphics/src/main/native-glass/gtk/glass_window.h index 413e90fcdcf..4db405e1cda 100644 --- a/modules/javafx.graphics/src/main/native-glass/gtk/glass_window.h +++ b/modules/javafx.graphics/src/main/native-glass/gtk/glass_window.h @@ -112,6 +112,7 @@ class WindowContext : public DeletedMemDebug<0xCC> { virtual bool is_visible() = 0; virtual void set_bounds(int, int, bool, bool, int, int, int, int, float, float) = 0; virtual void set_resizable(bool) = 0; + virtual bool is_resizable() = 0; virtual void request_focus() = 0; virtual void set_focusable(bool)= 0; virtual bool grab_focus() = 0; @@ -228,6 +229,7 @@ class WindowContextBase: public WindowContext { void remove_child(WindowContextTop*); void set_visible(bool); bool is_visible(); + bool is_resizable(); bool set_view(jobject); bool grab_focus(); bool grab_mouse_drag_focus(); @@ -303,6 +305,7 @@ class WindowContextTop: public WindowContextBase { void set_maximized(bool); void set_bounds(int, int, bool, bool, int, int, int, int, float, float); void set_resizable(bool); + bool is_resizable(); void request_focus(); void set_focusable(bool); void set_title(const char*);