From 20e6c7a2be3ac8c23f736cc2b247d7ebab5b5c00 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Tue, 28 Feb 2023 13:17:28 -0500
Subject: [PATCH] Fix 5 or 6 issues with charting

---
 Project.xml                                   |   6 +-
 haxe_libraries/README.md                      |   3 +
 .../charting/ChartEditorDialogHandler.hx      | 250 +++++++++---------
 .../debug/charting/ChartEditorNoteSprite.hx   |  24 +-
 .../ui/debug/charting/ChartEditorState.hx     |  84 +++---
 source/module.xml                             |   9 +-
 6 files changed, 206 insertions(+), 170 deletions(-)
 create mode 100644 haxe_libraries/README.md

diff --git a/Project.xml b/Project.xml
index ef58d5b53..393248698 100644
--- a/Project.xml
+++ b/Project.xml
@@ -156,9 +156,13 @@
 	<haxeflag name="--macro" value="include('funkin')" />
 	
 	<!-- Ensure all UI components are available at runtime. -->
+	<haxeflag name="--macro" value="include('haxe.ui.backend.flixel.components')" />
+	<haxeflag name="--macro" value="include('haxe.ui.containers.dialogs')" />
+	<haxeflag name="--macro" value="include('haxe.ui.containers.menus')" />
+	<haxeflag name="--macro" value="include('haxe.ui.containers.properties')" />
+	<haxeflag name="--macro" value="include('haxe.ui.core')" />
   <haxeflag name="--macro" value="include('haxe.ui.components')" />
   <haxeflag name="--macro" value="include('haxe.ui.containers')" />
-	<haxeflag name="--macro" value="include('haxe.ui.containers.menus')" />
 
 	<!--
 		Ensure additional class packages are available at runtime (some only really used by scripts).
diff --git a/haxe_libraries/README.md b/haxe_libraries/README.md
new file mode 100644
index 000000000..8ea199c6d
--- /dev/null
+++ b/haxe_libraries/README.md
@@ -0,0 +1,3 @@
+# haxe_libraries
+
+Used by Lix
\ No newline at end of file
diff --git a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx
index 99b03381e..cecbbfb64 100644
--- a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx
@@ -1,5 +1,6 @@
 package funkin.ui.debug.charting;
 
+import haxe.io.Path;
 import flixel.FlxSprite;
 import flixel.util.FlxTimer;
 import funkin.input.Cursor;
@@ -8,6 +9,7 @@ import funkin.play.character.CharacterData.CharacterDataParser;
 import funkin.play.song.SongData.SongDataParser;
 import funkin.play.song.SongData.SongPlayableChar;
 import funkin.play.song.SongData.SongTimeChange;
+import haxe.ui.core.Component;
 import haxe.ui.components.Button;
 import haxe.ui.components.DropDown;
 import haxe.ui.components.Image;
@@ -18,7 +20,6 @@ import haxe.ui.components.TextField;
 import haxe.ui.containers.Box;
 import haxe.ui.containers.dialogs.Dialog;
 import haxe.ui.containers.dialogs.Dialogs;
-import haxe.ui.containers.properties.Property;
 import haxe.ui.containers.properties.PropertyGrid;
 import haxe.ui.containers.properties.PropertyGroup;
 import haxe.ui.containers.VBox;
@@ -27,16 +28,19 @@ import haxe.ui.events.UIEvent;
 
 using Lambda;
 
+/**
+ * Handles dialogs for the new Chart Editor.
+ */
 class ChartEditorDialogHandler
 {
-  static final CHART_EDITOR_DIALOG_ABOUT_LAYOUT = Paths.ui('chart-editor/dialogs/about');
-  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_SONG_METADATA_LAYOUT = Paths.ui('chart-editor/dialogs/song-metadata');
-  static final CHART_EDITOR_DIALOG_SONG_METADATA_CHARGROUP_LAYOUT = Paths.ui('chart-editor/dialogs/song-metadata-chargroup');
-  static final CHART_EDITOR_DIALOG_UPLOAD_VOCALS_LAYOUT = Paths.ui('chart-editor/dialogs/upload-vocals');
-  static final CHART_EDITOR_DIALOG_UPLOAD_VOCALS_ENTRY_LAYOUT = Paths.ui('chart-editor/dialogs/upload-vocals-entry');
-  static final CHART_EDITOR_DIALOG_USER_GUIDE_LAYOUT = Paths.ui('chart-editor/dialogs/user-guide');
+  static final CHART_EDITOR_DIALOG_ABOUT_LAYOUT:String = Paths.ui('chart-editor/dialogs/about');
+  static final CHART_EDITOR_DIALOG_WELCOME_LAYOUT:String = Paths.ui('chart-editor/dialogs/welcome');
+  static final CHART_EDITOR_DIALOG_UPLOAD_INST_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-inst');
+  static final CHART_EDITOR_DIALOG_SONG_METADATA_LAYOUT:String = Paths.ui('chart-editor/dialogs/song-metadata');
+  static final CHART_EDITOR_DIALOG_SONG_METADATA_CHARGROUP_LAYOUT:String = Paths.ui('chart-editor/dialogs/song-metadata-chargroup');
+  static final CHART_EDITOR_DIALOG_UPLOAD_VOCALS_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-vocals');
+  static final CHART_EDITOR_DIALOG_UPLOAD_VOCALS_ENTRY_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-vocals-entry');
+  static final CHART_EDITOR_DIALOG_USER_GUIDE_LAYOUT:String = Paths.ui('chart-editor/dialogs/user-guide');
 
   /**
    * 
@@ -55,55 +59,23 @@ class ChartEditorDialogHandler
 
     // TODO: Add callbacks to the dialog buttons
 
-    // Switch the graphic for frames.
-    var bfSpritePlaceholder:Image = dialog.findComponent('bfSprite', Image);
-
-    // TODO: Replace this bullshit with a custom HaxeUI component that loads the sprite from the game's assets.
-
-    if (bfSpritePlaceholder != null)
-    {
-      var bfSprite:FlxSprite = new FlxSprite(0, 0);
-
-      bfSprite.visible = false;
-
-      var frames = Paths.getSparrowAtlas(bfSpritePlaceholder.resource);
-      bfSprite.frames = frames;
-
-      bfSprite.animation.addByPrefix('idle', 'Boyfriend DJ0', 24, true);
-      bfSprite.animation.play('idle');
-
-      bfSpritePlaceholder.rootComponent.add(bfSprite);
-      bfSpritePlaceholder.visible = false;
-
-      new FlxTimer().start(0.10, (_timer:FlxTimer) ->
-      {
-        bfSprite.x = bfSpritePlaceholder.screenLeft;
-        bfSprite.y = bfSpritePlaceholder.screenTop;
-        bfSprite.setGraphicSize(Std.int(bfSpritePlaceholder.width), Std.int(bfSpritePlaceholder.height));
-        bfSprite.visible = true;
-      });
-    }
-
     // Add handlers to the "Create From Song" section.
     var linkCreateBasic:Link = dialog.findComponent('splashCreateFromSongBasic', Link);
-    linkCreateBasic.onClick = (_event) ->
-    {
+    linkCreateBasic.onClick = (_event) -> {
       dialog.hideDialog(DialogButton.CANCEL);
 
       // Create song wizard
       var uploadInstDialog = openUploadInstDialog(state, false);
-      uploadInstDialog.onDialogClosed = (_event) ->
-      {
+      uploadInstDialog.onDialogClosed = (_event) -> {
         state.isHaxeUIDialogOpen = false;
         if (_event.button == DialogButton.APPLY)
         {
           var songMetadataDialog = openSongMetadataDialog(state);
-          songMetadataDialog.onDialogClosed = (_event) ->
-          {
+          songMetadataDialog.onDialogClosed = (_event) -> {
             state.isHaxeUIDialogOpen = false;
             if (_event.button == DialogButton.APPLY)
             {
-              var uploadVocalsDialog = openUploadVocalsDialog(state);
+              var uploadVocalsDialog = openUploadVocalsDialog(state, false);
             }
           };
         }
@@ -145,8 +117,7 @@ class ChartEditorDialogHandler
 
       var linkTemplateSong:Link = new Link();
       linkTemplateSong.text = songName;
-      linkTemplateSong.onClick = (_event) ->
-      {
+      linkTemplateSong.onClick = (_event) -> {
         dialog.hideDialog(DialogButton.CANCEL);
 
         // Load song from template
@@ -165,72 +136,111 @@ class ChartEditorDialogHandler
 
     var instrumentalBox:Box = dialog.findComponent('instrumentalBox', Box);
 
-    instrumentalBox.onMouseOver = (_event) ->
-    {
+    instrumentalBox.onMouseOver = (_event) -> {
       instrumentalBox.swapClass('upload-bg', 'upload-bg-hover');
       Cursor.cursorMode = Pointer;
     }
 
-    instrumentalBox.onMouseOut = (_event) ->
-    {
+    instrumentalBox.onMouseOut = (_event) -> {
       instrumentalBox.swapClass('upload-bg-hover', 'upload-bg');
       Cursor.cursorMode = Default;
     }
 
     var onDropFile:String->Void;
 
-    instrumentalBox.onClick = (_event) ->
-    {
+    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);
-        }
+        {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) ->
-    {
+    onDropFile = (path:String) -> {
       trace('Dropped file: ' + path);
       state.loadInstrumentalFromPath(path);
       dialog.hideDialog(DialogButton.APPLY);
       removeDropHandler(onDropFile);
     };
 
-    addDropHandler(onDropFile);
+    addDropHandler(instrumentalBox, onDropFile);
 
     return dialog;
   }
 
-  static function addDropHandler(handler:String->Void)
+  static var dropHandlers:Array<
+    {
+      component:Component,
+      handler:(String->Void)
+    }> = [];
+
+  static function addDropHandler(component:Component, handler:String->Void):Void
   {
     #if desktop
-    FlxG.stage.window.onDropFile.add(handler);
+    if (!FlxG.stage.window.onDropFile.has(onDropFile)) FlxG.stage.window.onDropFile.add(onDropFile);
+
+    dropHandlers.push(
+      {
+        component: component,
+        handler: handler
+      });
     #else
     trace('addDropHandler not implemented for this platform');
     #end
   }
 
-  static function removeDropHandler(handler:String->Void)
+  static function removeDropHandler(handler:String->Void):Void
   {
     #if desktop
     FlxG.stage.window.onDropFile.remove(handler);
     #end
   }
 
+  static function clearDropHandlers():Void
+  {
+    #if desktop
+    dropHandlers = [];
+    FlxG.stage.window.onDropFile.remove(onDropFile);
+    #end
+  }
+
+  static function onDropFile(path:String):Void
+  {
+    // a VERY short timer to wait for the mouse position to update
+    new FlxTimer().start(0.01, function(_) {
+      trace("mouseX: " + FlxG.mouse.screenX + ", mouseY: " + FlxG.mouse.screenY);
+
+      for (handler in dropHandlers)
+      {
+        if (handler.component.hitTest(FlxG.mouse.screenX, FlxG.mouse.screenY))
+        {
+          trace('File dropped on component! ' + handler.component.id);
+          handler.handler(path);
+          return;
+        }
+      }
+
+      trace('File dropped on nothing!' + path);
+    });
+  }
+
+  /**
+   * Opens the dialog in the wizard where the user can set song metadata like name and artist and BPM.
+   * @param state The ChartEditorState instance.
+   * @return The dialog to open.
+   */
   public static function openSongMetadataDialog(state:ChartEditorState):Dialog
   {
     var dialog:Dialog = openDialog(state, CHART_EDITOR_DIALOG_SONG_METADATA_LAYOUT, true, false);
 
     var dialogSongName:TextField = dialog.findComponent('dialogSongName', TextField);
-    dialogSongName.onChange = (event:UIEvent) ->
-    {
-      var valid = event.target.text != null && event.target.text != "";
+    dialogSongName.onChange = function(event:UIEvent) {
+      var valid:Bool = event.target.text != null && event.target.text != '';
 
       if (valid)
       {
@@ -245,9 +255,8 @@ class ChartEditorDialogHandler
     state.currentSongMetadata.songName = null;
 
     var dialogSongArtist:TextField = dialog.findComponent('dialogSongArtist', TextField);
-    dialogSongArtist.onChange = (event:UIEvent) ->
-    {
-      var valid = event.target.text != null && event.target.text != "";
+    dialogSongArtist.onChange = function(event:UIEvent) {
+      var valid:Bool = event.target.text != null && event.target.text != '';
 
       if (valid)
       {
@@ -262,8 +271,7 @@ class ChartEditorDialogHandler
     state.currentSongMetadata.artist = null;
 
     var dialogStage:DropDown = dialog.findComponent('dialogStage', DropDown);
-    dialogStage.onChange = (event:UIEvent) ->
-    {
+    dialogStage.onChange = function(event:UIEvent) {
       var valid = event.data != null && event.data.id != null;
 
       if (event.data.id == null) return;
@@ -272,16 +280,14 @@ class ChartEditorDialogHandler
     state.currentSongMetadata.playData.stage = null;
 
     var dialogNoteSkin:DropDown = dialog.findComponent('dialogNoteSkin', DropDown);
-    dialogNoteSkin.onChange = (event:UIEvent) ->
-    {
+    dialogNoteSkin.onChange = (event:UIEvent) -> {
       if (event.data.id == null) return;
       state.currentSongMetadata.playData.noteSkin = event.data.id;
     };
     state.currentSongMetadata.playData.noteSkin = null;
 
     var dialogBPM:NumberStepper = dialog.findComponent('dialogBPM', NumberStepper);
-    dialogBPM.onChange = (event:UIEvent) ->
-    {
+    dialogBPM.onChange = (event:UIEvent) -> {
       if (event.value == null || event.value <= 0) return;
 
       var timeChanges = state.currentSongMetadata.timeChanges;
@@ -301,11 +307,9 @@ class ChartEditorDialogHandler
 
     var dialogCharGrid:PropertyGrid = dialog.findComponent('dialogCharGrid', PropertyGrid);
     var dialogCharAdd:Button = dialog.findComponent('dialogCharAdd', Button);
-    dialogCharAdd.onClick = (_event) ->
-    {
+    dialogCharAdd.onClick = (_event) -> {
       var charGroup:PropertyGroup;
-      charGroup = buildCharGroup(state, null, () ->
-      {
+      charGroup = buildCharGroup(state, null, () -> {
         dialogCharGrid.removeComponent(charGroup);
       });
       dialogCharGrid.addComponent(charGroup);
@@ -317,8 +321,7 @@ class ChartEditorDialogHandler
     dialogCharGrid.addComponent(buildCharGroup(state, 'bf', null));
 
     var dialogContinue:Button = dialog.findComponent('dialogContinue', Button);
-    dialogContinue.onClick = (_event) ->
-    {
+    dialogContinue.onClick = (_event) -> {
       dialog.hideDialog(DialogButton.APPLY);
     };
 
@@ -329,8 +332,7 @@ class ChartEditorDialogHandler
   {
     var groupKey = key;
 
-    var getCharData = () ->
-    {
+    var getCharData = () -> {
       if (groupKey == null) groupKey = 'newChar${state.currentSongMetadata.playData.playableChars.keys().count()}';
 
       var result = state.currentSongMetadata.playData.playableChars.get(groupKey);
@@ -342,16 +344,14 @@ class ChartEditorDialogHandler
       return result;
     }
 
-    var moveCharGroup = (target:String) ->
-    {
+    var moveCharGroup = (target:String) -> {
       var charData = getCharData();
       state.currentSongMetadata.playData.playableChars.remove(groupKey);
       state.currentSongMetadata.playData.playableChars.set(target, charData);
       groupKey = target;
     }
 
-    var removeGroup = () ->
-    {
+    var removeGroup = () -> {
       state.currentSongMetadata.playData.playableChars.remove(groupKey);
       removeFunc();
     }
@@ -361,8 +361,7 @@ class ChartEditorDialogHandler
     var charGroup:PropertyGroup = cast state.buildComponent(CHART_EDITOR_DIALOG_SONG_METADATA_CHARGROUP_LAYOUT);
 
     var charGroupPlayer:DropDown = charGroup.findComponent('charGroupPlayer', DropDown);
-    charGroupPlayer.onChange = (event:UIEvent) ->
-    {
+    charGroupPlayer.onChange = (event:UIEvent) -> {
       charGroup.text = event.data.text;
       moveCharGroup(event.data.id);
     };
@@ -374,22 +373,19 @@ class ChartEditorDialogHandler
     }
 
     var charGroupOpponent:DropDown = charGroup.findComponent('charGroupOpponent', DropDown);
-    charGroupOpponent.onChange = (event:UIEvent) ->
-    {
+    charGroupOpponent.onChange = (event:UIEvent) -> {
       charData.opponent = event.data.id;
     };
     charGroupOpponent.value = getCharData().opponent;
 
     var charGroupGirlfriend:DropDown = charGroup.findComponent('charGroupGirlfriend', DropDown);
-    charGroupGirlfriend.onChange = (event:UIEvent) ->
-    {
+    charGroupGirlfriend.onChange = (event:UIEvent) -> {
       charData.girlfriend = event.data.id;
     };
     charGroupGirlfriend.value = getCharData().girlfriend;
 
     var charGroupRemove:Button = charGroup.findComponent('charGroupRemove', Button);
-    charGroupRemove.onClick = (_event:MouseEvent) ->
-    {
+    charGroupRemove.onClick = (_event:MouseEvent) -> {
       removeGroup();
     };
 
@@ -413,7 +409,11 @@ class ChartEditorDialogHandler
 
     var dialogContainer = dialog.findComponent('vocalContainer');
 
-    var onDropFile:String->Void;
+    var dialogNoVocals:Button = dialog.findComponent('dialogNoVocals', Button);
+    dialogNoVocals.onClick = function(_event) {
+      // Dismiss
+      dialog.hideDialog(DialogButton.APPLY);
+    };
 
     for (charKey in charIdsForVocals)
     {
@@ -426,39 +426,46 @@ class ChartEditorDialogHandler
       var vocalsEntryLabel:Label = vocalsEntry.findComponent('vocalsEntryLabel', Label);
       vocalsEntryLabel.text = 'Click to browse for a vocal track for $charName.';
 
-      vocalsEntry.onClick = (_event) ->
-      {
+      var onDropFile:String->Void = function(fullPath:String) {
+        trace('Selected file: $fullPath');
+        var directory:String = Path.directory(fullPath);
+        var filename:String = Path.withoutDirectory(directory);
+
+        vocalsEntryLabel.text = 'Vocals for $charName (click to browse)\n${filename}';
+        state.loadVocalsFromPath(fullPath, charKey);
+        dialogNoVocals.hidden = true;
+        removeDropHandler(onDropFile);
+      };
+
+      vocalsEntry.onClick = function(_event) {
         Dialogs.openBinaryFile('Open $charName Vocals', [
-          {label: "Audio File (.ogg)", extension: "ogg"}], function(selectedFile)
-        {
-          if (selectedFile != null)
-          {
-            trace('Selected file: ' + selectedFile.name + "~" + selectedFile.fullPath);
-            vocalsEntryLabel.text = 'Vocals for $charName (click to browse)\n${selectedFile.name}';
-            state.loadVocalsFromBytes(selectedFile.bytes);
-            removeDropHandler(onDropFile);
-          }
+          {label: 'Audio File (.ogg)', extension: 'ogg'}], function(selectedFile) {
+            if (selectedFile != null)
+            {
+              trace('Selected file: ' + selectedFile.name);
+              vocalsEntryLabel.text = 'Vocals for $charName (click to browse)\n${selectedFile.name}';
+              state.loadVocalsFromBytes(selectedFile.bytes, charKey);
+              dialogNoVocals.hidden = true;
+              removeDropHandler(onDropFile);
+            }
         });
+
+        // onDropFile
+        addDropHandler(vocalsEntry, onDropFile);
       }
 
       dialogContainer.addComponent(vocalsEntry);
     }
 
     var dialogContinue:Button = dialog.findComponent('dialogContinue', Button);
-    dialogContinue.onClick = (_event) ->
-    {
+    dialogContinue.onClick = function(_event) {
+      // Dismiss
       dialog.hideDialog(DialogButton.APPLY);
     };
 
     // TODO: Redo the logic for file drop handler to be more robust.
     // We need to distinguish which component the mouse is over when the file is dropped.
 
-    onDropFile = (path:String) ->
-    {
-      trace('Dropped file: ' + path);
-    };
-    addDropHandler(onDropFile);
-
     return dialog;
   }
 
@@ -483,8 +490,7 @@ class ChartEditorDialogHandler
     dialog.showDialog(modal);
 
     state.isHaxeUIDialogOpen = true;
-    dialog.onDialogClosed = (_event) ->
-    {
+    dialog.onDialogClosed = (_event) -> {
       state.isHaxeUIDialogOpen = false;
     };
 
diff --git a/source/funkin/ui/debug/charting/ChartEditorNoteSprite.hx b/source/funkin/ui/debug/charting/ChartEditorNoteSprite.hx
index 85a1f86a2..a20b43dbd 100644
--- a/source/funkin/ui/debug/charting/ChartEditorNoteSprite.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorNoteSprite.hx
@@ -1,7 +1,6 @@
 package funkin.ui.debug.charting;
 
 import flixel.FlxObject;
-import flixel.FlxBasic;
 import flixel.FlxSprite;
 import flixel.graphics.frames.FlxFramesCollection;
 import flixel.graphics.frames.FlxTileFrames;
@@ -14,6 +13,14 @@ import funkin.play.song.SongData.SongNoteData;
  */
 class ChartEditorNoteSprite extends FlxSprite
 {
+  /**
+   * The list of available note skin to validate against.
+   */
+  public static final NOTE_STYLES:Array<String> = ['Normal', 'Pixel'];
+
+  /**
+   * The ChartEditorState this note belongs to.
+   */
   public var parentState:ChartEditorState;
 
   /**
@@ -22,6 +29,11 @@ class ChartEditorNoteSprite extends FlxSprite
    */
   public var noteData(default, set):SongNoteData;
 
+  /**
+   * The name of the note style currently in use.
+   */
+  public var noteStyle(get, null):String;
+
   /**
    * This note is the previous sprite in a sustain chain.
    */
@@ -222,14 +234,20 @@ class ChartEditorNoteSprite extends FlxSprite
     return this.childNoteSprite;
   }
 
-  public function playNoteAnimation()
+  function get_noteStyle():String
+  {
+    // Fall back to 'Normal' if it's not a valid note style.
+    return if (NOTE_STYLES.contains(this.parentState.currentSongNoteSkin)) this.parentState.currentSongNoteSkin else 'Normal';
+  }
+
+  public function playNoteAnimation():Void
   {
     // Decide whether to display a note or a sustain.
     var baseAnimationName:String = 'tap';
     if (this.parentNoteSprite != null) baseAnimationName = (this.childNoteSprite != null) ? 'hold' : 'holdEnd';
 
     // Play the appropriate animation for the type, direction, and skin.
-    var animationName = '${baseAnimationName}${this.noteData.getDirectionName()}${this.parentState.currentSongNoteSkin}';
+    var animationName:String = '${baseAnimationName}${this.noteData.getDirectionName()}${this.noteStyle}';
 
     this.animation.play(animationName);
 
diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index 935e80228..a1bda9fcd 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -1,5 +1,7 @@
 package funkin.ui.debug.charting;
 
+import haxe.ui.notifications.NotificationType;
+import haxe.ui.notifications.NotificationManager;
 import haxe.DynamicAccess;
 import haxe.io.Path;
 import flixel.addons.display.FlxSliceSprite;
@@ -92,6 +94,9 @@ class ChartEditorState extends HaxeUIState
   static final CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT = Paths.ui('chart-editor/toolbox/player-preview');
   static final CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT = Paths.ui('chart-editor/toolbox/opponent-preview');
 
+  // Validation
+  static final SUPPORTED_MUSIC_FORMATS:Array<String> = ['ogg'];
+
   /**
    * The base grid size for the chart editor.
    */
@@ -124,9 +129,9 @@ class ChartEditorState extends HaxeUIState
   static final GRID_TOP_PAD:Int = 8;
 
   /**
-   * Duration, in seconds, until toast notifications are automatically hidden.
+   * Duration, in milliseconds, until toast notifications are automatically hidden.
    */
-  static final NOTIFICATION_DISMISS_TIME:Float = 3.0;
+  static final NOTIFICATION_DISMISS_TIME:Int = 5000;
 
   // Start performing rapid undo after this many seconds.
   static final RAPID_UNDO_DELAY:Float = 0.4;
@@ -898,7 +903,6 @@ class ChartEditorState extends HaxeUIState
 
   var renderedSelectionSquares:FlxTypedSpriteGroup<FlxSprite>;
 
-  var notifBar:SideBar;
   var playbarHead:Slider;
 
   public function new()
@@ -1090,9 +1094,6 @@ class ChartEditorState extends HaxeUIState
 
   function buildAdditionalUI():Void
   {
-    notifBar = cast buildComponent(CHART_EDITOR_NOTIFBAR_LAYOUT);
-    add(notifBar);
-
     playbarHeadLayout = buildComponent(CHART_EDITOR_PLAYBARHEAD_LAYOUT);
 
     playbarHeadLayout.width = FlxG.width - 8;
@@ -1281,7 +1282,7 @@ class ChartEditorState extends HaxeUIState
       if (audioInstTrack != null) audioInstTrack.pitch = pitch;
       if (audioVocalTrackGroup != null) audioVocalTrackGroup.pitch = pitch;
       #end
-      playbackSpeedLabel.text = 'Playback Speed - ${Std.int(event.value * 100) / 100}x';
+      playbackSpeedLabel.text = 'Playback Speed - ${Std.int(pitch * 100) / 100}x';
     });
 
     addUIChangeListener('menubarItemToggleToolboxTools', (event:UIEvent) -> {
@@ -1340,7 +1341,7 @@ class ChartEditorState extends HaxeUIState
     #end
   }
 
-  function onWindowClose(exitCode:Int)
+  function onWindowClose(exitCode:Int):Void
   {
     trace('Window exited with exit code: $exitCode');
     trace('Should save chart? $saveDataDirty');
@@ -1351,12 +1352,12 @@ class ChartEditorState extends HaxeUIState
     }
   }
 
-  function cleanupAutoSave()
+  function cleanupAutoSave():Void
   {
     WindowUtil.windowExit.remove(onWindowClose);
   }
 
-  public override function update(elapsed:Float)
+  public override function update(elapsed:Float):Void
   {
     // dispatchEvent gets called here.
     super.update(elapsed);
@@ -1387,10 +1388,16 @@ class ChartEditorState extends HaxeUIState
     #if debug
     if (FlxG.keys.justPressed.F)
     {
-      // This breaks the layout don't use it.
-      // showNotification('Hi there :)');
-
-      // autoSave();
+      NotificationManager.instance.addNotification(
+        {
+          title: 'This is a Notification',
+          body: 'Hello, world!',
+          type: NotificationType.Info,
+          expiryMs: NOTIFICATION_DISMISS_TIME
+          // styleNames: 'cssStyleName',
+          // icon: 'assetPath',
+          // actions: ['action1', 'action2']
+        });
     }
 
     if (FlxG.keys.justPressed.E)
@@ -1416,7 +1423,7 @@ class ChartEditorState extends HaxeUIState
     // dispatchEvent gets called here.
     if (!super.beatHit()) return false;
 
-    if (shouldPlayMetronome && audioInstTrack.playing)
+    if (shouldPlayMetronome && (audioInstTrack != null && audioInstTrack.playing))
     {
       playMetronomeTick(Conductor.currentBeat % 4 == 0);
     }
@@ -1432,7 +1439,7 @@ class ChartEditorState extends HaxeUIState
     // dispatchEvent gets called here.
     if (!super.stepHit()) return false;
 
-    if (audioInstTrack.playing)
+    if (audioInstTrack != null && audioInstTrack.playing)
     {
       healthIconDad.onStepHit(Conductor.currentStep);
       healthIconBF.onStepHit(Conductor.currentStep);
@@ -1447,7 +1454,7 @@ class ChartEditorState extends HaxeUIState
   /**
    * Handle keybinds for scrolling the chart editor grid.
   **/
-  function handleScrollKeybinds()
+  function handleScrollKeybinds():Void
   {
     // Don't scroll when the cursor is over the UI.
     if (isCursorOverHaxeUI) return;
@@ -1456,16 +1463,17 @@ class ChartEditorState extends HaxeUIState
     var scrollAmount:Float = 0;
     // Amount to scroll the playhead relative to the grid.
     var playheadAmount:Float = 0;
+    var shouldPause:Bool = false;
 
     // Up Arrow = Scroll Up
     if (FlxG.keys.justPressed.UP)
     {
-      scrollAmount = -GRID_SIZE * 0.25;
+      scrollAmount = -GRID_SIZE * 0.25 * 5;
     }
     // Down Arrow = Scroll Down
     if (FlxG.keys.justPressed.DOWN)
     {
-      scrollAmount = GRID_SIZE * 0.25;
+      scrollAmount = GRID_SIZE * 0.25 * 5;
     }
 
     // PAGE UP = Jump Up 1 Measure
@@ -2089,7 +2097,7 @@ class ChartEditorState extends HaxeUIState
   /**
    * Handle using `renderedNotes` to display notes from `currentSongChartNoteData`.
    */
-  function handleNoteDisplay()
+  function handleNoteDisplay():Void
   {
     if (noteDisplayDirty)
     {
@@ -2963,10 +2971,20 @@ class ChartEditorState extends HaxeUIState
 
   /**
    * Loads an instrumental from an absolute file path, replacing the current instrumental.
+   * 
+   * @param path The absolute path to the audio file.
    */
   public function loadInstrumentalFromPath(path:String):Void
   {
     #if sys
+    // Validate file extension.
+    var fileExtension:String = Path.extension(path);
+    if (!SUPPORTED_MUSIC_FORMATS.contains(fileExtension))
+    {
+      trace('[WARN] Unsupported file extension: $fileExtension');
+      return;
+    }
+
     var fileBytes:haxe.io.Bytes = sys.io.File.getBytes(path);
     loadInstrumentalFromBytes(fileBytes);
     #else
@@ -3025,17 +3043,17 @@ class ChartEditorState extends HaxeUIState
   /**
    * Loads a vocal track from an absolute file path.
    */
-  public function loadVocalsFromPath(path:String):Void
+  public function loadVocalsFromPath(path:String, ?charKey:String):Void
   {
     #if sys
     var fileBytes:haxe.io.Bytes = sys.io.File.getBytes(path);
-    loadVocalsFromBytes(fileBytes);
+    loadVocalsFromBytes(fileBytes, charKey);
     #else
     trace("[WARN] This platform can't load audio from a file path, you'll need to fetch the bytes some other way.");
     #end
   }
 
-  public function loadVocalsFromAsset(path:String):Void
+  public function loadVocalsFromAsset(path:String, ?charKey:String):Void
   {
     var vocalTrack:FlxSound = FlxG.sound.load(path, 1.0, false);
     audioVocalTrackGroup.add(vocalTrack);
@@ -3044,7 +3062,7 @@ class ChartEditorState extends HaxeUIState
   /**
    * Loads a vocal track from audio byte data.
    */
-  public function loadVocalsFromBytes(bytes:haxe.io.Bytes):Void
+  public function loadVocalsFromBytes(bytes:haxe.io.Bytes, ?charKey:String):Void
   {
     var openflSound = new openfl.media.Sound();
     openflSound.loadCompressedDataFromByteArray(openfl.utils.ByteArray.fromBytes(bytes), bytes.length);
@@ -3065,7 +3083,7 @@ class ChartEditorState extends HaxeUIState
 
     if (song == null)
     {
-      // showNotification('Failed to load song template.');
+      // showNotification('Failed to load song.');
       return;
     }
 
@@ -3218,24 +3236,12 @@ class ChartEditorState extends HaxeUIState
     ChartEditorNoteSprite.noteFrameCollection = null;
   }
 
-  /**
-   * Displays a notification to the user. The only action is to dismiss.
-   */
-  function showNotification(text:String)
-  {
-    // Make it appear.
-    notifBar.show();
-
-    // Auto dismiss.
-    new FlxTimer().start(NOTIFICATION_DISMISS_TIME, (_:FlxTimer) -> dismissNotification());
-  }
-
   /**
    * Dismiss any existing notifications, if there are any.
    */
-  function dismissNotification():Void
+  function dismissNotifications():Void
   {
-    notifBar.hide();
+    NotificationManager.instance.clearNotifications();
   }
 
   /**
diff --git a/source/module.xml b/source/module.xml
index fcedcb346..dd7c9ad20 100644
--- a/source/module.xml
+++ b/source/module.xml
@@ -7,14 +7,13 @@
 			This needs to be done HERE and not via the `include` macro because `Toolkit.init()`
 			reads this to build the component registry.
 		-->
-		<class package="haxe.ui.core" loadAll="true" />
-
+		<class package="haxe.ui.backend.flixel.components" loadAll="true" />
 		<class package="haxe.ui.components" loadAll="true" />
-
-		<class package="haxe.ui.containers" loadAll="true" />
-		<class package="haxe.ui.containers.menus" loadAll="true" />
 		<class package="haxe.ui.containers.dialogs" loadAll="true" />
+		<class package="haxe.ui.containers.menus" loadAll="true" />
 		<class package="haxe.ui.containers.properties" loadAll="true" />
+		<class package="haxe.ui.containers" loadAll="true" />
+		<class package="haxe.ui.core" loadAll="true" />
 		
 		<!-- Custom components. -->
 		<class package="funkin.ui.haxeui.components" loadAll="true" />