From 2f0c76a70dfc8ec26f2649cc50012130a1e6019b Mon Sep 17 00:00:00 2001 From: James Date: Mon, 18 Jul 2022 13:32:26 -0400 Subject: [PATCH] Add multi-texture import, drag and drop support --- .../main/com/mbrlabs/mundus/editor/Editor.kt | 5 + .../mundus/editor/events/FilesDroppedEvent.kt | 15 ++ .../editor/ui/modules/dialogs/BaseDialog.kt | 14 ++ .../editor/ui/modules/dialogs/SkyboxDialog.kt | 21 ++- .../dialogs/importer/ImportTextureDialog.kt | 98 ++++++++++++- .../editor/ui/widgets/FileChooserField.java | 5 +- .../editor/ui/widgets/ImageChooserField.java | 137 +++++++++++++++--- 7 files changed, 264 insertions(+), 31 deletions(-) create mode 100644 editor/src/main/com/mbrlabs/mundus/editor/events/FilesDroppedEvent.kt diff --git a/editor/src/main/com/mbrlabs/mundus/editor/Editor.kt b/editor/src/main/com/mbrlabs/mundus/editor/Editor.kt index 0fe52c43a..e81a48958 100644 --- a/editor/src/main/com/mbrlabs/mundus/editor/Editor.kt +++ b/editor/src/main/com/mbrlabs/mundus/editor/Editor.kt @@ -28,6 +28,7 @@ import com.mbrlabs.mundus.commons.utils.ShaderUtils import com.mbrlabs.mundus.editor.core.project.ProjectContext import com.mbrlabs.mundus.editor.core.project.ProjectManager import com.mbrlabs.mundus.editor.core.registry.Registry +import com.mbrlabs.mundus.editor.events.FilesDroppedEvent import com.mbrlabs.mundus.editor.events.FullScreenEvent import com.mbrlabs.mundus.editor.events.ProjectChangedEvent import com.mbrlabs.mundus.editor.events.SceneChangedEvent @@ -192,6 +193,10 @@ class Editor : Lwjgl3WindowAdapter(), ApplicationListener, override fun pause() {} override fun resume() {} + override fun filesDropped(files: Array?) { + Mundus.postEvent(FilesDroppedEvent(files)) + } + override fun dispose() { Mundus.dispose() } diff --git a/editor/src/main/com/mbrlabs/mundus/editor/events/FilesDroppedEvent.kt b/editor/src/main/com/mbrlabs/mundus/editor/events/FilesDroppedEvent.kt new file mode 100644 index 000000000..d03ad943d --- /dev/null +++ b/editor/src/main/com/mbrlabs/mundus/editor/events/FilesDroppedEvent.kt @@ -0,0 +1,15 @@ +package com.mbrlabs.mundus.editor.events + +/** + * Event which is posted when files are dropped from an operating system into the mundus window. + * + * @author JamesTKhan + * @version July 18, 2022 + */ +class FilesDroppedEvent(val files: Array?) { + + interface FilesDroppedListener { + @Subscribe + fun onFilesDropped(event: FilesDroppedEvent) + } +} \ No newline at end of file diff --git a/editor/src/main/com/mbrlabs/mundus/editor/ui/modules/dialogs/BaseDialog.kt b/editor/src/main/com/mbrlabs/mundus/editor/ui/modules/dialogs/BaseDialog.kt index 232ca364c..c96587eaf 100644 --- a/editor/src/main/com/mbrlabs/mundus/editor/ui/modules/dialogs/BaseDialog.kt +++ b/editor/src/main/com/mbrlabs/mundus/editor/ui/modules/dialogs/BaseDialog.kt @@ -16,13 +16,27 @@ package com.mbrlabs.mundus.editor.ui.modules.dialogs +import com.badlogic.gdx.scenes.scene2d.Stage import com.kotcrab.vis.ui.widget.VisDialog /** + * Base dialog to extend from. Tracks if it is currently open or not. + * * @author Marcus Brummer * @version 25-11-2015 */ open class BaseDialog(title: String) : VisDialog(title) { + protected var dialogOpen = false + + override fun show(stage: Stage?): VisDialog { + dialogOpen = true + return super.show(stage) + } + + override fun close() { + super.close() + dialogOpen = false + } init { addCloseButton() diff --git a/editor/src/main/com/mbrlabs/mundus/editor/ui/modules/dialogs/SkyboxDialog.kt b/editor/src/main/com/mbrlabs/mundus/editor/ui/modules/dialogs/SkyboxDialog.kt index c0bee31d4..e672e7f74 100644 --- a/editor/src/main/com/mbrlabs/mundus/editor/ui/modules/dialogs/SkyboxDialog.kt +++ b/editor/src/main/com/mbrlabs/mundus/editor/ui/modules/dialogs/SkyboxDialog.kt @@ -22,6 +22,7 @@ import com.badlogic.gdx.scenes.scene2d.InputEvent import com.badlogic.gdx.scenes.scene2d.Stage import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener import com.badlogic.gdx.scenes.scene2d.utils.ClickListener +import com.badlogic.gdx.utils.Array import com.kotcrab.vis.ui.util.FloatDigitsOnlyFilter import com.kotcrab.vis.ui.util.dialog.Dialogs import com.kotcrab.vis.ui.widget.VisCheckBox @@ -44,6 +45,7 @@ import com.mbrlabs.mundus.editor.events.SceneChangedEvent import com.mbrlabs.mundus.editor.shader.Shaders import com.mbrlabs.mundus.editor.ui.widgets.ImageChooserField import com.mbrlabs.mundus.editor.utils.createDefaultSkybox +import java.util.HashMap /** * @author Marcus Brummer @@ -57,12 +59,12 @@ class SkyboxDialog : BaseDialog("Skybox"), ProjectChangedEvent.ProjectChangedLis private lateinit var rotateEnabled: VisCheckBox private var rotateSpeed = VisTextField() - private val positiveX: ImageChooserField = ImageChooserField(100, this) - private var negativeX: ImageChooserField = ImageChooserField(100, this) - private var positiveY: ImageChooserField = ImageChooserField(100, this) - private var negativeY: ImageChooserField = ImageChooserField(100, this) - private var positiveZ: ImageChooserField = ImageChooserField(100, this) - private var negativeZ: ImageChooserField = ImageChooserField(100, this) + private val positiveX: ImageChooserField = ImageChooserField(100, false, this) + private var negativeX: ImageChooserField = ImageChooserField(100, false, this) + private var positiveY: ImageChooserField = ImageChooserField(100, false, this) + private var negativeY: ImageChooserField = ImageChooserField(100, false, this) + private var positiveZ: ImageChooserField = ImageChooserField(100, false, this) + private var negativeZ: ImageChooserField = ImageChooserField(100, false, this) private var createBtn = VisTextButton("Create skybox") private var defaultBtn = VisTextButton("Create default skybox") @@ -377,4 +379,11 @@ class SkyboxDialog : BaseDialog("Skybox"), ProjectChangedEvent.ProjectChangedLis validateFields() } + override fun onImagesChosen( + images: Array?, + failedFiles: HashMap + ) { + // Unused + } + } diff --git a/editor/src/main/com/mbrlabs/mundus/editor/ui/modules/dialogs/importer/ImportTextureDialog.kt b/editor/src/main/com/mbrlabs/mundus/editor/ui/modules/dialogs/importer/ImportTextureDialog.kt index b6e4f77cf..489ded74a 100644 --- a/editor/src/main/com/mbrlabs/mundus/editor/ui/modules/dialogs/importer/ImportTextureDialog.kt +++ b/editor/src/main/com/mbrlabs/mundus/editor/ui/modules/dialogs/importer/ImportTextureDialog.kt @@ -16,29 +16,35 @@ package com.mbrlabs.mundus.editor.ui.modules.dialogs.importer +import com.badlogic.gdx.files.FileHandle import com.badlogic.gdx.scenes.scene2d.InputEvent import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.scenes.scene2d.utils.ClickListener import com.badlogic.gdx.utils.Align +import com.badlogic.gdx.utils.Array import com.badlogic.gdx.utils.Disposable +import com.kotcrab.vis.ui.util.dialog.Dialogs import com.kotcrab.vis.ui.widget.VisTable import com.kotcrab.vis.ui.widget.VisTextButton import com.mbrlabs.mundus.editor.Mundus import com.mbrlabs.mundus.editor.assets.AssetAlreadyExistsException import com.mbrlabs.mundus.editor.core.project.ProjectManager import com.mbrlabs.mundus.editor.events.AssetImportEvent +import com.mbrlabs.mundus.editor.events.FilesDroppedEvent import com.mbrlabs.mundus.editor.ui.UI import com.mbrlabs.mundus.editor.ui.modules.dialogs.BaseDialog import com.mbrlabs.mundus.editor.ui.widgets.ImageChooserField +import com.mbrlabs.mundus.editor.ui.widgets.ImageChooserField.validateImageFile import com.mbrlabs.mundus.editor.utils.Log import com.mbrlabs.mundus.editor.utils.isImage +import java.io.File import java.io.IOException /** * @author Marcus Brummer * @version 07-06-2016 */ -class ImportTextureDialog : BaseDialog("Import Texture"), Disposable { +class ImportTextureDialog : BaseDialog("Import Texture"), FilesDroppedEvent.FilesDroppedListener, ImageChooserField.ImageChosenListener, Disposable { companion object { private val TAG = ImportTextureDialog::class.java.simpleName @@ -49,13 +55,17 @@ class ImportTextureDialog : BaseDialog("Import Texture"), Disposable { private val projectManager: ProjectManager = Mundus.inject() init { + Mundus.registerEventListener(this) + isModal = true isMovable = true val root = VisTable() add(root).expand().fill() - importTextureTable = ImportTextureTable() + importTextureTable = ImportTextureTable(this) + root.add("Drag and drop import is supported.").align(Align.center).row() + root.add("Preview Mode only supported for single texture selection.").align(Align.center).row() root.add(importTextureTable).minWidth(300f).expand().fill().left().top() } @@ -71,10 +81,10 @@ class ImportTextureDialog : BaseDialog("Import Texture"), Disposable { /** */ - private inner class ImportTextureTable : VisTable(), Disposable { + private inner class ImportTextureTable(listener: ImageChooserField.ImageChosenListener) : VisTable(), Disposable { // UI elements private val importBtn = VisTextButton("IMPORT") - private val imageChooserField = ImageChooserField(300) + private val imageChooserField = ImageChooserField(300, true, listener) init { this.setupUI() @@ -115,6 +125,10 @@ class ImportTextureDialog : BaseDialog("Import Texture"), Disposable { }) } + fun getImageChooserField(): ImageChooserField { + return imageChooserField + } + fun removeTexture() { imageChooserField.removeImage() } @@ -124,4 +138,80 @@ class ImportTextureDialog : BaseDialog("Import Texture"), Disposable { } } + override fun onFilesDropped(event: FilesDroppedEvent) { + if (!dialogOpen) return + if (event.files == null || event.files.isEmpty()) return + + if (event.files.size == 1) { + val fileHandle = FileHandle(File(event.files[0])) + val errorMessage = validateImageFile(fileHandle) + + if (errorMessage == null) { + importTextureTable.getImageChooserField().setImage(fileHandle) + } else { + Dialogs.showErrorDialog(stage, errorMessage) + } + return + } + + // + val failedFiles = HashMap() + val files = Array() + + for (filePath in event.files) { + val fileHandle = FileHandle(File(filePath)) + val errorMessage = validateImageFile(fileHandle) + if (errorMessage == null) { + files.add(fileHandle) + } else { + failedFiles[fileHandle] = errorMessage + } + } + + importFiles(files, failedFiles) + } + + private fun importFiles(files: Array, failedFiles: HashMap) { + for (file in files) { + try { + val assetManager = projectManager.current().assetManager + val asset = assetManager.createTextureAsset(file) + Mundus.postEvent(AssetImportEvent(asset)) + } catch (ee: AssetAlreadyExistsException) { + Log.exception(TAG, ee) + failedFiles[file] = "There already exists a texture with the same name" + } + } + + if (failedFiles.isEmpty()) { + close() + UI.toaster.success("Textures imported") + return + } + + val dialogMessage = buildString { + append("The following could not be imported\n\n") + for (file in failedFiles) { + append(file.key.name()) + append(" : ") + append(file.value) + append("\n") + } + } + + Dialogs.showErrorDialog(stage, dialogMessage) + close() + } + + override fun onImageChosen() { + // Unused + } + + override fun onImagesChosen( + images: Array, + failedFiles: HashMap + ) { + importFiles(images, failedFiles) + } + } diff --git a/editor/src/main/com/mbrlabs/mundus/editor/ui/widgets/FileChooserField.java b/editor/src/main/com/mbrlabs/mundus/editor/ui/widgets/FileChooserField.java index 29c9a9380..206aa4792 100644 --- a/editor/src/main/com/mbrlabs/mundus/editor/ui/widgets/FileChooserField.java +++ b/editor/src/main/com/mbrlabs/mundus/editor/ui/widgets/FileChooserField.java @@ -40,8 +40,8 @@ public interface FileSelected { private FileChooser.SelectionMode mode = FileChooser.SelectionMode.FILES; private FileSelected fileSelected; - private VisTextField textField; - private VisTextButton fcBtn; + private final VisTextField textField; + private final VisTextButton fcBtn; private String path; private FileHandle fileHandle; @@ -107,6 +107,7 @@ public void clicked(InputEvent event, float x, float y) { super.clicked(event, x, y); FileChooser fileChooser = UI.INSTANCE.getFileChooser(); fileChooser.setSelectionMode(mode); + fileChooser.setMultiSelectionEnabled(false); fileChooser.setListener(new SingleFileChooserListener() { @Override protected void selected(FileHandle file) { diff --git a/editor/src/main/com/mbrlabs/mundus/editor/ui/widgets/ImageChooserField.java b/editor/src/main/com/mbrlabs/mundus/editor/ui/widgets/ImageChooserField.java index 9612f9839..c77059b35 100644 --- a/editor/src/main/com/mbrlabs/mundus/editor/ui/widgets/ImageChooserField.java +++ b/editor/src/main/com/mbrlabs/mundus/editor/ui/widgets/ImageChooserField.java @@ -25,14 +25,19 @@ import com.badlogic.gdx.scenes.scene2d.utils.ClickListener; import com.badlogic.gdx.scenes.scene2d.utils.Drawable; import com.badlogic.gdx.scenes.scene2d.utils.TextureRegionDrawable; +import com.badlogic.gdx.utils.Array; import com.kotcrab.vis.ui.util.dialog.Dialogs; import com.kotcrab.vis.ui.widget.VisTable; import com.kotcrab.vis.ui.widget.VisTextButton; import com.kotcrab.vis.ui.widget.file.FileChooser; import com.kotcrab.vis.ui.widget.file.SingleFileChooserListener; +import com.kotcrab.vis.ui.widget.file.StreamingFileChooserListener; import com.mbrlabs.mundus.editor.ui.UI; import com.mbrlabs.mundus.editor.utils.FileFormatUtils; +import java.util.HashMap; +import java.util.Map; + /** * @author Marcus Brummer * @version 10-01-2016 @@ -43,30 +48,32 @@ public class ImageChooserField extends VisTable { new TextureRegion(new Texture(Gdx.files.internal("ui/img_placeholder.png")))); private final int width; - private final VisTextButton fcBtn; + private final boolean multiSelectEnabled; + private final ImageChosenListener listener; + + private Array selectedFiles = null; private final Image img; private Texture texture; private FileHandle fileHandle; - private ImageChosenListener listener = null; - - public ImageChooserField(int width) { + public ImageChooserField(int width, boolean multiSelect, ImageChosenListener listener) { super(); this.width = width; fcBtn = new VisTextButton("Select"); img = new Image(PLACEHOLDER_IMG); + this.listener = listener; + + multiSelectEnabled = multiSelect; + if (multiSelectEnabled) { + selectedFiles = new Array<>(); + } setupUI(); setupListeners(); } - public ImageChooserField(int width, ImageChosenListener listener) { - this(width); - this.listener = listener; - } - public FileHandle getFile() { return this.fileHandle; } @@ -107,24 +114,116 @@ public void clicked(InputEvent event, float x, float y) { super.clicked(event, x, y); FileChooser fileChooser = UI.INSTANCE.getFileChooser(); fileChooser.setSelectionMode(FileChooser.SelectionMode.FILES); - fileChooser.setListener(new SingleFileChooserListener() { - public void selected(FileHandle file) { - if (FileFormatUtils.isImage(file)) { - setImage(file); - if (listener != null) - listener.onImageChosen(); - } else { - Dialogs.showErrorDialog(UI.INSTANCE, "This is no image"); + fileChooser.setMultiSelectionEnabled(multiSelectEnabled); + + if (multiSelectEnabled) { + fileChooser.setListener(new StreamingFileChooserListener() { + @Override + public void begin() { + // Clear the files array before we start + selectedFiles.clear(); } - } - }); + + public void selected(FileHandle file) { + // Called for each selected file + selectedFiles.add(file); + } + + @Override + public void end() { + if (selectedFiles.size > 1) { + handleMultipleSelect(selectedFiles); + } else { + handleSingleSelect(selectedFiles.get(0)); + } + } + }); + } else { + fileChooser.setListener(new SingleFileChooserListener() { + public void selected(FileHandle file) { + handleSingleSelect(file); + } + }); + } + UI.INSTANCE.addActor(fileChooser.fadeIn()); } }); } + /** + * Multi-Selection calls the listener onImagesChosen with the valid image files + * and a hashmap containing the files that failed validation paired with the error message. + * + * If the listener is not set for multi-select then nothing will happen. The listener implementation + * is expected to handle the success and failedFiles from the calling end as needed (like showing error messages). + * + * @param selectedFiles Array of multiple selected files + */ + private void handleMultipleSelect(Array selectedFiles) { + if (listener == null) { + return; + } + + // + HashMap failedFiles = new HashMap<>(); + + // Put files that fail validation into failedFiles hashmap with an error message + for (FileHandle file : selectedFiles) { + String errorMessage = validateImageFile(file); + if (errorMessage != null) { + failedFiles.put(file, errorMessage); + } + } + + // Remove failed files from file array + for(Map.Entry entry : failedFiles.entrySet()) { + selectedFiles.removeValue(entry.getKey(), true); + } + + listener.onImagesChosen(selectedFiles, failedFiles); + } + + /** + * Validates and either sets the image or displays an error if the image fails validation + * + * @param fileHandle the selected file + */ + private void handleSingleSelect(FileHandle fileHandle) { + String errorMessage = validateImageFile(fileHandle); + if (errorMessage == null) { + setImage(fileHandle); + if (listener != null) + listener.onImageChosen(); + } else { + Dialogs.showErrorDialog(UI.INSTANCE, errorMessage); + } + } + + public static String validateImageFile(FileHandle fileHandle) { + if (!fileHandle.exists()) { + return "File does not exist or unable to import."; + } + + if (!FileFormatUtils.isImage(fileHandle)) { + return "Format not supported. Supported formats: png, jpg, jpeg, tga."; + } + + return null; + } + public interface ImageChosenListener { + /** + * Called for single selection file choosers + */ void onImageChosen(); + + /** + * Called for multi-select image chooser + * @param images array of images that passed image validations + * @param failedFiles Hashmap of failed file (key) that did not pass image validation with error message (value) + */ + void onImagesChosen(Array images, HashMap failedFiles); } }