重构 WorldListPage 以使用 ListCell 展示元素 (#5224)

This commit is contained in:
Glavo
2026-01-14 20:33:55 +08:00
committed by GitHub
parent 600bd6e9ee
commit 7d76fea48d
3 changed files with 241 additions and 288 deletions

View File

@@ -1,80 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.versions;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import org.jackhuang.hmcl.game.World;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import java.nio.file.Path;
public final class WorldListItem extends Control {
private final World world;
private final Path backupsDir;
private final WorldListPage parent;
private final Profile profile;
private final String id;
public WorldListItem(WorldListPage parent, World world, Path backupsDir, Profile profile, String id) {
this.world = world;
this.backupsDir = backupsDir;
this.parent = parent;
this.profile = profile;
this.id = id;
}
@Override
protected Skin<?> createDefaultSkin() {
return new WorldListItemSkin(this);
}
public World getWorld() {
return world;
}
public void export() {
WorldManageUIUtils.export(world);
}
public void delete() {
WorldManageUIUtils.delete(world, () -> parent.remove(this));
}
public void copy() {
WorldManageUIUtils.copyWorld(world, parent::refresh);
}
public void reveal() {
FXUtils.openFolder(world.getFile());
}
public void showManagePage() {
Controllers.navigate(new WorldManagePage(world, backupsDir, profile, id));
}
public void launch() {
Versions.launchAndEnterWorld(profile, id, world.getFileName());
}
public void generateLaunchScript() {
Versions.generateLaunchScriptForQuickEnterWorld(profile, id, world.getFileName());
}
}

View File

@@ -1,170 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXPopup;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.SkinBase;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.game.World;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.util.ChunkBaseApp;
import org.jackhuang.hmcl.util.i18n.I18n;
import java.time.Instant;
import java.util.stream.Stream;
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;
public final class WorldListItemSkin extends SkinBase<WorldListItem> {
private final BorderPane root;
public WorldListItemSkin(WorldListItem skinnable) {
super(skinnable);
World world = skinnable.getWorld();
root = new BorderPane();
root.getStyleClass().add("md-list-cell");
root.setPadding(new Insets(8));
{
StackPane left = new StackPane();
FXUtils.installSlowTooltip(left, world.getFile().toString());
root.setLeft(left);
left.setPadding(new Insets(0, 8, 0, 0));
ImageView imageView = new ImageView();
left.getChildren().add(imageView);
FXUtils.limitSize(imageView, 32, 32);
imageView.setImage(world.getIcon() == null ? FXUtils.newBuiltinImage("/assets/img/unknown_server.png") : world.getIcon());
}
{
TwoLineListItem item = new TwoLineListItem();
root.setCenter(item);
item.setMouseTransparent(true);
if (world.getWorldName() != null)
item.setTitle(parseColorEscapes(world.getWorldName()));
item.setSubtitle(i18n("world.datetime", formatDateTime(Instant.ofEpochMilli(world.getLastPlayed()))));
if (world.getGameVersion() != null)
item.addTag(I18n.getDisplayVersion(world.getGameVersion()));
if (world.isLocked())
item.addTag(i18n("world.locked"));
}
{
HBox right = new HBox(8);
root.setRight(right);
right.setAlignment(Pos.CENTER_RIGHT);
JFXButton btnMore = new JFXButton();
right.getChildren().add(btnMore);
btnMore.getStyleClass().add("toggle-icon4");
btnMore.setGraphic(SVG.MORE_VERT.createIcon());
btnMore.setOnAction(event -> showPopupMenu(JFXPopup.PopupHPosition.RIGHT, 0, root.getHeight()));
}
RipplerContainer container = new RipplerContainer(root);
container.setOnMouseClicked(event -> {
if (event.getClickCount() != 1)
return;
if (event.getButton() == MouseButton.PRIMARY)
skinnable.showManagePage();
else if (event.getButton() == MouseButton.SECONDARY)
showPopupMenu(JFXPopup.PopupHPosition.LEFT, event.getX(), event.getY());
});
getChildren().setAll(container);
}
// Popup Menu
public void showPopupMenu(JFXPopup.PopupHPosition hPosition, double initOffsetX, double initOffsetY) {
PopupMenu popupMenu = new PopupMenu();
JFXPopup popup = new JFXPopup(popupMenu);
WorldListItem item = getSkinnable();
World world = item.getWorld();
if (world.getGameVersion() != null && world.getGameVersion().isAtLeast("1.20", "23w14a")) {
IconedMenuItem launchItem = new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("version.launch_and_enter_world"), item::launch, popup);
launchItem.setDisable(world.isLocked());
popupMenu.getContent().add(launchItem);
popupMenu.getContent().addAll(
new IconedMenuItem(SVG.SCRIPT, i18n("version.launch_script"), item::generateLaunchScript, popup),
new MenuSeparator()
);
}
popupMenu.getContent().add(new IconedMenuItem(SVG.SETTINGS, i18n("world.manage.button"), item::showManagePage, popup));
if (ChunkBaseApp.isSupported(world)) {
popupMenu.getContent().addAll(
new MenuSeparator(),
new IconedMenuItem(SVG.EXPLORE, i18n("world.chunkbase.seed_map"), () -> ChunkBaseApp.openSeedMap(world), popup),
new IconedMenuItem(SVG.VISIBILITY, i18n("world.chunkbase.stronghold"), () -> ChunkBaseApp.openStrongholdFinder(world), popup),
new IconedMenuItem(SVG.FORT, i18n("world.chunkbase.nether_fortress"), () -> ChunkBaseApp.openNetherFortressFinder(world), popup)
);
if (world.getGameVersion() != null && world.getGameVersion().compareTo("1.13") >= 0) {
popupMenu.getContent().add(new IconedMenuItem(SVG.LOCATION_CITY, i18n("world.chunkbase.end_city"),
() -> ChunkBaseApp.openEndCityFinder(world), popup));
}
}
IconedMenuItem exportMenuItem = new IconedMenuItem(SVG.OUTPUT, i18n("world.export"), item::export, popup);
IconedMenuItem deleteMenuItem = new IconedMenuItem(SVG.DELETE, i18n("world.delete"), item::delete, popup);
IconedMenuItem duplicateMenuItem = new IconedMenuItem(SVG.CONTENT_COPY, i18n("world.duplicate"), item::copy, popup);
boolean worldLocked = world.isLocked();
Stream.of(exportMenuItem, deleteMenuItem, duplicateMenuItem)
.forEach(iconedMenuItem -> iconedMenuItem.setDisable(worldLocked));
popupMenu.getContent().addAll(
new MenuSeparator(),
exportMenuItem,
deleteMenuItem,
duplicateMenuItem
);
popupMenu.getContent().addAll(
new MenuSeparator(),
new IconedMenuItem(SVG.FOLDER_OPEN, i18n("folder.world"), item::reveal, popup)
);
JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(root, popup);
popup.show(root, vPosition, hPosition, initOffsetX, vPosition == JFXPopup.PopupVPosition.TOP ? initOffsetY : -initOffsetY);
}
}

View File

@@ -17,18 +17,32 @@
*/ */
package org.jackhuang.hmcl.ui.versions; package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXCheckBox; import com.jfoenix.controls.JFXCheckBox;
import com.jfoenix.controls.JFXListView;
import com.jfoenix.controls.JFXPopup;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.ListCell;
import javafx.scene.control.Skin; import javafx.scene.control.Skin;
import javafx.scene.control.Tooltip;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.stage.FileChooser; import javafx.stage.FileChooser;
import org.jackhuang.hmcl.game.World; import org.jackhuang.hmcl.game.World;
import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.*; import org.jackhuang.hmcl.ui.*;
import org.jackhuang.hmcl.ui.construct.Validator; import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.util.ChunkBaseApp;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber; import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
@@ -36,15 +50,18 @@ import java.io.IOException;
import java.nio.file.FileAlreadyExistsException; import java.nio.file.FileAlreadyExistsException;
import java.nio.file.InvalidPathException; import java.nio.file.InvalidPathException;
import java.nio.file.Path; import java.nio.file.Path;
import java.time.Instant;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
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; import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG; import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class WorldListPage extends ListPageBase<WorldListItem> implements VersionPage.VersionLoadable { public final class WorldListPage extends ListPageBase<World> implements VersionPage.VersionLoadable {
private final BooleanProperty showAll = new SimpleBooleanProperty(this, "showAll", false); private final BooleanProperty showAll = new SimpleBooleanProperty(this, "showAll", false);
private Path savesDir; private Path savesDir;
@@ -52,19 +69,15 @@ public final class WorldListPage extends ListPageBase<WorldListItem> implements
private List<World> worlds; private List<World> worlds;
private Profile profile; private Profile profile;
private String id; private String id;
private GameVersionNumber gameVersion;
private int refreshCount = 0;
public WorldListPage() { public WorldListPage() {
FXUtils.applyDragListener(this, it -> "zip".equals(FileUtils.getExtension(it)), modpacks -> { FXUtils.applyDragListener(this, it -> "zip".equals(FileUtils.getExtension(it)), modpacks -> {
installWorld(modpacks.get(0)); installWorld(modpacks.get(0));
}); });
showAll.addListener(e -> { showAll.addListener(e -> updateWorldList());
if (worlds != null)
itemsProperty().setAll(worlds.stream()
.filter(world -> isShowAll() || world.getGameVersion() == null || world.getGameVersion().equals(gameVersion))
.map(world -> new WorldListItem(this, world, backupsDir, profile, id)).toList());
});
} }
@Override @Override
@@ -81,33 +94,46 @@ public final class WorldListPage extends ListPageBase<WorldListItem> implements
refresh(); refresh();
} }
public void remove(WorldListItem item) { private void updateWorldList() {
itemsProperty().remove(item); if (worlds == null) {
getItems().clear();
} else if (showAll.get()) {
getItems().setAll(worlds);
} else {
GameVersionNumber gameVersion = profile.getRepository().getGameVersion(id).map(GameVersionNumber::asGameVersion).orElse(null);
getItems().setAll(worlds.stream()
.filter(world -> world.getGameVersion() == null || world.getGameVersion().equals(gameVersion))
.toList());
}
} }
public void refresh() { public void refresh() {
if (profile == null || id == null) if (profile == null || id == null)
return; return;
int currentRefresh = ++refreshCount;
setLoading(true); setLoading(true);
Task.runAsync(() -> gameVersion = profile.getRepository().getGameVersion(id).map(GameVersionNumber::asGameVersion).orElse(null)) Task.supplyAsync(Schedulers.io(), () -> {
.thenApplyAsync(unused -> { // Ensure the game version number is parsed
try (Stream<World> stream = World.getWorlds(savesDir)) { profile.getRepository().getGameVersion(id);
return stream.parallel().collect(Collectors.toList()); try (Stream<World> stream = World.getWorlds(savesDir)) {
} return stream.toList();
}) }
.whenComplete(Schedulers.javafx(), (result, exception) -> { }).whenComplete(Schedulers.javafx(), (result, exception) -> {
worlds = result; if (refreshCount != currentRefresh) {
setLoading(false); // A newer refresh task is running, discard this result
if (exception == null) { return;
itemsProperty().setAll(result.stream() }
.filter(world -> isShowAll() || world.getGameVersion() == null || world.getGameVersion().equals(gameVersion))
.map(world -> new WorldListItem(this, world, backupsDir, profile, id)) worlds = result;
.collect(Collectors.toList())); updateWorldList();
} else {
LOG.warning("Failed to load world list page", exception); if (exception != null)
} LOG.warning("Failed to load world list page", exception);
}).start();
setLoading(false);
}).start();
} }
public void add() { public void add() {
@@ -133,8 +159,8 @@ public final class WorldListPage extends ListPageBase<WorldListItem> implements
Controllers.prompt(i18n("world.name.enter"), (name, resolve, reject) -> { Controllers.prompt(i18n("world.name.enter"), (name, resolve, reject) -> {
Task.runAsync(() -> world.install(savesDir, name)) Task.runAsync(() -> world.install(savesDir, name))
.whenComplete(Schedulers.javafx(), () -> { .whenComplete(Schedulers.javafx(), () -> {
itemsProperty().add(new WorldListItem(this, new World(savesDir.resolve(name)), backupsDir, profile, id));
resolve.run(); resolve.run();
refresh();
}, e -> { }, e -> {
if (e instanceof FileAlreadyExistsException) if (e instanceof FileAlreadyExistsException)
reject.accept(i18n("world.import.failed", i18n("world.import.already_exists"))); reject.accept(i18n("world.import.failed", i18n("world.import.already_exists")));
@@ -150,19 +176,39 @@ public final class WorldListPage extends ListPageBase<WorldListItem> implements
}).start(); }).start();
} }
public boolean isShowAll() { private void showManagePage(World world) {
return showAll.get(); Controllers.navigate(new WorldManagePage(world, backupsDir, profile, id));
}
public void export(World world) {
WorldManageUIUtils.export(world);
}
public void delete(World world) {
WorldManageUIUtils.delete(world, this::refresh);
}
public void copy(World world) {
WorldManageUIUtils.copyWorld(world, this::refresh);
}
public void reveal(World world) {
FXUtils.openFolder(world.getFile());
}
public void launch(World world) {
Versions.launchAndEnterWorld(profile, id, world.getFileName());
}
public void generateLaunchScript(World world) {
Versions.generateLaunchScriptForQuickEnterWorld(profile, id, world.getFileName());
} }
public BooleanProperty showAllProperty() { public BooleanProperty showAllProperty() {
return showAll; return showAll;
} }
public void setShowAll(boolean showAll) { private final class WorldListPageSkin extends ToolbarListPageSkin<World, WorldListPage> {
this.showAll.set(showAll);
}
private final class WorldListPageSkin extends ToolbarListPageSkin<WorldListItem, WorldListPage> {
WorldListPageSkin() { WorldListPageSkin() {
super(WorldListPage.this); super(WorldListPage.this);
@@ -179,5 +225,162 @@ public final class WorldListPage extends ListPageBase<WorldListItem> implements
createToolbarButton2(i18n("world.add"), SVG.ADD, skinnable::add), createToolbarButton2(i18n("world.add"), SVG.ADD, skinnable::add),
createToolbarButton2(i18n("world.download"), SVG.DOWNLOAD, skinnable::download)); createToolbarButton2(i18n("world.download"), SVG.DOWNLOAD, skinnable::download));
} }
@Override
protected ListCell<World> createListCell(JFXListView<World> listView) {
return new WorldListCell(getSkinnable());
}
}
private static final class WorldListCell extends ListCell<World> {
private final WorldListPage page;
private final RipplerContainer graphic;
private final ImageView imageView;
private final Tooltip leftTooltip;
private final TwoLineListItem content;
public WorldListCell(WorldListPage page) {
this.page = page;
var root = new BorderPane();
root.getStyleClass().add("md-list-cell");
root.setPadding(new Insets(8));
{
StackPane left = new StackPane();
this.leftTooltip = new Tooltip();
FXUtils.installSlowTooltip(left, leftTooltip);
root.setLeft(left);
left.setPadding(new Insets(0, 8, 0, 0));
this.imageView = new ImageView();
left.getChildren().add(imageView);
FXUtils.limitSize(imageView, 32, 32);
}
{
this.content = new TwoLineListItem();
root.setCenter(content);
content.setMouseTransparent(true);
}
{
HBox right = new HBox(8);
root.setRight(right);
right.setAlignment(Pos.CENTER_RIGHT);
JFXButton btnMore = new JFXButton();
right.getChildren().add(btnMore);
btnMore.getStyleClass().add("toggle-icon4");
btnMore.setGraphic(SVG.MORE_VERT.createIcon());
btnMore.setOnAction(event -> {
World world = getItem();
if (world != null)
showPopupMenu(world, JFXPopup.PopupHPosition.RIGHT, 0, root.getHeight());
});
}
this.graphic = new RipplerContainer(root);
graphic.setOnMouseClicked(event -> {
if (event.getClickCount() != 1)
return;
World world = getItem();
if (world == null)
return;
if (event.getButton() == MouseButton.PRIMARY)
page.showManagePage(world);
else if (event.getButton() == MouseButton.SECONDARY)
showPopupMenu(world, JFXPopup.PopupHPosition.LEFT, event.getX(), event.getY());
});
}
@Override
protected void updateItem(World world, boolean empty) {
super.updateItem(world, empty);
this.content.getTags().clear();
if (empty || world == null) {
setGraphic(null);
imageView.setImage(null);
leftTooltip.setText("");
content.setTitle("");
content.setSubtitle("");
} else {
imageView.setImage(world.getIcon() == null ? FXUtils.newBuiltinImage("/assets/img/unknown_server.png") : world.getIcon());
leftTooltip.setText(world.getFile().toString());
content.setTitle(world.getWorldName() != null ? parseColorEscapes(world.getWorldName()) : "");
if (world.getGameVersion() != null)
content.addTag(I18n.getDisplayVersion(world.getGameVersion()));
if (world.isLocked())
content.addTag(i18n("world.locked"));
content.setSubtitle(i18n("world.datetime", formatDateTime(Instant.ofEpochMilli(world.getLastPlayed()))));
setGraphic(graphic);
}
}
// Popup Menu
public void showPopupMenu(World world, JFXPopup.PopupHPosition hPosition, double initOffsetX, double initOffsetY) {
PopupMenu popupMenu = new PopupMenu();
JFXPopup popup = new JFXPopup(popupMenu);
if (world.getGameVersion() != null && world.getGameVersion().isAtLeast("1.20", "23w14a")) {
IconedMenuItem launchItem = new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("version.launch_and_enter_world"), () -> page.launch(world), popup);
launchItem.setDisable(world.isLocked());
popupMenu.getContent().add(launchItem);
popupMenu.getContent().addAll(
new IconedMenuItem(SVG.SCRIPT, i18n("version.launch_script"), () -> page.generateLaunchScript(world), popup),
new MenuSeparator()
);
}
popupMenu.getContent().add(new IconedMenuItem(SVG.SETTINGS, i18n("world.manage.button"), () -> page.showManagePage(world), popup));
if (ChunkBaseApp.isSupported(world)) {
popupMenu.getContent().addAll(
new MenuSeparator(),
new IconedMenuItem(SVG.EXPLORE, i18n("world.chunkbase.seed_map"), () -> ChunkBaseApp.openSeedMap(world), popup),
new IconedMenuItem(SVG.VISIBILITY, i18n("world.chunkbase.stronghold"), () -> ChunkBaseApp.openStrongholdFinder(world), popup),
new IconedMenuItem(SVG.FORT, i18n("world.chunkbase.nether_fortress"), () -> ChunkBaseApp.openNetherFortressFinder(world), popup)
);
if (world.getGameVersion() != null && world.getGameVersion().compareTo("1.13") >= 0) {
popupMenu.getContent().add(new IconedMenuItem(SVG.LOCATION_CITY, i18n("world.chunkbase.end_city"),
() -> ChunkBaseApp.openEndCityFinder(world), popup));
}
}
IconedMenuItem exportMenuItem = new IconedMenuItem(SVG.OUTPUT, i18n("world.export"), () -> page.export(world), popup);
IconedMenuItem deleteMenuItem = new IconedMenuItem(SVG.DELETE, i18n("world.delete"), () -> page.delete(world), popup);
IconedMenuItem duplicateMenuItem = new IconedMenuItem(SVG.CONTENT_COPY, i18n("world.duplicate"), () -> page.copy(world), popup);
boolean worldLocked = world.isLocked();
Stream.of(exportMenuItem, deleteMenuItem, duplicateMenuItem)
.forEach(iconedMenuItem -> iconedMenuItem.setDisable(worldLocked));
popupMenu.getContent().addAll(
new MenuSeparator(),
exportMenuItem,
deleteMenuItem,
duplicateMenuItem
);
popupMenu.getContent().addAll(
new MenuSeparator(),
new IconedMenuItem(SVG.FOLDER_OPEN, i18n("folder.world"), () -> page.reveal(world), popup)
);
JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(this, popup);
popup.show(this, vPosition, hPosition, initOffsetX, vPosition == JFXPopup.PopupVPosition.TOP ? initOffsetY : -initOffsetY);
}
} }
} }