From c64b9ec29f2cffc1a7f52eddf76bfb57ee97e5c8 Mon Sep 17 00:00:00 2001 From: Emanuel Rabina Date: Mon, 19 Feb 2024 22:18:28 +1300 Subject: [PATCH] Convert Explorer to use new application builder-like API --- .../ultraq/redhorizon/cli/ExplorerCli.groovy | 12 +- .../cli/mediaplayer/MediaPlayer.groovy | 7 +- .../cli/mediaplayer/MediaPlayerCli.groovy | 13 +- .../redhorizon/engine/Application.groovy | 52 +++++-- .../engine/audio/AudioSystem.groovy | 2 +- .../engine/graphics/GraphicsSystem.groovy | 4 +- .../imgui/DebugOverlayRenderPass.groovy | 51 +++--- .../engine/input/InputEventStream.groovy | 12 +- .../engine/input/RemoveControlFunction.groovy | 16 +- .../nodes/FullScreenContainer.groovy | 2 - .../events/DeregisterEventFunction.groovy | 15 +- .../redhorizon/explorer/Explorer.groovy | 147 ++++++++---------- .../redhorizon/explorer/PlaybackScript.groovy | 2 +- 13 files changed, 176 insertions(+), 159 deletions(-) diff --git a/redhorizon-cli/source/nz/net/ultraq/redhorizon/cli/ExplorerCli.groovy b/redhorizon-cli/source/nz/net/ultraq/redhorizon/cli/ExplorerCli.groovy index 61484da7..e62ce0b5 100644 --- a/redhorizon-cli/source/nz/net/ultraq/redhorizon/cli/ExplorerCli.groovy +++ b/redhorizon-cli/source/nz/net/ultraq/redhorizon/cli/ExplorerCli.groovy @@ -1,12 +1,12 @@ -/* +/* * Copyright 2022, Emanuel Rabina (http://www.ultraq.net.nz/) - * + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -27,7 +27,7 @@ import java.util.concurrent.Callable /** * CLI wrapper for launching the Red Horizon Explorer 🔎 - * + * * @author Emanuel Rabina */ @Command(name = 'explorer') @@ -42,7 +42,7 @@ class ExplorerCli implements Callable { @Override Integer call() { - new Explorer(commandSpec.parent().version()[0], paletteOptions.loadPalette()).start() + new Explorer(commandSpec.parent().version()[0], paletteOptions.loadPalette()) return 0 } } diff --git a/redhorizon-cli/source/nz/net/ultraq/redhorizon/cli/mediaplayer/MediaPlayer.groovy b/redhorizon-cli/source/nz/net/ultraq/redhorizon/cli/mediaplayer/MediaPlayer.groovy index dadc9a69..89761a97 100644 --- a/redhorizon-cli/source/nz/net/ultraq/redhorizon/cli/mediaplayer/MediaPlayer.groovy +++ b/redhorizon-cli/source/nz/net/ultraq/redhorizon/cli/mediaplayer/MediaPlayer.groovy @@ -53,7 +53,6 @@ class MediaPlayer { private static final Logger logger = LoggerFactory.getLogger(MediaPlayer) private ResourceFile mediaFile - private Scene scene private Node mediaNode /** @@ -62,13 +61,11 @@ class MediaPlayer { MediaPlayer(ResourceFile resourceFile, AudioConfiguration audioConfig, GraphicsConfiguration graphicsConfig) { mediaFile = resourceFile - scene = new Scene() new Application('Media Player') .addAudioSystem(audioConfig) .addGraphicsSystem(graphicsConfig) .addTimeSystem() - .useScene(scene) .onApplicationStart(this::onApplicationStart) .onApplicationStop(this::onApplicationStop) .start() @@ -78,7 +75,7 @@ class MediaPlayer { * Create the appropriate media node for the media file, adding it to the * scene. */ - private void onApplicationStart(Application application) { + private void onApplicationStart(Application application, Scene scene) { logger.info('File details: {}', mediaFile) @@ -101,7 +98,7 @@ class MediaPlayer { /** * Remove the media node on cleanup. */ - private void onApplicationStop() { + private void onApplicationStop(Application application, Scene scene) { scene.removeNode(mediaNode) } diff --git a/redhorizon-cli/source/nz/net/ultraq/redhorizon/cli/mediaplayer/MediaPlayerCli.groovy b/redhorizon-cli/source/nz/net/ultraq/redhorizon/cli/mediaplayer/MediaPlayerCli.groovy index 769f2b27..c595aa4b 100644 --- a/redhorizon-cli/source/nz/net/ultraq/redhorizon/cli/mediaplayer/MediaPlayerCli.groovy +++ b/redhorizon-cli/source/nz/net/ultraq/redhorizon/cli/mediaplayer/MediaPlayerCli.groovy @@ -75,13 +75,14 @@ class MediaPlayerCli implements Callable { Thread.currentThread().name = 'Media Player [main]' logger.info('Red Horizon Media Player {}', commandSpec.parent().version()[0]) - def audioConfig = audioOptions.asAudioConfiguration() - def graphicsConfig = graphicsOptions.asGraphicsConfiguration( - renderResolution: new Dimension(1280, 800) - ) - fileOptions.useFile(logger) { resourceFile -> - new MediaPlayer(resourceFile, audioConfig, graphicsConfig) + new MediaPlayer( + resourceFile, + audioOptions.asAudioConfiguration(), + graphicsOptions.asGraphicsConfiguration( + renderResolution: new Dimension(1280, 800) + ) + ) } return 0 diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/Application.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/Application.groovy index cd4f7743..1df1042d 100644 --- a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/Application.groovy +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/Application.groovy @@ -21,12 +21,15 @@ import nz.net.ultraq.redhorizon.engine.audio.AudioSystem import nz.net.ultraq.redhorizon.engine.graphics.GraphicsConfiguration import nz.net.ultraq.redhorizon.engine.graphics.GraphicsSystem import nz.net.ultraq.redhorizon.engine.graphics.WindowCreatedEvent +import nz.net.ultraq.redhorizon.engine.graphics.WindowMaximizedEvent import nz.net.ultraq.redhorizon.engine.graphics.imgui.DebugOverlayRenderPass import nz.net.ultraq.redhorizon.engine.graphics.imgui.GuiEvent +import nz.net.ultraq.redhorizon.engine.graphics.pipeline.OverlayRenderPass import nz.net.ultraq.redhorizon.engine.input.InputEventStream import nz.net.ultraq.redhorizon.engine.input.KeyEvent import nz.net.ultraq.redhorizon.engine.scenegraph.Scene import nz.net.ultraq.redhorizon.engine.time.TimeSystem +import nz.net.ultraq.redhorizon.events.EventTarget import static nz.net.ultraq.redhorizon.engine.graphics.imgui.GuiEvent.EVENT_TYPE_STOP import org.slf4j.Logger @@ -35,7 +38,6 @@ import static org.lwjgl.glfw.GLFW.* import groovy.transform.TupleConstructor import java.util.concurrent.Semaphore -import java.util.function.Function /** * A base for developing an application that uses the Red Horizon engine and @@ -56,7 +58,7 @@ import java.util.function.Function * @author Emanuel Rabina */ @TupleConstructor(defaults = false) -class Application { +class Application implements EventTarget { private static final Logger logger = LoggerFactory.getLogger(Application) @@ -66,8 +68,8 @@ class Application { private final InputEventStream inputEventStream = new InputEventStream() private Scene scene - private Function applicationStart - private Function applicationStop + private ApplicationEventHandler applicationStart + private ApplicationEventHandler applicationStop private boolean applicationStopped private Semaphore applicationStoppingSemaphore = new Semaphore(1) @@ -75,7 +77,7 @@ class Application { * Add the audio system to this application. The audio system will run on its * own thread. */ - Application addAudioSystem(AudioConfiguration config) { + Application addAudioSystem(AudioConfiguration config = new AudioConfiguration()) { engine << new AudioSystem(config) return this @@ -86,7 +88,8 @@ class Application { * on its own thread, and the window it creates will be the source of user * input into the application. */ - Application addGraphicsSystem(GraphicsConfiguration config) { + Application addGraphicsSystem(GraphicsConfiguration config = new GraphicsConfiguration(), + OverlayRenderPass... overlayRenderPasses) { var graphicsSystem = new GraphicsSystem(windowTitle, inputEventStream, config) graphicsSystem.on(WindowCreatedEvent) { event -> @@ -95,10 +98,16 @@ class Application { graphicsSystem.on(SystemReadyEvent) { event -> var audioSystem = engine.systems.find { it instanceof AudioSystem } as AudioSystem graphicsSystem.renderPipeline.addOverlayPass( - new DebugOverlayRenderPass(audioSystem.renderer, graphicsSystem.renderer, config.debug) + new DebugOverlayRenderPass(config.debug) + .addAudioRenderer(audioSystem.renderer) + .addGraphicsRenderer(graphicsSystem.renderer) .toggleWith(inputEventStream, GLFW_KEY_D) ) + overlayRenderPasses.each { overlayRenderPass -> + graphicsSystem.renderPipeline.addOverlayPass(overlayRenderPass) + } } + graphicsSystem.relay(WindowMaximizedEvent, this) engine << graphicsSystem return this } @@ -109,13 +118,13 @@ class Application { */ Application addTimeSystem() { - var timeSystem = new TimeSystem() - engine << timeSystem + engine << new TimeSystem() return this } /** - * Use this application with the given scene. + * Use this application with the given scene. If this method is never used, + * then an empty scene will be created during startup. */ Application useScene(Scene scene) { @@ -130,6 +139,10 @@ class Application { logger.debug('Initializing application...') + if (!scene) { + scene = new Scene() + } + // Universal quit on exit inputEventStream.on(KeyEvent) { event -> if (event.action == GLFW_PRESS && event.key == GLFW_KEY_ESCAPE) { @@ -147,7 +160,7 @@ class Application { engine.start() engine.systems*.scene = scene scene.inputEventStream = inputEventStream - applicationStart.apply(this) + applicationStart.apply(this, scene) engine.waitUntilStopped() } @@ -160,7 +173,7 @@ class Application { applicationStoppingSemaphore.tryAcquireAndRelease { -> if (!applicationStopped) { logger.debug('Stopping application...') - applicationStop.apply(this) + applicationStop.apply(this, scene) engine.stop() applicationStopped = true } @@ -170,7 +183,7 @@ class Application { /** * Configure some code to run after engine startup. */ - Application onApplicationStart(Function handler) { + Application onApplicationStart(ApplicationEventHandler handler) { applicationStart = handler return this @@ -179,9 +192,20 @@ class Application { /** * Configure some code to run before engine shutdown. */ - Application onApplicationStop(Function handler) { + Application onApplicationStop(ApplicationEventHandler handler) { applicationStop = handler return this } + + /** + * The functional interface called for each application lifecycle event. It + * is provided the application itself, plus either the configured or default + * scene, whichever is current at that point in time. + */ + @FunctionalInterface + static interface ApplicationEventHandler { + + void apply(Application application, Scene scene); + } } diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/audio/AudioSystem.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/audio/AudioSystem.groovy index 04066347..c95358e3 100644 --- a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/audio/AudioSystem.groovy +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/audio/AudioSystem.groovy @@ -52,7 +52,7 @@ class AudioSystem extends EngineSystem implements AudioRequests { */ AudioSystem(AudioConfiguration config) { - this.config = config ?: new AudioConfiguration() + this.config = config } @Override diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/GraphicsSystem.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/GraphicsSystem.groovy index 1929740c..33a4eb3b 100644 --- a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/GraphicsSystem.groovy +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/GraphicsSystem.groovy @@ -48,8 +48,8 @@ class GraphicsSystem extends EngineSystem implements GraphicsRequests { private static final Logger logger = LoggerFactory.getLogger(GraphicsSystem) private final String windowTitle - private final GraphicsConfiguration config private final InputEventStream inputEventStream + private final GraphicsConfiguration config private final BlockingQueue>> creationRequests = new LinkedBlockingQueue<>() private final BlockingQueue deletionRequests = new LinkedBlockingQueue<>() @@ -69,7 +69,7 @@ class GraphicsSystem extends EngineSystem implements GraphicsRequests { this.windowTitle = windowTitle this.inputEventStream = inputEventStream - this.config = config ?: new GraphicsConfiguration() + this.config = config } /** diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/imgui/DebugOverlayRenderPass.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/imgui/DebugOverlayRenderPass.groovy index e2858697..8376e26e 100644 --- a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/imgui/DebugOverlayRenderPass.groovy +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/imgui/DebugOverlayRenderPass.groovy @@ -64,14 +64,30 @@ class DebugOverlayRenderPass implements OverlayRenderPass { private int debugWindowSizeY = 200 /** - * Constructor, tie the debug overlay to the renderer so it can get the - * information it needs to build the display. - * - * @param audioRenderer - * @param graphicsRenderer - * @param enabled + * Constructor, create a new blank overlay. This is made more useful by + * adding the renderers via the {@code add*} methods to get stats on their + * use. */ - DebugOverlayRenderPass(AudioRenderer audioRenderer, GraphicsRenderer graphicsRenderer, boolean enabled) { + DebugOverlayRenderPass(boolean enabled) { + + ImGuiLoggingAppender.instance.on(ImGuiLogEvent) { event -> + if (event.persistentKey) { + persistentLines[event.persistentKey] = event.message + } + else { + while (!debugLines.offer(event.message)) { + debugLines.poll() + } + } + } + + this.enabled = enabled + } + + /** + * Add the audio renderer to get stats on audio sources, buffers, etc. + */ + DebugOverlayRenderPass addAudioRenderer(AudioRenderer audioRenderer) { audioRenderer.on(AudioRendererEvent) { event -> switch (event) { @@ -81,6 +97,13 @@ class DebugOverlayRenderPass implements OverlayRenderPass { case SourceDeletedEvent -> activeSources.decrementAndGet() } } + return this + } + + /** + * Add the graphics renderer to get status on draws, textures, etc. + */ + DebugOverlayRenderPass addGraphicsRenderer(GraphicsRenderer graphicsRenderer) { graphicsRenderer.on(GraphicsRendererEvent) { event -> switch (event) { @@ -93,19 +116,7 @@ class DebugOverlayRenderPass implements OverlayRenderPass { case TextureDeletedEvent -> activeTextures.decrementAndGet() } } - - ImGuiLoggingAppender.instance.on(ImGuiLogEvent) { event -> - if (event.persistentKey) { - persistentLines[event.persistentKey] = event.message - } - else { - while (!debugLines.offer(event.message)) { - debugLines.poll() - } - } - } - - this.enabled = enabled + return this } @Override diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/input/InputEventStream.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/input/InputEventStream.groovy index 7af00335..95a51f0e 100644 --- a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/input/InputEventStream.groovy +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/input/InputEventStream.groovy @@ -1,12 +1,12 @@ -/* +/* * Copyright 2021, Emanuel Rabina (http://www.ultraq.net.nz/) - * + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -44,8 +44,8 @@ class InputEventStream implements EventTarget { var deregisterEventFunction = on(control.event, control) trigger(new ControlAddedEvent(control)) - return { unused -> - deregisterEventFunction.apply(null) + return { -> + deregisterEventFunction.deregister() trigger(new ControlRemovedEvent(control)) } } diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/input/RemoveControlFunction.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/input/RemoveControlFunction.groovy index a6d59a35..af3062a8 100644 --- a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/input/RemoveControlFunction.groovy +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/input/RemoveControlFunction.groovy @@ -1,12 +1,12 @@ -/* +/* * Copyright 2023, Emanuel Rabina (http://www.ultraq.net.nz/) - * + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -15,14 +15,14 @@ */ package nz.net.ultraq.redhorizon.engine.input - -import java.util.function.Function - /** * A function that can be used to remove a control binding that was added via * {@link InputEventStream#addControl}. * * @author Emanuel Rabina */ -interface RemoveControlFunction extends Function { +@FunctionalInterface +interface RemoveControlFunction { + + void remove() } diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/nodes/FullScreenContainer.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/nodes/FullScreenContainer.groovy index 2cef4744..c71b6a95 100644 --- a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/nodes/FullScreenContainer.groovy +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/scenegraph/nodes/FullScreenContainer.groovy @@ -56,7 +56,5 @@ class FullScreenContainer extends Node { } } } - - addChild(new Outline()) } } diff --git a/redhorizon-events/source/nz/net/ultraq/redhorizon/events/DeregisterEventFunction.groovy b/redhorizon-events/source/nz/net/ultraq/redhorizon/events/DeregisterEventFunction.groovy index 604836f7..3cb74676 100644 --- a/redhorizon-events/source/nz/net/ultraq/redhorizon/events/DeregisterEventFunction.groovy +++ b/redhorizon-events/source/nz/net/ultraq/redhorizon/events/DeregisterEventFunction.groovy @@ -1,12 +1,12 @@ -/* +/* * Copyright 2023, Emanuel Rabina (http://www.ultraq.net.nz/) - * + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -16,13 +16,14 @@ package nz.net.ultraq.redhorizon.events -import java.util.function.Function - /** * A function that can be used to deregister an event listener added via * {@link EventTarget#on}. * * @author Emanuel Rabina */ -interface DeregisterEventFunction extends Function { +@FunctionalInterface +interface DeregisterEventFunction { + + void deregister() } diff --git a/redhorizon-explorer/source/nz/net/ultraq/redhorizon/explorer/Explorer.groovy b/redhorizon-explorer/source/nz/net/ultraq/redhorizon/explorer/Explorer.groovy index ffda7bb1..39ec4070 100644 --- a/redhorizon-explorer/source/nz/net/ultraq/redhorizon/explorer/Explorer.groovy +++ b/redhorizon-explorer/source/nz/net/ultraq/redhorizon/explorer/Explorer.groovy @@ -19,11 +19,10 @@ package nz.net.ultraq.redhorizon.explorer import nz.net.ultraq.preferences.Preferences import nz.net.ultraq.redhorizon.classic.filetypes.MixFile import nz.net.ultraq.redhorizon.engine.Application -import nz.net.ultraq.redhorizon.engine.audio.AudioConfiguration import nz.net.ultraq.redhorizon.engine.geometry.Dimension import nz.net.ultraq.redhorizon.engine.graphics.GraphicsConfiguration import nz.net.ultraq.redhorizon.engine.graphics.WindowMaximizedEvent -import nz.net.ultraq.redhorizon.engine.input.KeyEvent +import nz.net.ultraq.redhorizon.engine.scenegraph.Scene import nz.net.ultraq.redhorizon.engine.scenegraph.nodes.Animation import nz.net.ultraq.redhorizon.engine.scenegraph.nodes.FullScreenContainer import nz.net.ultraq.redhorizon.engine.scenegraph.nodes.Sound @@ -38,7 +37,7 @@ import nz.net.ultraq.redhorizon.filetypes.VideoFile import org.joml.Vector3f import org.slf4j.Logger import org.slf4j.LoggerFactory -import static org.lwjgl.glfw.GLFW.* +import static org.lwjgl.glfw.GLFW.GLFW_KEY_O import java.util.concurrent.CopyOnWriteArrayList @@ -48,16 +47,17 @@ import java.util.concurrent.CopyOnWriteArrayList * * @author Emanuel Rabina */ -class Explorer extends Application { +class Explorer { private static final Logger logger = LoggerFactory.getLogger(Explorer) private static final Preferences userPreferences = new Preferences() private static final Dimension renderResolution = new Dimension(1280, 800) private final List entries = new CopyOnWriteArrayList<>() - private final EntryList entryList - private final MixDatabase mixDatabase + private final EntryList entryList = new EntryList(entries) + private final MixDatabase mixDatabase = new MixDatabase() + private Scene scene private File currentDirectory private InputStream selectedFileInputStream private Palette palette @@ -70,77 +70,20 @@ class Explorer extends Application { */ Explorer(String version, Palette palette) { - super("Explorer - ${version}", - new AudioConfiguration(), - new GraphicsConfiguration( - maximized: userPreferences.get(ExplorerPreferences.WINDOW_MAXIMIZED), - renderResolution: renderResolution, - startWithChrome: true - ) - ) - - entryList = new EntryList(entries) - mixDatabase = new MixDatabase() - - buildList(new File(System.getProperty("user.dir"))) - // TODO: Be able to choose which palette to apply to a paletted file this.palette = palette - } - -// @Override - protected void applicationStart() { - - // Include the explorer GUI in the render pipeline - inputEventStream.on(KeyEvent) { keyEvent -> - if (keyEvent.action == GLFW_PRESS && keyEvent.key == GLFW_KEY_O) { - entryList.toggle() - } - } - graphicsSystem.renderPipeline.addOverlayPass(entryList) - - graphicsSystem.on(WindowMaximizedEvent) { event -> - userPreferences.set(ExplorerPreferences.WINDOW_MAXIMIZED, event.maximized) - } - - // Handle events from the explorer GUI - entryList.on(EntrySelectedEvent) { event -> - clearPreview() - def entry = event.entry - if (entry instanceof MixEntry) { - if (entry.name == '..') { - buildList(currentDirectory) - } - else { - previewEntry(entry) - } - } - else if (entry instanceof FileEntry) { - def file = entry.file - if (file.directory) { - buildList(file) - } - else if (file.name.endsWith('.mix')) { - buildList(new MixFile(file)) - } - else { - previewFile(file) - } - } - } - - // Universal quit on exit - inputEventStream.on(KeyEvent) { event -> - if (event.action == GLFW_PRESS && event.key == GLFW_KEY_ESCAPE) { - stop() - } - } - } - -// @Override - protected void applicationStop() { - scene.clear() + new Application("Explorer - ${version}") + .addAudioSystem() + .addGraphicsSystem(new GraphicsConfiguration( + maximized: userPreferences.get(ExplorerPreferences.WINDOW_MAXIMIZED), + renderResolution: renderResolution, + startWithChrome: true + ), entryList) + .addTimeSystem() + .onApplicationStart(this::onApplicationStart) + .onApplicationStop(this::onApplicationStop) + .start() } /** @@ -155,7 +98,8 @@ class Explorer extends Application { if (directory.parent) { entries << new FileEntry(directory.parentFile, '/..') } - directory.listFiles() + directory + .listFiles() .sort { file1, file2 -> file1.directory && !file2.directory ? -1 : !file1.directory && file2.directory ? 1 : @@ -207,14 +151,59 @@ class Explorer extends Application { */ private void clearPreview() { + selectedFileInputStream?.close() scene.clear() scene.camera.center(new Vector3f()) } + private void onApplicationStart(Application application, Scene scene) { + + this.scene = scene + + application.on(WindowMaximizedEvent) { event -> + userPreferences.set(ExplorerPreferences.WINDOW_MAXIMIZED, event.maximized) + } + + // Also toggle the explorer GUI with the same key for toggling the ImGui chrome + entryList.toggleWith(scene.inputEventStream, GLFW_KEY_O) + + buildList(new File(System.getProperty("user.dir"))) + + // Handle events from the explorer GUI + entryList.on(EntrySelectedEvent) { event -> + clearPreview() + def entry = event.entry + if (entry instanceof MixEntry) { + if (entry.name == '..') { + buildList(currentDirectory) + } + else { + previewEntry(entry) + } + } + else if (entry instanceof FileEntry) { + def file = entry.file + if (file.directory) { + buildList(file) + } + else if (file.name.endsWith('.mix')) { + buildList(new MixFile(file)) + } + else { + previewFile(file) + } + } + } + } + + @SuppressWarnings('unused') + private void onApplicationStop(Application application, Scene scene) { + + clearPreview() + } + /** * Update the preview area for the given file data and type. - * - * @param file */ private void preview(Object file) { @@ -240,8 +229,6 @@ class Explorer extends Application { /** * Update the preview area with the media for the selected mix file entry. - * - * @param entry */ private void previewEntry(MixEntry entry) { @@ -265,8 +252,6 @@ class Explorer extends Application { /** * Update the preview area with the media for the selected file. - * - * @param file */ private void previewFile(File file) { diff --git a/redhorizon-explorer/source/nz/net/ultraq/redhorizon/explorer/PlaybackScript.groovy b/redhorizon-explorer/source/nz/net/ultraq/redhorizon/explorer/PlaybackScript.groovy index fb3ea8a8..51fc4aad 100644 --- a/redhorizon-explorer/source/nz/net/ultraq/redhorizon/explorer/PlaybackScript.groovy +++ b/redhorizon-explorer/source/nz/net/ultraq/redhorizon/explorer/PlaybackScript.groovy @@ -78,6 +78,6 @@ class PlaybackScript extends Script { @Override void onSceneRemoved(Scene scene) { - removePlaybackControl.apply(null) + removePlaybackControl.remove() } }