From 3f6cbb61d4c9ee6f2eb3f36ce26a4e0831d243c0 Mon Sep 17 00:00:00 2001
From: Eric Myllyoja <ericmyllyoja@gmail.com>
Date: Fri, 25 Nov 2022 20:48:05 -0500
Subject: [PATCH] WIP on new chart wizard

---
 .../charting/ChartEditorDialogHandler.hx      |  82 +++++++++++++-
 .../ui/debug/charting/ChartEditorState.hx     | 103 ++++++++++++++----
 2 files changed, 162 insertions(+), 23 deletions(-)

diff --git a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx
index fa6cf782c..a7008bc5a 100644
--- a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx
@@ -1,5 +1,9 @@
 package funkin.ui.debug.charting;
 
+import funkin.input.Cursor;
+import haxe.ui.containers.Box;
+import haxe.ui.containers.dialogs.Dialogs;
+import haxe.ui.components.Link;
 import flixel.util.FlxTimer;
 import flixel.FlxSprite;
 import haxe.ui.containers.dialogs.Dialog;
@@ -9,7 +13,8 @@ import haxe.ui.components.Image;
 class ChartEditorDialogHandler
 {
 	static final CHART_EDITOR_DIALOG_ABOUT_LAYOUT = Paths.ui('chart-editor/dialogs/about');
-	static final CHART_EDITOR_DIALOG_SPLASH_LAYOUT = Paths.ui('chart-editor/dialogs/splash');
+	static final CHART_EDITOR_DIALOG_WELCOME_LAYOUT = Paths.ui('chart-editor/dialogs/welcome');
+	static final CHART_EDITOR_DIALOG_UPLOAD_INST_LAYOUT = Paths.ui('chart-editor/dialogs/upload-inst');
 	static final CHART_EDITOR_DIALOG_USER_GUIDE_LAYOUT = Paths.ui('chart-editor/dialogs/user-guide');
 
 	/**
@@ -23,9 +28,9 @@ class ChartEditorDialogHandler
 	/**
 	 * Builds and opens a dialog letting the user create a new chart, open a recent chart, or load from a template.
 	 */
-	public static function openSplashDialog(state:ChartEditorState, closable:Bool = true):Void
+	public static function openWelcomeDialog(state:ChartEditorState, closable:Bool = true):Void
 	{
-		var dialog:Dialog = openDialog(state, CHART_EDITOR_DIALOG_SPLASH_LAYOUT, true, closable);
+		var dialog:Dialog = openDialog(state, CHART_EDITOR_DIALOG_WELCOME_LAYOUT, true, closable);
 
 		// TODO: Add callbacks to the dialog buttons
 
@@ -57,6 +62,77 @@ class ChartEditorDialogHandler
 				bfSprite.visible = true;
 			});
 		}
+
+		// Add handlers to the "Create From Song" section.
+		var linkCreateBasic:Link = dialog.findComponent('splashCreateFromSongBasic', Link);
+		linkCreateBasic.onClick = (_event) ->
+		{
+			dialog.hideDialog(DialogButton.CANCEL);
+			openUploadInstDialog(state, false);
+		};
+
+		// Get the list of songs and insert them as links into the "Create From Song" section.
+	}
+
+	public static function openUploadInstDialog(state:ChartEditorState, ?closable:Bool = true):Void
+	{
+		var dialog:Dialog = openDialog(state, CHART_EDITOR_DIALOG_UPLOAD_INST_LAYOUT, true, closable);
+
+		var instrumentalBox:Box = dialog.findComponent('instrumentalBox', Box);
+
+		instrumentalBox.onMouseOver = (_event) ->
+		{
+			instrumentalBox.swapClass('upload-bg', 'upload-bg-hover');
+			Cursor.cursorMode = Pointer;
+		}
+
+		instrumentalBox.onMouseOut = (_event) ->
+		{
+			instrumentalBox.swapClass('upload-bg-hover', 'upload-bg');
+			Cursor.cursorMode = Default;
+		}
+
+		var onDropFile:String->Void;
+
+		instrumentalBox.onClick = (_event) ->
+		{
+			Dialogs.openBinaryFile("Open Instrumental", [{label: "Audio File (.ogg)", extension: "ogg"}], function(selectedFile)
+			{
+				if (selectedFile != null)
+				{
+					trace('Selected file: ' + selectedFile);
+					state.loadInstrumentalFromBytes(selectedFile.bytes);
+					dialog.hideDialog(DialogButton.APPLY);
+					removeDropHandler(onDropFile);
+				}
+			});
+		}
+
+		onDropFile = (path:String) ->
+		{
+			trace('Dropped file: ' + path);
+			state.loadInstrumentalFromPath(path);
+			dialog.hideDialog(DialogButton.APPLY);
+			removeDropHandler(onDropFile);
+		};
+
+		addDropHandler(onDropFile);
+	}
+
+	static function addDropHandler(handler:String->Void)
+	{
+		#if desktop
+		FlxG.stage.window.onDropFile.add(handler);
+		#else
+		trace('addDropHandler not implemented for this platform');
+		#end
+	}
+
+	static function removeDropHandler(handler:String->Void)
+	{
+		#if desktop
+		FlxG.stage.window.onDropFile.remove(handler);
+		#end
 	}
 
 	/**
diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index 584180fbf..a7256402a 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -1,5 +1,6 @@
 package funkin.ui.debug.charting;
 
+import lime.media.AudioBuffer;
 import funkin.input.Cursor;
 import flixel.FlxSprite;
 import flixel.addons.display.FlxSliceSprite;
@@ -184,7 +185,15 @@ class ChartEditorState extends HaxeUIState
 	/**
 	 * This is the song's length in PIXELS, same format as scrollPosition.
 	 */
-	var songLength:Int;
+	var songLength(get, default):Int;
+
+	function get_songLength():Int
+	{
+		if (songLength <= 0)
+			return 1000;
+
+		return songLength;
+	}
 
 	/**
 	 * songLength, converted to steps.
@@ -278,6 +287,14 @@ class ChartEditorState extends HaxeUIState
 		return Screen.instance.hasSolidComponentUnderPoint(FlxG.mouse.screenX, FlxG.mouse.screenY);
 	}
 
+	var isCursorOverHaxeUIButton(get, null):Bool;
+
+	function get_isCursorOverHaxeUIButton():Bool
+	{
+		return Screen.instance.hasSolidComponentUnderPoint(FlxG.mouse.screenX, FlxG.mouse.screenY, haxe.ui.components.Button)
+			|| Screen.instance.hasSolidComponentUnderPoint(FlxG.mouse.screenX, FlxG.mouse.screenY, haxe.ui.components.Link);
+	}
+
 	/**
 	 * The variation ID for the difficulty which is currently being edited.
 	 */
@@ -734,7 +751,9 @@ class ChartEditorState extends HaxeUIState
 		setupUIListeners();
 
 		// TODO: We should be loading the music later when the user requests it.
-		loadMusic();
+		// loadDefaultMusic();
+
+		ChartEditorDialogHandler.openSplashDialog(this, false);
 	}
 
 	function buildDefaultSongData()
@@ -955,6 +974,9 @@ class ChartEditorState extends HaxeUIState
 
 		// Add functionality to the menu items.
 
+		addUIClickListener('menubarItemNewChart', (event:MouseEvent) -> ChartEditorDialogHandler.openSplashDialog(this, true));
+		addUIClickListener('menubarItemLoadInst', (event:MouseEvent) -> ChartEditorDialogHandler.openUploadInstDialog(this, true));
+
 		addUIClickListener('menubarItemUndo', (event:MouseEvent) -> undoLastCommand());
 
 		addUIClickListener('menubarItemRedo', (event:MouseEvent) -> redoLastCommand());
@@ -1688,8 +1710,11 @@ class ChartEditorState extends HaxeUIState
 			gridCursor.visible = false;
 			gridCursor.x = -9999;
 			gridCursor.y = -9999;
+		}
 
-			Cursor.cursorMode = Default;
+		if (isCursorOverHaxeUIButton && Cursor.cursorMode == Default)
+		{
+			Cursor.cursorMode = Pointer;
 		}
 	}
 
@@ -2203,7 +2228,7 @@ class ChartEditorState extends HaxeUIState
 	 */
 	function handleMusicPlayback()
 	{
-		if (audioInstTrack.playing)
+		if (audioInstTrack != null && audioInstTrack.playing)
 		{
 			if (FlxG.mouse.pressedMiddle)
 			{
@@ -2227,7 +2252,7 @@ class ChartEditorState extends HaxeUIState
 
 				Conductor.update(audioInstTrack.time);
 				// Resync vocals.
-				if (Math.abs(audioInstTrack.time - audioVocalTrack.time) > 100)
+				if (audioVocalTrack != null && Math.abs(audioInstTrack.time - audioVocalTrack.time) > 100)
 					audioVocalTrack.time = audioInstTrack.time;
 
 				// We need time in fractional steps here to allow the song to actually play.
@@ -2249,18 +2274,25 @@ class ChartEditorState extends HaxeUIState
 
 	function startAudioPlayback()
 	{
-		audioInstTrack.play();
-		audioVocalTrack.play();
+		if (audioInstTrack != null)
+			audioInstTrack.play();
+		if (audioVocalTrack != null)
+			audioVocalTrack.play();
 	}
 
 	function stopAudioPlayback()
 	{
-		audioInstTrack.pause();
-		audioVocalTrack.pause();
+		if (audioInstTrack != null)
+			audioInstTrack.pause();
+		if (audioVocalTrack != null)
+			audioVocalTrack.pause();
 	}
 
 	function toggleAudioPlayback()
 	{
+		if (audioInstTrack == null)
+			return;
+
 		if (audioInstTrack.playing)
 		{
 			stopAudioPlayback();
@@ -2377,15 +2409,32 @@ class ChartEditorState extends HaxeUIState
 	}
 
 	/**
-	 * Load a music track for playback.
+	 * Loads an instrumental from an absolute file path, replacing the current instrumental.
 	 */
-	function loadMusic()
+	public function loadInstrumentalFromPath(path:String):Void
 	{
-		// TODO: How to load music by selecting with a file dialog?
-		audioInstTrack = FlxG.sound.play(Paths.inst('dadbattle'), 1.0, false);
+		var fileBytes:haxe.io.Bytes = sys.io.File.getBytes(path);
+		loadInstrumentalFromBytes(fileBytes);
+	}
+
+	/**
+	 * Loads an instrumental from audio byte data, replacing the current instrumental.
+	 */
+	public function loadInstrumentalFromBytes(bytes:haxe.io.Bytes):Void
+	{
+		audioInstTrack = FlxG.sound.load(openfl.media.Sound.loadCompressedDataFromByteArray(ByteArray.fromBytes(bytes)), 1.0, false);
 		audioInstTrack.autoDestroy = false;
 		audioInstTrack.pause();
 
+		// Tell the user the load was successful.
+		// TODO: Un-bork this.
+		// showNotification('Loaded instrumental track successfully.');
+
+		postLoadInstrumental();
+	}
+
+	function postLoadInstrumental()
+	{
 		// Prevent the time from skipping back to 0 when the song ends.
 		audioInstTrack.onComplete = function()
 		{
@@ -2393,11 +2442,6 @@ class ChartEditorState extends HaxeUIState
 			audioVocalTrack.pause();
 		};
 
-		audioVocalTrack = FlxG.sound.play(Paths.voices('dadbattle'), 1.0, false);
-		audioVocalTrack.autoDestroy = false;
-		audioVocalTrack.pause();
-
-		// TODO: Make sure Conductor works properly with changing BPMs.
 		var DAD_BATTLE_BPM = 180;
 		var BOPEEBO_BPM = 100;
 		Conductor.forceBPM(DAD_BATTLE_BPM);
@@ -2416,6 +2460,23 @@ class ChartEditorState extends HaxeUIState
 		moveSongToScrollPosition();
 	}
 
+	/**
+	 * Load a music track for playback.
+	 */
+	function loadDefaultMusic()
+	{
+		// TODO: How to load music by selecting with a file dialog?
+		audioInstTrack = FlxG.sound.play(Paths.inst('dadbattle'), 1.0, false);
+		audioInstTrack.autoDestroy = false;
+		audioInstTrack.pause();
+
+		audioVocalTrack = FlxG.sound.play(Paths.voices('dadbattle'), 1.0, false);
+		audioVocalTrack.autoDestroy = false;
+		audioVocalTrack.pause();
+
+		postLoadInstrumental();
+	}
+
 	/**
 	 * When setting the scroll position, except when automatically scrolling during song playback,
 	 * we need to update the conductor's current step time and the timestamp of the audio tracks.
@@ -2426,8 +2487,10 @@ class ChartEditorState extends HaxeUIState
 		Conductor.update(scrollPositionInMs);
 
 		// Update the songPosition in the audio tracks.
-		audioInstTrack.time = scrollPositionInMs + playheadPositionInMs;
-		audioVocalTrack.time = scrollPositionInMs + playheadPositionInMs;
+		if (audioInstTrack != null)
+			audioInstTrack.time = scrollPositionInMs + playheadPositionInMs;
+		if (audioVocalTrack != null)
+			audioVocalTrack.time = scrollPositionInMs + playheadPositionInMs;
 
 		// We need to update the note sprites because we changed the scroll position.
 		noteDisplayDirty = true;