修复 popup 第一次无法计算高度的问题 & 应用此方法到世界管理界面 (#4412)

Close #4287
This commit is contained in:
ENC_Euphony 2025-09-12 00:01:22 +08:00 committed by GitHub
parent 913ce55d43
commit 228a1e4887
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 71 additions and 44 deletions

View File

@ -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<Screen> 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
}
}

View File

@ -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<GameListItem> {
@ -105,7 +101,7 @@ public class GameListItemSkin extends SkinBase<GameListItem> {
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<GameListItem> {
} 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<Screen> 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
}
}

View File

@ -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<WorldListItem> {
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);
}
}