diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java index 5a07574e1..71d987205 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java @@ -31,7 +31,9 @@ import javafx.collections.ObservableMap; import javafx.event.Event; import javafx.event.EventDispatcher; import javafx.event.EventType; +import javafx.geometry.Bounds; import javafx.geometry.Pos; +import javafx.geometry.Rectangle2D; import javafx.scene.Cursor; import javafx.scene.Node; import javafx.scene.Scene; @@ -48,6 +50,7 @@ import javafx.scene.shape.Rectangle; import javafx.scene.text.Text; import javafx.scene.text.TextFlow; import javafx.stage.FileChooser; +import javafx.stage.Screen; import javafx.stage.Stage; import javafx.util.Callback; import javafx.util.Duration; @@ -1378,4 +1381,65 @@ public final class FXUtils { return new FileChooser.ExtensionFilter(i18n("extension.png"), IMAGE_EXTENSIONS.stream().map(ext -> "*." + ext).toArray(String[]::new)); } + + /** + * Intelligently determines the popup position to prevent the menu from exceeding screen boundaries. + * Supports multi-monitor setups by detecting the current screen where the component is located. + * Now handles first-time popup display by forcing layout measurement. + * + * @param root the root node to calculate position relative to + * @param popupInstance the popup instance to position + * @return the optimal vertical position for the popup menu + */ + public static JFXPopup.PopupVPosition determineOptimalPopupPosition(Node root, JFXPopup popupInstance) { + // Get the screen bounds in screen coordinates + Bounds screenBounds = root.localToScreen(root.getBoundsInLocal()); + + // Convert Bounds to Rectangle2D for getScreensForRectangle method + Rectangle2D boundsRect = new Rectangle2D( + screenBounds.getMinX(), screenBounds.getMinY(), + screenBounds.getWidth(), screenBounds.getHeight() + ); + + // Find the screen that contains this component (supports multi-monitor) + List screens = Screen.getScreensForRectangle(boundsRect); + Screen currentScreen = screens.isEmpty() ? Screen.getPrimary() : screens.get(0); + Rectangle2D visualBounds = currentScreen.getVisualBounds(); + + double screenHeight = visualBounds.getHeight(); + double screenMinY = visualBounds.getMinY(); + double itemScreenY = screenBounds.getMinY(); + + // Calculate available space relative to the current screen + double availableSpaceAbove = itemScreenY - screenMinY; + double availableSpaceBelow = screenMinY + screenHeight - itemScreenY - root.getBoundsInLocal().getHeight(); + + // Get popup content and ensure it's properly measured + Region popupContent = popupInstance.getPopupContent(); + + double menuHeight; + if (popupContent.getHeight() <= 0) { + // Force layout measurement if height is not yet available + popupContent.autosize(); + popupContent.applyCss(); + popupContent.layout(); + + // Get the measured height, or use a reasonable fallback + menuHeight = popupContent.getHeight(); + if (menuHeight <= 0) { + // Fallback: estimate based on number of menu items + // Each menu item is roughly 36px height + separators + padding + menuHeight = 300; // Conservative estimate for the current menu structure + } + } else { + menuHeight = popupContent.getHeight(); + } + + // Add some margin for safety + menuHeight += 20; + + return (availableSpaceAbove > menuHeight && availableSpaceBelow < menuHeight) + ? JFXPopup.PopupVPosition.BOTTOM // Show menu below the button, expanding downward + : JFXPopup.PopupVPosition.TOP; // Show menu above the button, expanding upward + } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItemSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItemSkin.java index c8e812579..6cad34694 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItemSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItemSkin.java @@ -20,15 +20,12 @@ package org.jackhuang.hmcl.ui.versions; import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXPopup; import com.jfoenix.controls.JFXRadioButton; -import javafx.geometry.Bounds; import javafx.geometry.Pos; -import javafx.geometry.Rectangle2D; import javafx.scene.Cursor; import javafx.scene.control.SkinBase; import javafx.scene.input.MouseButton; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; -import javafx.stage.Screen; import org.jackhuang.hmcl.setting.Theme; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; @@ -38,8 +35,7 @@ import org.jackhuang.hmcl.ui.construct.PopupMenu; import org.jackhuang.hmcl.ui.construct.RipplerContainer; import org.jackhuang.hmcl.util.Lazy; -import java.util.List; - +import static org.jackhuang.hmcl.ui.FXUtils.determineOptimalPopupPosition; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public class GameListItemSkin extends SkinBase { @@ -105,7 +101,7 @@ public class GameListItemSkin extends SkinBase { btnManage.setOnAction(e -> { currentSkinnable = skinnable; - JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(root); + JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(root, popup.get()); popup.get().show(root, vPosition, JFXPopup.PopupHPosition.RIGHT, 0, vPosition == JFXPopup.PopupVPosition.TOP ? root.getHeight() : -root.getHeight()); }); btnManage.getStyleClass().add("toggle-icon4"); @@ -132,45 +128,9 @@ public class GameListItemSkin extends SkinBase { } else if (e.getButton() == MouseButton.SECONDARY) { currentSkinnable = skinnable; - JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(root); + JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(root, popup.get()); popup.get().show(root, vPosition, JFXPopup.PopupHPosition.LEFT, e.getX(), vPosition == JFXPopup.PopupVPosition.TOP ? e.getY() : e.getY() - root.getHeight()); } }); } - - /** - * Intelligently determines the popup position to prevent the menu from exceeding screen boundaries. - * Supports multi-monitor setups by detecting the current screen where the component is located. - * - * @param root the root node to calculate position relative to - * @return the optimal vertical position for the popup menu - */ - private static JFXPopup.PopupVPosition determineOptimalPopupPosition(BorderPane root) { - // Get the screen bounds in screen coordinates - Bounds screenBounds = root.localToScreen(root.getBoundsInLocal()); - - // Convert Bounds to Rectangle2D for getScreensForRectangle method - Rectangle2D boundsRect = new Rectangle2D( - screenBounds.getMinX(), screenBounds.getMinY(), - screenBounds.getWidth(), screenBounds.getHeight() - ); - - // Find the screen that contains this component (supports multi-monitor) - List screens = Screen.getScreensForRectangle(boundsRect); - Screen currentScreen = screens.isEmpty() ? Screen.getPrimary() : screens.get(0); - Rectangle2D visualBounds = currentScreen.getVisualBounds(); - - double screenHeight = visualBounds.getHeight(); - double screenMinY = visualBounds.getMinY(); - double itemScreenY = screenBounds.getMinY(); - - // Calculate available space relative to the current screen - double availableSpaceAbove = itemScreenY - screenMinY; - double availableSpaceBelow = screenMinY + screenHeight - itemScreenY - root.getHeight(); - double menuHeight = popup.get().getPopupContent().getHeight(); - - return (availableSpaceAbove > menuHeight && availableSpaceBelow < menuHeight) - ? JFXPopup.PopupVPosition.BOTTOM // Show menu below the button, expanding downward - : JFXPopup.PopupVPosition.TOP; // Show menu above the button, expanding upward - } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItemSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItemSkin.java index 3c222f03c..9a0d75a89 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItemSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItemSkin.java @@ -37,6 +37,7 @@ import org.jackhuang.hmcl.util.versioning.GameVersionNumber; import java.time.Instant; +import static org.jackhuang.hmcl.ui.FXUtils.determineOptimalPopupPosition; import static org.jackhuang.hmcl.util.StringUtils.parseColorEscapes; import static org.jackhuang.hmcl.util.i18n.I18n.formatDateTime; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; @@ -137,6 +138,8 @@ public final class WorldListItemSkin extends SkinBase { new IconedMenuItem(SVG.OUTPUT, i18n("world.export"), item::export, popup), new IconedMenuItem(SVG.FOLDER_OPEN, i18n("folder.world"), item::reveal, popup)); - popup.show(root, JFXPopup.PopupVPosition.TOP, hPosition, initOffsetX, initOffsetY); + JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(root, popup); + + popup.show(root, vPosition, hPosition, initOffsetX, vPosition == JFXPopup.PopupVPosition.TOP ? initOffsetY : -initOffsetY); } }