mirror of
https://github.com/HMCL-dev/HMCL.git
synced 2025-09-24 03:33:46 -04:00
parent
913ce55d43
commit
228a1e4887
@ -31,7 +31,9 @@ import javafx.collections.ObservableMap;
|
|||||||
import javafx.event.Event;
|
import javafx.event.Event;
|
||||||
import javafx.event.EventDispatcher;
|
import javafx.event.EventDispatcher;
|
||||||
import javafx.event.EventType;
|
import javafx.event.EventType;
|
||||||
|
import javafx.geometry.Bounds;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.geometry.Rectangle2D;
|
||||||
import javafx.scene.Cursor;
|
import javafx.scene.Cursor;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.Scene;
|
import javafx.scene.Scene;
|
||||||
@ -48,6 +50,7 @@ import javafx.scene.shape.Rectangle;
|
|||||||
import javafx.scene.text.Text;
|
import javafx.scene.text.Text;
|
||||||
import javafx.scene.text.TextFlow;
|
import javafx.scene.text.TextFlow;
|
||||||
import javafx.stage.FileChooser;
|
import javafx.stage.FileChooser;
|
||||||
|
import javafx.stage.Screen;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
import javafx.util.Callback;
|
import javafx.util.Callback;
|
||||||
import javafx.util.Duration;
|
import javafx.util.Duration;
|
||||||
@ -1378,4 +1381,65 @@ public final class FXUtils {
|
|||||||
return new FileChooser.ExtensionFilter(i18n("extension.png"),
|
return new FileChooser.ExtensionFilter(i18n("extension.png"),
|
||||||
IMAGE_EXTENSIONS.stream().map(ext -> "*." + ext).toArray(String[]::new));
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,15 +20,12 @@ package org.jackhuang.hmcl.ui.versions;
|
|||||||
import com.jfoenix.controls.JFXButton;
|
import com.jfoenix.controls.JFXButton;
|
||||||
import com.jfoenix.controls.JFXPopup;
|
import com.jfoenix.controls.JFXPopup;
|
||||||
import com.jfoenix.controls.JFXRadioButton;
|
import com.jfoenix.controls.JFXRadioButton;
|
||||||
import javafx.geometry.Bounds;
|
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.geometry.Rectangle2D;
|
|
||||||
import javafx.scene.Cursor;
|
import javafx.scene.Cursor;
|
||||||
import javafx.scene.control.SkinBase;
|
import javafx.scene.control.SkinBase;
|
||||||
import javafx.scene.input.MouseButton;
|
import javafx.scene.input.MouseButton;
|
||||||
import javafx.scene.layout.BorderPane;
|
import javafx.scene.layout.BorderPane;
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.stage.Screen;
|
|
||||||
import org.jackhuang.hmcl.setting.Theme;
|
import org.jackhuang.hmcl.setting.Theme;
|
||||||
import org.jackhuang.hmcl.ui.FXUtils;
|
import org.jackhuang.hmcl.ui.FXUtils;
|
||||||
import org.jackhuang.hmcl.ui.SVG;
|
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.ui.construct.RipplerContainer;
|
||||||
import org.jackhuang.hmcl.util.Lazy;
|
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;
|
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||||
|
|
||||||
public class GameListItemSkin extends SkinBase<GameListItem> {
|
public class GameListItemSkin extends SkinBase<GameListItem> {
|
||||||
@ -105,7 +101,7 @@ public class GameListItemSkin extends SkinBase<GameListItem> {
|
|||||||
btnManage.setOnAction(e -> {
|
btnManage.setOnAction(e -> {
|
||||||
currentSkinnable = skinnable;
|
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());
|
popup.get().show(root, vPosition, JFXPopup.PopupHPosition.RIGHT, 0, vPosition == JFXPopup.PopupVPosition.TOP ? root.getHeight() : -root.getHeight());
|
||||||
});
|
});
|
||||||
btnManage.getStyleClass().add("toggle-icon4");
|
btnManage.getStyleClass().add("toggle-icon4");
|
||||||
@ -132,45 +128,9 @@ public class GameListItemSkin extends SkinBase<GameListItem> {
|
|||||||
} else if (e.getButton() == MouseButton.SECONDARY) {
|
} else if (e.getButton() == MouseButton.SECONDARY) {
|
||||||
currentSkinnable = skinnable;
|
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());
|
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -37,6 +37,7 @@ import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
|||||||
|
|
||||||
import java.time.Instant;
|
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.StringUtils.parseColorEscapes;
|
||||||
import static org.jackhuang.hmcl.util.i18n.I18n.formatDateTime;
|
import static org.jackhuang.hmcl.util.i18n.I18n.formatDateTime;
|
||||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
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.OUTPUT, i18n("world.export"), item::export, popup),
|
||||||
new IconedMenuItem(SVG.FOLDER_OPEN, i18n("folder.world"), item::reveal, 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user