From 05da1fd856bee40e9b2eb4bed17d8b1f5120b8bf Mon Sep 17 00:00:00 2001 From: extio-de Date: Sun, 27 Sep 2026 10:49:14 +0200 Subject: [PATCH] feat(FileSelectionDialogModule): prompt dialog for new folder name, gate move button on recursive --- .../StorageServiceAutoConfiguration.java | 13 +- .../dialog/FileSelectionDialogModule.java | 121 ++++----- .../FileSelectionNewFolderDialogModule.java | 250 ++++++++++++++++++ 3 files changed, 317 insertions(+), 67 deletions(-) create mode 100644 src/main/java/de/extio/game_engine/storage/dialog/FileSelectionNewFolderDialogModule.java diff --git a/src/main/java/de/extio/game_engine/storage/StorageServiceAutoConfiguration.java b/src/main/java/de/extio/game_engine/storage/StorageServiceAutoConfiguration.java index f4b4af6..f7d4b7d 100644 --- a/src/main/java/de/extio/game_engine/storage/StorageServiceAutoConfiguration.java +++ b/src/main/java/de/extio/game_engine/storage/StorageServiceAutoConfiguration.java @@ -11,6 +11,7 @@ import de.extio.game_engine.i18n.LocalizationService; import de.extio.game_engine.renderer.work.RenderingBoPool; import de.extio.game_engine.storage.dialog.FileSelectionDialogModule; import de.extio.game_engine.storage.dialog.FileSelectionMoveDialogModule; +import de.extio.game_engine.storage.dialog.FileSelectionNewFolderDialogModule; @AutoConfiguration @ConditionalOnProperty(name = "game-engine.storage.enabled", havingValue = "true", matchIfMissing = true) @@ -34,7 +35,15 @@ public class StorageServiceAutoConfiguration { @ConditionalOnMissingBean @ConditionalOnProperty(name = "game-engine.storage.dialog.enabled", havingValue = "true", matchIfMissing = true) @ConditionalOnProperty(name = "game-engine.renderer.enabled", havingValue = "true", matchIfMissing = true) - FileSelectionDialogModule fileSelectionDialog(final ApplicationContext applicationContext, final EventService eventService, final LocalizationService localizationService, final RenderingBoPool renderingBoPool, final StorageService storageService, final FileSelectionMoveDialogModule fileSelectionMoveDialog) { - return new FileSelectionDialogModule(applicationContext, eventService, localizationService, renderingBoPool, storageService, fileSelectionMoveDialog); + FileSelectionNewFolderDialogModule fileSelectionNewFolderDialog(final ApplicationContext applicationContext, final EventService eventService, final LocalizationService localizationService, final RenderingBoPool renderingBoPool) { + return new FileSelectionNewFolderDialogModule(applicationContext, eventService, localizationService, renderingBoPool); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(name = "game-engine.storage.dialog.enabled", havingValue = "true", matchIfMissing = true) + @ConditionalOnProperty(name = "game-engine.renderer.enabled", havingValue = "true", matchIfMissing = true) + FileSelectionDialogModule fileSelectionDialog(final ApplicationContext applicationContext, final EventService eventService, final LocalizationService localizationService, final RenderingBoPool renderingBoPool, final StorageService storageService, final FileSelectionMoveDialogModule fileSelectionMoveDialog, final FileSelectionNewFolderDialogModule fileSelectionNewFolderDialog) { + return new FileSelectionDialogModule(applicationContext, eventService, localizationService, renderingBoPool, storageService, fileSelectionMoveDialog, fileSelectionNewFolderDialog); } } diff --git a/src/main/java/de/extio/game_engine/storage/dialog/FileSelectionDialogModule.java b/src/main/java/de/extio/game_engine/storage/dialog/FileSelectionDialogModule.java index 55344c4..0365f66 100644 --- a/src/main/java/de/extio/game_engine/storage/dialog/FileSelectionDialogModule.java +++ b/src/main/java/de/extio/game_engine/storage/dialog/FileSelectionDialogModule.java @@ -38,6 +38,7 @@ import de.extio.game_engine.storage.StorageItemDescriptor; import de.extio.game_engine.storage.StorageResource; import de.extio.game_engine.storage.StorageService; import de.extio.game_engine.storage.dialog.FileSelectionMoveDialogModule.FileSelectionMoveDialogResponse; +import de.extio.game_engine.storage.dialog.FileSelectionNewFolderDialogModule.FileSelectionNewFolderDialogResponse; public class FileSelectionDialogModule extends AbstractClientModule { @@ -82,8 +83,6 @@ public class FileSelectionDialogModule extends AbstractClientModule { 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"; @@ -110,13 +109,16 @@ public class FileSelectionDialogModule extends AbstractClientModule { private final FileSelectionMoveDialogModule moveDialogModule; - public FileSelectionDialogModule(final ApplicationContext applicationContext, final EventService eventService, final LocalizationService localizationService, final RenderingBoPool renderingBoPool, final StorageService storageService, final FileSelectionMoveDialogModule moveDialogModule) { + private final FileSelectionNewFolderDialogModule newFolderDialogModule; + + public FileSelectionDialogModule(final ApplicationContext applicationContext, final EventService eventService, final LocalizationService localizationService, final RenderingBoPool renderingBoPool, final StorageService storageService, final FileSelectionMoveDialogModule moveDialogModule, final FileSelectionNewFolderDialogModule newFolderDialogModule) { this.applicationContext = applicationContext; this.eventService = eventService; this.localizationService = localizationService; this.renderingBoPool = renderingBoPool; this.storageService = storageService; this.moveDialogModule = moveDialogModule; + this.newFolderDialogModule = newFolderDialogModule; } private Window dialogWindow; @@ -143,8 +145,6 @@ public class FileSelectionDialogModule extends AbstractClientModule { private String nameFieldValue; - private String newFolderValue; - private final Map> folderPathByControlId = new HashMap<>(); private final Map itemByControlId = new HashMap<>(); @@ -189,6 +189,7 @@ public class FileSelectionDialogModule extends AbstractClientModule { public void onShow() { this.eventService.register(UiControlEvent.class, this.getId(), this::onUiControlEvent); this.eventService.register(FileSelectionMoveDialogResponse.class, this.getId(), this::onMoveResponse); + this.eventService.register(FileSelectionNewFolderDialogResponse.class, this.getId(), this::onNewFolderResponse); this.getModuleService().changeDisplayState(this.dialogWindow.getId(), true); if (this.modal) { this.getModuleService().hideExcept(this.getId(), this.dialogWindow.getId()); @@ -199,6 +200,7 @@ public class FileSelectionDialogModule extends AbstractClientModule { public void onHide() { this.eventService.unregister(UiControlEvent.class, this.getId()); this.eventService.unregister(FileSelectionMoveDialogResponse.class, this.getId()); + this.eventService.unregister(FileSelectionNewFolderDialogResponse.class, this.getId()); this.getModuleService().changeDisplayState(this.dialogWindow.getId(), false); if (this.modal) { this.getModuleService().restoreVisibility(); @@ -217,7 +219,6 @@ public class FileSelectionDialogModule extends AbstractClientModule { 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(); @@ -256,12 +257,7 @@ public class FileSelectionDialogModule extends AbstractClientModule { 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_FOLDER -> this.openNewFolderDialog(); case BUTTON_NEW_ITEM -> this.selectNewItem(); case BUTTON_RENAME -> this.renameSelection(); case BUTTON_DELETE -> this.deleteSelection(); @@ -355,33 +351,6 @@ public class FileSelectionDialogModule extends AbstractClientModule { } yOffset += FIELD_HEIGHT + SPACING; - final boolean showFolderField = this.recursive; - 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))); @@ -422,16 +391,18 @@ public class FileSelectionDialogModule extends AbstractClientModule { this.dialogWindow.putRenderingBo(deleteButton); buttonX += BUTTON_WIDTH + SPACING; - final var moveButton = this.renderingBoPool.acquire(BUTTON_MOVE, ControlRenderingBo.class) - .setType(ButtonControl.class) - .setCaption(this.localizationService.translate("ecyoa-644")) - .setVisible(true) - .setEnabled(this.selectedDescriptor != null) - .withDimensionAbsolute(BUTTON_WIDTH, BUTTON_HEIGHT) - .withPositionRelative(buttonX, bottomRowY) - .setLayer(RenderingBoLayer.UI1); - this.dialogWindow.putRenderingBo(moveButton); - buttonX += BUTTON_WIDTH + SPACING; + if (this.recursive) { + final var moveButton = this.renderingBoPool.acquire(BUTTON_MOVE, ControlRenderingBo.class) + .setType(ButtonControl.class) + .setCaption(this.localizationService.translate("ecyoa-644")) + .setVisible(true) + .setEnabled(this.selectedDescriptor != null) + .withDimensionAbsolute(BUTTON_WIDTH, BUTTON_HEIGHT) + .withPositionRelative(buttonX, bottomRowY) + .setLayer(RenderingBoLayer.UI1); + this.dialogWindow.putRenderingBo(moveButton); + buttonX += BUTTON_WIDTH + SPACING; + } final var okButton = this.renderingBoPool.acquire(BUTTON_OK, ControlRenderingBo.class) .setType(ButtonControl.class) @@ -585,19 +556,48 @@ public class FileSelectionDialogModule extends AbstractClientModule { }; } - private void createFolder() { - if (!this.canCreateFolder()) { + private void openNewFolderDialog() { + if (!this.recursive) { return; } - final var newPath = this.appendPath(this.currentPath, this.newFolderValue.trim()); - this.virtualFolders.add(newPath); - this.currentPath = newPath; - this.newFolderValue = ""; + this.newFolderDialogModule.open(this.childFolderNames(), this.dialogWindow); + } + + private void onNewFolderResponse(final FileSelectionNewFolderDialogResponse event) { + if (event == null || !event.confirmed() || event.name() == null || event.name().isBlank()) { + return; + } + final var name = event.name().trim(); + final var targetPath = this.appendPath(this.currentPath, name); + if (this.virtualFolders.contains(targetPath) || this.folderExistsInStorage(name)) { + return; + } + this.virtualFolders.add(targetPath); + this.currentPath = targetPath; this.selectedDescriptor = null; this.selectionIsNew = false; this.nameFieldValue = ""; this.buildDialog(); } + + private Set childFolderNames() { + final var names = new HashSet(); + for (final var descriptor : this.loadDescriptors()) { + if (descriptor == null) { + continue; + } + final var path = this.safePath(descriptor.path()); + if (this.isPathPrefix(this.currentPath, path) && path.size() > this.currentPath.size()) { + names.add(path.get(this.currentPath.size())); + } + } + for (final var virtualPath : this.virtualFolders) { + if (this.isPathPrefix(this.currentPath, virtualPath) && virtualPath.size() > this.currentPath.size()) { + names.add(virtualPath.get(this.currentPath.size())); + } + } + return names; + } private void selectNewItem() { if (!this.canSelectNewItem()) { @@ -685,7 +685,6 @@ public class FileSelectionDialogModule extends AbstractClientModule { this.selectedDescriptor = null; this.selectionIsNew = false; this.nameFieldValue = ""; - this.newFolderValue = ""; this.virtualFolders.clear(); this.folderPathByControlId.clear(); this.itemByControlId.clear(); @@ -699,15 +698,7 @@ public class FileSelectionDialogModule extends AbstractClientModule { } private boolean canCreateFolder() { - if (!this.recursive) { - 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); + return this.recursive; } private boolean folderExistsInStorage(final String name) { diff --git a/src/main/java/de/extio/game_engine/storage/dialog/FileSelectionNewFolderDialogModule.java b/src/main/java/de/extio/game_engine/storage/dialog/FileSelectionNewFolderDialogModule.java new file mode 100644 index 0000000..6fbc1e3 --- /dev/null +++ b/src/main/java/de/extio/game_engine/storage/dialog/FileSelectionNewFolderDialogModule.java @@ -0,0 +1,250 @@ +package de.extio.game_engine.storage.dialog; + +import java.util.Objects; +import java.util.Set; + +import org.springframework.context.ApplicationContext; + +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.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.TextfieldControl; +import de.extio.game_engine.renderer.model.bo.ControlRenderingBo.TextfieldData; +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.CoordI2; +import de.extio.game_engine.spatial2.model.ImmutableCoordI2; + +public class FileSelectionNewFolderDialogModule extends AbstractClientModule { + + public record FileSelectionNewFolderDialogResponse( + boolean confirmed, + String name) implements Event { + } + + private static final int WINDOW_WIDTH = 500; + + private static final int WINDOW_HEIGHT = 230; + + 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 String NAME_FIELD_ID = "FileSelectionNewFolderDialog_NameField"; + + private static final String BUTTON_OK = "FileSelectionNewFolderDialog_Button_Ok"; + + private static final String BUTTON_CANCEL = "FileSelectionNewFolderDialog_Button_Cancel"; + + private final ApplicationContext applicationContext; + + private final EventService eventService; + + private final LocalizationService localizationService; + + private final RenderingBoPool renderingBoPool; + + public FileSelectionNewFolderDialogModule(final ApplicationContext applicationContext, final EventService eventService, final LocalizationService localizationService, final RenderingBoPool renderingBoPool) { + this.applicationContext = applicationContext; + this.eventService = eventService; + this.localizationService = localizationService; + this.renderingBoPool = renderingBoPool; + } + + private Window dialogWindow; + + private boolean opened; + + private String nameFieldValue = ""; + + private Set existingFolderNames = Set.of(); + + @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.getModuleService().changeActiveState(this.getId(), true); + } + + @Override + public void onUnload() { + this.getModuleService().unloadModule(this.dialogWindow.getId()); + } + + @Override + public void onShow() { + this.eventService.register(UiControlEvent.class, this.getId(), this::onUiControlEvent); + this.getModuleService().changeDisplayState(this.dialogWindow.getId(), true); + } + + @Override + public void onHide() { + this.eventService.unregister(UiControlEvent.class, this.getId()); + this.getModuleService().changeDisplayState(this.dialogWindow.getId(), false); + this.resetState(); + } + + public void open(final Set existingFolderNames, final Window parentWindow) { + this.existingFolderNames = existingFolderNames != null ? Set.copyOf(existingFolderNames) : Set.of(); + this.nameFieldValue = ""; + this.opened = true; + this.dialogWindow.setParent(parentWindow); + this.buildDialog(); + this.getModuleService().changeDisplayState(this.getId(), true); + } + + private void onUiControlEvent(final UiControlEvent event) { + if (event.getId() == null) { + return; + } + switch (event.getId()) { + case NAME_FIELD_ID -> { + if (event.getPayload() instanceof final String text) { + final var value = text != null ? text : ""; + if (!Objects.equals(this.nameFieldValue, value)) { + this.nameFieldValue = value; + this.buildDialog(); + } + } + } + case BUTTON_OK -> this.onOk(); + case BUTTON_CANCEL -> this.onCancel(); + default -> { + } + } + } + + private void buildDialog() { + this.dialogWindow.clearRenderingBos(); + + final var contentWidth = WINDOW_WIDTH - Window.MARGIN_LEFT - Window.MARGIN_RIGHT; + final var buttonRowY = WINDOW_HEIGHT - Window.MARGIN_BOTTOM - BUTTON_HEIGHT; + final var nameFieldY = buttonRowY - SPACING - FIELD_HEIGHT; + final var nameLabelY = nameFieldY - SPACING / 2 - LABEL_HEIGHT; + + final var titleBo = this.renderingBoPool.acquire("FileSelectionNewFolderDialog_Title", DrawFontRenderingBo.class) + .setText(this.localizationService.translate("ecyoa-190")) + .setSize(28) + .setAlignment(HorizontalAlignment.CENTER) + .withDimensionAbsolute(contentWidth, TITLE_HEIGHT) + .withPositionRelative(Window.MARGIN_LEFT, Window.MARGIN_TOP); + this.dialogWindow.putRenderingBo(titleBo); + + final var nameLabel = this.renderingBoPool.acquire("FileSelectionNewFolderDialog_Label_Name", ControlRenderingBo.class) + .setType(LabelControl.class) + .setCaption(this.localizationService.translate("ecyoa-646") + ":") + .setFontSize(18) + .setVisible(true) + .setEnabled(false) + .withDimensionAbsolute(contentWidth, LABEL_HEIGHT) + .withPositionRelative(Window.MARGIN_LEFT, nameLabelY) + .setLayer(RenderingBoLayer.UI0); + this.dialogWindow.putRenderingBo(nameLabel); + + 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(contentWidth, FIELD_HEIGHT) + .withPositionRelative(Window.MARGIN_LEFT, nameFieldY) + .setLayer(RenderingBoLayer.UI1); + this.dialogWindow.putRenderingBo(nameField); + + 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.MARGIN_LEFT, buttonRowY) + .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, buttonRowY) + .setLayer(RenderingBoLayer.UI1); + this.dialogWindow.putRenderingBo(cancelButton); + + this.dialogWindow.draw(); + } + + private void onOk() { + if (!this.canConfirm()) { + return; + } + this.eventService.fire(new FileSelectionNewFolderDialogResponse(true, this.nameFieldValue.trim())); + this.onClose(); + } + + private void onCancel() { + if (this.opened) { + this.eventService.fire(new FileSelectionNewFolderDialogResponse(false, null)); + this.onClose(); + } + } + + private void onClose() { + this.getModuleService().changeDisplayState(this.getId(), false); + } + + private void resetState() { + this.opened = false; + this.nameFieldValue = ""; + this.existingFolderNames = Set.of(); + } + + private boolean canConfirm() { + final var name = this.nameFieldValue != null ? this.nameFieldValue.trim() : ""; + if (name.isEmpty()) { + return false; + } + for (final var existing : this.existingFolderNames) { + if (existing.equalsIgnoreCase(name)) { + return false; + } + } + return true; + } + + private ImmutableCoordI2 centeredPosition(final 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); + } + +}