feat(FileSelectionDialogModule): add reusable file selection dialog for resource management
This commit is contained in:
13
README.md
13
README.md
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user