diff --git a/modules/javafx.base/src/main/java/com/sun/javafx/PreviewFeature.java b/modules/javafx.base/src/main/java/com/sun/javafx/PreviewFeature.java index 9219d35869c..6b371fc0f97 100644 --- a/modules/javafx.base/src/main/java/com/sun/javafx/PreviewFeature.java +++ b/modules/javafx.base/src/main/java/com/sun/javafx/PreviewFeature.java @@ -38,7 +38,8 @@ public enum PreviewFeature { // Add preview feature constants here: // TEST_FEATURE("Test Feature") - ; + STAGE_STYLE_EXTENDED("StageStyle.EXTENDED"), + HEADER_BAR("HeaderBar"); PreviewFeature(String featureName) { this.featureName = featureName; @@ -52,6 +53,16 @@ public enum PreviewFeature { private static final boolean enabled = Boolean.getBoolean(ENABLE_PREVIEW_PROPERTY); private static final boolean suppressWarning = Boolean.getBoolean(SUPPRESS_WARNING_PROPERTY); private static final Set enabledFeatures = new HashSet<>(); + private static boolean enabledForTesting; + + /** + * Enables preview features and suppresses the warning. + *

+ * This method is only used for testing purposes. + */ + public static void enableForTesting() { + enabledForTesting = true; + } /** * Verifies that preview features are enabled, and throws an exception otherwise. @@ -62,7 +73,9 @@ public enum PreviewFeature { * @throws RuntimeException if preview features are not enabled */ public void checkEnabled() { - if (!enabled) { + if (enabledForTesting) { + // do nothing + } else if (!enabled) { throw new RuntimeException(""" %s is a preview feature of JavaFX %s. Preview features may be removed in a future release, or upgraded to permanent features of JavaFX. 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..645fb3e15f5 100644 --- a/modules/javafx.base/src/test/java/test/util/ReflectionUtils.java +++ b/modules/javafx.base/src/test/java/test/util/ReflectionUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 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 @@ -26,15 +26,20 @@ 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. */ - public static Object getFieldValue(Object object, String fieldName) { + @SuppressWarnings("unchecked") + public static T getFieldValue(Object object, String fieldName) { Function, Field> getField = cls -> { try { var field = cls.getDeclaredField(fieldName); @@ -50,7 +55,7 @@ public static Object getFieldValue(Object object, String fieldName) { Field field = getField.apply(cls); if (field != null) { try { - return field.get(object); + return (T)field.get(object); } catch (IllegalAccessException e) { throw new AssertionError(e); } @@ -61,4 +66,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/ui/Application.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/Application.java index b3e9f42e0e9..8029cd75d7a 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2025, 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 @@ -711,6 +711,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/HeaderButtonMetrics.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/HeaderButtonMetrics.java new file mode 100644 index 00000000000..1860cc9f24d --- /dev/null +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/HeaderButtonMetrics.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2025, 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.scene.layout.HeaderBar; +import javafx.stage.StageStyle; +import java.util.Objects; + +/** + * Provides metrics about the header buttons of {@link StageStyle#EXTENDED} windows. + * + * @param leftInset the size of the left inset + * @param rightInset the size of the right inset + * @param minHeight the minimum height of the window buttons + * @see HeaderButtonOverlay + * @see HeaderBar + */ +public record HeaderButtonMetrics(Dimension2D leftInset, Dimension2D rightInset, double minHeight) { + + public static HeaderButtonMetrics EMPTY = new HeaderButtonMetrics(new Dimension2D(0, 0), new Dimension2D(0, 0), 0); + + public HeaderButtonMetrics { + Objects.requireNonNull(leftInset); + Objects.requireNonNull(rightInset); + + if (minHeight < 0) { + throw new IllegalArgumentException("minHeight cannot be negative"); + } + } + + public double totalInsetWidth() { + return leftInset.getWidth() + rightInset.getWidth(); + } + + public double maxInsetHeight() { + return Math.max(leftInset.getHeight(), rightInset.getHeight()); + } +} diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/HeaderButtonOverlay.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/HeaderButtonOverlay.java new file mode 100644 index 00000000000..54a2d14a910 --- /dev/null +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/HeaderButtonOverlay.java @@ -0,0 +1,683 @@ +/* + * Copyright (c) 2025, 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.binding.ObjectConstant; +import com.sun.javafx.util.Utils; +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.SimpleDoubleProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.value.ObservableValue; +import javafx.css.CssMetaData; +import javafx.css.PseudoClass; +import javafx.css.SimpleStyleableDoubleProperty; +import javafx.css.SimpleStyleableIntegerProperty; +import javafx.css.SimpleStyleableObjectProperty; +import javafx.css.StyleConverter; +import javafx.css.Styleable; +import javafx.css.StyleableDoubleProperty; +import javafx.css.StyleableIntegerProperty; +import javafx.css.StyleableObjectProperty; +import javafx.css.StyleableProperty; +import javafx.geometry.Dimension2D; +import javafx.geometry.HPos; +import javafx.geometry.Insets; +import javafx.geometry.VPos; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.layout.HeaderBar; +import javafx.scene.layout.HeaderButtonType; +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 header-button-container}
CSS propertyValuesDefaultComment
-fx-button-placement[ left | right ]right + * Specifies the placement of the header buttons on the left or the right side of the window. + *
-fx-button-vertical-alignment[ center | stretch ]center + * Specifies the vertical alignment of the header buttons, either centering the buttons + * within the preferred height or stretching the buttons to fill the preferred height. + *
-fx-button-default-height<double>0 + * Specifies the default height of the header buttons, which is used when the application + * does not specify a preferred button height. + *
+ * + * + * + * + * + * + * + * + * + * + *
CSS properties of {@code iconify-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}
+ * + * @implNote This control is used by the WinWindow.createHeaderButtonOverlay() and + * GtkWindow.createHeaderButtonOverlay() implementations for {@link StageStyle#EXTENDED} + * windows. It is not used by the macOS implementation. + */ +public class HeaderButtonOverlay extends Region { + + private static final CssMetaData BUTTON_DEFAULT_HEIGHT_METADATA = + new CssMetaData<>("-fx-button-default-height", StyleConverter.getSizeConverter()) { + @Override + public boolean isSettable(HeaderButtonOverlay overlay) { + return true; + } + + @Override + public StyleableProperty getStyleableProperty(HeaderButtonOverlay overlay) { + return overlay.buttonDefaultHeight; + } + }; + + private static final CssMetaData BUTTON_PLACEMENT_METADATA = + new CssMetaData<>("-fx-button-placement", + StyleConverter.getEnumConverter(ButtonPlacement.class), + ButtonPlacement.RIGHT) { + @Override + public boolean isSettable(HeaderButtonOverlay overlay) { + return true; + } + + @Override + public StyleableProperty getStyleableProperty(HeaderButtonOverlay overlay) { + return overlay.buttonPlacement; + } + }; + + private static final CssMetaData BUTTON_VERTICAL_ALIGNMENT_METADATA = + new CssMetaData<>("-fx-button-vertical-alignment", + StyleConverter.getEnumConverter(ButtonVerticalAlignment.class), + ButtonVerticalAlignment.CENTER) { + @Override + public boolean isSettable(HeaderButtonOverlay overlay) { + return true; + } + + @Override + public StyleableProperty getStyleableProperty(HeaderButtonOverlay overlay) { + return overlay.buttonVerticalAlignment; + } + }; + + private static final List> METADATA = + Stream.concat(getClassCssMetaData().stream(), + Stream.of(BUTTON_DEFAULT_HEIGHT_METADATA, + BUTTON_PLACEMENT_METADATA, + BUTTON_VERTICAL_ALIGNMENT_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"; + private static final String UTILITY_STYLE_CLASS = "utility"; + + /** + * The metrics (placement and size) of header buttons. + */ + private final ObjectProperty metrics = new SimpleObjectProperty<>( + this, "metrics", new HeaderButtonMetrics(new Dimension2D(0, 0), new Dimension2D(0, 0), 0)); + + /** + * Specifies the preferred height of header buttons. + *

+ * Negative values are interpreted as "no preference", which causes the buttons to be laid out + * with their preferred height set to the value of {@link #buttonDefaultHeight}. + */ + private final DoubleProperty prefButtonHeight = new SimpleDoubleProperty( + this, "prefButtonHeight", HeaderBar.USE_DEFAULT_SIZE); + + /** + * Specifies the default height of header buttons. + *

+ * This property corresponds to the {@code -fx-button-default-height} CSS property. + */ + private final StyleableDoubleProperty buttonDefaultHeight = new SimpleStyleableDoubleProperty( + BUTTON_DEFAULT_HEIGHT_METADATA, this, "buttonDefaultHeight"); + + /** + * Specifies the placement of the header 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", ButtonPlacement.RIGHT) { + @Override + protected void invalidated() { + requestLayout(); + } + }; + + /** + * Specifies the vertical alignment of the header buttons. + *

+ * This property corresponds to the {@code -fx-button-vertical-alignment} CSS property. + */ + private final StyleableObjectProperty buttonVerticalAlignment = + new SimpleStyleableObjectProperty<>( + BUTTON_VERTICAL_ALIGNMENT_METADATA, this, "buttonVerticalAlignment", + ButtonVerticalAlignment.CENTER) { + @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 iconifyButton = new ButtonRegion(HeaderButtonType.ICONIFY, "iconify-button", 0); + private final ButtonRegion maximizeButton = new ButtonRegion(HeaderButtonType.MAXIMIZE, "maximize-button", 1); + private final ButtonRegion closeButton = new ButtonRegion(HeaderButtonType.CLOSE, "close-button", 2); + private final Subscription subscriptions; + private final boolean utility; + private final boolean rightToLeft; + + private Node buttonAtMouseDown; + + public HeaderButtonOverlay(ObservableValue stylesheet, boolean utility, boolean rightToLeft) { + this.utility = utility; + this.rightToLeft = rightToLeft; + + 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(_ -> updateStyleClass()); // use a value subscriber, not an invalidation subscriber + + subscriptions = Subscription.combine( + focusedSubscription, + resizableSubscription, + maximizedSubscription, + updateStylesheetSubscription, + stylesheet.subscribe(this::updateStylesheet), + prefButtonHeight.subscribe(this::requestLayout), + buttonDefaultHeight.subscribe(this::requestLayout)); + + getStyleClass().setAll("header-button-container"); + + if (utility) { + iconifyButton.managedProperty().bind(ObjectConstant.valueOf(false)); + maximizeButton.managedProperty().bind(ObjectConstant.valueOf(false)); + getChildren().add(closeButton); + getStyleClass().add(UTILITY_STYLE_CLASS); + } else { + getChildren().addAll(iconifyButton, maximizeButton, closeButton); + } + } + + public void dispose() { + subscriptions.unsubscribe(); + } + + public ReadOnlyObjectProperty metricsProperty() { + return metrics; + } + + public DoubleProperty prefButtonHeightProperty() { + return prefButtonHeight; + } + + protected Region getButtonGlyph(HeaderButtonType buttonType) { + return (Region)(switch (buttonType) { + case ICONIFY -> iconifyButton; + case MAXIMIZE -> maximizeButton; + case CLOSE -> closeButton; + }).getChildrenUnmodifiable().getFirst(); + } + + /** + * 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 HeaderButtonType buttonAt(double x, double y) { + 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 HeaderButtonType.CLOSE; + } + + 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) { + HeaderButtonType buttonType = buttonAt(x, y); + Node node = buttonType != null ? switch (buttonType) { + case ICONIFY -> iconifyButton; + case MAXIMIZE -> maximizeButton; + case CLOSE -> closeButton; + } : null; + + if (type == MouseEvent.ENTER || type == MouseEvent.MOVE || type == MouseEvent.DRAG) { + handleMouseOver(node); + } else if (type == MouseEvent.EXIT) { + handleMouseExit(); + } else if (type == MouseEvent.UP && button == MouseEvent.BUTTON_LEFT) { + handleMouseUp(node, buttonType); + } else if (node != null && type == MouseEvent.DOWN && button == MouseEvent.BUTTON_LEFT) { + handleMouseDown(node); + } + + if (type == MouseEvent.ENTER || type == MouseEvent.EXIT) { + return false; + } + + return node != null || buttonAtMouseDown != null; + } + + private void handleMouseOver(Node button) { + iconifyButton.pseudoClassStateChanged(HOVER_PSEUDOCLASS, button == iconifyButton); + 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[] {iconifyButton, 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, HeaderButtonType 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 ICONIFY -> stage.setIconified(true); + case MAXIMIZE -> stage.setMaximized(!stage.isMaximized()); + case CLOSE -> stage.close(); + } + } + } + + private void onFocusedChanged(boolean focused) { + iconifyButton.pseudoClassStateChanged(ACTIVE_PSEUDOCLASS, focused); + maximizeButton.pseudoClassStateChanged(ACTIVE_PSEUDOCLASS, focused); + closeButton.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(iconifyButton, 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; + } + + private double getEffectiveButtonHeight() { + double prefHeight = prefButtonHeight.get(); + return prefHeight >= 0 ? prefHeight : buttonDefaultHeight.get(); + } + + private double getButtonOffsetY(double buttonHeight) { + return switch (buttonVerticalAlignment.get()) { + case STRETCH -> 0; + case CENTER -> (getEffectiveButtonHeight() - buttonHeight) / 2; + }; + } + + private void ensureRegionPrefHeight(Region region, double height) { + var prefHeight = (StyleableDoubleProperty)region.prefHeightProperty(); + + if (prefHeight.getStyleOrigin() == null) { + prefHeight.applyStyle(null, height); + } + } + + @Override + protected void layoutChildren() { + boolean left; + Region button1, button2, button3; + + if (rightToLeft) { + button1 = orderedButtons.get(2); + button2 = orderedButtons.get(1); + button3 = orderedButtons.get(0); + left = buttonPlacement.get() != ButtonPlacement.LEFT; + } else { + button1 = orderedButtons.get(0); + button2 = orderedButtons.get(1); + button3 = orderedButtons.get(2); + left = buttonPlacement.get() == ButtonPlacement.LEFT; + } + + double buttonHeight = switch (buttonVerticalAlignment.get()) { + case STRETCH -> getEffectiveButtonHeight(); + case CENTER -> buttonDefaultHeight.get(); + }; + + ensureRegionPrefHeight(button1, buttonHeight); + ensureRegionPrefHeight(button2, buttonHeight); + ensureRegionPrefHeight(button3, buttonHeight); + + double width = getWidth(); + 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; + + // A centered button doesn't stretch to fill the preferred height. Instead, we center the button + // vertically within the preferred height, and also add a horizontal offset so that the buttons + // have the same distance from the top and the left/right side of the window. + if (buttonVerticalAlignment.get() == ButtonVerticalAlignment.CENTER) { + if (left) { + double offset = getButtonOffsetY(button1Height); + button1X = snapPositionX(button1X + offset); + button2X = snapPositionX(button2X + offset); + button3X = snapPositionX(button3X + offset); + totalWidth = snapSizeX(totalWidth + offset * 2); + } else { + double offset = getButtonOffsetY(button3Height); + button1X = snapPositionX(button1X - offset); + button2X = snapPositionX(button2X - offset); + button3X = snapPositionX(button3X - offset); + totalWidth = snapSizeX(totalWidth + offset * 2); + } + + totalHeight = snapSizeY(getEffectiveButtonHeight()); + } else { + totalHeight = snapSizeY(Math.max(button1Height, Math.max(button2Height, button3Height))); + } + + Dimension2D currentSize = left ? metrics.get().leftInset() : metrics.get().rightInset(); + + // Update the overlay metrics if they have changed. + if (currentSize.getWidth() != totalWidth || currentSize.getHeight() != totalHeight) { + var empty = new Dimension2D(0, 0); + var size = new Dimension2D(totalWidth, totalHeight); + HeaderButtonMetrics newMetrics = left + ? new HeaderButtonMetrics(size, empty, buttonDefaultHeight.get()) + : new HeaderButtonMetrics(empty, size, buttonDefaultHeight.get()); + metrics.set(newMetrics); + } + + layoutInArea(button1, button1X, getButtonOffsetY(button1Height), button1Width, button1Height, + BASELINE_OFFSET_SAME_AS_HEIGHT, Insets.EMPTY, true, true, + HPos.LEFT, VPos.TOP, false); + + layoutInArea(button2, button2X, getButtonOffsetY(button2Height), button2Width, button2Height, + BASELINE_OFFSET_SAME_AS_HEIGHT, Insets.EMPTY, true, true, + HPos.LEFT, VPos.TOP, false); + + layoutInArea(button3, button3X, getButtonOffsetY(button3Height), button3Width, button3Height, + BASELINE_OFFSET_SAME_AS_HEIGHT, Insets.EMPTY, true, true, + HPos.LEFT, VPos.TOP, false); + } + + @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)); + } + + 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(); + + HeaderButtonOverlay.this.orderedButtons.sort( + Comparator.comparing(ButtonRegion::getButtonOrder)); + } + }; + + private final Region glyph = new Region(); + private final HeaderButtonType type; + + ButtonRegion(HeaderButtonType type, String styleClass, int order) { + this.type = type; + orderedButtons.add(this); + buttonOrder.set(order); + glyph.getStyleClass().setAll("glyph"); + getChildren().add(glyph); + getStyleClass().setAll("header-button", styleClass); + } + + public HeaderButtonType 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; + } + } + + private enum ButtonPlacement { + LEFT, RIGHT + } + + private enum ButtonVerticalAlignment { + STRETCH, CENTER + } +} 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 a7b337e9e39..a54a8b254f8 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2025, 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 @@ -26,7 +26,7 @@ import com.sun.glass.events.MouseEvent; import com.sun.glass.events.ViewEvent; - +import com.sun.javafx.tk.HeaderAreaType; import java.lang.annotation.Native; import java.lang.ref.WeakReference; import java.util.Map; @@ -67,8 +67,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, @@ -363,6 +364,18 @@ public void handleSwipeGestureEvent(View view, long time, int type, int yAbs) { } + /** + * Returns the header area type at the specified coordinates, or {@code null} + * if the specified coordinates do not intersect with a header area. + * + * @param x the X coordinate + * @param y the Y coordinate + * @return the header area type, or {@code null} + */ + public HeaderAreaType pickHeaderArea(double x, double y) { + return null; + } + public Accessible getSceneAccessible() { return null; } @@ -527,7 +540,7 @@ public void setEventHandler(EventHandler eventHandler) { this.eventHandler = eventHandler; } - private boolean shouldHandleEvent() { + protected boolean shouldHandleEvent() { // Don't send any more events if the application has shutdown if (Application.GetApplication() == null) { return false; @@ -552,10 +565,10 @@ private boolean handleKeyEvent(long time, int action, return false; } - private void handleMouseEvent(long time, int type, int button, int x, int y, - int xAbs, int yAbs, - int modifiers, boolean isPopupTrigger, - boolean isSynthesized) { + protected void handleMouseEvent(long time, int type, int button, int x, int y, + int xAbs, int yAbs, + int modifiers, boolean isPopupTrigger, + boolean isSynthesized) { if (shouldHandleEvent()) { eventHandler.handleMouseEvent(this, time, type, button, x, y, xAbs, yAbs, modifiers, @@ -563,10 +576,16 @@ 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 handleNonClientMouseEvent(long time, int type, int button, int x, int y, + int xAbs, int yAbs, int modifiers, int clickCount) { + return false; + } + + 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, @@ -911,15 +930,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(); @@ -943,6 +953,25 @@ protected void notifyMouse(int type, int button, int x, int y, int xAbs, lastClickedTime = now; } + // If this is an extended window, we give the non-client handler the first chance to handle the event. + // Note that a full-screen window has no non-client area, and thus the non-client event handler + // is not notified. + // 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 event 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 = window != null + && window.isExtendedWindow() + && !isSynthesized + && !inFullscreen + && shouldHandleEvent() + && handleNonClientMouseEvent(now, type, button, x, y, xAbs, yAbs, modifiers, clickCount); + + if (handled) { + 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..4a9df6eb404 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2025, 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,20 @@ */ 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.property.BooleanProperty; +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.beans.property.ReadOnlyDoubleProperty; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleDoubleProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.scene.layout.HeaderBar; +import javafx.scene.layout.Region; import java.lang.annotation.Native; - import java.util.Collections; import java.util.LinkedList; import java.util.List; @@ -116,6 +124,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 +139,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 +147,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 +166,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 +206,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 +244,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 +262,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 +276,6 @@ protected Window(Window owner, Screen screen, int styleMask) { styleMask &= ~TRANSPARENT; } - this.owner = owner; this.styleMask = styleMask; this.isDecorated = (this.styleMask & Window.TITLED) != 0; @@ -285,6 +296,54 @@ protected Window(Window owner, Screen screen, int styleMask) { } } + /** + * Specifies the preferred header button height. Sub-classes can use this value in their header button + * visualization, but they are not required to accommodate the preferred height. + *

+ * Implementations should choose a sensible default height for their header button visualization if + * {@link HeaderBar#USE_DEFAULT_SIZE} is specified. + */ + private final DoubleProperty prefHeaderButtonHeight = + new SimpleDoubleProperty(this, "prefHeaderButtonHeight", HeaderBar.USE_DEFAULT_SIZE); + + public final ReadOnlyDoubleProperty prefHeaderButtonHeightProperty() { + return prefHeaderButtonHeight; + } + + /** + * Sets the preferred header button height. + *

+ * Note: Sub-classes must not use this method to change the preferred header button height. + * It is only invoked by the toolkit's {@link com.sun.javafx.tk.TKStage} implementation. + */ + public final void setPrefHeaderButtonHeight(double height) { + prefHeaderButtonHeight.set(height); + } + + /** + * Specifies the header button overlay. This property is managed by sub-classes that provide an overlay + * for header buttons. It is not required to use an overlay for header buttons; implementations can also + * use other ways of visualizing header buttons. + */ + protected final ObjectProperty headerButtonOverlay = + new SimpleObjectProperty<>(this, "headerButtonOverlay"); + + public final ReadOnlyObjectProperty headerButtonOverlayProperty() { + return headerButtonOverlay; + } + + /** + * Specifies the metrics for header buttons, which applications can use for layout purposes. This property + * is managed by sub-classes that support header buttons. Implementations are required to provide metrics + * for header buttons even if they don't use a header button overlay. + */ + protected final ObjectProperty headerButtonMetrics = + new SimpleObjectProperty<>(this, "headerButtonMetrics", HeaderButtonMetrics.EMPTY); + + public final ReadOnlyObjectProperty headerButtonMetricsProperty() { + return headerButtonMetrics; + } + public boolean isClosed() { Application.checkEventThread(); return this.ptr == 0L; @@ -375,9 +434,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; @@ -625,29 +681,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(); } } @@ -655,11 +709,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; } @@ -674,6 +726,14 @@ public boolean isUnifiedWindow() { return (this.styleMask & Window.UNIFIED) != 0; } + 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; @@ -1148,15 +1208,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 +1265,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 +1315,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 +1350,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/gtk/GtkApplication.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkApplication.java index 4706c15ff01..f2a33641fdd 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2025, 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 @@ -438,6 +438,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/GtkView.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkView.java index a638e877356..6aa25eb95c6 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 @@ -24,8 +24,10 @@ */ package com.sun.glass.ui.gtk; +import com.sun.glass.ui.HeaderButtonOverlay; import com.sun.glass.ui.Pixels; import com.sun.glass.ui.View; +import com.sun.javafx.tk.HeaderAreaType; import java.nio.Buffer; import java.nio.ByteBuffer; @@ -136,4 +138,34 @@ protected void notifyInputMethodLinux(String str, int commitLength, int cursor, notifyInputMethod(str, attBounds, attBounds, attValues, 0, cursor, 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.pickHeaderArea(wx, wy) == HeaderAreaType.DRAGBAR) { + window.showSystemMenu(x, y); + } + } + } + + @Override + protected boolean handleNonClientMouseEvent(long time, int type, int button, int x, int y, int xAbs, int yAbs, + int modifiers, int clickCount) { + return getWindow() instanceof GtkWindow window + && window.headerButtonOverlayProperty().get() instanceof HeaderButtonOverlay overlay + && overlay.handleMouseEvent(type, button, x / window.getPlatformScaleX(), y / window.getPlatformScaleY()); + } } 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..4bc3e1b1f39 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, 2025, 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 @@ -26,15 +26,22 @@ import com.sun.glass.ui.Cursor; import com.sun.glass.events.WindowEvent; +import com.sun.glass.ui.HeaderButtonMetrics; 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.HeaderButtonOverlay; +import com.sun.javafx.tk.HeaderAreaType; class GtkWindow extends Window { public GtkWindow(Window owner, Screen screen, int styleMask) { super(owner, screen, styleMask); + + if (isExtendedWindow()) { + prefHeaderButtonHeightProperty().subscribe(this::onPrefHeaderButtonHeightChanged); + } } @Override @@ -91,6 +98,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); @@ -117,6 +126,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 @@ -198,4 +209,94 @@ public long getRawHandle() { long ptr = super.getRawHandle(); return ptr == 0L ? 0L : _getNativeWindowImpl(ptr); } + + /** + * 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); + } + + /** + * Creates or disposes the {@link HeaderButtonOverlay} when the preferred header button height has changed. + *

+ * If the preferred height is zero, the overlay is disposed; if the preferred height is non-zero, the + * {@link #headerButtonOverlay} and {@link #headerButtonMetrics} properties will hold the overlay and + * its metrics. + * + * @param height the preferred header button height + */ + private void onPrefHeaderButtonHeightChanged(Number height) { + // Return early if we can keep the existing overlay instance. + if (height.doubleValue() != 0 && headerButtonOverlay.get() != null) { + return; + } + + if (headerButtonOverlay.get() instanceof HeaderButtonOverlay overlay) { + overlay.dispose(); + } + + if (height.doubleValue() == 0) { + headerButtonOverlay.set(null); + headerButtonMetrics.set(HeaderButtonMetrics.EMPTY); + } else { + HeaderButtonOverlay overlay = createHeaderButtonOverlay(); + overlay.metricsProperty().subscribe(headerButtonMetrics::set); + headerButtonOverlay.set(overlay); + } + } + + /** + * Creates a new {@code HeaderButtonOverlay} instance. + */ + private HeaderButtonOverlay createHeaderButtonOverlay() { + var overlay = new HeaderButtonOverlay( + PlatformThemeObserver.getInstance().stylesheetProperty(), + isUtilityWindow(), + (getStyleMask() & RIGHT_TO_LEFT) != 0); + + // 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. + overlay.metricsProperty().subscribe(metrics -> { + int w = (int)(metrics.totalInsetWidth() * platformScaleX); + int h = (int)(metrics.maxInsetHeight() * platformScaleY); + _setSystemMinimumSize(super.getRawHandle(), w, h); + }); + + overlay.prefButtonHeightProperty().bind(prefHeaderButtonHeightProperty()); + return overlay; + } + + /** + * 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 (headerButtonOverlay.get() instanceof HeaderButtonOverlay overlay && overlay.buttonAt(wx, wy) != null) { + return false; + } + + View.EventHandler eventHandler = view.getEventHandler(); + if (eventHandler == null) { + return false; + } + + return eventHandler.pickHeaderArea(wx, wy) == HeaderAreaType.DRAGBAR; + } } 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..2ad5bbb3b87 --- /dev/null +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/PlatformThemeObserver.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2025, 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..fe0986359b4 --- /dev/null +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/WindowDecorationTheme.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2025, 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..8c436dbb4b1 --- /dev/null +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/WindowManager.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2025, 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 c40291255b9..8ccab58d9c9 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2025, 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 @@ -222,6 +222,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 a9d93a75bf5..b2813abcf9c 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2025, 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 @@ -397,6 +397,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/MacView.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/mac/MacView.java index 84cb95dcb98..862bdcf57e5 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/mac/MacView.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/mac/MacView.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2025, 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,9 +24,12 @@ */ package com.sun.glass.ui.mac; +import com.sun.glass.events.MouseEvent; import com.sun.glass.ui.Pixels; import com.sun.glass.ui.View; import com.sun.glass.ui.Window; +import com.sun.javafx.tk.HeaderAreaType; + import java.nio.Buffer; import java.nio.ByteBuffer; import java.nio.IntBuffer; @@ -182,5 +185,25 @@ protected void notifyInputMethodMac(String str, int attrib, int length, } } } -} + @Override + protected boolean handleNonClientMouseEvent(long time, int type, int button, int x, int y, int xAbs, int yAbs, + int modifiers, int clickCount) { + if (shouldHandleEvent() && type == MouseEvent.DOWN) { + var window = (MacWindow)getWindow(); + double wx = x / window.getPlatformScaleX(); + double wy = y / window.getPlatformScaleY(); + + View.EventHandler eventHandler = getEventHandler(); + if (eventHandler != null && eventHandler.pickHeaderArea(wx, wy) == HeaderAreaType.DRAGBAR) { + if (clickCount == 2) { + window.performTitleBarDoubleClickAction(); + } else if (clickCount == 1) { + window.performWindowDrag(); + } + } + } + + return false; + } +} 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..49adc4c0f61 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, 2025, 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 @@ -26,10 +26,13 @@ import com.sun.glass.events.WindowEvent; import com.sun.glass.ui.Cursor; +import com.sun.glass.ui.HeaderButtonMetrics; 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 javafx.geometry.Dimension2D; +import javafx.scene.layout.HeaderBar; import java.nio.ByteBuffer; /** @@ -44,6 +47,10 @@ final class MacWindow extends Window { protected MacWindow(Window owner, Screen screen, int styleMask) { super(owner, screen, styleMask); + + if (isExtendedWindow()) { + prefHeaderButtonHeightProperty().subscribe(this::onPrefHeaderButtonHeightChanged); + } } @Override native protected long _createWindow(long ownerPtr, long screenPtr, int mask); @@ -150,5 +157,64 @@ protected void _requestInput(long ptr, String text, int type, double width, doub protected void _releaseInput(long ptr) { throw new UnsupportedOperationException("Not supported yet."); } + + public void performWindowDrag() { + _performWindowDrag(getRawHandle()); + } + + public void performTitleBarDoubleClickAction() { + _performTitleBarDoubleClickAction(getRawHandle()); + } + + private native void _performWindowDrag(long ptr); + + private native void _performTitleBarDoubleClickAction(long ptr); + + private native boolean _isRightToLeftLayoutDirection(); + + private native void _setWindowButtonStyle(long ptr, int toolbarStyle, boolean buttonsVisible); + + private void onPrefHeaderButtonHeightChanged(Number height) { + double h = height != null ? height.doubleValue() : HeaderBar.USE_DEFAULT_SIZE; + var toolbarStyle = NSWindowToolbarStyle.ofHeight(h); + _setWindowButtonStyle(getRawHandle(), toolbarStyle.style, h != 0); + updateHeaderButtonMetrics(toolbarStyle, h); + } + + private void updateHeaderButtonMetrics(NSWindowToolbarStyle toolbarStyle, double prefButtonHeight) { + double minHeight = NSWindowToolbarStyle.SMALL.size.getHeight(); + var empty = new Dimension2D(0, 0); + var size = isUtilityWindow() ? toolbarStyle.utilitySize : toolbarStyle.size; + + HeaderButtonMetrics metrics = prefButtonHeight != 0 + ? _isRightToLeftLayoutDirection() + ? new HeaderButtonMetrics(empty, size, minHeight) + : new HeaderButtonMetrics(size, empty, minHeight) + : new HeaderButtonMetrics(empty, empty, minHeight); + + headerButtonMetrics.set(metrics); + } + + private enum NSWindowToolbarStyle { + SMALL(68, 28, 1), // NSWindowToolbarStyleExpanded + MEDIUM(78, 38, 4), // NSWindowToolbarStyleUnifiedCompact + LARGE(90, 52, 3); // NSWindowToolbarStyleUnified + + NSWindowToolbarStyle(double width, double height, int style) { + this.size = new Dimension2D(width, height); + this.utilitySize = new Dimension2D(height, height); // width intentionally set to height + this.style = style; + } + + final Dimension2D size; + final Dimension2D utilitySize; + 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/com/sun/glass/ui/monocle/MonocleApplication.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/monocle/MonocleApplication.java index eb7ae3ade33..7732bb36248 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2025, 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 @@ -301,6 +301,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 eaa7e102c40..68e401d2181 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2025, 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 @@ -336,6 +336,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/WinHeaderButtonOverlay.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/win/WinHeaderButtonOverlay.java new file mode 100644 index 00000000000..95a557ddd55 --- /dev/null +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/win/WinHeaderButtonOverlay.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2025, 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.win; + +import com.sun.glass.ui.HeaderButtonOverlay; +import com.sun.javafx.binding.StringConstant; +import javafx.beans.value.ObservableValue; +import javafx.scene.Scene; +import javafx.scene.layout.HeaderButtonType; +import javafx.stage.Window; + +/** + * Windows-specific version of {@link HeaderButtonOverlay} that tweaks the scaling of header button glyphs. + */ +public class WinHeaderButtonOverlay extends HeaderButtonOverlay { + + private static final String HEADER_BUTTONS_STYLESHEET = "WindowDecoration.css"; + + /** + * These are additional scale factors for the header button glyphs at various DPI scales to account + * for differences in the way the glyphs are rendered by JavaFX and Windows. Slightly adjusting + * the scaling makes the JavaFX glyphs look more similar to the native glyphs drawn by Windows. + * The values must be listed in 25% increments. DPI scales outside of the listed range default + * to an additional scaling factor of 1. + */ + private static final double[][] SCALE_FACTORS = new double[][] { + { 1.0, 1.15 }, + { 1.25, 1.1 }, + { 1.5, 1.15 }, + { 1.75, 1.0 }, + { 2.0, 1.15 }, + { 2.25, 1.05 }, + { 2.5, 0.95 }, + }; + + public WinHeaderButtonOverlay(boolean utility, boolean rightToLeft) { + super(getStylesheet(), utility, rightToLeft); + + var windowProperty = sceneProperty().flatMap(Scene::windowProperty); + + windowProperty + .flatMap(Window::renderScaleXProperty) + .orElse(1.0) + .map(v -> getGlyphScaleFactor(v.doubleValue())) + .subscribe(this::updateGlyphScaleX); + + windowProperty + .flatMap(Window::renderScaleYProperty) + .orElse(1.0) + .map(v -> getGlyphScaleFactor(v.doubleValue())) + .subscribe(this::updateGlyphScaleY); + } + + private double getGlyphScaleFactor(double scale) { + for (double[] mapping : SCALE_FACTORS) { + if (scale >= (mapping[0] - 0.125) && scale <= (mapping[0] + 0.125)) { + return mapping[1]; + } + } + + return 1.0; + } + + private void updateGlyphScaleX(double scale) { + for (var buttonType : HeaderButtonType.values()) { + getButtonGlyph(buttonType).setScaleX(scale); + } + } + + private void updateGlyphScaleY(double scale) { + for (var buttonType : HeaderButtonType.values()) { + getButtonGlyph(buttonType).setScaleY(scale); + } + } + + private static ObservableValue getStylesheet() { + var url = WinHeaderButtonOverlay.class.getResource(HEADER_BUTTONS_STYLESHEET); + if (url == null) { + throw new RuntimeException("Resource not found: " + HEADER_BUTTONS_STYLESHEET); + } + + return StringConstant.valueOf(url.toExternalForm()); + } +} 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..0a7e84d1e72 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, 2025, 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,8 +24,11 @@ */ package com.sun.glass.ui.win; +import com.sun.glass.ui.HeaderButtonOverlay; import com.sun.glass.ui.Pixels; import com.sun.glass.ui.View; +import com.sun.javafx.tk.HeaderAreaType; + import java.util.Map; /** @@ -95,5 +98,49 @@ 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.pickHeaderArea(wx, wy) == HeaderAreaType.DRAGBAR) { + window.showSystemMenu(x, y); + } + } + } + + @Override + protected boolean handleNonClientMouseEvent(long time, int type, int button, int x, int y, int xAbs, int yAbs, + int modifiers, int clickCount) { + if (!shouldHandleEvent()) { + return false; + } + + if (getWindow() instanceof WinWindow window && + window.headerButtonOverlayProperty().get() instanceof HeaderButtonOverlay overlay) { + double wx = x / window.getPlatformScaleX(); + double wy = y / window.getPlatformScaleY(); + + // Give the header button overlay the first chance to handle the event. + if (overlay.handleMouseEvent(type, button, wx, wy)) { + return true; + } + } + + // If the overlay didn't handle the event, we pass it down to the application. + handleMouseEvent(time, type, button, x, y, xAbs, yAbs, modifiers, false, false); + return true; + } +} 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..7a48e6e82c1 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, 2025, 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,8 @@ package com.sun.glass.ui.win; import com.sun.glass.ui.Cursor; +import com.sun.glass.ui.HeaderButtonMetrics; +import com.sun.glass.ui.HeaderButtonOverlay; import com.sun.glass.ui.Pixels; import com.sun.glass.ui.Screen; import com.sun.glass.ui.View; @@ -40,10 +42,10 @@ class WinWindow extends Window { public static final long ANCHOR_NO_CAPTURE = (1L << 63); - float fxReqWidth; - float fxReqHeight; - int pfReqWidth; - int pfReqHeight; + private float fxReqWidth; + private float fxReqHeight; + private int pfReqWidth; + private int pfReqHeight; private native static void _initIDs(); static { @@ -52,6 +54,10 @@ class WinWindow extends Window { protected WinWindow(Window owner, Screen screen, int styleMask) { super(owner, screen, styleMask); + + if (isExtendedWindow()) { + prefHeaderButtonHeightProperty().subscribe(this::onPrefHeaderButtonHeightChanged); + } } @Override @@ -115,6 +121,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 @@ -257,6 +267,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); @@ -315,10 +326,110 @@ void setDeferredClosing(boolean dc) { @Override public void close() { if (!deferredClosing) { + if (headerButtonOverlay.get() instanceof HeaderButtonOverlay overlay) { + overlay.dispose(); + } + super.close(); } else { closingRequested = true; setVisible(false); } } + + /** + * 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); + } + + /** + * Creates or disposes the {@link HeaderButtonOverlay} when the preferred header button height has changed. + *

+ * If the preferred height is zero, the overlay is disposed; if the preferred height is non-zero, the + * {@link #headerButtonOverlay} and {@link #headerButtonMetrics} properties will hold the overlay and + * its metrics. + * + * @param height the preferred header button height + */ + private void onPrefHeaderButtonHeightChanged(Number height) { + // Return early if we can keep the existing overlay instance. + if (height.doubleValue() != 0 && headerButtonOverlay.get() != null) { + return; + } + + if (headerButtonOverlay.get() instanceof HeaderButtonOverlay overlay) { + overlay.dispose(); + } + + if (height.doubleValue() == 0) { + headerButtonOverlay.set(null); + headerButtonMetrics.set(HeaderButtonMetrics.EMPTY); + } else { + HeaderButtonOverlay overlay = createHeaderButtonOverlay(); + overlay.metricsProperty().subscribe(headerButtonMetrics::set); + headerButtonOverlay.set(overlay); + } + } + + /** + * Creates a new {@code HeaderButtonOverlay} instance. + */ + private HeaderButtonOverlay createHeaderButtonOverlay() { + var overlay = new WinHeaderButtonOverlay( + isUtilityWindow(), + (getStyleMask() & RIGHT_TO_LEFT) != 0); + + overlay.prefButtonHeightProperty().bind(prefHeaderButtonHeightProperty()); + return overlay; + } + + /** + * 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 (headerButtonOverlay.get() instanceof HeaderButtonOverlay overlay ? overlay.buttonAt(wx, wy) : null) { + case ICONIFY: 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(); + return switch (eventHandler != null ? eventHandler.pickHeaderArea(wx, wy) : null) { + case DRAGBAR -> HT.CAPTION.value; + case ICONIFY -> HT.MINBUTTON.value; + case MAXIMIZE -> HT.MAXBUTTON.value; + case CLOSE -> HT.CLOSE.value; + case null -> 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 98928106a5a..98184395cad 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2025, 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 @@ -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.text.Font; @@ -190,6 +191,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); } @@ -358,6 +367,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/scene/layout/HeaderButtonBehavior.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/scene/layout/HeaderButtonBehavior.java new file mode 100644 index 00000000000..e787cbdbb63 --- /dev/null +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/scene/layout/HeaderButtonBehavior.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2025, 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.scene.layout; + +import com.sun.javafx.PlatformUtil; +import javafx.beans.value.ObservableValue; +import javafx.css.PseudoClass; +import javafx.event.EventHandler; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.HeaderButtonType; +import javafx.stage.Modality; +import javafx.stage.Stage; +import javafx.util.Subscription; +import java.util.Objects; +import java.util.Optional; + +public final class HeaderButtonBehavior implements EventHandler { + + private static final PseudoClass MAXIMIZED_PSEUDO_CLASS = PseudoClass.getPseudoClass("maximized"); + + private final Node node; + private final HeaderButtonType type; + private final Subscription subscription; + + public HeaderButtonBehavior(Node node, HeaderButtonType type) { + this.node = Objects.requireNonNull(node); + this.type = Objects.requireNonNull(type); + + ObservableValue stage = node.sceneProperty() + .flatMap(Scene::windowProperty) + .map(w -> w instanceof Stage s ? s : null); + + if (type == HeaderButtonType.MAXIMIZE) { + subscription = Subscription.combine( + stage.flatMap(Stage::resizableProperty).subscribe(this::onResizableChanged), + stage.flatMap(Stage::fullScreenProperty).subscribe(this::onFullScreenChanged), + stage.flatMap(Stage::maximizedProperty).subscribe(this::onMaximizedChanged), + () -> node.removeEventHandler(MouseEvent.MOUSE_RELEASED, this) + ); + } else { + subscription = Subscription.combine( + stage.flatMap(Stage::fullScreenProperty).subscribe(this::onFullScreenChanged), + () -> node.removeEventHandler(MouseEvent.MOUSE_RELEASED, this) + ); + } + + node.addEventHandler(MouseEvent.MOUSE_RELEASED, this); + + if (!node.focusTraversableProperty().isBound()) { + node.setFocusTraversable(false); + } + } + + public void dispose() { + subscription.unsubscribe(); + } + + @Override + public void handle(MouseEvent event) { + if (!node.getLayoutBounds().contains(event.getX(), event.getY())) { + return; + } + + switch (type) { + case CLOSE -> getStage().ifPresent(Stage::close); + case ICONIFY -> getStage().ifPresent(stage -> stage.setIconified(true)); + case MAXIMIZE -> getStage().ifPresent(stage -> { + // On macOS, a non-modal window is put into full-screen mode when the maximize button is clicked, + // but enlarged to cover the desktop when the option key is pressed at the same time. + if (PlatformUtil.isMac() && stage.getModality() == Modality.NONE && !event.isAltDown()) { + stage.setFullScreen(!stage.isFullScreen()); + } else { + stage.setMaximized(!stage.isMaximized()); + } + }); + } + } + + private Optional getStage() { + Scene scene = node.getScene(); + if (scene == null) { + return Optional.empty(); + } + + return scene.getWindow() instanceof Stage stage + ? Optional.of(stage) + : Optional.empty(); + } + + private void onResizableChanged(Boolean resizable) { + if (!node.disableProperty().isBound()) { + node.setDisable(resizable == Boolean.FALSE); + } + } + + private void onFullScreenChanged(Boolean fullScreen) { + if (!node.visibleProperty().isBound() && !node.managedProperty().isBound()) { + node.setVisible(fullScreen != Boolean.TRUE); + node.setManaged(fullScreen != Boolean.TRUE); + } + } + + private void onMaximizedChanged(Boolean maximized) { + node.pseudoClassStateChanged(MAXIMIZED_PSEUDO_CLASS, maximized == Boolean.TRUE); + } +} diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/stage/StageHelper.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/stage/StageHelper.java index 576999b89b0..7f9d7d086f3 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/stage/StageHelper.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/stage/StageHelper.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2025, 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,7 +25,9 @@ package com.sun.javafx.stage; +import com.sun.glass.ui.HeaderButtonMetrics; import com.sun.javafx.util.Utils; +import javafx.beans.value.ObservableValue; import javafx.stage.Stage; import javafx.stage.Window; @@ -71,6 +73,18 @@ public static void setImportant(Stage stage, boolean important) { stageAccessor.setImportant(stage, important); } + public static void setPrefHeaderButtonHeight(Stage stage, double height) { + stageAccessor.setPrefHeaderButtonHeight(stage, height); + } + + public static double getPrefHeaderButtonHeight(Stage stage) { + return stageAccessor.getPrefHeaderButtonHeight(stage); + } + + public static ObservableValue getHeaderButtonMetrics(Stage stage) { + return stageAccessor.getHeaderButtonMetrics(stage); + } + public static void setStageAccessor(StageAccessor a) { if (stageAccessor != null) { System.out.println("Warning: Stage accessor already set: " + stageAccessor); @@ -86,7 +100,10 @@ public static StageAccessor getStageAccessor() { public static interface StageAccessor { void doVisibleChanging(Window window, boolean visible); void doVisibleChanged(Window window, boolean visible); - public void setPrimary(Stage stage, boolean primary); - public void setImportant(Stage stage, boolean important); + void setPrimary(Stage stage, boolean primary); + void setImportant(Stage stage, boolean important); + void setPrefHeaderButtonHeight(Stage stage, double height); + double getPrefHeaderButtonHeight(Stage stage); + ObservableValue getHeaderButtonMetrics(Stage stage); } } diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/stage/StagePeerListener.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/stage/StagePeerListener.java index 664eff49ca2..95ecdffb12b 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/stage/StagePeerListener.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/stage/StagePeerListener.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2025, 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,7 @@ package com.sun.javafx.stage; +import com.sun.glass.ui.HeaderButtonMetrics; import javafx.stage.Stage; @@ -38,6 +39,7 @@ public static interface StageAccessor { public void setResizable(Stage stage, boolean resizable); public void setFullScreen(Stage stage, boolean fs); public void setAlwaysOnTop(Stage stage, boolean aot); + public void setHeaderButtonMetrics(Stage stage, HeaderButtonMetrics metrics); } public StagePeerListener(Stage stage, StageAccessor stageAccessor) { @@ -72,5 +74,7 @@ public void changedAlwaysOnTop(boolean aot) { stageAccessor.setAlwaysOnTop(stage, aot); } - + public void changedHeaderButtonMetrics(HeaderButtonMetrics metrics) { + stageAccessor.setHeaderButtonMetrics(stage, metrics); + } } diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/HeaderAreaType.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/HeaderAreaType.java new file mode 100644 index 00000000000..aa36000be5c --- /dev/null +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/HeaderAreaType.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2025, 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; + +/** + * Identifies the semantic parts of the header area. + */ +public enum HeaderAreaType { + DRAGBAR, + ICONIFY, + MAXIMIZE, + CLOSE +} 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 4c953a01cc6..c44463cff65 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, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2025, 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 @@ -85,4 +85,10 @@ public interface TKScene { public void entireSceneNeedsRepaint(); public TKClipboard createDragboard(boolean isDragSource); + + 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 681793a575b..988bb0db211 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, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2025, 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 @@ -84,7 +84,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( @@ -120,4 +120,14 @@ public void touchEventNext( public void touchEventEnd(); public Accessible getSceneAccessible(); + + /** + * Returns the header area type at the specified coordinates, or {@code null} + * if the specified coordinates do not intersect with a header area. + * + * @param x the X coordinate relative to the scene + * @param y the Y coordinate relative to the scene + * @return the header area type, or {@code null} + */ + public HeaderAreaType pickHeaderArea(double x, double y); } diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/TKStage.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/TKStage.java index 6bd02811633..44ebf3cff76 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/TKStage.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/TKStage.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2025, 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 @@ -122,6 +122,8 @@ public void setBounds(float x, float y, boolean xSet, boolean ySet, public void setFullScreen(boolean fullScreen); + public void setPrefHeaderButtonHeight(double height); + // ================================================================================================================= // Functions diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/GlassStage.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/GlassStage.java index 9045f1161a4..105595bbbbd 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/GlassStage.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/GlassStage.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2025, 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 @@ -74,6 +74,9 @@ protected GlassStage() { this.stageListener = listener; } + @Override + public void setPrefHeaderButtonHeight(double height) {} + protected final GlassScene getScene() { return scene; } 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 71d0df234d4..b8a9ee20da1 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2025, 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 @@ -42,6 +42,7 @@ import com.sun.javafx.logging.PulseLogger; import static com.sun.javafx.logging.PulseLogger.PULSE_LOGGING_ENABLED; import com.sun.javafx.scene.input.KeyCodeMap; +import com.sun.javafx.tk.HeaderAreaType; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; @@ -386,16 +387,16 @@ public void handleMouseEvent(View view, long time, int type, int button, QuantumToolkit.runWithoutRenderLock(mouseNotification); } - @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"); } WindowStage stage = scene.getWindowStage(); try { - QuantumToolkit.runWithoutRenderLock(() -> { + return QuantumToolkit.runWithoutRenderLock(() -> { if (scene.sceneListener != null) { double pScaleX, pScaleY, spx, spy, sx, sy; final Window w = view.getWindow(); @@ -415,12 +416,12 @@ public void handleMouseEvent(View view, long time, int type, int button, 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); + return scene.sceneListener.menuEvent(x / pScaleX, y / pScaleY, + sx + (xAbs - spx) / pScaleX, + sy + (yAbs - spy) / pScaleY, + isKeyboardTrigger); } - return null; + return false; }); } finally { if (PULSE_LOGGING_ENABLED) { @@ -797,8 +798,7 @@ public Void get() { 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(); @@ -1246,4 +1246,15 @@ public Accessible getSceneAccessible() { } return null; } + + @Override + public HeaderAreaType pickHeaderArea(double x, double y) { + return QuantumToolkit.runWithoutRenderLock(() -> { + if (scene.sceneListener != null) { + return scene.sceneListener.pickHeaderArea(x, y); + } + + return null; + }); + } } 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..9d23d8aa92e 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, 2025, 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 63453336847..4481b6b3ce9 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2025, 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 @@ -1232,6 +1232,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..e95be547afb 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, 2025, 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..f146f663cf4 --- /dev/null +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/tk/quantum/ViewSceneOverlay.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2025, 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 com.sun.javafx.scene.SceneHelper; +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 boolean rootDirty; + 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; + } + + if (this.root != null) { + NodeHelper.setScenes(this.root, null, null); + } + + this.root = root; + + if (root != null) { + NodeHelper.setScenes(root, fxScene, null); + } + + rootDirty = true; + } + + public void synchronize() { + if (rootDirty || (root != null && !NodeHelper.isDirtyEmpty(root))) { + rootDirty = false; + + if (root != null) { + syncPeer(root); + painter.setOverlayRoot(NodeHelper.getPeer(root)); + } else { + painter.setOverlayRoot(null); + SceneHelper.getPeer(fxScene).entireSceneNeedsRepaint(); + } + } + } + + 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 c156e143d2f..2fd0651a487 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, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2008, 2025, 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 @@ -42,9 +42,11 @@ import com.sun.javafx.PlatformUtil; import com.sun.javafx.iio.common.PushbroomScaler; import com.sun.javafx.iio.common.ScalerFactory; +import com.sun.javafx.stage.StagePeerListener; import com.sun.javafx.tk.FocusCause; import com.sun.javafx.tk.TKScene; import com.sun.javafx.tk.TKStage; +import com.sun.javafx.tk.TKStageListener; import com.sun.prism.Image; import com.sun.prism.PixelFormat; import java.util.Locale; @@ -129,7 +131,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,39 +140,57 @@ private void initPlatformWindow() { windowMask |= Window.TRANSPARENT; } focusable = false; + resizable = 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; + windowMask |= Window.TITLED | Window.CLOSABLE | Window.MINIMIZABLE | Window.MAXIMIZABLE; + break; + case EXTENDED: + windowMask |= Window.EXTENDED | Window.CLOSABLE | Window.MINIMIZABLE | Window.MAXIMIZABLE; break; case UTILITY: 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; } + + if (ownerWindow != null || modality != Modality.NONE) { + windowMask &= ~(Window.MINIMIZABLE | Window.MAXIMIZABLE); + } } + 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 (platformWindow.isExtendedWindow()) { + platformWindow.headerButtonOverlayProperty().subscribe(overlay -> { + ViewScene scene = getViewScene(); + if (scene != null) { + scene.setOverlay(isInFullScreen ? null : overlay); + } + }); + + platformWindow.headerButtonMetricsProperty().subscribe(this::notifyHeaderButtonMetricsChanged); + } + if (fxStage != null && fxStage.getScene() != null) { javafx.scene.paint.Paint paint = fxStage.getScene().getFill(); if (paint instanceof javafx.scene.paint.Color) { @@ -205,6 +225,12 @@ private void computeAndSetBackground(List stops) { } } + private void notifyHeaderButtonMetricsChanged() { + if (stageListener instanceof StagePeerListener listener && platformWindow != null) { + listener.changedHeaderButtonMetrics(platformWindow.headerButtonMetricsProperty().get()); + } + } + public final Window getPlatformWindow() { return platformWindow; } @@ -225,8 +251,20 @@ StageStyle getStyle() { return style; } + @Override + public void setTKStageListener(TKStageListener listener) { + super.setTKStageListener(listener); + notifyHeaderButtonMetricsChanged(); + } + @Override public TKScene createTKScene(boolean depthBuffer, boolean msaa) { - ViewScene scene = new ViewScene(depthBuffer, msaa); + ViewScene scene = new ViewScene(fxStage != null ? fxStage.getScene() : null, depthBuffer, msaa); + + // The window-provided overlay is not visible in full-screen mode. + if (!isInFullScreen) { + scene.setOverlay(platformWindow.headerButtonOverlayProperty().get()); + } + return scene; } @@ -635,8 +673,9 @@ private void applyFullScreen() { } else { if (warning != null) { warning.cancel(); - setWarning(null); } + + setWarning(null); v.exitFullscreen(false); } } else if (!isVisible() && warning != null) { @@ -648,11 +687,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.headerButtonOverlayProperty().get()); + } } @Override public void setFullScreen(boolean fullScreen) { @@ -856,4 +895,10 @@ public void releaseInput() { rtl = b; } + @Override + public void setPrefHeaderButtonHeight(double height) { + if (platformWindow != null) { + platformWindow.setPrefHeaderButtonHeight(height); + } + } } 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 ce916656260..5f093e8eec1 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2025, 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 @@ -36,6 +36,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; @@ -215,12 +218,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 37affef8a36..260f21928c8 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, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2008, 2025, 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,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: *

    @@ -150,6 +152,17 @@ public enum ConditionalFeature { */ UNIFIED_WINDOW, + /** + * Indicates that a system supports {@link StageStyle#EXTENDED}. + *

    + * This feature is currently supported on Windows, Linux, and macOS. + * + * @since 25 + * @deprecated This is a preview feature which may be changed or removed in a future release. + */ + @Deprecated(since = "25") + 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 57dcc6835bf..4f137dd2206 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 bf939193c21..c419c9666b1 100644 --- a/modules/javafx.graphics/src/main/java/javafx/scene/Scene.java +++ b/modules/javafx.graphics/src/main/java/javafx/scene/Scene.java @@ -78,6 +78,8 @@ import javafx.geometry.*; import javafx.scene.image.WritableImage; import javafx.scene.input.*; +import javafx.scene.layout.HeaderBar; +import javafx.scene.layout.HeaderButtonType; import javafx.scene.paint.Color; import javafx.scene.paint.Paint; import javafx.stage.PopupWindow; @@ -587,6 +589,10 @@ void addToDirtyList(Node n) { } private void doCSSPass() { + if (peer != null) { + peer.processOverlayCSS(); + } + final Parent sceneRoot = getRoot(); // // JDK-8120624: when the tree is synchronized, the dirty bits are @@ -636,6 +642,10 @@ void unregisterClearInitialCssStageFlag(Node node) { } void doLayoutPass() { + if (peer != null) { + peer.layoutOverlay(); + } + final Parent r = getRoot(); if (r != null) { r.layout(); @@ -1934,7 +1944,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) { @@ -1968,12 +1978,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) { @@ -2521,6 +2535,10 @@ private void synchronizeSceneNodes() { Scene.inSynchronizer = true; + 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 // scene and then create the dirty nodes array list @@ -2781,9 +2799,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 @@ -3038,6 +3056,44 @@ public void touchEventEnd() { } } + private final PickRay pickRay = new PickRay(); + + @Override + public HeaderAreaType pickHeaderArea(double x, double y) { + Node root = Scene.this.getRoot(); + 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 HeaderBar ? true : null; + + while (intersectedNode != null) { + if (intersectedNode instanceof HeaderBar) { + return draggable == Boolean.TRUE ? HeaderAreaType.DRAGBAR : null; + } + + if (HeaderBar.getButtonType(intersectedNode) instanceof HeaderButtonType type) { + return switch (type) { + case ICONIFY -> HeaderAreaType.ICONIFY; + case MAXIMIZE -> HeaderAreaType.MAXIMIZE; + case CLOSE -> HeaderAreaType.CLOSE; + }; + } + + if (draggable == null && HeaderBar.isDraggable(intersectedNode) instanceof Boolean value) { + draggable = value; + } + + intersectedNode = intersectedNode.getParent(); + } + + return null; + } + @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..1fde6af4c53 --- /dev/null +++ b/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderBar.java @@ -0,0 +1,895 @@ +/* + * Copyright (c) 2025, 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.HeaderButtonMetrics; +import com.sun.javafx.PreviewFeature; +import com.sun.javafx.geom.Vec2d; +import com.sun.javafx.scene.layout.HeaderButtonBehavior; +import com.sun.javafx.stage.StageHelper; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.BooleanPropertyBase; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.ObjectPropertyBase; +import javafx.beans.property.ReadOnlyDoubleProperty; +import javafx.beans.property.ReadOnlyDoubleWrapper; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.beans.value.ObservableValue; +import javafx.css.StyleableDoubleProperty; +import javafx.geometry.Dimension2D; +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.scene.Scene; +import javafx.scene.input.ContextMenuEvent; +import javafx.scene.input.MouseEvent; +import javafx.stage.Stage; +import javafx.stage.StageStyle; +import javafx.util.Subscription; + +/** + * 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 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}. + * All areas can be {@code null}. The default {@link #minHeightProperty() minHeight} of the {@code HeaderBar} is + * set to match the height of the platform-specific default header buttons. + * + *

    Single header bar

    + * Most applications should only add a single {@code HeaderBar} to the scene graph, placed at the top of the + * scene and extending its entire width. This ensures that the reported values for + * {@link #leftSystemInsetProperty() leftSystemInset} and {@link #rightSystemInsetProperty() rightSystemInset}, + * which describe the area reserved for the system-provided window buttons, correctly align with the location + * of the {@code HeaderBar} and are taken into account when the contents of the {@code HeaderBar} are laid out. + * + *

    Multiple header bars

    + * Applications that use multiple header bars might need to configure the additional padding inserted into the + * layout to account for the system-reserved areas. For example, when two header bars are placed next to each + * other in the horizontal direction, the default configuration incorrectly adds additional padding between the + * two header bars. In this case, the {@link #leadingSystemPaddingProperty() leadingSystemPadding} and + * {@link #trailingSystemPaddingProperty() trailingSystemPadding} properties can be used to remove the padding + * that is not needed. + * + *

    Header button height

    + * Applications can specify the preferred height for system-provided header buttons by setting the static + * {@link #setPrefButtonHeight(Stage, double)} property on the {@code Stage} associated with the header bar. + * This can be used to achieve a more cohesive visual appearance by having the system-provided header buttons + * match the height of the client-area header bar. + * + *

    Custom header buttons

    + * If more control over the header buttons is desired, applications can opt out of the system-provided header + * buttons by setting {@link #setPrefButtonHeight(Stage, double)} to zero and place custom header buttons in + * the JavaFX scene graph instead. Any JavaFX control can be used as a custom header button by setting its + * semantic type with the {@link #setButtonType(Node, HeaderButtonType)} method. + * + *

    System menu

    + * 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 the header bar is not consumed by the application. + * + *

    Layout constraints

    + * The {@code leading} and {@code trailing} children will be resized to their preferred widths and extend the + * height of the {@code HeaderBar}. The {@code center} child will be resized to fill the available space. + * {@code HeaderBar} honors the minimum, preferred, and maximum sizes 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. + *

    + * 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.
    + * + *

    Special layout of centered child

    + * If a child is configured to be centered in the {@link #centerProperty() center} area (i.e. its {@code alignment} + * constraint is either {@code null}, {@link Pos#CENTER}, {@link Pos#TOP_CENTER}, or {@link Pos#BOTTOM_CENTER}), + * it will be centered with respect to the entire header bar, and not with respect to the {@code center} area only. + * This means that, for a header bar that extends the entire width of the {@code Stage}, the child will appear to + * be horizontally centered within the {@code Stage}. + *

    + * If a child should instead be centered with respect to the {@code center} area only, a possible solution is to + * place another layout container like {@link BorderPane} in the {@code center} area, and then center the child + * within the other layout container. + * + *

    Example

    + * Usually, {@code HeaderBar} is placed in a root container like {@code BorderPane} to align it + * with the top of the scene: + *
    {@code
    + * 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();
    + *     }
    + * }
    + * }
    + * + * @since 25 + * @deprecated This is a preview feature which may be changed or removed in a future release. + */ +@Deprecated(since = "25") +public class HeaderBar extends Region { + + private static final Dimension2D EMPTY = new Dimension2D(0, 0); + private static final String DRAGGABLE = "headerbar-draggable"; + private static final String BUTTON_TYPE = "headerbar-button-type"; + private static final String ALIGNMENT = "headerbar-alignment"; + private static final String MARGIN = "headerbar-margin"; + + /** + * 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); + } + + /** + * Specifies the {@code HeaderButtonType} of the child, indicating its semantic use in the header bar. + *

    + * This property can be set on any {@link Node}. Specifying a header button type also provides the behavior + * associated with the button type. If the default behavior is not desired, applications can register an + * event filter on the child node that consumes the {@link MouseEvent#MOUSE_RELEASED} event. + * + * @param child the child node + * @param value the {@code HeaderButtonType}, or {@code null} + */ + public static void setButtonType(Node child, HeaderButtonType value) { + Pane.setConstraint(child, BUTTON_TYPE, value); + + if (child.getProperties().get(HeaderButtonBehavior.class) instanceof HeaderButtonBehavior behavior) { + behavior.dispose(); + } + + if (value != null) { + child.getProperties().put(HeaderButtonBehavior.class, new HeaderButtonBehavior(child, value)); + } else { + child.getProperties().remove(HeaderButtonBehavior.class); + } + } + + /** + * Returns the {@code HeaderButtonType} of the specified child. + * + * @param child the child node + * @return the {@code HeaderButtonType}, or {@code null} + */ + public static HeaderButtonType getButtonType(Node child) { + return (HeaderButtonType)Pane.getConstraint(child, BUTTON_TYPE); + } + + /** + * Sentinel value that can be used for {@link #setPrefButtonHeight(Stage, double)} to indicate that + * the platform should choose the platform-specific default button height. + */ + public static final double USE_DEFAULT_SIZE = -1; + + /** + * Specifies the preferred height of the system-provided header buttons of the specified stage. + *

    + * Any value except zero and {@link #USE_DEFAULT_SIZE} is only a hint for the platform window toolkit. + * The platform might accommodate the preferred height in various ways, such as by stretching the header + * buttons (fully or partially) to fill the preferred height, or centering the header buttons (fully or + * partially) within the preferred height. Some platforms might only accommodate the preferred height + * within platform-specific constraints, or ignore it entirely. + *

    + * Setting the preferred height to zero hides the system-provided header buttons, allowing applications to + * use custom header buttons instead (see {@link #setButtonType(Node, HeaderButtonType)}). + *

    + * The default value {@code #USE_DEFAULT_SIZE} indicates that the platform should choose the button height. + * + * @param stage the {@code Stage} + * @param height the preferred height, or 0 to hide the system-provided header buttons + */ + public static void setPrefButtonHeight(Stage stage, double height) { + StageHelper.setPrefHeaderButtonHeight(stage, height); + } + + /** + * Returns the preferred height of the system-provided header buttons of the specified stage. + * + * @param stage the {@code Stage} + * @return the preferred height of the system-provided header buttons + */ + public static double getPrefButtonHeight(Stage stage) { + return StageHelper.getPrefHeaderButtonHeight(stage); + } + + /** + * 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 Subscription subscription = Subscription.EMPTY; + private HeaderButtonMetrics currentMetrics; + private boolean currentFullScreen; + + /** + * Creates a new {@code HeaderBar}. + */ + public HeaderBar() { + PreviewFeature.HEADER_BAR.checkEnabled(); + + // 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(); + + ObservableValue stage = sceneProperty() + .flatMap(Scene::windowProperty) + .map(w -> w instanceof Stage s ? s : null); + + stage.flatMap(Stage::fullScreenProperty) + .orElse(false) + .subscribe(this::onFullScreenChanged); + + stage.subscribe(this::onStageChanged); + } + + /** + * 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) { + this(); + setLeading(leading); + setCenter(center); + setTrailing(trailing); + } + + private void onStageChanged(Stage stage) { + subscription.unsubscribe(); + + if (stage != null) { + subscription = StageHelper.getHeaderButtonMetrics(stage).subscribe(this::onMetricsChanged); + } + } + + private void onMetricsChanged(HeaderButtonMetrics metrics) { + currentMetrics = metrics; + updateInsets(); + } + + private void onFullScreenChanged(boolean fullScreen) { + currentFullScreen = fullScreen; + updateInsets(); + } + + private void updateInsets() { + if (currentFullScreen || currentMetrics == null) { + leftSystemInset.set(EMPTY); + rightSystemInset.set(EMPTY); + minSystemHeight.set(0); + } else { + leftSystemInset.set(currentMetrics.leftInset()); + rightSystemInset.set(currentMetrics.rightInset()); + minSystemHeight.set(currentMetrics.minHeight()); + } + } + + /** + * Describes the size of the left system-reserved inset, which is an area reserved for the iconify, maximize, + * and close window buttons. If there are no window buttons on the left side of the window, the returned area + * is an empty {@code Dimension2D}. + *

    + * Note that the left system inset refers to the left side of the window, independent of layout orientation. + */ + private final ReadOnlyObjectWrapper leftSystemInset = + new ReadOnlyObjectWrapper<>(this, "leftSystemInset", EMPTY) { + @Override + protected void invalidated() { + requestLayout(); + } + }; + + public final ReadOnlyObjectProperty leftSystemInsetProperty() { + return leftSystemInset.getReadOnlyProperty(); + } + + public final Dimension2D getLeftSystemInset() { + return leftSystemInset.get(); + } + + /** + * Describes the size of the right system-reserved inset, which is an area reserved for the iconify, maximize, + * and close window buttons. If there are no window buttons on the right side of the window, the returned area + * is an empty {@code Dimension2D}. + *

    + * Note that the right system inset refers to the right side of the window, independent of layout orientation. + */ + private final ReadOnlyObjectWrapper rightSystemInset = + new ReadOnlyObjectWrapper<>(this, "rightSystemInset", EMPTY) { + @Override + protected void invalidated() { + requestLayout(); + } + }; + + public final ReadOnlyObjectProperty rightSystemInsetProperty() { + return rightSystemInset.getReadOnlyProperty(); + } + + public final Dimension2D getRightSystemInset() { + return rightSystemInset.get(); + } + + /** + * The system-provided minimum recommended height for the {@code HeaderBar}, which usually corresponds + * to the height of the default header buttons. Applications can use this value as a sensible lower limit + * for the height of the {@code HeaderBar}. + *

    + * By default, {@link #minHeightProperty() minHeight} is set to the value of {@code minSystemHeight}, + * unless {@code minHeight} is explicitly set by a stylesheet or application code. + */ + private final ReadOnlyDoubleWrapper minSystemHeight = + new ReadOnlyDoubleWrapper(this, "minSystemHeight") { + @Override + protected void invalidated() { + double height = get(); + var minHeight = (StyleableDoubleProperty)minHeightProperty(); + + // Only change minHeight if it was not set by a stylesheet or application code. + if (minHeight.getStyleOrigin() == null) { + minHeight.applyStyle(null, height); + } + } + }; + + public final ReadOnlyDoubleProperty minSystemHeightProperty() { + return minSystemHeight.getReadOnlyProperty(); + } + + public final double getMinSystemHeight() { + return minSystemHeight.get(); + } + + /** + * The leading area of the {@code HeaderBar}. + *

    + * The leading area corresponds to the left area in a left-to-right layout, and to the right area + * in a right-to-left layout. + * + * @defaultValue {@code null} + */ + private final ObjectProperty leading = new NodeProperty("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}. + * + * @defaultValue {@code null} + */ + private final ObjectProperty center = new NodeProperty("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}. + *

    + * The trailing area corresponds to the right area in a left-to-right layout, and to the left area + * in a right-to-left layout. + * + * @defaultValue {@code null} + */ + private final ObjectProperty trailing = new NodeProperty("trailing"); + + public final ObjectProperty trailingProperty() { + return trailing; + } + + public final Node getTrailing() { + return trailing.get(); + } + + public final void setTrailing(Node value) { + trailing.set(value); + } + + /** + * Specifies whether additional padding should be added to the leading side of the {@code HeaderBar}. + * The size of the additional padding corresponds to the size of the system-reserved area that contains + * the default header buttons (iconify, maximize, and close). If the system-reserved area contains no + * header buttons, no additional padding is added to the leading side of the {@code HeaderBar}. + *

    + * Applications that use a single {@code HeaderBar} extending the entire width of the window should + * set this property to {@code true} to prevent the header buttons from overlapping the content of the + * {@code HeaderBar}. + * + * @defaultValue {@code true} + * @see #trailingSystemPaddingProperty() trailingSystemPadding + */ + private final BooleanProperty leadingSystemPadding = new BooleanPropertyBase(true) { + @Override + public Object getBean() { + return HeaderBar.this; + } + + @Override + public String getName() { + return "leadingSystemPadding"; + } + + @Override + protected void invalidated() { + requestLayout(); + } + }; + + public final BooleanProperty leadingSystemPaddingProperty() { + return leadingSystemPadding; + } + + public final boolean isLeadingSystemPadding() { + return leadingSystemPadding.get(); + } + + public final void setLeadingSystemPadding(boolean value) { + leadingSystemPadding.set(value); + } + + /** + * Specifies whether additional padding should be added to the trailing side of the {@code HeaderBar}. + * The size of the additional padding corresponds to the size of the system-reserved area that contains + * the default header buttons (iconify, maximize, and close). If the system-reserved area contains no + * header buttons, no additional padding is added to the trailing side of the {@code HeaderBar}. + *

    + * Applications that use a single {@code HeaderBar} extending the entire width of the window should + * set this property to {@code true} to prevent the header buttons from overlapping the content of the + * {@code HeaderBar}. + * + * @defaultValue {@code true} + * @see #leadingSystemPaddingProperty() leadingSystemPadding + */ + private final BooleanProperty trailingSystemPadding = new BooleanPropertyBase(true) { + @Override + public Object getBean() { + return HeaderBar.this; + } + + @Override + public String getName() { + return "trailingSystemPadding"; + } + + @Override + protected void invalidated() { + requestLayout(); + } + }; + + public final BooleanProperty trailingSystemPaddingProperty() { + return trailingSystemPadding; + } + + public final boolean isTrailingSystemPadding() { + return trailingSystemPadding.get(); + } + + public final void setTrailingSystemPadding(boolean value) { + trailingSystemPadding.set(value); + } + + private boolean isLeftSystemPadding(NodeOrientation nodeOrientation) { + return nodeOrientation == NodeOrientation.LEFT_TO_RIGHT && isLeadingSystemPadding() + || nodeOrientation == NodeOrientation.RIGHT_TO_LEFT && isTrailingSystemPadding(); + } + + private boolean isRightSystemPadding(NodeOrientation nodeOrientation) { + return nodeOrientation == NodeOrientation.LEFT_TO_RIGHT && isTrailingSystemPadding() + || nodeOrientation == NodeOrientation.RIGHT_TO_LEFT && isLeadingSystemPadding(); + } + + @Override + protected double computeMinWidth(double height) { + Node leading = getLeading(); + Node center = getCenter(); + Node trailing = getTrailing(); + Insets insets = getInsets(); + double leftPrefWidth; + double rightPrefWidth; + double centerMinWidth; + double systemPaddingWidth = 0; + + 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); + } + + NodeOrientation nodeOrientation = getEffectiveNodeOrientation(); + + if (isLeftSystemPadding(nodeOrientation)) { + systemPaddingWidth += getLeftSystemInset().getWidth(); + } + + if (isRightSystemPadding(nodeOrientation)) { + systemPaddingWidth += getRightSystemInset().getWidth(); + } + + return insets.getLeft() + + leftPrefWidth + + centerMinWidth + + rightPrefWidth + + insets.getRight() + + systemPaddingWidth; + } + + @Override + protected double computeMinHeight(double width) { + Node leading = getLeading(); + Node center = getCenter(); + Node 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(); + Node center = getCenter(); + Node 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(); + NodeOrientation nodeOrientation = getEffectiveNodeOrientation(); + boolean rtl = nodeOrientation == NodeOrientation.RIGHT_TO_LEFT; + double width = Math.max(getWidth(), minWidth(-1)); + double height = Math.max(getHeight(), minHeight(-1)); + double leftWidth = 0; + double rightWidth = 0; + double insideY = insets.getTop(); + double insideHeight = height - insideY - insets.getBottom(); + double insideX, insideWidth; + double leftSystemPaddingWidth = isLeftSystemPadding(nodeOrientation) ? getLeftSystemInset().getWidth() : 0; + double rightSystemPaddingWidth = isRightSystemPadding(nodeOrientation) ? getRightSystemInset().getWidth() : 0; + + if (rtl) { + left = getTrailing(); + right = getLeading(); + insideX = insets.getRight() + leftSystemPaddingWidth; + insideWidth = width - insideX - insets.getLeft() - rightSystemPaddingWidth; + } else { + left = getLeading(); + right = getTrailing(); + insideX = insets.getLeft() + leftSystemPaddingWidth; + insideWidth = width - insideX - insets.getRight() - rightSystemPaddingWidth; + } + + if (left != null && left.isManaged()) { + Insets leftMargin = adjustMarginForRTL(getNodeMargin(left), rtl); + double adjustedWidth = adjustWidthByMargin(insideWidth, leftMargin); + double childWidth = resizeChild(left, adjustedWidth, false, insideHeight, leftMargin); + leftWidth = snapSpaceX(leftMargin.getLeft()) + childWidth + 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); + double childWidth = resizeChild(right, adjustedWidth, false, insideHeight, rightMargin); + rightWidth = snapSpaceX(rightMargin.getLeft()) + childWidth + 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); + Pos alignment = getAlignment(center); + + if (alignment == null || alignment.getHpos() == HPos.CENTER) { + double adjustedWidth = adjustWidthByMargin(insideWidth - leftWidth - rightWidth, centerMargin); + double childWidth = resizeChild(center, adjustedWidth, true, insideHeight, centerMargin); + double idealX = width / 2 - childWidth / 2; + double minX = insideX + leftWidth + centerMargin.getLeft(); + double maxX = insideX + insideWidth - rightWidth - centerMargin.getRight(); + double adjustedX; + + if (idealX < minX) { + adjustedX = minX; + } else if (idealX + childWidth > maxX) { + adjustedX = maxX - childWidth; + } else { + adjustedX = idealX; + } + + positionInArea( + center, + adjustedX, insideY, + childWidth, insideHeight, 0, + new Insets(centerMargin.getTop(), 0, centerMargin.getBottom(), 0), + HPos.LEFT, alignment != null ? alignment.getVpos() : VPos.CENTER, + isSnapToPixel()); + } else { + layoutInArea( + center, + insideX + leftWidth, insideY, + insideWidth - leftWidth - rightWidth, insideHeight, 0, + centerMargin, + alignment.getHpos(), alignment.getVpos()); + } + } + } + + 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 double resizeChild(Node child, double adjustedWidth, boolean fillWidth, double insideHeight, Insets margin) { + double adjustedHeight = adjustHeightByMargin(insideHeight, margin); + double childWidth = fillWidth ? adjustedWidth : Math.min(snapSizeX(child.prefWidth(adjustedHeight)), adjustedWidth); + Vec2d size = boundedNodeSizeWithBias(child, childWidth, adjustedHeight, true, true, TEMP_VEC2D); + size.x = snapSizeX(size.x); + size.y = snapSizeX(size.y); + child.resize(size.x, size.y); + return size.x; + } + + 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, false) + : computeChildPrefAreaHeight(child, -1, margin, width, false); + } + + return 0; + } + + private Insets getNodeMargin(Node child) { + Insets margin = getMargin(child); + return margin != null ? margin : Insets.EMPTY; + } + + 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) { + getChildren().remove(value); + } + + value = get(); + + if (value != null) { + getChildren().add(value); + } + } + } +} diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderButtonType.java b/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderButtonType.java new file mode 100644 index 00000000000..0136715ebe5 --- /dev/null +++ b/modules/javafx.graphics/src/main/java/javafx/scene/layout/HeaderButtonType.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2025, 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 javafx.scene.Node; +import javafx.stage.Stage; + +/** + * Identifies the semantic type of a button in a custom {@link HeaderBar}, which enables integrations + * with the platform window manager. For example, hovering over a {@link #MAXIMIZE} button on Windows + * will summon snap layouts. + * + * @since 25 + * @deprecated This is a preview feature which may be changed or removed in a future release. + * @see HeaderBar#setButtonType(Node, HeaderButtonType) + */ +@Deprecated(since = "25") +public enum HeaderButtonType { + + /** + * Identifies the iconify button. + * + * @see Stage#isIconified() + * @see Stage#setIconified(boolean) + */ + ICONIFY, + + /** + * Identifies the maximize button. + *

    + * This button toggles the {@link Stage#isMaximized()} or {@link Stage#isFullScreen()} property, + * depending on platform-specific invocation semantics. For example, on macOS the button will + * put the window into full-screen mode by default, but maximize it to cover the desktop when + * the option key is pressed. + *

    + * If the window is maximized, the button will have the {@code maximized} pseudo-class. + * + * @see Stage#isMaximized() + * @see Stage#setMaximized(boolean) + * @see Stage#isFullScreen() + * @see Stage#setFullScreen(boolean) + */ + MAXIMIZE, + + /** + * Identifies the close button. + * + * @see Stage#close() + */ + CLOSE +} diff --git a/modules/javafx.graphics/src/main/java/javafx/stage/Stage.java b/modules/javafx.graphics/src/main/java/javafx/stage/Stage.java index 294cc5ac212..3731503e31c 100644 --- a/modules/javafx.graphics/src/main/java/javafx/stage/Stage.java +++ b/modules/javafx.graphics/src/main/java/javafx/stage/Stage.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2025, 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 @@ -39,7 +39,10 @@ import javafx.scene.Scene; import javafx.scene.image.Image; import javafx.scene.input.KeyCombination; +import javafx.scene.layout.HeaderBar; +import com.sun.glass.ui.HeaderButtonMetrics; +import com.sun.javafx.PreviewFeature; import com.sun.javafx.collections.VetoableListDecorator; import com.sun.javafx.collections.TrackableObservableList; import com.sun.javafx.scene.SceneHelper; @@ -198,6 +201,21 @@ public void setPrimary(Stage stage, boolean primary) { public void setImportant(Stage stage, boolean important) { stage.setImportant(important); } + + @Override + public void setPrefHeaderButtonHeight(Stage stage, double height) { + stage.setPrefHeaderButtonHeight(height); + } + + @Override + public double getPrefHeaderButtonHeight(Stage stage) { + return stage.getPrefHeaderButtonHeight(); + } + + @Override + public ObservableValue getHeaderButtonMetrics(Stage stage) { + return stage.headerButtonMetricsProperty(); + } }); } @@ -227,6 +245,11 @@ public void setFullScreen(Stage stage, boolean fs) { public void setAlwaysOnTop(Stage stage, boolean aot) { stage.alwaysOnTopPropertyImpl().set(aot); } + + @Override + public void setHeaderButtonMetrics(Stage stage, HeaderButtonMetrics metrics) { + stage.headerButtonMetricsProperty().set(metrics); + } }; /** @@ -434,9 +457,7 @@ public void showAndWait() { private StageStyle style; // default is set in constructor /** - * Specifies the style for this stage. This must be done prior to making - * the stage visible. The style is one of: StageStyle.DECORATED, - * StageStyle.UNDECORATED, StageStyle.TRANSPARENT, or StageStyle.UTILITY. + * Specifies the style for this stage. This must be done prior to making the stage visible. * * @param style the style for this stage. * @@ -445,7 +466,11 @@ public void showAndWait() { * * @defaultValue StageStyle.DECORATED */ + @SuppressWarnings("deprecation") public final void initStyle(StageStyle style) { + if (style == StageStyle.EXTENDED) { + PreviewFeature.STAGE_STYLE_EXTENDED.checkEnabled(); + } if (hasBeenVisible) { throw new IllegalStateException("Cannot set style once stage has been set visible"); } @@ -1094,6 +1119,7 @@ private void doVisibleChanging(boolean value) { (int) Math.ceil(getMinHeight())); getPeer().setMaximumSize((int) Math.floor(getMaxWidth()), (int) Math.floor(getMaxHeight())); + getPeer().setPrefHeaderButtonHeight(getPrefHeaderButtonHeight()); setPeerListener(new StagePeerListener(this, STAGE_ACCESSOR)); } } @@ -1259,4 +1285,29 @@ public final String getFullScreenExitHint() { public final ObjectProperty fullScreenExitHintProperty() { return fullScreenExitHint; } + + private ObjectProperty headerButtonMetrics; + + private ObjectProperty headerButtonMetricsProperty() { + if (headerButtonMetrics == null) { + headerButtonMetrics = new SimpleObjectProperty<>(this, "headerButtonMetrics"); + } + + return headerButtonMetrics; + } + + private double prefHeaderButtonHeight = HeaderBar.USE_DEFAULT_SIZE; + + private double getPrefHeaderButtonHeight() { + return prefHeaderButtonHeight; + } + + private void setPrefHeaderButtonHeight(double height) { + prefHeaderButtonHeight = height; + + TKStage peer = getPeer(); + if (peer != null) { + peer.setPrefHeaderButtonHeight(height); + } + } } 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..aa05ec4a2ad 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, 2025, 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,14 @@ package javafx.stage; +import javafx.application.ConditionalFeature; +import javafx.application.Platform; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HeaderBar; +import javafx.scene.layout.HeaderButtonType; + /** * This enum defines the possible styles for a {@code Stage}. * @since JavaFX 2.0 @@ -66,5 +74,59 @@ 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. + *

    + * 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}. + * + *

    Usage

    + * An extended window has the default header buttons (iconify, maximize, close), but no system-provided + * draggable header bar. Applications need to provide their own header bar by placing a {@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 correctly. Usually, {@code HeaderBar} 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();
    +     *     }
    +     * }
    +     * }
    + * + *

    Color scheme

    + * The color scheme of the default header 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. + * + *

    Custom header buttons

    + * If more control over the header buttons is desired, applications can opt out of the default header buttons + * by setting {@link HeaderBar#setPrefButtonHeight(Stage, double)} to zero and providing custom header buttons + * instead. Any JavaFX control can be used as a custom header button by setting its semantic type with the + * {@link HeaderBar#setButtonType(Node, HeaderButtonType)} method. + * + *

    Title text

    + * 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, for example + * in the title of miniaturized preview windows. + * + * @since 25 + * @deprecated This is a preview feature which may be changed or removed in a future release. + */ + @Deprecated(since = "25") + 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 b621bc7d882..cfa220381a5 100644 --- a/modules/javafx.graphics/src/main/native-glass/gtk/GlassApplication.cpp +++ b/modules/javafx.graphics/src/main/native-glass/gtk/GlassApplication.cpp @@ -510,6 +510,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..16f033357ed 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, 2025, 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; } @@ -375,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 @@ -524,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_general.cpp b/modules/javafx.graphics/src/main/native-glass/gtk/glass_general.cpp index 28e07eaa6a9..ea08f79bdb6 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 @@ -81,6 +81,7 @@ jfieldID jWindowPtr; jfieldID jCursorPtr; jmethodID jGtkWindowNotifyStateChanged; +jmethodID jGtkWindowDragAreaHitTest; jmethodID jClipboardContentChanged; @@ -269,8 +270,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 9e0289fe207..24b339a026d 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 @@ -194,7 +194,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 6417f52c2e8..858e6d3e111 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; @@ -259,7 +262,12 @@ 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; + } + bool press = event->type == GDK_BUTTON_PRESS; guint state = event->state; guint mask = 0; @@ -324,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) { @@ -557,6 +565,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, @@ -628,7 +640,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) { @@ -636,6 +667,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() { disableIME(); gtk_widget_destroy(gtk_widget); @@ -722,7 +757,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); } @@ -1011,11 +1046,12 @@ 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; + if (is_resizable() && !is_disabled) { + 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; @@ -1044,6 +1080,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); @@ -1196,6 +1236,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; @@ -1346,6 +1392,183 @@ 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 + * no non-client regions. + */ +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) { + WindowContextBase::process_mouse_button(event); + return; + } + + // Double-clicking on the drag area maximizes the window (or restores its size). + if (is_resizable() && 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_resizable() && !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, true); + event->type = GDK_BUTTON_RELEASE; + 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); + 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, true); + event->type = GDK_BUTTON_RELEASE; + 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); + 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 + || !is_resizable() + || !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 5d58c9d2e77..82bd72169ed 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 @@ -44,7 +44,8 @@ enum WindowManager { enum WindowFrameType { TITLED, UNTITLED, - TRANSPARENT + TRANSPARENT, + EXTENDED }; enum WindowType { @@ -111,12 +112,14 @@ class WindowContext : public DeletedMemDebug<0xCC> { virtual void paint(void* data, jint width, jint height) = 0; virtual WindowGeometry get_geometry() = 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; 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; @@ -126,6 +129,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; @@ -145,7 +149,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; @@ -169,6 +173,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() {} }; @@ -191,6 +196,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; @@ -234,12 +241,14 @@ 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(); 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,7 +256,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*); @@ -259,6 +268,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(); @@ -274,9 +284,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; @@ -294,6 +305,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*, bool synthesized = false); void work_around_compiz_state(); WindowGeometry get_geometry(); @@ -302,11 +315,13 @@ 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*); 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*); @@ -319,6 +334,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(); @@ -341,6 +357,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/mac/GlassViewDelegate.h b/modules/javafx.graphics/src/main/native-glass/mac/GlassViewDelegate.h index 2f278686c6c..db5e1894dca 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, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2025, 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 @@ -99,6 +99,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; 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 e33978c25c1..a97fc9bcf2b 100644 --- a/modules/javafx.graphics/src/main/native-glass/mac/GlassViewDelegate.m +++ b/modules/javafx.graphics/src/main/native-glass/mac/GlassViewDelegate.m @@ -1140,6 +1140,11 @@ - (BOOL)suppressMouseEnterExitOnMouseDown return YES; } +- (void)performWindowDrag +{ + [[nsView window] performWindowDragWithEvent:[NSApp currentEvent]]; +} + static jstring convertNSStringToJString(id aString, int length) { GET_MAIN_JENV; 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 8737e791ebd..0e6bb4bee4b 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2025, 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 @@ -194,6 +194,15 @@ - (void)windowWillEnterFullScreen:(NSNotification *)notification NSUInteger mask = [self->nsWindow styleMask]; self->isWindowResizable = ((mask & NSWindowStyleMaskResizable) != 0); [[self->view delegate] setResizableForFullscreen:YES]; + + // When we switch to full-screen mode, we always need the standard window buttons to be shown. + [[self->nsWindow standardWindowButton:NSWindowCloseButton] setHidden:NO]; + [[self->nsWindow standardWindowButton:NSWindowMiniaturizeButton] setHidden:NO]; + [[self->nsWindow standardWindowButton:NSWindowZoomButton] setHidden:NO]; + + if (nsWindow.toolbar != nil) { + nsWindow.toolbar.visible = NO; + } } - (void)windowDidEnterFullScreen:(NSNotification *)notification @@ -206,12 +215,23 @@ - (void)windowDidEnterFullScreen:(NSNotification *)notification - (void)windowWillExitFullScreen:(NSNotification *)notification { //NSLog(@"windowWillExitFullScreen"); + + // When we exit full-screen mode, hide the standard window buttons if they were previously hidden. + if (!self->isStandardButtonsVisible) { + [[self->nsWindow standardWindowButton:NSWindowCloseButton] setHidden:YES]; + [[self->nsWindow standardWindowButton:NSWindowMiniaturizeButton] setHidden:YES]; + [[self->nsWindow standardWindowButton:NSWindowZoomButton] setHidden:YES]; + } } - (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.h b/modules/javafx.graphics/src/main/native-glass/mac/GlassWindow.h index cd7477a592d..33006c83ff1 100644 --- a/modules/javafx.graphics/src/main/native-glass/mac/GlassWindow.h +++ b/modules/javafx.graphics/src/main/native-glass/mac/GlassWindow.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2025, 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 @@ -50,6 +50,7 @@ BOOL isTransparent; BOOL isDecorated; BOOL isResizable; + BOOL isStandardButtonsVisible; BOOL suppressWindowMoveEvent; BOOL suppressWindowResizeEvent; 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 0cf50ae1ffb..44d945f274d 100644 --- a/modules/javafx.graphics/src/main/native-glass/mac/GlassWindow.m +++ b/modules/javafx.graphics/src/main/native-glass/mac/GlassWindow.m @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2025, 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 @@ -374,39 +374,57 @@ static jlong _createWindowCommonDo(JNIEnv *env, jobject jWindow, jlong jOwnerPtr NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; { + bool isTitled = (jStyleMask & com_sun_glass_ui_Window_TITLED) != 0; + bool isClosable = (jStyleMask & com_sun_glass_ui_Window_CLOSABLE) != 0; + bool isMinimizable = (jStyleMask & com_sun_glass_ui_Window_MINIMIZABLE) != 0; + bool isMaximizable = (jStyleMask & com_sun_glass_ui_Window_MAXIMIZABLE) != 0; + bool isTransparent = (jStyleMask & com_sun_glass_ui_Window_TRANSPARENT) != 0; + bool isUtility = (jStyleMask & com_sun_glass_ui_Window_UTILITY) != 0; + bool isPopup = (jStyleMask & com_sun_glass_ui_Window_POPUP) != 0; + bool isUnified = (jStyleMask & com_sun_glass_ui_Window_UNIFIED) != 0; + bool isExtended = (jStyleMask & com_sun_glass_ui_Window_EXTENDED) != 0; + NSUInteger styleMask = NSWindowStyleMaskBorderless; // only titled windows get title - if ((jStyleMask&com_sun_glass_ui_Window_TITLED) != 0) + if (isTitled) { styleMask = styleMask|NSWindowStyleMaskTitled; } - bool isUtility = (jStyleMask & com_sun_glass_ui_Window_UTILITY) != 0; - bool isPopup = (jStyleMask & com_sun_glass_ui_Window_POPUP) != 0; - // only nontransparent windows get decorations - if ((jStyleMask&com_sun_glass_ui_Window_TRANSPARENT) == 0) + if (!isTransparent) { - if ((jStyleMask&com_sun_glass_ui_Window_CLOSABLE) != 0) + if (isClosable) { styleMask = styleMask|NSWindowStyleMaskClosable; } - if (((jStyleMask&com_sun_glass_ui_Window_MINIMIZABLE) != 0) || - ((jStyleMask&com_sun_glass_ui_Window_MAXIMIZABLE) != 0)) + if (isMinimizable || isMaximizable) { // on Mac OS X there is one set for min/max buttons, // so if clients requests either one, we turn them both on styleMask = styleMask|NSWindowStyleMaskMiniaturizable; } - if ((jStyleMask&com_sun_glass_ui_Window_UNIFIED) != 0) { + if (isExtended) { + styleMask = styleMask | NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView; + } + + if (isUnified) { styleMask = styleMask|NSWindowStyleMaskTexturedBackground; } if (isUtility) { - styleMask = styleMask | NSWindowStyleMaskUtilityWindow | NSWindowStyleMaskNonactivatingPanel; + styleMask = styleMask | NSWindowStyleMaskNonactivatingPanel; + + // The NSWindowStyleMaskUtilityWindow style makes the close button appear very small (because the + // title bar is thinner than normal). This doesn't work well with client-side title bars in extended + // windows: the point of a client-side title bar is its ability to host custom controls, so it can't + // be very thin. We therefore only add this style for non-extended windows. + if (!isExtended) { + styleMask |= NSWindowStyleMaskUtilityWindow; + } } } @@ -424,15 +442,27 @@ 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]; + window->isStandardButtonsVisible = YES; + + if (isExtended) { + [window->nsWindow setTitleVisibility:NSWindowTitleHidden]; + [window->nsWindow setTitlebarAppearsTransparent:YES]; + [window->nsWindow setToolbar:[NSToolbar new]]; + } - if ((jStyleMask & com_sun_glass_ui_Window_UNIFIED) != 0) { + if (isUnified) { //Prevent the textured effect from disappearing on border thickness recalculation [window->nsWindow setAutorecalculatesContentBorderThickness:NO forEdge:NSMaxYEdge]; } - if ((jStyleMask & com_sun_glass_ui_Window_UTILITY) != 0) { - [[window->nsWindow standardWindowButton:NSWindowMiniaturizeButton] setHidden:YES]; - [[window->nsWindow standardWindowButton:NSWindowZoomButton] setHidden:YES]; + if (isUtility) { + // When we hide the standard window buttons, they are still part of the button group that activates + // the hover appearance (the icons inside the buttons) when the cursor is over any of the buttons. + // This leads to the close button receiving the hover appearance when the mouse cursor is over one + // of the hidden buttons. Setting the hidden buttons' frame to an empty rectangle fixes this. + [[window->nsWindow standardWindowButton:NSWindowMiniaturizeButton] setFrame:CGRectMake(0, 0, 0, 0)]; + [[window->nsWindow standardWindowButton:NSWindowZoomButton] setFrame:CGRectMake(0, 0, 0, 0)]; + if (!jOwnerPtr) { [window->nsWindow setLevel:NSNormalWindowLevel]; } @@ -443,7 +473,7 @@ 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 = isTitled || isExtended; /* 10.7 full screen window support */ if ([NSWindow instancesRespondToSelector:@selector(toggleFullScreen:)]) { NSWindowCollectionBehavior behavior = [window->nsWindow collectionBehavior]; @@ -461,8 +491,7 @@ static jlong _createWindowCommonDo(JNIEnv *env, jobject jWindow, jlong jOwnerPtr [window->nsWindow setCollectionBehavior: behavior]; } - window->isTransparent = (jStyleMask & com_sun_glass_ui_Window_TRANSPARENT) != 0; - if (window->isTransparent == YES) + if (isTransparent) { [window->nsWindow setBackgroundColor:[NSColor clearColor]]; [window->nsWindow setHasShadow:NO]; @@ -474,6 +503,7 @@ static jlong _createWindowCommonDo(JNIEnv *env, jobject jWindow, jlong jOwnerPtr [window->nsWindow setOpaque:YES]; } + window->isTransparent = isTransparent; window->isSizeAssigned = NO; window->isLocationAssigned = NO; @@ -1437,3 +1467,106 @@ 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); + [[window->view delegate] performWindowDrag]; + } + GLASS_POOL_EXIT; + 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: _setWindowButtonStyle + * Signature: (JIZ)V + */ +JNIEXPORT void JNICALL Java_com_sun_glass_ui_mac_MacWindow__1setWindowButtonStyle +(JNIEnv *env, jobject jWindow, jlong jPtr, jint toolbarStyle, jboolean buttonsVisible) +{ + LOG("Java_com_sun_glass_ui_mac_MacWindow__1setWindowButtonStyle"); + if (!jPtr) return; + + GLASS_ASSERT_MAIN_JAVA_THREAD(env); + GLASS_POOL_ENTER; + { + GlassWindow *window = getGlassWindow(env, jPtr); + if (window) { + window->isStandardButtonsVisible = buttonsVisible; + + if (window->nsWindow) { + [window->nsWindow setToolbarStyle:toolbarStyle]; + [[window->nsWindow standardWindowButton:NSWindowCloseButton] setHidden:!buttonsVisible]; + [[window->nsWindow standardWindowButton:NSWindowMiniaturizeButton] setHidden:!buttonsVisible]; + [[window->nsWindow standardWindowButton:NSWindowZoomButton] setHidden:!buttonsVisible]; + } + } + } + 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/FullScreenWindow.cpp b/modules/javafx.graphics/src/main/native-glass/win/FullScreenWindow.cpp index 48f507b4ae4..5286f72aebb 100644 --- a/modules/javafx.graphics/src/main/native-glass/win/FullScreenWindow.cpp +++ b/modules/javafx.graphics/src/main/native-glass/win/FullScreenWindow.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2025, 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 @@ -329,7 +329,7 @@ LRESULT FullScreenWindow::WindowProc(UINT msg, WPARAM wParam, LPARAM lParam) case WM_MOUSEWHEEL: case WM_MOUSEHWHEEL: case WM_MOUSELEAVE: { - BOOL handled = HandleViewMouseEvent(GetHWND(), msg, wParam, lParam); + BOOL handled = HandleViewMouseEvent(GetHWND(), msg, wParam, lParam, FALSE); 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 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 f1dd1f2d5c1..3f180e047d8 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, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2025, 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) { + HandleNonClientMouseEvents(msg, wParam, lParam); + + // We need to return 0 for 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) { + HandleNonClientMouseEvents(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,43 @@ 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, m_isExtended); + 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::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(); @@ -763,6 +829,93 @@ 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; + } + + // 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); + result = LRESULT(res); + + return TRUE; +} + bool GlassWindow::HandleCommand(WORD cmdID) { return HandleMenuCommand(GetHWND(), cmdID); } @@ -1087,6 +1240,60 @@ void GlassWindow::SetIcon(HICON hIcon) m_hIcon = hIcon; } +void GlassWindow::ShowSystemMenu(int x, int y) +{ + WINDOWPLACEMENT placement; + if (!::GetWindowPlacement(GetHWND(), &placement)) { + return; + } + + // Mirror the X coordinate we get from JavaFX if this is a RTL window. + 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; + } + + 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; + + 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 = !m_isResizable || maximized ? MF_DISABLED : MF_ENABLED; + SetMenuItemInfo(systemMenu, SC_SIZE, FALSE, &menuItemInfo); + + menuItemInfo.fState = canMinimize ? MF_ENABLED : MF_DISABLED; + SetMenuItemInfo(systemMenu, SC_MINIMIZE, FALSE, &menuItemInfo); + + menuItemInfo.fState = canMaximize ? MF_ENABLED : MF_DISABLED; + 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 * @@ -1145,6 +1352,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 +1375,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 +1421,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); @@ -1908,4 +2124,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 a56b060f340..f88dbdd29dd 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, 2025, 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) { @@ -102,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); @@ -144,6 +146,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 +187,10 @@ 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); + void HandleNonClientMouseEvents(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..3e1fbfd1b6d 100644 --- a/modules/javafx.graphics/src/main/native-glass/win/Utils.h +++ b/modules/javafx.graphics/src/main/native-glass/win/Utils.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2025, 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 @@ -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 2e08f00783d..8eb09ebc3a6 100644 --- a/modules/javafx.graphics/src/main/native-glass/win/ViewContainer.cpp +++ b/modules/javafx.graphics/src/main/native-glass/win/ViewContainer.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2025, 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 @@ -690,7 +690,7 @@ void ViewContainer::HandleViewTypedEvent(HWND hwnd, UINT msg, WPARAM wParam, LPA SendViewTypedEvent(repCount, wChar); } -BOOL ViewContainer::HandleViewMouseEvent(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) +BOOL ViewContainer::HandleViewMouseEvent(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, BOOL extendedWindow) { if (!GetGlassView()) { return FALSE; @@ -701,7 +701,54 @@ BOOL ViewContainer::HandleViewMouseEvent(HWND hwnd, UINT msg, WPARAM wParam, LPA POINT pt; // client coords jdouble wheelRotation = 0.0; - if (msg == WM_MOUSELEAVE) { + // Windows with the EXTENDED style have an unusual anatomy: the entire window (excluding borders) comprises + // the client area with regards to geometry, but not with regards to hit testing. The title bar is classified + // as a non-client hit-testing area (by responding to the WM_NCHITTEST message), which enables window manager + // interactions (dragging, snap layouts, etc). + // When the mouse cursor moves from the client area to the title bar, we technically receive a WM_MOUSELEAVE + // message that would normally cause a MouseEvent.EXIT event to be emitted. However, from the point of view + // of the JavaFX application, the mouse cursor is still on the client-side title bar and therefore we wouldn't + // expect to receive an MouseEvent.EXIT event. The following code detects this situation and prevents the + // MouseEvent.EXIT event from being fired. + if (msg == WM_MOUSELEAVE && extendedWindow) { + DWORD msgPos = ::GetMessagePos(); // screen coordinates + pt.x = GET_X_LPARAM(msgPos); + pt.y = GET_Y_LPARAM(msgPos); + RECT windowRect; + + // We know that the cursor has moved from the client area to the title bar when the following two + // conditions are met: + // 1. The window under the cursor is our own window. This allows us to disambiguate the situation + // when the cursor was moved to another overlapping window that just happens to be placed over + // our title bar. + // 2. The cursor position is still within the client area of our window. This allows us to detect + // when the cursor was moved to the resize border of our window, which isn't part of the client + // area and should therefore emit an EXIT event. + if (::ChildWindowFromPointEx(::GetDesktopWindow(), pt, CWP_SKIPINVISIBLE) == hwnd + && ::GetClientRect(hwnd, &windowRect) + && ::ClientToScreen(hwnd, reinterpret_cast(&windowRect.left)) + && ::ClientToScreen(hwnd, reinterpret_cast(&windowRect.right)) + && ::PtInRect(&windowRect, pt)) { // pt is still in screen coordinates here + TRACKMOUSEEVENT trackData; + trackData.cbSize = sizeof(trackData); + trackData.dwFlags = TME_LEAVE | TME_NONCLIENT; + trackData.hwndTrack = hwnd; + trackData.dwHoverTime = HOVER_DEFAULT; + + // The cursor is now on the non-client hit-testing area of our window, and we need to enable + // non-client mouse tracking to get a WM_NCMOUSELEAVE message when the cursor leaves the + // non-client hit-testing area. + if (::TrackMouseEvent(&trackData)) { + m_bTrackingMouse = TRUE; + } + } else { + type = com_sun_glass_events_MouseEvent_EXIT; + m_bTrackingMouse = FALSE; + m_lastMouseMovePosition = -1; + } + + ::ScreenToClient(hwnd, &pt); + } else if (msg == WM_MOUSELEAVE) { type = com_sun_glass_events_MouseEvent_EXIT; // get the coords (the message does not contain them) lParam = ::GetMessagePos(); @@ -934,6 +981,160 @@ 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 + + // Windows with the EXTENDED style have an unusual anatomy: the entire window (excluding borders) comprises + // the client area with regards to geometry, but not with regards to hit testing. The title bar is classified + // as a non-client hit-testing area (by responding to the WM_NCHITTEST message), which enables window manager + // interactions (dragging, snap layouts, etc). + // When the mouse cursor moves from the title bar to the client area, we technically receive a WM_NCMOUSELEAVE + // message that would normally cause a MouseEvent.EXIT event to be emitted. However, from the point of view + // of the JavaFX application, the mouse cursor is still on the window's client area and therefore we wouldn't + // expect to receive an MouseEvent.EXIT event. The following code detects this situation and prevents the + // MouseEvent.EXIT event from being fired. + if (msg == WM_NCMOUSELEAVE) { + DWORD msgPos = ::GetMessagePos(); // screen coordinates + pt.x = GET_X_LPARAM(msgPos); + pt.y = GET_Y_LPARAM(msgPos); + + // Skip the MouseEvent.EXIT event when the cursor is now directly over our window. In contrast to + // similar code in ViewContainer::HandleViewMouseEvent(), we don't need to test whether the cursor + // is within the client area. If the cursor has left the non-client area, but is still directly over + // our window, it can't be anywhere else but in the client area. + if (::ChildWindowFromPointEx(::GetDesktopWindow(), pt, CWP_SKIPINVISIBLE) == hwnd) { + TRACKMOUSEEVENT trackData; + trackData.cbSize = sizeof(trackData); + trackData.dwFlags = TME_LEAVE; + trackData.hwndTrack = hwnd; + trackData.dwHoverTime = HOVER_DEFAULT; + + // Since the cursor is now on the client area of our window, we need to enable mouse tracking + // to get a WM_MOUSELEAVE message when the cursor leaves the client area. + if (::TrackMouseEvent(&trackData)) { + m_bTrackingMouse = TRUE; + } + } else { + type = com_sun_glass_events_MouseEvent_EXIT; + m_bTrackingMouse = FALSE; + m_lastMouseMovePosition = -1; + } + + ::ScreenToClient(hwnd, &pt); + } else if (msg >= WM_NCMOUSEMOVE + && msg <= WM_NCXBUTTONDBLCLK + && (wParam == HTCAPTION || wParam == HTMINBUTTON || wParam == HTMAXBUTTON || wParam == HTCLOSE)) { + 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_MOVE; + break; + case WM_NCLBUTTONDOWN: + type = com_sun_glass_events_MouseEvent_DOWN; + button = com_sun_glass_events_MouseEvent_BUTTON_LEFT; + break; + case WM_NCLBUTTONUP: + type = com_sun_glass_events_MouseEvent_UP; + button = com_sun_glass_events_MouseEvent_BUTTON_LEFT; + break; + case WM_NCRBUTTONDOWN: + type = com_sun_glass_events_MouseEvent_DOWN; + button = com_sun_glass_events_MouseEvent_BUTTON_RIGHT; + break; + case WM_NCRBUTTONUP: + type = com_sun_glass_events_MouseEvent_UP; + button = com_sun_glass_events_MouseEvent_BUTTON_RIGHT; + break; + case WM_NCMBUTTONDOWN: + type = com_sun_glass_events_MouseEvent_DOWN; + button = com_sun_glass_events_MouseEvent_BUTTON_OTHER; + break; + case WM_NCMBUTTONUP: + type = com_sun_glass_events_MouseEvent_UP; + button = com_sun_glass_events_MouseEvent_BUTTON_OTHER; + break; + case WM_NCXBUTTONDOWN: + type = com_sun_glass_events_MouseEvent_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_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_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_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_UP && button == com_sun_glass_events_MouseEvent_BUTTON_RIGHT, + isSynthesized); + CheckAndClearException(env); +} + void ViewContainer::NotifyCaptureChanged(HWND hwnd, HWND to) { m_mouseButtonDownCounter = 0; @@ -948,7 +1149,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..ca44e3594da 100644 --- a/modules/javafx.graphics/src/main/native-glass/win/ViewContainer.h +++ b/modules/javafx.graphics/src/main/native-glass/win/ViewContainer.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2016, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2025, 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 @@ -69,7 +69,8 @@ class ViewContainer { void HandleViewKeyEvent(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam); 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); + BOOL HandleViewMouseEvent(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, BOOL extendedWindow); + 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..09c033057c3 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, 2025, 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..2cd5dc09c7c --- /dev/null +++ b/modules/javafx.graphics/src/main/resources/com/sun/glass/ui/gtk/WindowDecorationGnome.css @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2025, 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. + */ + +.header-button-container { + -fx-button-placement: right; + -fx-button-vertical-alignment: center; + -fx-button-default-height: 36; +} + +.iconify-button, +.maximize-button, +.close-button { + -fx-background-color: #00000009; + -fx-background-radius: 15; + -fx-background-insets: 6; + -fx-pref-width: 36; +} + +.iconify-button.dark, +.maximize-button.dark, +.close-button.dark { + -fx-background-color: #ffffff09; +} + +.iconify-button:active, +.maximize-button:active, +.close-button:active { + -fx-background-color: #00000015; +} + +.iconify-button.dark:active, +.maximize-button.dark:active, +.close-button.dark:active { + -fx-background-color: #ffffff15 +} + +.iconify-button:active:hover, +.maximize-button:active:hover, +.close-button:active:hover { + -fx-background-color: #00000025; +} + +.iconify-button.dark:active:hover, +.maximize-button.dark:active:hover, +.close-button.dark:active:hover { + -fx-background-color: #ffffff25; +} + +.iconify-button:pressed, +.maximize-button:pressed, +.close-button:pressed { + -fx-background-color: #00000035 !important; +} + +.iconify-button.dark:pressed, +.maximize-button.dark:pressed, +.close-button.dark:pressed { + -fx-background-color: #ffffff35 !important; +} + +.iconify-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; +} + +.iconify-button:active > .glyph, +.maximize-button:active > .glyph, +.close-button:active > .glyph { + -fx-background-color: #333; +} + +.iconify-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; +} + +.iconify-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.9238 L 14 9.9238 L 14 15 L 9 15 L 9 9.9238 z"; +} + +.close-button > .glyph { + -fx-shape: "m 8.1465,8.1465 a 0.5,0.5 0 0 0 0,0.707 L 11.293,12 8.1465,15.1465 a 0.5,0.5 0 0 0 0,0.707 0.5,0.5 0 0 0 0.707,0 L 12,12.707 l 3.1465,3.1465 a 0.5,0.5 0 0 0 0.707,0 0.5,0.5 0 0 0 0,-0.707 L 12.707,12 15.8535,8.8535 a 0.5,0.5 0 0 0 0,-0.707 0.5,0.5 0 0 0 -0.707,0 L 12,11.293 8.8535,8.1465 a 0.5,0.5 0 0 0 -0.707,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..50820947548 --- /dev/null +++ b/modules/javafx.graphics/src/main/resources/com/sun/glass/ui/gtk/WindowDecorationKDE.css @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2025, 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. + */ + +.header-button-container { + -fx-button-placement: right; + -fx-button-vertical-alignment: center; + -fx-button-default-height: 36; +} + +.iconify-button, +.maximize-button, +.close-button { + -fx-background-color: transparent; + -fx-background-radius: 13; + -fx-background-insets: 8; + -fx-pref-width: 36; +} + +.iconify-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; +} + +.iconify-button.dark > .glyph, +.maximize-button.dark > .glyph, +.close-button.dark > .glyph { + -fx-background-color: #ced0d6; +} + +.iconify-button:hover, +.maximize-button:hover, +.close-button:hover { + -fx-background-color: #27292d; +} + +.iconify-button.dark:hover, +.maximize-button.dark:hover, +.close-button.dark:hover { + -fx-background-color: #dfe1e6; +} + +.iconify-button:hover > .glyph, +.maximize-button:hover > .glyph, +.close-button:hover > .glyph { + -fx-background-color: #ced0d6; +} + +.iconify-button.dark:hover > .glyph, +.maximize-button.dark:hover > .glyph, +.close-button.dark:hover > .glyph { + -fx-background-color: #393b40; +} + +.iconify-button:pressed, +.maximize-button:pressed, +.close-button:pressed { + -fx-background-color: #474b52 !important; +} + +.iconify-button.dark:pressed, +.maximize-button.dark:pressed, +.close-button.dark:pressed { + -fx-background-color: #6c7076 !important; +} + +.maximize-button:disabled { + -fx-managed: false; + visibility: hidden; +} + +.iconify-button > .glyph { + -fx-shape: "m 5,7.5 a 0.5,0.5 0 0 0 -0.3535,0.1465 0.5,0.5 0 0 0 0,0.707 l 5,5 a 0.5001,0.5001 0 0 0 0.707,0 l 5,-5 a 0.5,0.5 0 0 0 0,-0.707 0.5,0.5 0 0 0 -0.707,0 L 10,12.293 5.3535,7.6465 A 0.5,0.5 0 0 0 5,7.5 Z"; +} + +.maximize-button > .glyph { + -fx-shape: "m 9.6465,6.6465 -5,5 a 0.5,0.5 0 0 0 0,0.707 0.5,0.5 0 0 0 0.707,0 L 10,7.707 14.6465,12.3535 a 0.5,0.5 0 0 0 0.707,0 0.5,0.5 0 0 0 0,-0.707 l -5,-5 a 0.5001,0.5001 0 0 0 -0.707,0 z"; +} + +.maximize-button.restore > .glyph { + -fx-shape: "m 9.6465,5.1465 -4.5,4.5 a 0.5001,0.5001 0 0 0 0,0.707 l 4.5,4.5 a 0.5001,0.5001 0 0 0 0.707,0 l 4.5,-4.5 a 0.5001,0.5001 0 0 0 0,-0.707 l -4.5,-4.5 a 0.5001,0.5001 0 0 0 -0.707,0 z M 10,6.207 13.793,10 10,13.793 6.207,10 Z"; +} + +.close-button > .glyph { + -fx-shape: "m 6,5.5 a 0.5,0.5 0 0 0 -0.3535,0.1465 0.5,0.5 0 0 0 0,0.707 L 9.293,10 5.6465,13.6465 a 0.5,0.5 0 0 0 0,0.707 0.5,0.5 0 0 0 0.707,0 L 10,10.707 l 3.6465,3.6465 a 0.5,0.5 0 0 0 0.707,0 0.5,0.5 0 0 0 0,-0.707 L 10.707,10 14.3535,6.3535 a 0.5,0.5 0 0 0 0,-0.707 0.5,0.5 0 0 0 -0.707,0 L 10,9.293 6.3535,5.6465 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..5f91ee9cfc2 --- /dev/null +++ b/modules/javafx.graphics/src/main/resources/com/sun/glass/ui/win/WindowDecoration.css @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2025, 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. + */ + +.header-button-container { + -fx-button-placement: right; + -fx-button-vertical-alignment: stretch; + -fx-button-default-height: 29; +} + +.iconify-button, +.maximize-button, +.close-button { + -fx-background-color: transparent; + -fx-pref-width: 46; +} + +.header-button-container.utility > .close-button { + -fx-pref-width: 29; +} + +.iconify-button:hover, +.maximize-button:hover { + -fx-background-color: #00000015; +} + +.iconify-button.dark:hover, +.maximize-button.dark:hover { + -fx-background-color: #ffffff15; +} + +.close-button:hover { + -fx-background-color: #c42b1c; +} + +.iconify-button:pressed, +.maximize-button:pressed, +.close-button:pressed { + -fx-opacity: 0.8; +} + +.iconify-button > .glyph, +.maximize-button > .glyph, +.close-button > .glyph { + -fx-background-color: #777; + -fx-scale-shape: false; +} + +.iconify-button:active > .glyph, +.maximize-button:active > .glyph, +.close-button:active > .glyph { + -fx-background-color: black; +} + +.iconify-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; +} + +.iconify-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; +} + +.iconify-button > .glyph { + -fx-shape: "m 0.4234,4.6755 q -0.0872,0 -0.164,-0.0332 Q 0.1827,4.6091 0.1245,4.551 0.0664,4.4929 0.0332,4.4161 0,4.3393 0,4.2521 0,4.1649 0.0332,4.0881 0.0664,4.0113 0.1245,3.9511 0.1827,3.8909 0.2595,3.8577 0.3362,3.8245 0.4234,3.8245 H 8.0783 q 0.0872,0 0.164,0.0332 0.0768,0.0332 0.1349,0.0934 0.0581,0.0602 0.0913,0.137 0.0332,0.0768 0.0332,0.164 0,0.0872 -0.0332,0.164 -0.0332,0.0768 -0.0913,0.1349 -0.0581,0.0581 -0.1349,0.0913 -0.0768,0.0332 -0.164,0.0332 z"; +} + +.maximize-button > .glyph { + -fx-shape: "M 1.2534,8.5 Q 1.0044,8.5 0.7761,8.3983 0.5479,8.2966 0.3756,8.1244 0.2034,7.9521 0.1017,7.7239 0,7.4956 0,7.2466 V 1.2534 Q 0,1.0044 0.1017,0.7761 0.2034,0.5479 0.3756,0.3756 0.5479,0.2034 0.7761,0.1017 1.0044,0 1.2534,0 H 7.2466 Q 7.4956,0 7.7239,0.1017 7.9521,0.2034 8.1244,0.3756 8.2966,0.5479 8.3983,0.7761 8.5,1.0044 8.5,1.2534 V 7.2466 Q 8.5,7.4956 8.3983,7.7239 8.2966,7.9521 8.1244,8.1244 7.9521,8.2966 7.7239,8.3983 7.4956,8.5 7.2466,8.5 Z M 7.2258,7.6492 q 0.0872,0 0.1639,-0.0332 0.0768,-0.0332 0.1349,-0.0913 0.0581,-0.0581 0.0913,-0.1349 0.0332,-0.0768 0.0332,-0.1639 V 1.2742 q 0,-0.0872 -0.0332,-0.1639 Q 7.5828,1.0334 7.5247,0.9753 7.4666,0.9172 7.3898,0.884 7.313,0.8508 7.2258,0.8508 H 1.2742 q -0.0872,0 -0.1639,0.0332 -0.0768,0.0332 -0.1349,0.0913 Q 0.9172,1.0334 0.884,1.1102 0.8508,1.187 0.8508,1.2742 v 5.9517 q 0,0.0872 0.0332,0.1639 0.0332,0.0768 0.0913,0.1349 0.0581,0.0581 0.1349,0.0913 0.0768,0.0332 0.1639,0.0332 z"; +} + +.maximize-button.restore > .glyph { + -fx-shape: "m 7.6492,2.5193 q 0,-0.3445 -0.1370,-0.6495 Q 7.3752,1.5647 7.1407,1.3385 6.9063,1.1123 6.5970,0.9816 6.2878,0.8508 5.9475,0.8508 H 1.7722 Q 1.8386,0.6599 1.9590,0.5022 2.0793,0.3445 2.2371,0.2324 2.3948,0.1204 2.5836,0.0602 2.7725,0 2.9758,0 H 5.9475 Q 6.4746,0 6.9395,0.2013 7.4043,0.4026 7.7509,0.7471 8.0974,1.0916 8.2987,1.5564 8.5,2.0212 8.5,2.5483 V 5.5242 Q 8.5,5.7275 8.4398,5.9164 8.3796,6.1052 8.2676,6.2629 8.1555,6.4207 7.9978,6.541 7.8401,6.6614 7.6492,6.7278 Z M 1.2534,8.5 Q 1.0044,8.5 0.7761,8.3983 0.5479,8.2966 0.3756,8.1244 0.2034,7.9521 0.1017,7.7239 0,7.4956 0,7.2466 V 2.9551 Q 0,2.7019 0.1017,2.4757 0.2034,2.2495 0.3756,2.0773 0.5479,1.905 0.774,1.8033 1.0002,1.7017 1.2534,1.7017 h 4.2915 q 0.2532,0 0.4814,0.1017 0.2283,0.1017 0.3984,0.2719 0.1702,0.1702 0.2719,0.3984 0.1017,0.2283 0.1017,0.4814 V 7.2466 q 0,0.2532 -0.1017,0.4794 Q 6.595,7.9521 6.4227,8.1244 6.2505,8.2966 6.0243,8.3983 5.7981,8.5 5.5449,8.5 Z M 5.5242,7.6492 q 0.0872,0 0.1639,-0.0332 0.0768,-0.0332 0.1370,-0.0913 0.0602,-0.0581 0.0934,-0.1349 0.0332,-0.0768 0.0332,-0.1639 v -4.25 q 0,-0.0872 -0.0332,-0.166 Q 5.8853,2.731 5.8271,2.6729 5.769,2.6147 5.6902,2.5815 5.6113,2.5483 5.5242,2.5483 h -4.25 q -0.0872,0 -0.1639,0.0332 -0.0768,0.0332 -0.1349,0.0934 -0.0581,0.0602 -0.0913,0.137 -0.0332,0.0768 -0.0332,0.1639 v 4.25 q 0,0.0872 0.0332,0.1639 0.0332,0.0768 0.0913,0.1349 0.0581,0.0581 0.1349,0.0913 0.0768,0.0332 0.1639,0.0332 z"; +} + +.close-button > .glyph { + -fx-shape: "M 4.25,4.8518 0.7263,8.3755 Q 0.6018,8.5 0.4275,8.5 0.2449,8.5 0.1224,8.3776 0,8.2551 0,8.0725 0,7.8982 0.1245,7.7737 L 3.6482,4.25 0.1245,0.7263 Q 0,0.6018 0,0.4233 0,0.3362 0.0332,0.2573 0.0664,0.1785 0.1245,0.1224 0.1826,0.0664 0.2615,0.0332 0.3403,0 0.4275,0 0.6018,0 0.7263,0.1245 L 4.25,3.6482 7.7737,0.1245 Q 7.8982,0 8.0767,0 q 0.0872,0 0.1639,0.0332 0.0768,0.0332 0.1349,0.0913 0.0581,0.0581 0.0913,0.1349 Q 8.5,0.3362 8.5,0.4233 8.5,0.6018 8.3755,0.7263 L 4.8518,4.25 8.3755,7.7737 Q 8.5,7.8982 8.5,8.0725 8.5,8.1597 8.4668,8.2385 8.4336,8.3174 8.3776,8.3755 8.3215,8.4336 8.2427,8.4668 8.1638,8.5 8.0767,8.5 7.8982,8.5 7.7737,8.3755 Z"; +} diff --git a/modules/javafx.graphics/src/test/addExports b/modules/javafx.graphics/src/test/addExports index 87cbf28492c..63b3fa6e015 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 # @@ -54,6 +55,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 --add-opens java.desktop/javax.imageio=ALL-UNNAMED # # compile time additions diff --git a/modules/javafx.graphics/src/test/java/test/com/sun/glass/ui/HeaderButtonOverlayTest.java b/modules/javafx.graphics/src/test/java/test/com/sun/glass/ui/HeaderButtonOverlayTest.java new file mode 100644 index 00000000000..b061239713c --- /dev/null +++ b/modules/javafx.graphics/src/test/java/test/com/sun/glass/ui/HeaderButtonOverlayTest.java @@ -0,0 +1,459 @@ +/* + * Copyright (c) 2025, 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.HeaderButtonOverlay; +import com.sun.javafx.binding.ObjectConstant; +import javafx.beans.value.ObservableValue; +import javafx.geometry.Dimension2D; +import javafx.geometry.NodeOrientation; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.layout.HeaderButtonType; +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.*; + +@SuppressWarnings("unused") +public class HeaderButtonOverlayTest { + + private static final Dimension2D EMPTY = new Dimension2D(0, 0); + + /** + * Asserts that the buttons are laid out on the right side of the control (left-to-right orientation). + */ + @Test + void rightPlacement_stretchAlignment() { + var overlay = new HeaderButtonOverlay(getStylesheet(""" + .header-button-container { -fx-button-placement: right; + -fx-button-default-height: 20; + -fx-button-vertical-alignment: stretch; } + .header-button { -fx-pref-width: 20; } + """), false, false); + + 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, 20); + assertLayoutBounds(children.get(1), 160, 0, 20, 20); + assertLayoutBounds(children.get(2), 180, 0, 20, 20); + assertEquals(EMPTY, overlay.metricsProperty().get().leftInset()); + assertEquals(new Dimension2D(60, 20), overlay.metricsProperty().get().rightInset()); + } + + /** + * Asserts that the buttons are laid out on the right side of the control (right-to-left orientation). + */ + @Test + void rightPlacement_stretchAlignment_rightToLeft() { + var overlay = new HeaderButtonOverlay(getStylesheet(""" + .header-button-container { -fx-button-placement: right; + -fx-button-default-height: 20; + -fx-button-vertical-alignment: stretch; } + .header-button { -fx-pref-width: 20; } + """), false, true); + + 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, 20); + assertLayoutBounds(children.get(1), 20, 0, 20, 20); + assertLayoutBounds(children.get(2), 0, 0, 20, 20); + assertEquals(new Dimension2D(60, 20), overlay.metricsProperty().get().leftInset()); + assertEquals(EMPTY, overlay.metricsProperty().get().rightInset()); + } + + /** + * Asserts that the buttons are laid out on the right side of the control (left-to-right orientation) + * with center alignment (including offsets caused by center alignment). + */ + @Test + void rightPlacement_centerAlignment() { + var overlay = new HeaderButtonOverlay(getStylesheet(""" + .header-button-container { -fx-button-placement: right; + -fx-button-default-height: 20; + -fx-button-vertical-alignment: center; } + .header-button { -fx-pref-width: 20; -fx-pref-height: 10; } + """), false, false); + + 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), 135, 5, 20, 10); + assertLayoutBounds(children.get(1), 155, 5, 20, 10); + assertLayoutBounds(children.get(2), 175, 5, 20, 10); + assertEquals(EMPTY, overlay.metricsProperty().get().leftInset()); + assertEquals(new Dimension2D(70, 20), overlay.metricsProperty().get().rightInset()); + } + + /** + * Asserts that the buttons are laid out on the left side of the control (right-to-left orientation) + * with center alignment (including offsets caused by center alignment). + */ + @Test + void rightPlacement_centerAlignment_rightToLeft() { + var overlay = new HeaderButtonOverlay(getStylesheet(""" + .header-button-container { -fx-button-placement: right; + -fx-button-default-height: 20; + -fx-button-vertical-alignment: center; } + .header-button { -fx-pref-width: 20; -fx-pref-height: 10; } + """), false, true); + + 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), 45, 5, 20, 10); + assertLayoutBounds(children.get(1), 25, 5, 20, 10); + assertLayoutBounds(children.get(2), 5, 5, 20, 10); + assertEquals(new Dimension2D(70, 20), overlay.metricsProperty().get().leftInset()); + assertEquals(EMPTY, overlay.metricsProperty().get().rightInset()); + } + + /** + * Asserts that the buttons are laid out on the left side of the control (left-to-right orientation). + */ + @Test + void leftPlacement_stretchAlignment() { + var overlay = new HeaderButtonOverlay(getStylesheet(""" + .header-button-container { -fx-button-placement: left; + -fx-button-default-height: 20; + -fx-button-vertical-alignment: stretch; } + .header-button { -fx-pref-width: 20; } + """), false, false); + + 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, 20); + assertLayoutBounds(children.get(1), 20, 0, 20, 20); + assertLayoutBounds(children.get(2), 40, 0, 20, 20); + assertEquals(new Dimension2D(60, 20), overlay.metricsProperty().get().leftInset()); + assertEquals(EMPTY, overlay.metricsProperty().get().rightInset()); + } + + /** + * Asserts that the buttons are laid out on the left side of the control (right-to-left orientation). + */ + @Test + void leftPlacement_stretchAlignment_rightToLeft() { + var overlay = new HeaderButtonOverlay(getStylesheet(""" + .header-button-container { -fx-button-placement: left; + -fx-button-default-height: 20; + -fx-button-vertical-alignment: stretch; } + .header-button { -fx-pref-width: 20; } + """), false, true); + + 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, 20); + assertLayoutBounds(children.get(1), 160, 0, 20, 20); + assertLayoutBounds(children.get(2), 140, 0, 20, 20); + assertEquals(EMPTY, overlay.metricsProperty().get().leftInset()); + assertEquals(new Dimension2D(60, 20), overlay.metricsProperty().get().rightInset()); + } + + /** + * Asserts that the buttons are laid out on the left side of the control (left-to-right orientation) + * with center alignment (including offsets caused by center alignment). + */ + @Test + void leftPlacement_centerAlignment() { + var overlay = new HeaderButtonOverlay(getStylesheet(""" + .header-button-container { -fx-button-placement: left; + -fx-button-default-height: 20; + -fx-button-vertical-alignment: center; } + .header-button { -fx-pref-width: 20; -fx-pref-height: 10; } + """), false, false); + + 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), 5, 5, 20, 10); + assertLayoutBounds(children.get(1), 25, 5, 20, 10); + assertLayoutBounds(children.get(2), 45, 5, 20, 10); + assertEquals(new Dimension2D(70, 20), overlay.metricsProperty().get().leftInset()); + assertEquals(EMPTY, overlay.metricsProperty().get().rightInset()); + } + + /** + * Asserts that the buttons are laid out on the left side of the control (right-to-left orientation) + * with center alignment (including offsets caused by center alignment). + */ + @Test + void leftPlacement_centerAlignment_rightToLeft() { + var overlay = new HeaderButtonOverlay(getStylesheet(""" + .header-button-container { -fx-button-placement: left; + -fx-button-default-height: 20; + -fx-button-vertical-alignment: center; } + .header-button { -fx-pref-width: 20; -fx-pref-height: 10; } + """), false, true); + + 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), 175, 5, 20, 10); + assertLayoutBounds(children.get(1), 155, 5, 20, 10); + assertLayoutBounds(children.get(2), 135, 5, 20, 10); + assertEquals(EMPTY, overlay.metricsProperty().get().leftInset()); + assertEquals(new Dimension2D(70, 20), overlay.metricsProperty().get().rightInset()); + } + + /** + * Asserts that the buttons are laid out in a custom order (left-to-right orientation). + */ + @Test + void customButtonOrder() { + var overlay = new HeaderButtonOverlay(getStylesheet(""" + .header-button-container { -fx-button-vertical-alignment: stretch; } + .header-button { -fx-pref-width: 20; -fx-pref-height: 10; } + .iconify-button { -fx-button-order: 5; } + .maximize-button { -fx-button-order: 1; } + .close-button { -fx-button-order: 3; } + """), false, false); + + var unused = new Scene(overlay); + var children = overlay.getChildrenUnmodifiable(); + overlay.resize(200, 100); + overlay.applyCss(); + overlay.layout(); + + assertTrue(children.get(0).getStyleClass().contains("iconify-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 HeaderButtonOverlay(getStylesheet(""" + .header-button-container { -fx-button-vertical-alignment: stretch; } + .header-button { -fx-pref-width: 20; -fx-pref-height: 10; } + .iconify-button { -fx-button-order: 5; } + .maximize-button { -fx-button-order: 1; } + .close-button { -fx-button-order: 3; } + """), false, true); + + 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("iconify-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); + } + + @Test + void utilityDecorationIsOnlyCloseButton() { + var overlay = new HeaderButtonOverlay(getStylesheet(""" + .header-button { -fx-pref-width: 20; -fx-pref-height: 10; } + """), true, false); + + var children = overlay.getChildrenUnmodifiable(); + assertEquals(1, children.size()); + assertTrue(children.getFirst().getStyleClass().contains("close-button")); + } + + @Test + void activePseudoClassCorrespondsToStageFocusedProperty() { + var overlay = new HeaderButtonOverlay(getStylesheet(""" + .header-button-container { -fx-button-placement: right; } + .header-button { -fx-pref-width: 20; -fx-pref-height: 10; } + """), false, false); + + var scene = new Scene(overlay); + var stage = new Stage(); + stage.setScene(scene); + stage.show(); + + assertTrue(stage.isFocused()); + assertTrue(overlay.getChildrenUnmodifiable().getFirst().getPseudoClassStates().stream().anyMatch( + pc -> pc.getPseudoClassName().equals("active"))); + + ReflectionUtils.invokeMethod(stage, "setFocused", new Class[] { boolean.class }, false); + + assertFalse(stage.isFocused()); + assertTrue(overlay.getChildrenUnmodifiable().getFirst().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 HeaderButtonOverlay(getStylesheet(""" + .header-button-container { -fx-button-placement: right; } + .header-button { -fx-pref-width: 20; -fx-pref-height: 10; } + """), false, false); + + 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 HeaderButtonOverlay(getStylesheet(""" + .header-button-container { -fx-button-placement: right; } + .header-button { -fx-pref-width: 20; -fx-pref-height: 10; } + """), false, false); + + 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 HeaderButtonOverlay(getStylesheet(""" + .header-button-container { -fx-button-placement: right; } + .header-button { -fx-pref-width: 20; -fx-pref-height: 10; } + """), false, false); + + 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 HeaderButtonOverlay#buttonAt(double, double)}. + */ + @Test + void pickButtonAtCoordinates() { + var overlay = new HeaderButtonOverlay(getStylesheet(""" + .header-button-container { -fx-button-placement: right; -fx-button-vertical-alignment: stretch; } + .header-button { -fx-pref-width: 20; -fx-pref-height: 10; } + """), false, false); + + var unused = new Scene(overlay); + overlay.resize(200, 100); + overlay.applyCss(); + overlay.layout(); + + assertNull(overlay.buttonAt(139, 5)); + assertEquals(HeaderButtonType.ICONIFY, overlay.buttonAt(140, 0)); + assertEquals(HeaderButtonType.MAXIMIZE, overlay.buttonAt(165, 5)); + assertEquals(HeaderButtonType.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/pgstub/StubStage.java b/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubStage.java index fbaff8d8df2..f33e0262421 100644 --- a/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubStage.java +++ b/modules/javafx.graphics/src/test/java/test/com/sun/javafx/pgstub/StubStage.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2025, 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 @@ -219,6 +219,10 @@ public void setFullScreen(boolean fullScreen) { notificationSender.changedFullscreen(fullScreen); } + @Override + public void setPrefHeaderButtonHeight(double height) { + } + @Override public void requestFocus() { notificationSender.changedFocused(true, FocusCause.ACTIVATED); 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..3f8ee5abbf2 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2025, 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 @@ -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..ca5b0983f84 --- /dev/null +++ b/modules/javafx.graphics/src/test/java/test/javafx/scene/layout/HeaderBarTest.java @@ -0,0 +1,438 @@ +/* + * Copyright (c) 2025, 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 com.sun.javafx.PreviewFeature; +import javafx.beans.property.DoubleProperty; +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.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import test.util.ReflectionUtils; + +import static org.junit.jupiter.api.Assertions.*; + +@SuppressWarnings("deprecation") +public class HeaderBarTest { + + HeaderBar headerBar; + + @BeforeEach + void setup() { + PreviewFeature.enableForTesting(); + headerBar = new HeaderBar(); + } + + @Test + void emptyHeaderBar() { + assertNull(headerBar.getLeading()); + assertNull(headerBar.getCenter()); + assertNull(headerBar.getTrailing()); + } + + @Test + void minHeight_correspondsToMinSystemHeight_ifNotSetByUser() { + DoubleProperty minSystemHeight = ReflectionUtils.getFieldValue(headerBar, "minSystemHeight"); + minSystemHeight.set(100); + assertEquals(100, headerBar.minHeight(-1)); + + headerBar.setMinHeight(50); + minSystemHeight.set(200); + assertEquals(50, headerBar.minHeight(-1)); + } + + @Nested + class LayoutTest { + @ParameterizedTest + @CsvSource({ + "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(Pos pos, double x, double y, double width, double height) { + var content = new MockResizable(100, 50); + HeaderBar.setAlignment(content, pos); + HeaderBar.setMargin(content, new Insets(10)); + headerBar.setLeading(content); + headerBar.resize(1000, 100); + headerBar.layout(); + + assertBounds(x, y, width, height, content); + } + + @ParameterizedTest + @CsvSource({ + "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(Pos pos, double x, double y, double width, double height) { + var content = new Rectangle(100, 50); + HeaderBar.setAlignment(content, pos); + HeaderBar.setMargin(content, new Insets(10)); + headerBar.setLeading(content); + headerBar.resize(1000, 100); + headerBar.layout(); + + assertBounds(x, y, width, height, content); + } + + @ParameterizedTest + @CsvSource({ + "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(Pos pos, double x, double y, double width, double height) { + var content = new MockResizable(100, 50); + HeaderBar.setAlignment(content, pos); + HeaderBar.setMargin(content, new Insets(10)); + headerBar.setTrailing(content); + headerBar.resize(1000, 100); + headerBar.layout(); + + assertBounds(x, y, width, height, content); + } + + @ParameterizedTest + @CsvSource({ + "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(Pos pos, double x, double y, double width, double height) { + var content = new Rectangle(100, 50); + HeaderBar.setAlignment(content, pos); + HeaderBar.setMargin(content, new Insets(10)); + headerBar.setTrailing(content); + headerBar.resize(1000, 100); + headerBar.layout(); + + assertBounds(x, y, width, height, content); + } + + @ParameterizedTest + @CsvSource({ + "TOP_LEFT, 10, 10, 200, 80", + "TOP_CENTER, 400, 10, 200, 80", + "TOP_RIGHT, 790, 10, 200, 80", + "CENTER_LEFT, 10, 10, 200, 80", + "CENTER, 400, 10, 200, 80", + "CENTER_RIGHT, 790, 10, 200, 80", + "BOTTOM_LEFT, 10, 10, 200, 80", + "BOTTOM_CENTER, 400, 10, 200, 80", + "BOTTOM_RIGHT, 790, 10, 200, 80" + }) + void alignmentOfCenterChildOnly_resizable( + Pos pos, double x, double y, double width, double height) { + var content = new MockResizable(0, 0, 100, 50, 200, 100); + HeaderBar.setAlignment(content, pos); + HeaderBar.setMargin(content, new Insets(10)); + headerBar.setCenter(content); + headerBar.resize(1000, 100); + headerBar.layout(); + + assertBounds(x, y, width, height, content); + } + + @ParameterizedTest + @CsvSource({ + "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(Pos pos, double x, double y, double width, double height) { + var content = new Rectangle(100, 50); + HeaderBar.setAlignment(content, pos); + HeaderBar.setMargin(content, new Insets(10)); + headerBar.setCenter(content); + headerBar.resize(1000, 100); + headerBar.layout(); + + assertBounds(x, y, width, height, content); + } + + @ParameterizedTest + @CsvSource({ + "TOP_LEFT, 60, 10, 200, 80", + "TOP_CENTER, 400, 10, 200, 80", + "TOP_RIGHT, 640, 10, 200, 80", + "CENTER_LEFT, 60, 10, 200, 80", + "CENTER, 400, 10, 200, 80", + "CENTER_RIGHT, 640, 10, 200, 80", + "BOTTOM_LEFT, 60, 10, 200, 80", + "BOTTOM_CENTER, 400, 10, 200, 80", + "BOTTOM_RIGHT, 640, 10, 200, 80" + }) + void alignmentOfCenterChild_resizable_withNonEmptyLeadingAndTrailingChild( + Pos pos, double x, double y, double width, double height) { + var leading = new MockResizable(50, 50); + var center = new MockResizable(0, 0, 100, 50, 200, 100); + var trailing = new MockResizable(150, 50); + HeaderBar.setAlignment(center, pos); + HeaderBar.setMargin(center, new Insets(10)); + headerBar.setLeading(leading); + headerBar.setCenter(center); + headerBar.setTrailing(trailing); + headerBar.resize(1000, 100); + headerBar.layout(); + + assertBounds(x, y, width, height, center); + } + + @ParameterizedTest + @CsvSource({ + "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( + 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); + HeaderBar.setMargin(center, new Insets(10)); + headerBar.setLeading(leading); + headerBar.setCenter(center); + headerBar.setTrailing(trailing); + headerBar.resize(1000, 100); + headerBar.layout(); + + assertBounds(x, y, width, height, center); + } + + @ParameterizedTest + @CsvSource({ + "TOP_LEFT, 160, 10, 680, 80", + "TOP_CENTER, 160, 10, 680, 80", + "TOP_RIGHT, 160, 10, 680, 80", + "CENTER_LEFT, 160, 10, 680, 80", + "CENTER, 160, 10, 680, 80", + "CENTER_RIGHT, 160, 10, 680, 80", + "BOTTOM_LEFT, 160, 10, 680, 80", + "BOTTOM_CENTER, 160, 10, 680, 80", + "BOTTOM_RIGHT, 160, 10, 680, 80" + }) + void alignmentOfCenterChild_withLeftSystemInset(Pos pos, double x, double y, double width, double height) { + ObjectProperty leftSystemInset = ReflectionUtils.getFieldValue(headerBar, "leftSystemInset"); + leftSystemInset.set(new Dimension2D(100, 100)); + alignmentOfCenterChildImpl(pos, 1000, 1000, x, y, width, height); + } + + @ParameterizedTest + @CsvSource({ + "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_andMaxWidthConstraint( + Pos pos, double x, double y, double width, double height) { + ObjectProperty leftSystemInset = ReflectionUtils.getFieldValue(headerBar, "leftSystemInset"); + leftSystemInset.set(new Dimension2D(100, 100)); + alignmentOfCenterChildImpl(pos, 1000, 100, x, y, width, height); + } + + @ParameterizedTest + @CsvSource({ + "TOP_LEFT, 60, 10, 680, 80", + "TOP_CENTER, 60, 10, 680, 80", + "TOP_RIGHT, 60, 10, 680, 80", + "CENTER_LEFT, 60, 10, 680, 80", + "CENTER, 60, 10, 680, 80", + "CENTER_RIGHT, 60, 10, 680, 80", + "BOTTOM_LEFT, 60, 10, 680, 80", + "BOTTOM_CENTER, 60, 10, 680, 80", + "BOTTOM_RIGHT, 60, 10, 680, 80" + }) + void alignmentOfCenterChild_withRightSystemInset(Pos pos, double x, double y, double width, double height) { + ObjectProperty rightSystemInset = ReflectionUtils.getFieldValue(headerBar, "rightSystemInset"); + rightSystemInset.set(new Dimension2D(100, 100)); + alignmentOfCenterChildImpl(pos, 1000, 1000, x, y, width, height); + } + + @ParameterizedTest + @CsvSource({ + "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_andMaxWidthConstraint( + Pos pos, double x, double y, double width, double height) { + ObjectProperty rightSystemInset = ReflectionUtils.getFieldValue(headerBar, "rightSystemInset"); + rightSystemInset.set(new Dimension2D(100, 100)); + alignmentOfCenterChildImpl(pos, 1000, 100, x, y, width, height); + } + + @ParameterizedTest + @CsvSource({ + "TOP_CENTER, 260, 10, 80, 80", + "CENTER, 260, 10, 80, 80", + "BOTTOM_CENTER, 260, 10, 80, 80" + }) + void alignmentOfCenterChild_withLeftSystemInset_andOffsetCausedByInsufficientHorizontalSpace( + Pos pos, double x, double y, double width, double height) { + ObjectProperty leftSystemInset = ReflectionUtils.getFieldValue(headerBar, "leftSystemInset"); + leftSystemInset.set(new Dimension2D(200, 100)); + alignmentOfCenterChildImpl(pos, 500, 100, x, y, width, height); + } + + @ParameterizedTest + @CsvSource({ + "TOP_CENTER, 60, 10, 80, 80", + "CENTER, 60, 10, 80, 80", + "BOTTOM_CENTER, 60, 10, 80, 80" + }) + void alignmentOfCenterChild_withRightSystemInset_andOffsetCausedByInsufficientHorizontalSpace( + Pos pos, double x, double y, double width, double height) { + ObjectProperty rightSystemInset = ReflectionUtils.getFieldValue(headerBar, "rightSystemInset"); + rightSystemInset.set(new Dimension2D(200, 100)); + alignmentOfCenterChildImpl(pos, 500, 100, x, y, width, height); + } + + private void alignmentOfCenterChildImpl(Pos pos, double headerBarWidth, double maxWidth, + double x, double y, double width, double height) { + var leading = new MockResizable(50, 50); + var center = new MockResizable(0, 0, 100, 50, maxWidth, 100); + var trailing = new MockResizable(150, 50); + HeaderBar.setAlignment(center, pos); + HeaderBar.setMargin(center, new Insets(10)); + headerBar.setLeading(leading); + headerBar.setCenter(center); + headerBar.setTrailing(trailing); + headerBar.resize(headerBarWidth, 100); + headerBar.layout(); + + 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_withoutReservedArea( + Pos pos, double x, double y, double width, double height) { + ObjectProperty leftSystemInset = 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.setLeadingSystemPadding(false); + 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_withoutReservedArea( + Pos pos, double x, double y, double width, double height) { + ObjectProperty rightSystemInset = 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.setTrailingSystemPadding(false); + 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()); + assertEquals(y, node.getLayoutY()); + assertEquals(width, bounds.getWidth()); + assertEquals(height, bounds.getHeight()); + } + } +} diff --git a/tests/manual/monkey/src/com/oracle/tools/fx/monkey/MainWindow.java b/tests/manual/monkey/src/com/oracle/tools/fx/monkey/MainWindow.java index 5a307954dcd..3ec673415a7 100644 --- a/tests/manual/monkey/src/com/oracle/tools/fx/monkey/MainWindow.java +++ b/tests/manual/monkey/src/com/oracle/tools/fx/monkey/MainWindow.java @@ -51,8 +51,8 @@ import com.oracle.tools.fx.monkey.tools.EmbeddedFxTextArea; import com.oracle.tools.fx.monkey.tools.EmbeddedJTextAreaWindow; import com.oracle.tools.fx.monkey.tools.KeyboardEventViewer; -import com.oracle.tools.fx.monkey.tools.ModalWindow; import com.oracle.tools.fx.monkey.tools.Native2AsciiPane; +import com.oracle.tools.fx.monkey.tools.StageTesterWindow; import com.oracle.tools.fx.monkey.tools.SystemInfoViewer; import com.oracle.tools.fx.monkey.util.FX; import com.oracle.tools.fx.monkey.util.HasSkinnable; @@ -149,6 +149,7 @@ private MenuBar createMenu() { FX.item(m, "Keyboard Event Viewer", this::openKeyboardViewer); FX.item(m, "Native to ASCII", this::openNative2Ascii); FX.item(m, "Platform Preferences Monitor", this::openPlatformPreferencesMonitor); + FX.item(m, "Stage Tester", this::openStageTesterWindow); FX.item(m, "System Info", this::openSystemInfo); // Logs FX.menu(m, "_Logging"); @@ -156,8 +157,7 @@ private MenuBar createMenu() { // Window FX.menu(m, "_Window"); FX.item(m, orientation); - FX.separator(m); - FX.item(m, "Open Modal Window", this::openModalWindow); + return m; } @@ -216,8 +216,8 @@ public int compare(DemoPage a, DemoPage b) { return pages; } - private void openModalWindow() { - new ModalWindow(this).show(); + private void openStageTesterWindow() { + new StageTesterWindow(this).show(); } private void openNative2Ascii() { diff --git a/tests/manual/monkey/src/com/oracle/tools/fx/monkey/tools/ModalWindow.java b/tests/manual/monkey/src/com/oracle/tools/fx/monkey/tools/ModalWindow.java deleted file mode 100644 index 230e0e28ea5..00000000000 --- a/tests/manual/monkey/src/com/oracle/tools/fx/monkey/tools/ModalWindow.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (c) 2023, 2025, 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.oracle.tools.fx.monkey.tools; - -import javafx.application.Platform; -import javafx.scene.Scene; -import javafx.scene.control.Button; -import javafx.scene.layout.BorderPane; -import javafx.scene.layout.HBox; -import javafx.stage.Modality; -import javafx.stage.Stage; -import javafx.stage.Window; - -/** - * Test Modal Window - */ -public class ModalWindow extends Stage { - public ModalWindow(Window owner) { - Button b1 = new Button("Does Nothing"); - b1.setDefaultButton(false); - - Button b2 = new Button("Platform.exit()"); - b2.setDefaultButton(false); - b2.setOnAction((ev) -> Platform.exit()); - - Button b3 = new Button("OK"); - b3.setOnAction((ev) -> hide()); - - HBox bp = new HBox(b1, b2, b3); - // FIX BUG: default button property ignored on macOS, ENTER goes to the first button - b3.setDefaultButton(true); - - BorderPane p = new BorderPane(); - p.setBottom(bp); - System.out.println(b2.isDefaultButton() + " " + b3.isDefaultButton()); - - setTitle("Modal Window"); - setScene(new Scene(p)); - initModality(Modality.APPLICATION_MODAL); - initOwner(owner); - setWidth(500); - setHeight(200); - } -} diff --git a/tests/manual/monkey/src/com/oracle/tools/fx/monkey/tools/StageTesterWindow.java b/tests/manual/monkey/src/com/oracle/tools/fx/monkey/tools/StageTesterWindow.java new file mode 100644 index 00000000000..ece3bb7084f --- /dev/null +++ b/tests/manual/monkey/src/com/oracle/tools/fx/monkey/tools/StageTesterWindow.java @@ -0,0 +1,304 @@ +/* + * Copyright (c) 2025, 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.oracle.tools.fx.monkey.tools; + +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.css.PseudoClass; +import javafx.geometry.Insets; +import javafx.geometry.NodeOrientation; +import javafx.geometry.Pos; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.SplitPane; +import javafx.scene.control.TextField; +import javafx.scene.layout.Background; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HeaderBar; +import javafx.scene.layout.HeaderButtonType; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; +import javafx.stage.Modality; +import javafx.stage.Stage; +import javafx.stage.StageStyle; +import javafx.stage.Window; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + +public final class StageTesterWindow extends Stage { + + public StageTesterWindow(Stage owner) { + var pane = new GridPane(); + pane.setHgap(10); + pane.setVgap(10); + + pane.add(new Label("Title"), 0, 0); + var titleTextField = new TextField("My Stage"); + pane.add(titleTextField, 1, 0); + + pane.add(new Label("Modality"), 0, 1); + var modalities = Arrays.stream(Modality.values()).map(Enum::name).toList(); + var modalityComboBox = new ComboBox<>(FXCollections.observableArrayList(modalities)); + modalityComboBox.getSelectionModel().select(0); + pane.add(modalityComboBox, 1, 1); + + pane.add(new Label("StageStyle"), 0, 2); + var stageStyles = Arrays.stream(StageStyle.values()).map(Enum::name).toList(); + var stageStyleComboBox = new ComboBox<>(FXCollections.observableArrayList(stageStyles)); + stageStyleComboBox.getSelectionModel().select(0); + pane.add(stageStyleComboBox, 1, 2); + + pane.add(new Label("NodeOrientation"), 0, 3); + var nodeOrientations = Arrays.stream(NodeOrientation.values()).map(Enum::name).toList(); + var nodeOrientationComboBox = new ComboBox<>(FXCollections.observableArrayList(nodeOrientations)); + nodeOrientationComboBox.getSelectionModel().select(2); + pane.add(nodeOrientationComboBox, 1, 3); + + pane.add(new Label("HeaderBar"), 0, 4); + var headerBarComboBox = new ComboBox<>(FXCollections.observableArrayList( + "None", "Simple", "Simple / custom buttons", "Split", "Split / custom buttons")); + headerBarComboBox.getSelectionModel().select(0); + pane.add(headerBarComboBox, 1, 4); + + pane.add(new Label("AlwaysOnTop"), 0, 5); + var alwaysOnTopCheckBox = new CheckBox(); + pane.add(alwaysOnTopCheckBox, 1, 5); + + pane.add(new Label("Resizable"), 0, 6); + var resizableCheckBox = new CheckBox(); + resizableCheckBox.setSelected(true); + pane.add(resizableCheckBox, 1, 6); + + pane.add(new Label("Iconified"), 0, 7); + var iconifiedCheckBox = new CheckBox(); + pane.add(iconifiedCheckBox, 1, 7); + + pane.add(new Label("Maximized"), 0, 8); + var maximizedCheckBox = new CheckBox(); + pane.add(maximizedCheckBox, 1, 8); + + pane.add(new Label("FullScreen"), 0, 9); + var fullScreenCheckBox = new CheckBox(); + pane.add(fullScreenCheckBox, 1, 9); + + pane.add(new Label("FullScreenExitHint"), 0, 10); + var fullScreenExitHintTextField = new TextField(); + pane.add(fullScreenExitHintTextField, 1, 10); + + var showStageButton = new Button("Show Stage"); + showStageButton.setOnAction(event -> { + var newStage = new Stage(); + newStage.initStyle(StageStyle.valueOf(stageStyleComboBox.getValue())); + newStage.initModality(Modality.valueOf(modalityComboBox.getValue())); + newStage.setTitle(titleTextField.getText()); + newStage.setAlwaysOnTop(alwaysOnTopCheckBox.isSelected()); + newStage.setResizable(resizableCheckBox.isSelected()); + newStage.setIconified(iconifiedCheckBox.isSelected()); + newStage.setMaximized(maximizedCheckBox.isSelected()); + newStage.setFullScreen(fullScreenCheckBox.isSelected()); + newStage.setFullScreenExitHint(fullScreenExitHintTextField.getText().isEmpty() + ? null : fullScreenExitHintTextField.getText()); + + if (newStage.getModality() != Modality.NONE) { + newStage.initOwner(StageTesterWindow.this); + } + + Parent root = switch (headerBarComboBox.getValue().toLowerCase(Locale.ROOT)) { + case "simple" -> createSimpleHeaderBarRoot(newStage, false); + case "simple / custom buttons" -> createSimpleHeaderBarRoot(newStage, true); + case "split" -> createSplitHeaderBarRoot(newStage, false); + case "split / custom buttons" -> createSplitHeaderBarRoot(newStage, true); + default -> new BorderPane(createWindowActions(newStage)); + }; + + var scene = new Scene(root); + scene.setNodeOrientation(NodeOrientation.valueOf(nodeOrientationComboBox.getValue())); + + newStage.setWidth(800); + newStage.setHeight(500); + newStage.setScene(scene); + newStage.show(); + }); + + var root = new BorderPane(pane); + root.setPadding(new Insets(20)); + root.setBottom(showStageButton); + BorderPane.setAlignment(showStageButton, Pos.CENTER); + BorderPane.setMargin(showStageButton, new Insets(30, 0, 0, 0)); + + initModality(Modality.APPLICATION_MODAL); + initOwner(owner); + setScene(new Scene(root)); + setTitle("Stage Tester"); + } + + private Parent createSimpleHeaderBarRoot(Stage stage, boolean customWindowButtons) { + var headerBar = new HeaderBar(); + headerBar.setBackground(Background.fill(Color.LIGHTSKYBLUE)); + headerBar.setCenter(new TextField() {{ setPromptText("Search..."); setMaxWidth(300); }}); + + var sizeComboBox = new ComboBox<>(FXCollections.observableArrayList("Small", "Medium", "Large")); + sizeComboBox.getSelectionModel().select(0); + + Runnable updateMinHeight = () -> headerBar.setMinHeight( + switch (sizeComboBox.getValue().toLowerCase(Locale.ROOT)) { + case "large" -> 80; + case "medium" -> 50; + default -> headerBar.getMinSystemHeight(); + }); + + sizeComboBox.valueProperty().subscribe(event -> updateMinHeight.run()); + headerBar.minSystemHeightProperty().subscribe(event -> updateMinHeight.run()); + + if (customWindowButtons) { + HeaderBar.setPrefButtonHeight(stage, 0); + } else { + var adaptiveButtonHeight = new CheckBox("Adaptive button height"); + + headerBar.heightProperty().subscribe(h -> { + if (adaptiveButtonHeight.isSelected()) { + HeaderBar.setPrefButtonHeight(stage, h.doubleValue()); + } + }); + + adaptiveButtonHeight.selectedProperty().subscribe(value -> { + if (value) { + HeaderBar.setPrefButtonHeight(stage, headerBar.getHeight()); + } else { + HeaderBar.setPrefButtonHeight(stage, HeaderBar.USE_DEFAULT_SIZE); + } + }); + + headerBar.setLeading(adaptiveButtonHeight); + } + + var trailingNodes = new HBox(sizeComboBox); + trailingNodes.setAlignment(Pos.CENTER); + trailingNodes.setSpacing(5); + headerBar.setTrailing(trailingNodes); + + if (customWindowButtons) { + trailingNodes.getChildren().addAll(createCustomWindowButtons()); + } + + var borderPane = new BorderPane(); + borderPane.setTop(headerBar); + borderPane.setCenter(createWindowActions(stage)); + + return borderPane; + } + + private Parent createSplitHeaderBarRoot(Stage stage, boolean customWindowButtons) { + var leftHeaderBar = new HeaderBar(); + leftHeaderBar.setBackground(Background.fill(Color.VIOLET)); + leftHeaderBar.setLeading(new Button("\u2728")); + leftHeaderBar.setCenter(new TextField() {{ setPromptText("Search..."); setMaxWidth(200); }}); + leftHeaderBar.setTrailingSystemPadding(false); + + var rightHeaderBar = new HeaderBar(); + rightHeaderBar.setBackground(Background.fill(Color.LIGHTSKYBLUE)); + rightHeaderBar.setLeadingSystemPadding(false); + + var sizeComboBox = new ComboBox<>(FXCollections.observableArrayList("Small", "Medium", "Large")); + sizeComboBox.getSelectionModel().select(0); + + Runnable updateMinHeight = () -> rightHeaderBar.setMinHeight( + switch (sizeComboBox.getValue().toLowerCase(Locale.ROOT)) { + case "large" -> 80; + case "medium" -> 50; + default -> rightHeaderBar.getMinSystemHeight(); + }); + + sizeComboBox.valueProperty().subscribe(event -> updateMinHeight.run()); + rightHeaderBar.minSystemHeightProperty().subscribe(event -> updateMinHeight.run()); + + var trailingNodes = new HBox(sizeComboBox); + trailingNodes.setAlignment(Pos.CENTER); + trailingNodes.setSpacing(5); + rightHeaderBar.setTrailing(trailingNodes); + + if (customWindowButtons) { + trailingNodes.getChildren().addAll(createCustomWindowButtons()); + HeaderBar.setPrefButtonHeight(stage, 0); + } + + rightHeaderBar.setTrailing(trailingNodes); + + var left = new BorderPane(); + left.setTop(leftHeaderBar); + left.setCenter(createWindowActions(stage)); + + var right = new BorderPane(); + right.setTop(rightHeaderBar); + + return new SplitPane(left, right); + } + + private List createCustomWindowButtons() { + var closeButton = new Button("Close"); + var iconifyButton = new Button("Iconify"); + var maximizeButton = new Button("Maximize"); + + maximizeButton.getPseudoClassStates().subscribe(() -> { + if (maximizeButton.getPseudoClassStates().contains(PseudoClass.getPseudoClass("maximized"))) { + maximizeButton.setText("Restore"); + } else { + maximizeButton.setText("Maximize"); + } + }); + + HeaderBar.setButtonType(iconifyButton, HeaderButtonType.ICONIFY); + HeaderBar.setButtonType(maximizeButton, HeaderButtonType.MAXIMIZE); + HeaderBar.setButtonType(closeButton, HeaderButtonType.CLOSE); + return List.of(iconifyButton, maximizeButton, closeButton); + } + + private Parent createWindowActions(Stage stage) { + var toggleFullScreenButton = new Button("Enter/Exit Full Screen"); + toggleFullScreenButton.setOnAction(event -> stage.setFullScreen(!stage.isFullScreen())); + + var toggleMaximizedButton = new Button("Maximize/Restore"); + toggleMaximizedButton.setOnAction(event -> stage.setMaximized(!stage.isMaximized())); + + var toggleIconifiedButton = new Button("Iconify"); + toggleIconifiedButton.setOnAction(event -> stage.setIconified(true)); + + var closeButton = new Button("Close"); + closeButton.setOnAction(event -> stage.close()); + + var root = new VBox(toggleFullScreenButton, toggleMaximizedButton, toggleIconifiedButton, closeButton); + root.setSpacing(10); + root.setAlignment(Pos.CENTER); + return root; + } +}