feat(FileSelectionDialogModule): add reusable file selection dialog for resource management

This commit is contained in:
extio-de
2026-09-26 10:44:49 +02:00
parent 40a77d4ec6
commit 81f45cc3ac
2 changed files with 861 additions and 0 deletions

View File

@@ -1062,6 +1062,19 @@ Storage directory is created automatically at startup in the `storage/` folder r
#### Exposed Spring Beans #### Exposed Spring Beans
- **`StorageService`**: Main interface for storing and loading data by ID or path - **`StorageService`**: Main interface for storing and loading data by ID or path
#### Reusable File Selection Dialog
The engine includes a reusable `FileSelectionDialogModule` for browsing, selecting, creating, renaming, and deleting stored resources. It is a generic UI module built on the module and container systems, so applications can embed the same file picker flow for load, save, and management scenarios without duplicating dialog logic.
Key capabilities:
- **Modes**: `LOAD`, `SAVE`, and `MANAGE`
- **Path navigation**: Browse virtual or stored directories, move up the hierarchy, and keep the current path in sync with the active storage context
- **Filtering**: Optional pattern-based searching for narrow file discovery
- **Creation and management**: Create folders, rename selected items, delete selected items, and resolve a final path/name for the selected resource
- **Result contract**: Emits a structured `FileSelectionDialogResponse` event with the request ID, selection state, resource, path, and existing flag
The dialog is designed for storage-backed workflows and can be opened with a parent window, an initial name, and optional modal behavior. This makes it a reusable library component for save-game prompts, configuration import/export, and other file-oriented UI flows.
--- ---
### Resource Management ### Resource Management

View File

@@ -0,0 +1,848 @@
package de.extio.game_engine.storage.dialog;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import de.extio.game_engine.event.Event;
import de.extio.game_engine.event.EventService;
import de.extio.game_engine.i18n.LocalizationService;
import de.extio.game_engine.module.AbstractClientModule;
import de.extio.game_engine.renderer.RendererControl;
import de.extio.game_engine.renderer.container.ScrollArea;
import de.extio.game_engine.renderer.container.Window;
import de.extio.game_engine.renderer.model.RenderingBoLayer;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.ButtonControl;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.LabelControl;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.LabelData;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.TextfieldControl;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.TextfieldData;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.ToggleButtonControl;
import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.ToggleButtonData;
import de.extio.game_engine.renderer.model.bo.DrawFontRenderingBo;
import de.extio.game_engine.renderer.model.bo.HorizontalAlignment;
import de.extio.game_engine.renderer.model.event.UiControlEvent;
import de.extio.game_engine.renderer.work.RenderingBoPool;
import de.extio.game_engine.spatial2.model.Area2;
import de.extio.game_engine.spatial2.model.ImmutableCoordI2;
import de.extio.game_engine.storage.StorageItemDescriptor;
import de.extio.game_engine.storage.StorageResource;
import de.extio.game_engine.storage.StorageService;
@Component
public class FileSelectionDialogModule extends AbstractClientModule {
public enum FileSelectionMode {
LOAD,
SAVE,
MANAGE
}
public record FileSelectionDialogResponse(
String requestId,
boolean confirmed,
FileSelectionMode mode,
StorageResource resource,
UUID id,
String name,
List<String> path,
boolean existing) implements Event {
}
private static final int WINDOW_WIDTH = 1100;
private static final int WINDOW_HEIGHT = 760;
private static final int TITLE_HEIGHT = 46;
private static final int LABEL_HEIGHT = 26;
private static final int FIELD_HEIGHT = 36;
private static final int BUTTON_HEIGHT = 42;
private static final int BUTTON_WIDTH = 150;
private static final int SPACING = 12;
private static final int COLUMN_COUNT = 3;
private static final String FOLDER_PREFIX = "FileSelectionDialog_Folder_";
private static final String ITEM_PREFIX = "FileSelectionDialog_Item_";
private static final String NAME_FIELD_ID = "FileSelectionDialog_NameField";
private static final String NEW_FOLDER_FIELD_ID = "FileSelectionDialog_NewFolderField";
private static final String BUTTON_NEW_FOLDER = "FileSelectionDialog_Button_NewFolder";
private static final String BUTTON_NEW_ITEM = "FileSelectionDialog_Button_NewItem";
private static final String BUTTON_RENAME = "FileSelectionDialog_Button_Rename";
private static final String BUTTON_DELETE = "FileSelectionDialog_Button_Delete";
private static final String BUTTON_OK = "FileSelectionDialog_Button_Ok";
private static final String BUTTON_CANCEL = "FileSelectionDialog_Button_Cancel";
@Autowired
private ApplicationContext applicationContext;
@Autowired
private EventService eventService;
@Autowired
private LocalizationService localizationService;
@Autowired
private RenderingBoPool renderingBoPool;
@Autowired
private StorageService storageService;
private Window dialogWindow;
private ScrollArea itemsScrollArea;
private String requestId;
private FileSelectionMode mode;
private List<String> basePath = List.of();
private List<String> currentPath = List.of();
private String filterPattern;
private boolean recursive;
private boolean modal;
private StorageItemDescriptor selectedDescriptor;
private boolean selectionIsNew;
private String nameFieldValue;
private String newFolderValue;
private final Map<String, List<String>> folderPathByControlId = new HashMap<>();
private final Map<String, StorageItemDescriptor> itemByControlId = new HashMap<>();
private final Set<List<String>> virtualFolders = new HashSet<>();
@Override
public boolean isProtected() {
return true;
}
@Override
public void onLoad() {
this.dialogWindow = this.applicationContext.getBean(Window.class);
this.dialogWindow.setNormalizedDimension(ImmutableCoordI2.create(WINDOW_WIDTH, WINDOW_HEIGHT));
this.dialogWindow.setNormalizedPosition(this.centeredPosition(this.dialogWindow.getNormalizedDimension()));
this.dialogWindow.setDraggable(true);
this.dialogWindow.setCloseButton(true);
this.dialogWindow.setOnCloseAction(this::onCancel);
this.itemsScrollArea = this.applicationContext.getBean(ScrollArea.class);
this.dialogWindow.addComponent(this.itemsScrollArea);
this.getModuleService().changeActiveState(this.getId(), true);
}
@Override
public void onUnload() {
this.getModuleService().unloadModule(this.dialogWindow.getId());
}
@Override
public void onActivate() {
}
@Override
public void onDeactivate() {
this.eventService.unregisterAll(this.getId());
}
@Override
public void onShow() {
this.eventService.register(UiControlEvent.class, this.getId(), this::onUiControlEvent);
this.getModuleService().changeDisplayState(this.dialogWindow.getId(), true);
if (this.modal) {
this.getModuleService().hideExcept(this.getId(), this.dialogWindow.getId());
}
}
@Override
public void onHide() {
this.eventService.unregister(UiControlEvent.class, this.getId());
this.getModuleService().changeDisplayState(this.dialogWindow.getId(), false);
if (this.modal) {
this.getModuleService().restoreVisibility();
}
this.resetState();
}
public void open(final String requestId, final FileSelectionMode mode, final List<String> basePath, final String filterPattern, final boolean recursive, final boolean modal, final String initialName, final Window parentWindow) {
this.requestId = requestId;
this.mode = mode != null ? mode : FileSelectionMode.LOAD;
this.basePath = basePath != null ? List.copyOf(basePath) : List.of();
this.currentPath = this.basePath;
this.filterPattern = filterPattern;
this.recursive = recursive;
this.modal = modal;
this.selectedDescriptor = null;
this.selectionIsNew = this.mode == FileSelectionMode.SAVE;
this.nameFieldValue = initialName != null && !initialName.isBlank() ? initialName.trim() : "";
this.newFolderValue = "";
this.virtualFolders.clear();
this.dialogWindow.setParent(parentWindow);
this.applyInitialSelection();
this.buildDialog();
this.getModuleService().changeDisplayState(this.getId(), true);
}
private void onUiControlEvent(final UiControlEvent event) {
if (event.getId() == null) {
return;
}
if (event.getId().startsWith(FOLDER_PREFIX)) {
final var targetPath = this.folderPathByControlId.get(event.getId());
if (targetPath != null) {
this.currentPath = targetPath;
this.selectedDescriptor = null;
this.selectionIsNew = false;
this.nameFieldValue = "";
this.buildDialog();
}
return;
}
if (event.getId().startsWith(ITEM_PREFIX)) {
final var descriptor = this.itemByControlId.get(event.getId());
if (descriptor != null) {
this.selectedDescriptor = descriptor;
this.selectionIsNew = false;
this.nameFieldValue = descriptor.name();
this.buildDialog();
}
return;
}
switch (event.getId()) {
case NAME_FIELD_ID -> {
if (event.getPayload() instanceof final String text) {
this.handleNameChanged(text);
}
}
case NEW_FOLDER_FIELD_ID -> {
if (event.getPayload() instanceof final String text) {
this.newFolderValue = text;
}
}
case BUTTON_NEW_FOLDER -> this.createFolder();
case BUTTON_NEW_ITEM -> this.selectNewItem();
case BUTTON_RENAME -> this.renameSelection();
case BUTTON_DELETE -> this.deleteSelection();
case BUTTON_OK -> this.onOk();
case BUTTON_CANCEL -> this.onCancel();
default -> {
}
}
}
private void buildDialog() {
this.dialogWindow.clearRenderingBos();
this.clearScrollArea();
this.folderPathByControlId.clear();
this.itemByControlId.clear();
final var contentWidth = WINDOW_WIDTH - Window.MARGIN_LEFT - Window.MARGIN_RIGHT;
int yOffset = Window.MARGIN_TOP;
final var title = this.localizationService.translate("ecyoa-185") + " - " + this.getModeLabel();
final var titleBo = this.renderingBoPool.acquire("FileSelectionDialog_Title", DrawFontRenderingBo.class)
.setText(title)
.setSize(28)
.setAlignment(HorizontalAlignment.CENTER)
.withDimensionAbsolute(contentWidth, TITLE_HEIGHT)
.withPositionRelative(Window.MARGIN_LEFT, yOffset);
this.dialogWindow.putRenderingBo(titleBo);
yOffset += TITLE_HEIGHT + SPACING;
final var pathLabel = this.renderingBoPool.acquire("FileSelectionDialog_Label_Path", ControlRenderingBo.class)
.setType(LabelControl.class)
.setCaption(this.localizationService.translate("ecyoa-188") + ": " + this.formatPath(this.currentPath))
.setFontSize(18)
.setVisible(true)
.setEnabled(false)
.withDimensionAbsolute(contentWidth, LABEL_HEIGHT)
.withPositionRelative(Window.MARGIN_LEFT, yOffset)
.setLayer(RenderingBoLayer.UI0);
this.dialogWindow.putRenderingBo(pathLabel);
yOffset += LABEL_HEIGHT + SPACING / 2;
if (this.filterPattern != null && !this.filterPattern.isBlank()) {
final var filterLabel = this.renderingBoPool.acquire("FileSelectionDialog_Label_Filter", ControlRenderingBo.class)
.setType(LabelControl.class)
.setCaption(this.localizationService.translate("ecyoa-189") + ": " + this.filterPattern)
.setFontSize(16)
.setVisible(true)
.setEnabled(false)
.withDimensionAbsolute(contentWidth, LABEL_HEIGHT)
.withPositionRelative(Window.MARGIN_LEFT, yOffset)
.setLayer(RenderingBoLayer.UI0);
this.dialogWindow.putRenderingBo(filterLabel);
yOffset += LABEL_HEIGHT + SPACING / 2;
}
final var nameLabel = this.renderingBoPool.acquire("FileSelectionDialog_Label_Name", ControlRenderingBo.class)
.setType(LabelControl.class)
.setCaption(this.localizationService.translate("ecyoa-48") + ":")
.setFontSize(18)
.setVisible(true)
.setEnabled(false)
.withDimensionAbsolute(contentWidth, LABEL_HEIGHT)
.withPositionRelative(Window.MARGIN_LEFT, yOffset)
.setLayer(RenderingBoLayer.UI0);
this.dialogWindow.putRenderingBo(nameLabel);
yOffset += LABEL_HEIGHT + SPACING / 2;
final var showNewItemButton = this.mode != FileSelectionMode.LOAD;
final var nameFieldWidth = showNewItemButton ? contentWidth - BUTTON_WIDTH - SPACING : contentWidth;
final var nameField = this.renderingBoPool.acquire(NAME_FIELD_ID, ControlRenderingBo.class)
.setType(TextfieldControl.class)
.setCaption(this.nameFieldValue != null ? this.nameFieldValue : "")
.setControlData(new TextfieldData(false, null))
.setVisible(true)
.setEnabled(true)
.withDimensionAbsolute(nameFieldWidth, FIELD_HEIGHT)
.withPositionRelative(Window.MARGIN_LEFT, yOffset)
.setLayer(RenderingBoLayer.UI1);
this.dialogWindow.putRenderingBo(nameField);
if (showNewItemButton) {
final var newItemButton = this.renderingBoPool.acquire(BUTTON_NEW_ITEM, ControlRenderingBo.class)
.setType(ButtonControl.class)
.setCaption(this.localizationService.translate("ecyoa-191"))
.setVisible(true)
.setEnabled(this.canSelectNewItem())
.withDimensionAbsolute(BUTTON_WIDTH, FIELD_HEIGHT)
.withPositionRelative(Window.MARGIN_LEFT + nameFieldWidth + SPACING, yOffset)
.setLayer(RenderingBoLayer.UI1);
this.dialogWindow.putRenderingBo(newItemButton);
}
yOffset += FIELD_HEIGHT + SPACING;
final boolean showFolderField = this.recursive && this.mode != FileSelectionMode.LOAD;
if (showFolderField) {
final var folderLabel = this.renderingBoPool.acquire("FileSelectionDialog_Label_NewFolder", ControlRenderingBo.class)
.setType(LabelControl.class)
.setCaption(this.localizationService.translate("ecyoa-190") + ":")
.setFontSize(18)
.setVisible(true)
.setEnabled(false)
.withDimensionAbsolute(contentWidth, LABEL_HEIGHT)
.withPositionRelative(Window.MARGIN_LEFT, yOffset)
.setLayer(RenderingBoLayer.UI0);
this.dialogWindow.putRenderingBo(folderLabel);
yOffset += LABEL_HEIGHT + SPACING / 2;
final var folderField = this.renderingBoPool.acquire(NEW_FOLDER_FIELD_ID, ControlRenderingBo.class)
.setType(TextfieldControl.class)
.setCaption(this.newFolderValue != null ? this.newFolderValue : "")
.setControlData(new TextfieldData(false, null))
.setVisible(true)
.setEnabled(true)
.withDimensionAbsolute(contentWidth, FIELD_HEIGHT)
.withPositionRelative(Window.MARGIN_LEFT, yOffset)
.setLayer(RenderingBoLayer.UI1);
this.dialogWindow.putRenderingBo(folderField);
yOffset += FIELD_HEIGHT + SPACING;
}
final var bottomRowY = WINDOW_HEIGHT - Window.MARGIN_BOTTOM - BUTTON_HEIGHT;
final var scrollHeight = Math.max(120, bottomRowY - yOffset - SPACING);
this.itemsScrollArea.setRelativeArea(new Area2(ImmutableCoordI2.create(Window.MARGIN_LEFT, yOffset), ImmutableCoordI2.create(contentWidth, scrollHeight)));
this.buildItemsList(contentWidth);
var buttonX = Window.MARGIN_LEFT;
if (this.mode != FileSelectionMode.LOAD && this.recursive) {
final var newFolderButton = this.renderingBoPool.acquire(BUTTON_NEW_FOLDER, ControlRenderingBo.class)
.setType(ButtonControl.class)
.setCaption(this.localizationService.translate("ecyoa-190"))
.setVisible(true)
.setEnabled(this.canCreateFolder())
.withDimensionAbsolute(BUTTON_WIDTH + 20, BUTTON_HEIGHT)
.withPositionRelative(buttonX, bottomRowY)
.setLayer(RenderingBoLayer.UI1);
this.dialogWindow.putRenderingBo(newFolderButton);
buttonX += BUTTON_WIDTH + 20 + SPACING;
}
final var renameButton = this.renderingBoPool.acquire(BUTTON_RENAME, ControlRenderingBo.class)
.setType(ButtonControl.class)
.setCaption(this.localizationService.translate("ecyoa-57"))
.setVisible(true)
.setEnabled(this.canRename())
.withDimensionAbsolute(BUTTON_WIDTH, BUTTON_HEIGHT)
.withPositionRelative(buttonX, bottomRowY)
.setLayer(RenderingBoLayer.UI1);
this.dialogWindow.putRenderingBo(renameButton);
buttonX += BUTTON_WIDTH + SPACING;
final var deleteButton = this.renderingBoPool.acquire(BUTTON_DELETE, ControlRenderingBo.class)
.setType(ButtonControl.class)
.setCaption(this.localizationService.translate("ecyoa-68"))
.setVisible(true)
.setEnabled(this.selectedDescriptor != null)
.withDimensionAbsolute(BUTTON_WIDTH, BUTTON_HEIGHT)
.withPositionRelative(buttonX, bottomRowY)
.setLayer(RenderingBoLayer.UI1);
this.dialogWindow.putRenderingBo(deleteButton);
buttonX += BUTTON_WIDTH + SPACING;
final var okButton = this.renderingBoPool.acquire(BUTTON_OK, ControlRenderingBo.class)
.setType(ButtonControl.class)
.setCaption(this.localizationService.translate("ecyoa-3"))
.setVisible(true)
.setEnabled(this.canConfirm())
.withDimensionAbsolute(BUTTON_WIDTH, BUTTON_HEIGHT)
.withPositionRelative(WINDOW_WIDTH - Window.MARGIN_RIGHT - BUTTON_WIDTH * 2 - SPACING, bottomRowY)
.setLayer(RenderingBoLayer.UI1);
this.dialogWindow.putRenderingBo(okButton);
final var cancelButton = this.renderingBoPool.acquire(BUTTON_CANCEL, ControlRenderingBo.class)
.setType(ButtonControl.class)
.setCaption(this.localizationService.translate("ecyoa-89"))
.setVisible(true)
.setEnabled(true)
.withDimensionAbsolute(BUTTON_WIDTH, BUTTON_HEIGHT)
.withPositionRelative(WINDOW_WIDTH - Window.MARGIN_RIGHT - BUTTON_WIDTH, bottomRowY)
.setLayer(RenderingBoLayer.UI1);
this.dialogWindow.putRenderingBo(cancelButton);
this.itemsScrollArea.draw();
this.dialogWindow.draw();
}
private void buildItemsList(final int contentWidth) {
final var descriptors = this.loadDescriptors();
final var currentItems = new ArrayList<StorageItemDescriptor>();
final var folderNames = new HashSet<String>();
for (final var descriptor : descriptors) {
if (descriptor == null) {
continue;
}
final var path = this.safePath(descriptor.path());
if (this.isPathPrefix(this.currentPath, path)) {
if (path.size() == this.currentPath.size()) {
currentItems.add(descriptor);
}
else if (this.recursive) {
folderNames.add(path.get(this.currentPath.size()));
}
}
}
if (this.recursive) {
for (final var virtualPath : this.virtualFolders) {
if (this.isPathPrefix(this.currentPath, virtualPath) && virtualPath.size() > this.currentPath.size()) {
folderNames.add(virtualPath.get(this.currentPath.size()));
}
}
}
final var sortedFolders = folderNames.stream().sorted(String::compareToIgnoreCase).toList();
currentItems.sort(Comparator.comparing(StorageItemDescriptor::name, String.CASE_INSENSITIVE_ORDER));
final var entries = new ArrayList<DisplayEntry>();
if (this.recursive && this.currentPath.size() > this.basePath.size()) {
final var upPath = List.copyOf(this.currentPath.subList(0, this.currentPath.size() - 1));
entries.add(DisplayEntry.folder("..", upPath));
}
for (final var folderName : sortedFolders) {
entries.add(DisplayEntry.folder(folderName + "/", this.appendPath(this.currentPath, folderName)));
}
for (final var item : currentItems) {
entries.add(DisplayEntry.item(item));
}
final var leftPad = Window.MARGIN_LEFT;
final var availableWidth = contentWidth - ScrollArea.SCROLLBAR_WIDTH_WITH_MARGIN - leftPad;
final var columnSpacing = SPACING;
final var columnWidth = Math.max(120, (availableWidth - columnSpacing * (COLUMN_COUNT - 1)) / COLUMN_COUNT);
final var rowSpacing = SPACING / 2;
int index = 0;
for (final var entry : entries) {
final var col = index % COLUMN_COUNT;
final var row = index / COLUMN_COUNT;
final var x = leftPad + col * (columnWidth + columnSpacing);
final var y = row * (FIELD_HEIGHT + rowSpacing);
if (entry.isFolder()) {
final var controlId = FOLDER_PREFIX + index + "_" + entry.caption().hashCode();
this.folderPathByControlId.put(controlId, entry.path());
final var folderButton = this.renderingBoPool.acquire(controlId, ControlRenderingBo.class)
.setType(ButtonControl.class)
.setCaption(entry.caption())
.setTooltip(this.formatPath(entry.path()))
.setVisible(true)
.setEnabled(true)
.withDimensionAbsolute(columnWidth, FIELD_HEIGHT)
.withPositionRelative(x, y)
.setLayer(RenderingBoLayer.UI1);
this.itemsScrollArea.putRenderingBo(folderButton);
}
else if (entry.item() != null) {
final var item = entry.item();
final var controlId = ITEM_PREFIX + item.id();
this.itemByControlId.put(controlId, item);
final var selected = this.selectedDescriptor != null && Objects.equals(this.selectedDescriptor.id(), item.id());
final var resourcePath = this.formatPath(this.safePath(item.path())) + "/" + item.name();
final var itemButton = this.renderingBoPool.acquire(controlId, ControlRenderingBo.class)
.setType(ToggleButtonControl.class)
.setCaption(item.name())
.setTooltip(resourcePath)
.setControlGroup("FileSelectionDialog_Items")
.setControlData(new ToggleButtonData(selected, true, null))
.setVisible(true)
.setEnabled(true)
.withDimensionAbsolute(columnWidth, FIELD_HEIGHT)
.withPositionRelative(x, y)
.setLayer(RenderingBoLayer.UI1);
this.itemsScrollArea.putRenderingBo(itemButton);
}
index++;
}
if (entries.isEmpty()) {
final var emptyLabel = this.renderingBoPool.acquire("FileSelectionDialog_Label_Empty", ControlRenderingBo.class)
.setType(LabelControl.class)
.setCaption(this.localizationService.translate("ecyoa-193"))
.setControlData(new LabelData(null, null, HorizontalAlignment.CENTER))
.setFontSize(18)
.setVisible(true)
.setEnabled(false)
.withDimensionAbsolute(columnWidth, FIELD_HEIGHT)
.withPositionRelative(leftPad, 10)
.setLayer(RenderingBoLayer.UI0);
this.itemsScrollArea.putRenderingBo(emptyLabel);
}
}
private List<StorageItemDescriptor> loadDescriptors() {
final var queryPath = this.recursive ? this.basePath : this.currentPath;
if (this.filterPattern != null && !this.filterPattern.isBlank()) {
return this.storageService.searchByPattern(queryPath, this.filterPattern, this.recursive);
}
return this.storageService.listPath(queryPath, this.recursive);
}
private void createFolder() {
if (!this.canCreateFolder()) {
return;
}
final var newPath = this.appendPath(this.currentPath, this.newFolderValue.trim());
this.virtualFolders.add(newPath);
this.currentPath = newPath;
this.newFolderValue = "";
this.selectedDescriptor = null;
this.selectionIsNew = false;
this.nameFieldValue = "";
this.buildDialog();
}
private void selectNewItem() {
if (!this.canSelectNewItem()) {
return;
}
this.selectedDescriptor = null;
this.selectionIsNew = true;
this.nameFieldValue = this.ensureFreeNameForNewItem(this.nameFieldValue);
this.buildDialog();
}
private void renameSelection() {
if (!this.canRename()) {
return;
}
final var newName = this.nameFieldValue.trim();
this.storageService.moveById(this.selectedDescriptor.id(), this.selectedDescriptor.path(), newName);
this.nameFieldValue = newName;
this.selectionIsNew = false;
this.selectedDescriptor = null;
this.buildDialog();
}
private void deleteSelection() {
if (this.selectedDescriptor == null) {
return;
}
this.storageService.deleteById(this.selectedDescriptor.id());
this.selectedDescriptor = null;
this.selectionIsNew = false;
this.nameFieldValue = "";
this.buildDialog();
}
private void onOk() {
if (!this.canConfirm()) {
return;
}
final var name = this.resolveSelectionName();
final var path = this.resolveSelectionPath();
final var existing = this.selectedDescriptor != null && name != null && name.equals(this.selectedDescriptor.name());
final var id = existing ? this.selectedDescriptor.id() : null;
final var resource = name != null ? new StorageResource(path, name) : null;
this.eventService.fire(new FileSelectionDialogResponse(this.requestId, true, this.mode, resource, id, name, path, existing));
this.close();
}
private void onCancel() {
this.eventService.fire(new FileSelectionDialogResponse(this.requestId, false, this.mode, null, null, null, null, false));
this.close();
}
private void close() {
this.getModuleService().changeDisplayState(this.getId(), false);
}
private void resetState() {
this.requestId = null;
this.mode = FileSelectionMode.LOAD;
this.basePath = List.of();
this.currentPath = List.of();
this.filterPattern = null;
this.recursive = false;
this.modal = false;
this.selectedDescriptor = null;
this.selectionIsNew = false;
this.nameFieldValue = "";
this.newFolderValue = "";
this.virtualFolders.clear();
this.folderPathByControlId.clear();
this.itemByControlId.clear();
this.clearScrollArea();
}
private void clearScrollArea() {
for (final var boId : this.itemsScrollArea.getRenderingBoIds().toArray(new String[0])) {
this.itemsScrollArea.removeRenderingBo(boId);
}
}
private boolean canCreateFolder() {
if (!this.recursive || this.mode == FileSelectionMode.LOAD) {
return false;
}
if (this.newFolderValue == null || this.newFolderValue.isBlank()) {
return false;
}
final var name = this.newFolderValue.trim();
final var targetPath = this.appendPath(this.currentPath, name);
return !this.virtualFolders.contains(targetPath) && !this.folderExistsInStorage(name);
}
private boolean folderExistsInStorage(final String name) {
final var descriptors = this.loadDescriptors();
for (final var descriptor : descriptors) {
if (descriptor == null) {
continue;
}
final var path = this.safePath(descriptor.path());
if (this.isPathPrefix(this.currentPath, path) && path.size() > this.currentPath.size()) {
if (name.equals(path.get(this.currentPath.size()))) {
return true;
}
}
}
return false;
}
private boolean canSelectNewItem() {
if (this.mode == FileSelectionMode.LOAD) {
return false;
}
return this.nameFieldValue != null && !this.nameFieldValue.isBlank();
}
private boolean canRename() {
if (this.selectedDescriptor == null || this.nameFieldValue == null || this.nameFieldValue.isBlank()) {
return false;
}
final var trimmed = this.nameFieldValue.trim();
if (trimmed.equalsIgnoreCase(this.selectedDescriptor.name())) {
return false;
}
return this.findDescriptorByName(trimmed) == null;
}
private boolean canConfirm() {
return switch (this.mode) {
case LOAD -> this.selectedDescriptor != null;
case SAVE -> this.resolveSelectionName() != null;
case MANAGE -> true;
};
}
private String resolveSelectionName() {
final var name = this.nameFieldValue != null ? this.nameFieldValue.trim() : "";
if (!name.isBlank()) {
return name;
}
if (this.selectedDescriptor != null) {
return this.selectedDescriptor.name();
}
return null;
}
private void handleNameChanged(final String text) {
if (Objects.equals(this.nameFieldValue, text)) {
return;
}
this.nameFieldValue = text;
final var trimmed = text != null ? text.trim() : "";
if (this.selectedDescriptor == null && !trimmed.isBlank()) {
final var match = this.findDescriptorByName(trimmed);
if (match != null) {
this.selectedDescriptor = match;
this.selectionIsNew = false;
this.buildDialog();
return;
}
}
if (this.selectedDescriptor == null) {
this.selectionIsNew = true;
}
this.buildDialog();
}
private void applyInitialSelection() {
if (this.nameFieldValue == null || this.nameFieldValue.isBlank()) {
this.selectionIsNew = false;
this.selectedDescriptor = null;
return;
}
final var match = this.findDescriptorByName(this.nameFieldValue);
if (this.mode != FileSelectionMode.SAVE && match != null) {
this.selectedDescriptor = match;
this.selectionIsNew = false;
this.nameFieldValue = match.name();
return;
}
if (this.selectionIsNew || this.mode == FileSelectionMode.SAVE) {
this.nameFieldValue = this.ensureFreeNameForNewItem(this.nameFieldValue);
}
}
private String ensureFreeNameForNewItem(final String baseName) {
if (baseName == null || baseName.isBlank()) {
return baseName;
}
if (this.findDescriptorByName(baseName) == null) {
return baseName;
}
int index = 2;
String candidate;
do {
candidate = baseName + " " + index++;
} while (this.findDescriptorByName(candidate) != null);
return candidate;
}
private StorageItemDescriptor findDescriptorByName(final String name) {
if (name == null || name.isBlank()) {
return null;
}
final var descriptors = this.loadDescriptors();
for (final var descriptor : descriptors) {
if (descriptor == null) {
continue;
}
final var path = this.safePath(descriptor.path());
if (path.size() == this.currentPath.size() && this.isPathPrefix(this.currentPath, path)) {
if (name.equalsIgnoreCase(descriptor.name())) {
return descriptor;
}
}
}
return null;
}
private record DisplayEntry(String caption, List<String> path, StorageItemDescriptor item) {
private static DisplayEntry folder(final String caption, final List<String> path) {
return new DisplayEntry(caption, path, null);
}
private static DisplayEntry item(final StorageItemDescriptor item) {
return new DisplayEntry(item.name(), null, item);
}
private boolean isFolder() {
return this.path != null;
}
}
private List<String> resolveSelectionPath() {
if (this.selectedDescriptor != null) {
return this.safePath(this.selectedDescriptor.path());
}
return this.currentPath;
}
private String formatPath(final List<String> path) {
if (path == null || path.isEmpty()) {
return "/";
}
return "/" + String.join("/", path);
}
private List<String> safePath(final List<String> path) {
return path != null ? List.copyOf(path) : List.of();
}
private boolean isPathPrefix(final List<String> prefix, final List<String> candidate) {
if (prefix.size() > candidate.size()) {
return false;
}
for (int i = 0; i < prefix.size(); i++) {
if (!Objects.equals(prefix.get(i), candidate.get(i))) {
return false;
}
}
return true;
}
private List<String> appendPath(final List<String> base, final String segment) {
final var result = new ArrayList<String>(base.size() + 1);
result.addAll(base);
result.add(segment);
return List.copyOf(result);
}
private String getModeLabel() {
return switch (this.mode) {
case LOAD -> this.localizationService.translate("ecyoa-186");
case SAVE -> this.localizationService.translate("ecyoa-66");
case MANAGE -> this.localizationService.translate("ecyoa-187");
};
}
private ImmutableCoordI2 centeredPosition(final de.extio.game_engine.spatial2.model.CoordI2 dimension) {
final var referenceResolution = RendererControl.REFERENCE_RESOLUTION;
final var x = (referenceResolution.getX() - dimension.getX()) / 2;
final var y = (referenceResolution.getY() - dimension.getY()) / 2;
return ImmutableCoordI2.create(x, y);
}
}