feat(FileSelectionDialogModule): add move-to-folder dialog, recency sort and lastModified tracking

This commit is contained in:
extio-de
2026-09-27 10:11:58 +02:00
parent 36a9feff20
commit fe25209860
5 changed files with 549 additions and 16 deletions

View File

@@ -3,6 +3,10 @@ package de.extio.game_engine.storage;
import java.util.List;
import java.util.UUID;
public record StorageItemDescriptor(UUID id, String name, List<String> path) {
public record StorageItemDescriptor(UUID id, String name, List<String> path, Long lastModified) {
public StorageItemDescriptor(final UUID id, final String name, final List<String> path) {
this(id, name, path, null);
}
}

View File

@@ -10,6 +10,7 @@ import de.extio.game_engine.event.EventService;
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;
@AutoConfiguration
@ConditionalOnProperty(name = "game-engine.storage.enabled", havingValue = "true", matchIfMissing = true)
@@ -25,7 +26,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) {
return new FileSelectionDialogModule(applicationContext, eventService, localizationService, renderingBoPool, storageService);
FileSelectionMoveDialogModule fileSelectionMoveDialog(final ApplicationContext applicationContext, final EventService eventService, final LocalizationService localizationService, final RenderingBoPool renderingBoPool, final StorageService storageService) {
return new FileSelectionMoveDialogModule(applicationContext, eventService, localizationService, renderingBoPool, storageService);
}
@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) {
return new FileSelectionDialogModule(applicationContext, eventService, localizationService, renderingBoPool, storageService, fileSelectionMoveDialog);
}
}

View File

@@ -7,6 +7,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
@@ -129,6 +130,30 @@ public class StorageServiceImpl implements StorageService {
return DATA_LOCATION.resolve(id.toString());
}
private StorageItemDescriptor withLastModified(final StorageItemDescriptor descriptor) {
if (descriptor == null) {
return null;
}
try {
final var lastModified = Files.getLastModifiedTime(this.getFilePath(descriptor.id())).toMillis();
return new StorageItemDescriptor(descriptor.id(), descriptor.name(), descriptor.path(), lastModified);
}
catch (final IOException e) {
return descriptor;
}
}
private List<StorageItemDescriptor> withLastModified(final List<StorageItemDescriptor> descriptors) {
if (descriptors == null) {
return null;
}
final var result = new ArrayList<StorageItemDescriptor>(descriptors.size());
for (final var descriptor : descriptors) {
result.add(this.withLastModified(descriptor));
}
return result;
}
private void addToIndexAndSave(final UUID id, final String name, final List<String> path) {
final StorageItemDescriptor descriptor = new StorageItemDescriptor(id, name, path == null ? List.of() : path);
this.index.add(descriptor);
@@ -383,29 +408,29 @@ public class StorageServiceImpl implements StorageService {
@Override
public Optional<StorageItemDescriptor> searchById(final UUID id) {
Objects.requireNonNull(id, "id");
return this.index.getById(id);
return this.index.getById(id).map(this::withLastModified);
}
@Override
public Optional<StorageItemDescriptor> searchByPath(final List<String> path, final String name) {
Objects.requireNonNull(name, "name");
return this.index.find(path, name);
return this.index.find(path, name).map(this::withLastModified);
}
@Override
public List<StorageItemDescriptor> searchByPattern(final List<String> basePath, final String pattern, final boolean recursive) {
Objects.requireNonNull(pattern, "pattern");
return this.index.findByFilenamePattern(basePath, pattern, recursive);
return this.withLastModified(this.index.findByFilenamePattern(basePath, pattern, recursive));
}
@Override
public List<StorageItemDescriptor> listPath(final List<String> path, final boolean recursive) {
return this.index.list(path, recursive);
return this.withLastModified(this.index.list(path, recursive));
}
@Override
public List<StorageItemDescriptor> listAll() {
return this.index.listAll();
return this.withLastModified(this.index.listAll());
}
}

View File

@@ -37,6 +37,7 @@ 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;
import de.extio.game_engine.storage.dialog.FileSelectionMoveDialogModule.FileSelectionMoveDialogResponse;
public class FileSelectionDialogModule extends AbstractClientModule {
@@ -91,6 +92,8 @@ public class FileSelectionDialogModule extends AbstractClientModule {
private static final String BUTTON_DELETE = "FileSelectionDialog_Button_Delete";
private static final String BUTTON_MOVE = "FileSelectionDialog_Button_Move";
private static final String BUTTON_OK = "FileSelectionDialog_Button_Ok";
private static final String BUTTON_CANCEL = "FileSelectionDialog_Button_Cancel";
@@ -105,12 +108,15 @@ public class FileSelectionDialogModule extends AbstractClientModule {
private final StorageService storageService;
public FileSelectionDialogModule(final ApplicationContext applicationContext, final EventService eventService, final LocalizationService localizationService, final RenderingBoPool renderingBoPool, final StorageService storageService) {
private final FileSelectionMoveDialogModule moveDialogModule;
public FileSelectionDialogModule(final ApplicationContext applicationContext, final EventService eventService, final LocalizationService localizationService, final RenderingBoPool renderingBoPool, final StorageService storageService, final FileSelectionMoveDialogModule moveDialogModule) {
this.applicationContext = applicationContext;
this.eventService = eventService;
this.localizationService = localizationService;
this.renderingBoPool = renderingBoPool;
this.storageService = storageService;
this.moveDialogModule = moveDialogModule;
}
private Window dialogWindow;
@@ -182,6 +188,7 @@ public class FileSelectionDialogModule extends AbstractClientModule {
@Override
public void onShow() {
this.eventService.register(UiControlEvent.class, this.getId(), this::onUiControlEvent);
this.eventService.register(FileSelectionMoveDialogResponse.class, this.getId(), this::onMoveResponse);
this.getModuleService().changeDisplayState(this.dialogWindow.getId(), true);
if (this.modal) {
this.getModuleService().hideExcept(this.getId(), this.dialogWindow.getId());
@@ -191,6 +198,7 @@ public class FileSelectionDialogModule extends AbstractClientModule {
@Override
public void onHide() {
this.eventService.unregister(UiControlEvent.class, this.getId());
this.eventService.unregister(FileSelectionMoveDialogResponse.class, this.getId());
this.getModuleService().changeDisplayState(this.dialogWindow.getId(), false);
if (this.modal) {
this.getModuleService().restoreVisibility();
@@ -257,6 +265,7 @@ public class FileSelectionDialogModule extends AbstractClientModule {
case BUTTON_NEW_ITEM -> this.selectNewItem();
case BUTTON_RENAME -> this.renameSelection();
case BUTTON_DELETE -> this.deleteSelection();
case BUTTON_MOVE -> this.openMoveDialog();
case BUTTON_OK -> this.onOk();
case BUTTON_CANCEL -> this.onCancel();
default -> {
@@ -346,7 +355,7 @@ public class FileSelectionDialogModule extends AbstractClientModule {
}
yOffset += FIELD_HEIGHT + SPACING;
final boolean showFolderField = this.recursive && this.mode != FileSelectionMode.LOAD;
final boolean showFolderField = this.recursive;
if (showFolderField) {
final var folderLabel = this.renderingBoPool.acquire("FileSelectionDialog_Label_NewFolder", ControlRenderingBo.class)
.setType(LabelControl.class)
@@ -379,7 +388,7 @@ public class FileSelectionDialogModule extends AbstractClientModule {
this.buildItemsList(contentWidth);
var buttonX = Window.MARGIN_LEFT;
if (this.mode != FileSelectionMode.LOAD && this.recursive) {
if (this.recursive) {
final var newFolderButton = this.renderingBoPool.acquire(BUTTON_NEW_FOLDER, ControlRenderingBo.class)
.setType(ButtonControl.class)
.setCaption(this.localizationService.translate("ecyoa-190"))
@@ -413,6 +422,17 @@ 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;
final var okButton = this.renderingBoPool.acquire(BUTTON_OK, ControlRenderingBo.class)
.setType(ButtonControl.class)
.setCaption(this.localizationService.translate("ecyoa-3"))
@@ -464,7 +484,7 @@ public class FileSelectionDialogModule extends AbstractClientModule {
}
final var sortedFolders = folderNames.stream().sorted(String::compareToIgnoreCase).toList();
currentItems.sort(Comparator.comparing(StorageItemDescriptor::name, String.CASE_INSENSITIVE_ORDER));
currentItems.sort(this.timestampThenNameComparator());
final var entries = new ArrayList<DisplayEntry>();
if (this.recursive && this.currentPath.size() > this.basePath.size()) {
@@ -547,7 +567,24 @@ public class FileSelectionDialogModule extends AbstractClientModule {
}
return this.storageService.listPath(queryPath, this.recursive);
}
private Comparator<StorageItemDescriptor> timestampThenNameComparator() {
return (a, b) -> {
final var timestampA = a.lastModified();
final var timestampB = b.lastModified();
if (timestampA != null && timestampB != null && !timestampA.equals(timestampB)) {
return Long.compare(timestampB, timestampA);
}
if (timestampA != null) {
return -1;
}
if (timestampB != null) {
return 1;
}
return String.CASE_INSENSITIVE_ORDER.compare(a.name(), b.name());
};
}
private void createFolder() {
if (!this.canCreateFolder()) {
return;
@@ -594,7 +631,25 @@ public class FileSelectionDialogModule extends AbstractClientModule {
this.nameFieldValue = "";
this.buildDialog();
}
private void openMoveDialog() {
if (this.selectedDescriptor == null) {
return;
}
this.moveDialogModule.open(this.selectedDescriptor, this.basePath, this.dialogWindow);
}
private void onMoveResponse(final FileSelectionMoveDialogResponse event) {
if (event == null || !event.confirmed() || this.requestId == null || this.selectedDescriptor == null || event.targetPath() == null) {
return;
}
this.storageService.moveById(this.selectedDescriptor.id(), event.targetPath(), this.selectedDescriptor.name());
this.selectedDescriptor = null;
this.selectionIsNew = false;
this.nameFieldValue = "";
this.buildDialog();
}
private void onOk() {
if (!this.canConfirm()) {
return;
@@ -609,7 +664,9 @@ public class FileSelectionDialogModule extends AbstractClientModule {
}
private void onCancel() {
this.eventService.fire(new FileSelectionDialogResponse(this.requestId, false, this.mode, null, null, null, null, false));
if (this.requestId != null) {
this.eventService.fire(new FileSelectionDialogResponse(this.requestId, false, this.mode, null, null, null, null, false));
}
this.close();
}
@@ -642,7 +699,7 @@ public class FileSelectionDialogModule extends AbstractClientModule {
}
private boolean canCreateFolder() {
if (!this.recursive || this.mode == FileSelectionMode.LOAD) {
if (!this.recursive) {
return false;
}
if (this.newFolderValue == null || this.newFolderValue.isBlank()) {

View File

@@ -0,0 +1,438 @@
package de.extio.game_engine.storage.dialog;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
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.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.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.CoordI2;
import de.extio.game_engine.spatial2.model.ImmutableCoordI2;
import de.extio.game_engine.storage.StorageItemDescriptor;
import de.extio.game_engine.storage.StorageService;
public class FileSelectionMoveDialogModule extends AbstractClientModule {
public record FileSelectionMoveDialogResponse(
boolean confirmed,
List<String> targetPath) implements Event {
}
private static final int WINDOW_WIDTH = 700;
private static final int WINDOW_HEIGHT = 620;
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 LIST_PADDING = 8;
private static final int ROW_SPACING = 6;
private static final String LOC_TITLE = "ecyoa-645";
private static final String FOLDER_PREFIX = "FileSelectionMoveDialog_Folder_";
private static final String NAME_FIELD_ID = "FileSelectionMoveDialog_NameField";
private static final String BUTTON_MOVE = "FileSelectionMoveDialog_Button_Move";
private static final String BUTTON_CANCEL = "FileSelectionMoveDialog_Button_Cancel";
private final ApplicationContext applicationContext;
private final EventService eventService;
private final LocalizationService localizationService;
private final RenderingBoPool renderingBoPool;
private final StorageService storageService;
public FileSelectionMoveDialogModule(final ApplicationContext applicationContext, final EventService eventService, final LocalizationService localizationService, final RenderingBoPool renderingBoPool, final StorageService storageService) {
this.applicationContext = applicationContext;
this.eventService = eventService;
this.localizationService = localizationService;
this.renderingBoPool = renderingBoPool;
this.storageService = storageService;
}
private Window moveWindow;
private ScrollArea folderScrollArea;
private StorageItemDescriptor descriptor;
private List<String> basePath = List.of();
private List<String> currentPath = List.of();
private String nameFieldValue = "";
private final Map<String, List<String>> folderPathByControlId = new HashMap<>();
@Override
public boolean isProtected() {
return true;
}
@Override
public void onLoad() {
this.moveWindow = this.applicationContext.getBean(Window.class);
this.moveWindow.setNormalizedDimension(ImmutableCoordI2.create(WINDOW_WIDTH, WINDOW_HEIGHT));
this.moveWindow.setNormalizedPosition(this.centeredPosition(this.moveWindow.getNormalizedDimension()));
this.moveWindow.setDraggable(true);
this.moveWindow.setCloseButton(true);
this.moveWindow.setOnCloseAction(this::onCancel);
this.folderScrollArea = this.applicationContext.getBean(ScrollArea.class);
this.moveWindow.addComponent(this.folderScrollArea);
this.getModuleService().changeActiveState(this.getId(), true);
}
@Override
public void onUnload() {
this.getModuleService().unloadModule(this.moveWindow.getId());
}
@Override
public void onShow() {
this.eventService.register(UiControlEvent.class, this.getId(), this::onUiControlEvent);
this.getModuleService().changeDisplayState(this.moveWindow.getId(), true);
}
@Override
public void onHide() {
this.eventService.unregister(UiControlEvent.class, this.getId());
this.getModuleService().changeDisplayState(this.moveWindow.getId(), false);
this.resetState();
}
public void open(final StorageItemDescriptor descriptor, final List<String> basePath, final Window parentWindow) {
this.descriptor = descriptor;
this.basePath = basePath != null ? List.copyOf(basePath) : List.of();
this.currentPath = this.basePath;
this.nameFieldValue = "";
this.folderPathByControlId.clear();
this.moveWindow.setParent(parentWindow);
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.buildDialog();
}
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_MOVE -> this.onMove();
case BUTTON_CANCEL -> this.onCancel();
default -> {
}
}
}
private void buildDialog() {
this.moveWindow.clearRenderingBos();
this.clearScrollArea();
this.folderPathByControlId.clear();
final var contentWidth = WINDOW_WIDTH - Window.MARGIN_LEFT - Window.MARGIN_RIGHT;
int yOffset = Window.MARGIN_TOP;
final var titleBo = this.renderingBoPool.acquire("FileSelectionMoveDialog_Title", DrawFontRenderingBo.class)
.setText(this.localizationService.translate(LOC_TITLE))
.setSize(28)
.setAlignment(HorizontalAlignment.CENTER)
.withDimensionAbsolute(contentWidth, TITLE_HEIGHT)
.withPositionRelative(Window.MARGIN_LEFT, yOffset);
this.moveWindow.putRenderingBo(titleBo);
yOffset += TITLE_HEIGHT + SPACING;
final var pathLabel = this.renderingBoPool.acquire("FileSelectionMoveDialog_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.moveWindow.putRenderingBo(pathLabel);
yOffset += LABEL_HEIGHT + SPACING / 2;
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 scrollHeight = Math.max(120, nameLabelY - SPACING - yOffset);
this.folderScrollArea.setRelativeArea(new Area2(ImmutableCoordI2.create(Window.MARGIN_LEFT, yOffset), ImmutableCoordI2.create(contentWidth, scrollHeight)));
this.buildFolderList(contentWidth);
final var nameLabel = this.renderingBoPool.acquire("FileSelectionMoveDialog_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.moveWindow.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.moveWindow.putRenderingBo(nameField);
final var moveButton = this.renderingBoPool.acquire(BUTTON_MOVE, ControlRenderingBo.class)
.setType(ButtonControl.class)
.setCaption(this.localizationService.translate("ecyoa-644"))
.setVisible(true)
.setEnabled(this.canMove())
.withDimensionAbsolute(BUTTON_WIDTH, BUTTON_HEIGHT)
.withPositionRelative(Window.MARGIN_LEFT, buttonRowY)
.setLayer(RenderingBoLayer.UI1);
this.moveWindow.putRenderingBo(moveButton);
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.moveWindow.putRenderingBo(cancelButton);
this.folderScrollArea.draw();
this.moveWindow.draw();
}
private void buildFolderList(final int contentWidth) {
final var descriptors = this.storageService.listPath(this.basePath, true);
final var folderNames = new HashSet<String>();
for (final var item : descriptors) {
if (item == null) {
continue;
}
final var path = this.safePath(item.path());
if (this.isPathPrefix(this.currentPath, path) && path.size() > this.currentPath.size()) {
folderNames.add(path.get(this.currentPath.size()));
}
}
final var leftPad = LIST_PADDING;
final var availableWidth = contentWidth - ScrollArea.SCROLLBAR_WIDTH_WITH_MARGIN - leftPad * 2;
int yOffset = 0;
if (this.currentPath.size() > this.basePath.size()) {
final var upPath = List.copyOf(this.currentPath.subList(0, this.currentPath.size() - 1));
final var controlId = FOLDER_PREFIX + "up_" + upPath.hashCode();
this.folderPathByControlId.put(controlId, upPath);
final var upButton = this.renderingBoPool.acquire(controlId, ControlRenderingBo.class)
.setType(ButtonControl.class)
.setCaption("..")
.setTooltip(this.formatPath(upPath))
.setVisible(true)
.setEnabled(true)
.withDimensionAbsolute(availableWidth, FIELD_HEIGHT)
.withPositionRelative(leftPad, yOffset)
.setLayer(RenderingBoLayer.UI1);
this.folderScrollArea.putRenderingBo(upButton);
yOffset += FIELD_HEIGHT + ROW_SPACING;
}
final var sortedFolders = folderNames.stream().sorted(String::compareToIgnoreCase).toList();
for (int index = 0; index < sortedFolders.size(); index++) {
final var folderName = sortedFolders.get(index);
final var targetPath = this.appendPath(this.currentPath, folderName);
final var controlId = FOLDER_PREFIX + index + "_" + targetPath.hashCode();
this.folderPathByControlId.put(controlId, targetPath);
final var folderButton = this.renderingBoPool.acquire(controlId, ControlRenderingBo.class)
.setType(ButtonControl.class)
.setCaption(folderName + "/")
.setTooltip(this.formatPath(targetPath))
.setVisible(true)
.setEnabled(true)
.withDimensionAbsolute(availableWidth, FIELD_HEIGHT)
.withPositionRelative(leftPad, yOffset)
.setLayer(RenderingBoLayer.UI1);
this.folderScrollArea.putRenderingBo(folderButton);
yOffset += FIELD_HEIGHT + ROW_SPACING;
}
if (sortedFolders.isEmpty() && this.currentPath.size() <= this.basePath.size()) {
final var emptyLabel = this.renderingBoPool.acquire("FileSelectionMoveDialog_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(availableWidth, FIELD_HEIGHT)
.withPositionRelative(leftPad, yOffset + 10)
.setLayer(RenderingBoLayer.UI0);
this.folderScrollArea.putRenderingBo(emptyLabel);
}
}
private void onMove() {
if (this.descriptor == null || !this.canMove()) {
return;
}
this.eventService.fire(new FileSelectionMoveDialogResponse(true, this.resolveTargetPath()));
this.onClose();
}
private void onCancel() {
if (this.descriptor != null) {
this.eventService.fire(new FileSelectionMoveDialogResponse(false, null));
this.onClose();
}
}
private void onClose() {
this.getModuleService().changeDisplayState(this.getId(), false);
}
private void resetState() {
this.descriptor = null;
this.basePath = List.of();
this.currentPath = List.of();
this.nameFieldValue = "";
this.folderPathByControlId.clear();
this.clearScrollArea();
}
private void clearScrollArea() {
for (final var boId : this.folderScrollArea.getRenderingBoIds().toArray(new String[0])) {
this.folderScrollArea.removeRenderingBo(boId);
}
}
private boolean canMove() {
if (this.descriptor == null) {
return false;
}
final var targetPath = this.resolveTargetPath();
if (targetPath.equals(this.safePath(this.descriptor.path()))) {
return false;
}
return !this.targetContainsSameName(targetPath);
}
private boolean targetContainsSameName(final List<String> targetPath) {
final var name = this.descriptor != null ? this.descriptor.name() : null;
if (name == null || name.isBlank()) {
return false;
}
final var descriptors = this.storageService.listPath(this.basePath, true);
for (final var item : descriptors) {
if (item == null) {
continue;
}
if (this.safePath(item.path()).equals(targetPath) && name.equalsIgnoreCase(item.name())) {
return true;
}
}
return false;
}
private List<String> resolveTargetPath() {
final var name = this.nameFieldValue != null ? this.nameFieldValue.trim() : "";
if (name.isEmpty()) {
return this.currentPath;
}
return this.appendPath(this.currentPath, name);
}
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 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);
}
}