From 3005aa1f3ba9bd02ea703f9a309ebca63fa59732 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Thu, 8 Jun 2023 16:48:34 -0400
Subject: [PATCH] Added legacy importer

---
 source/funkin/play/song/formats/FNFLegacy.hx  | 131 +++++
 .../charting/ChartEditorDialogHandler.hx      | 488 ++++++++++++++++--
 .../debug/charting/ChartEditorEventSprite.hx  |   8 +-
 .../debug/charting/ChartEditorThemeHandler.hx |  28 +-
 .../charting/ChartEditorToolboxHandler.hx     | 205 +++++---
 5 files changed, 722 insertions(+), 138 deletions(-)
 create mode 100644 source/funkin/play/song/formats/FNFLegacy.hx

diff --git a/source/funkin/play/song/formats/FNFLegacy.hx b/source/funkin/play/song/formats/FNFLegacy.hx
new file mode 100644
index 000000000..51dc602f3
--- /dev/null
+++ b/source/funkin/play/song/formats/FNFLegacy.hx
@@ -0,0 +1,131 @@
+package funkin.play.song.formats;
+
+typedef FNFLegacy =
+{
+  var song:LegacySongData;
+}
+
+typedef LegacySongData =
+{
+  var player1:String; // Boyfriend
+  var player2:String; // Opponent
+
+  var speed:LegacyScrollSpeeds;
+  var stageDefault:String;
+  var bpm:Float;
+  var notes:LegacyNoteData;
+  var song:String; // Song name
+};
+
+typedef LegacyScrollSpeeds =
+{
+  var easy:Float;
+  var normal:Float;
+  var hard:Float;
+};
+
+typedef LegacyNoteData =
+{
+  /**
+   * The easy difficulty.
+   */
+  var ?easy:Array<LegacyNoteSection>;
+
+  /**
+   * The normal difficulty.
+   */
+  var ?normal:Array<LegacyNoteSection>;
+
+  /**
+   * The hard difficulty.
+   */
+  var ?hard:Array<LegacyNoteSection>;
+};
+
+typedef LegacyNoteSection =
+{
+  /**
+   * Whether the section is a must-hit section.
+   * If true, 0-3 are boyfriends notes, 4-7 are opponents notes.
+   * If false, 0-3 are opponents notes, 4-7 are boyfriends notes.
+   */
+  var mustHitSection:Bool;
+
+  /**
+   * Array of note data:
+   * - Direction
+   * - Time (ms)
+   * - Sustain Duration (ms)
+   * - Note kind (true = "alt", or string)
+   */
+  var sectionNotes:Array<LegacyNote>;
+
+  var typeOfSection:Int;
+  var lengthInSteps:Int;
+}
+
+/**
+ * Notes in the old format are stored as an Array<Dynamic>
+ */
+abstract LegacyNote(Array<Dynamic>)
+{
+  public var time(get, set):Float;
+
+  function get_time():Float
+  {
+    return this[0];
+  }
+
+  function set_time(value:Float):Float
+  {
+    return this[0] = value;
+  }
+
+  public var data(get, set):Int;
+
+  function get_data():Int
+  {
+    return this[1];
+  }
+
+  function set_data(value:Int):Int
+  {
+    return this[1] = value;
+  }
+
+  public function getData(mustHitSection:Bool):Int
+  {
+    if (mustHitSection) return this[1];
+
+    return (this[1] + 4) % 8;
+  }
+
+  public var length(get, set):Float;
+
+  function get_length():Float
+  {
+    if (this.length < 3) return 0.0;
+    return this[2];
+  }
+
+  function set_length(value:Float):Float
+  {
+    return this[2] = value;
+  }
+
+  public var kind(get, set):String;
+
+  function get_kind():String
+  {
+    if (this.length < 4) return 'normal';
+
+    if (Std.isOfType(this[3], Bool)) return this[3] ? 'alt' : 'normal';
+
+    return this[3];
+  }
+
+  function set_kind(value:String):String
+  {
+    return this[3] = value;
+  }
+}
diff --git a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx
index 4240773e4..4044df5d8 100644
--- a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx
@@ -1,13 +1,21 @@
 package funkin.ui.debug.charting;
 
+import funkin.play.character.CharacterData;
+import funkin.util.Constants;
+import funkin.util.SerializerUtil;
+import funkin.play.song.SongData.SongChartData;
+import funkin.play.song.SongData.SongMetadata;
 import flixel.util.FlxTimer;
 import funkin.input.Cursor;
 import funkin.play.character.BaseCharacter;
 import funkin.play.character.CharacterData.CharacterDataParser;
 import funkin.play.song.Song;
+import funkin.play.song.SongMigrator;
+import funkin.play.song.SongValidator;
 import funkin.play.song.SongData.SongDataParser;
 import funkin.play.song.SongData.SongPlayableChar;
 import funkin.play.song.SongData.SongTimeChange;
+import funkin.util.FileUtil;
 import haxe.io.Path;
 import haxe.ui.components.Button;
 import haxe.ui.components.DropDown;
@@ -40,6 +48,9 @@ class ChartEditorDialogHandler
   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_OPEN_CHART_LAYOUT:String = Paths.ui('chart-editor/dialogs/open-chart');
+  static final CHART_EDITOR_DIALOG_OPEN_CHART_ENTRY_LAYOUT:String = Paths.ui('chart-editor/dialogs/open-chart-entry');
+  static final CHART_EDITOR_DIALOG_IMPORT_CHART_LAYOUT:String = Paths.ui('chart-editor/dialogs/import-chart');
   static final CHART_EDITOR_DIALOG_USER_GUIDE_LAYOUT:String = Paths.ui('chart-editor/dialogs/user-guide');
 
   /**
@@ -71,41 +82,32 @@ class ChartEditorDialogHandler
       //
       // Create Song Wizard
       //
+      openCreateSongWizard(state, false);
+    }
 
-      // Step 1. Upload Instrumental
-      var uploadInstDialog:Dialog = openUploadInstDialog(state, false);
-      uploadInstDialog.onDialogClosed = function(_event) {
-        state.isHaxeUIDialogOpen = false;
-        if (_event.button == DialogButton.APPLY)
-        {
-          // Step 2. Song Metadata
-          var songMetadataDialog:Dialog = openSongMetadataDialog(state);
-          songMetadataDialog.onDialogClosed = function(_event) {
-            state.isHaxeUIDialogOpen = false;
-            if (_event.button == DialogButton.APPLY)
-            {
-              // Step 3. Upload Vocals
-              // NOTE: Uploading vocals is optional, so we don't need to check if the user cancelled the wizard.
-              openUploadVocalsDialog(state, false); // var uploadVocalsDialog:Dialog
-            }
-            else
-            {
-              // User cancelled the wizard! Back to the welcome dialog.
-              openWelcomeDialog(state);
-            }
-          };
-        }
-        else
-        {
-          // User cancelled the wizard! Back to the welcome dialog.
-          openWelcomeDialog(state);
-        }
-      };
+    var linkImportChartLegacy:Link = dialog.findComponent('splashImportChartLegacy', Link);
+    linkImportChartLegacy.onClick = function(_event) {
+      // Hide the welcome dialog
+      dialog.hideDialog(DialogButton.CANCEL);
+
+      // Open the "Import Chart" dialog
+      openImportChartWizard(state, 'legacy', false);
+    };
+
+    var buttonBrowse:Button = dialog.findComponent('splashBrowse', Button);
+    buttonBrowse.onClick = function(_event) {
+      // Hide the welcome dialog
+      dialog.hideDialog(DialogButton.CANCEL);
+
+      // Open the "Open Chart" dialog
+      openBrowseWizard(state, false);
     }
 
     var splashTemplateContainer:VBox = dialog.findComponent('splashTemplateContainer', VBox);
 
     var songList:Array<String> = SongDataParser.listSongIds();
+    // Sort alphabetically
+    songList.sort((a, b) -> a > b ? 1 : -1);
 
     for (targetSongId in songList)
     {
@@ -130,6 +132,120 @@ class ChartEditorDialogHandler
     return dialog;
   }
 
+  /**
+   * Open the wizard for opening an existing chart from individual files.
+   * @param state
+   * @param closable
+   */
+  public static function openBrowseWizard(state:ChartEditorState, closable:Bool):Void
+  {
+    // Open the "Open Chart" wizard
+    // Step 1. Open Chart
+    var openChartDialog:Dialog = openChartDialog(state);
+    openChartDialog.onDialogClosed = function(_event) {
+      state.isHaxeUIDialogOpen = false;
+      if (_event.button == DialogButton.APPLY)
+      {
+        // Step 2. Upload instrumental
+        var uploadInstDialog:Dialog = openUploadInstDialog(state, closable);
+        uploadInstDialog.onDialogClosed = function(_event) {
+          state.isHaxeUIDialogOpen = false;
+          if (_event.button == DialogButton.APPLY)
+          {
+            // Step 3. Upload Vocals
+            // NOTE: Uploading vocals is optional, so we don't need to check if the user cancelled the wizard.
+            var uploadVocalsDialog:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog
+            uploadVocalsDialog.onDialogClosed = function(_event) {
+              state.isHaxeUIDialogOpen = false;
+              state.postLoadInstrumental();
+            }
+          }
+          else
+          {
+            // User cancelled the wizard! Back to the welcome dialog.
+            openWelcomeDialog(state);
+          }
+        };
+      }
+      else
+      {
+        // User cancelled the wizard! Back to the welcome dialog.
+        openWelcomeDialog(state);
+      }
+    };
+  }
+
+  public static function openImportChartWizard(state:ChartEditorState, format:String, closable:Bool):Void
+  {
+    // Open the "Open Chart" wizard
+    // Step 1. Open Chart
+    var openChartDialog:Dialog = openImportChartDialog(state, format);
+    openChartDialog.onDialogClosed = function(_event) {
+      state.isHaxeUIDialogOpen = false;
+      if (_event.button == DialogButton.APPLY)
+      {
+        // Step 2. Upload instrumental
+        var uploadInstDialog:Dialog = openUploadInstDialog(state, closable);
+        uploadInstDialog.onDialogClosed = function(_event) {
+          state.isHaxeUIDialogOpen = false;
+          if (_event.button == DialogButton.APPLY)
+          {
+            // Step 3. Upload Vocals
+            // NOTE: Uploading vocals is optional, so we don't need to check if the user cancelled the wizard.
+            var uploadVocalsDialog:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog
+            uploadVocalsDialog.onDialogClosed = function(_event) {
+              state.isHaxeUIDialogOpen = false;
+              state.postLoadInstrumental();
+            }
+          }
+          else
+          {
+            // User cancelled the wizard! Back to the welcome dialog.
+            openWelcomeDialog(state);
+          }
+        };
+      }
+      else
+      {
+        // User cancelled the wizard! Back to the welcome dialog.
+        openWelcomeDialog(state);
+      }
+    };
+  }
+
+  public static function openCreateSongWizard(state:ChartEditorState, closable:Bool):Void
+  {
+    // Step 1. Upload Instrumental
+    var uploadInstDialog:Dialog = openUploadInstDialog(state, closable);
+    uploadInstDialog.onDialogClosed = function(_event) {
+      state.isHaxeUIDialogOpen = false;
+      if (_event.button == DialogButton.APPLY)
+      {
+        // Step 2. Song Metadata
+        var songMetadataDialog:Dialog = openSongMetadataDialog(state);
+        songMetadataDialog.onDialogClosed = function(_event) {
+          state.isHaxeUIDialogOpen = false;
+          if (_event.button == DialogButton.APPLY)
+          {
+            // Step 3. Upload Vocals
+            // NOTE: Uploading vocals is optional, so we don't need to check if the user cancelled the wizard.
+            openUploadVocalsDialog(state, false); // var uploadVocalsDialog:Dialog
+          }
+          else
+          {
+            // User cancelled the wizard! Back to the welcome dialog.
+            openWelcomeDialog(state);
+          }
+        };
+      }
+      else
+      {
+        // User cancelled the wizard! Back to the welcome dialog.
+        openWelcomeDialog(state);
+      }
+    };
+  }
+
   /**
    * Builds and opens a dialog where the user uploads an instrumental for the current song.
    * @param state The current chart editor state.
@@ -214,11 +330,20 @@ class ChartEditorDialogHandler
       }
       else
       {
+        var message:String = if (!ChartEditorState.SUPPORTED_MUSIC_FORMATS.contains(path.ext))
+        {
+          'File format (${path.ext}) not supported for instrumental track (${path.file}.${path.ext})';
+        }
+        else
+        {
+          'Failed to load instrumental track (${path.file}.${path.ext})';
+        }
+
         // Tell the user the load was successful.
         NotificationManager.instance.addNotification(
           {
             title: 'Failure',
-            body: 'Failed to load instrumental track (${path.file}.${path.ext})',
+            body: message,
             type: NotificationType.Error,
             expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
           });
@@ -418,12 +543,6 @@ class ChartEditorDialogHandler
       moveCharGroup(event.data.id);
     };
 
-    if (key == null)
-    {
-      // Find the next available player character.
-      trace(charGroupPlayer.dataSource.data);
-    }
-
     var charGroupOpponent:DropDown = charGroup.findComponent('charGroupOpponent', DropDown);
     charGroupOpponent.onChange = function(event:UIEvent) {
       charData.opponent = event.data.id;
@@ -481,8 +600,8 @@ class ChartEditorDialogHandler
     for (charKey in charIdsForVocals)
     {
       trace('Adding vocal upload for character ${charKey}');
-      var charMetadata:BaseCharacter = CharacterDataParser.fetchCharacter(charKey);
-      var charName:String = charMetadata.characterName;
+      var charMetadata:CharacterData = CharacterDataParser.fetchCharacterData(charKey);
+      var charName:String = charMetadata.name;
 
       var vocalsEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_UPLOAD_VOCALS_ENTRY_LAYOUT);
 
@@ -509,11 +628,20 @@ class ChartEditorDialogHandler
         }
         else
         {
+          var message:String = if (!ChartEditorState.SUPPORTED_MUSIC_FORMATS.contains(path.ext))
+          {
+            'File format (${path.ext}) not supported for vocal track (${path.file}.${path.ext})';
+          }
+          else
+          {
+            'Failed to load vocal track (${path.file}.${path.ext})';
+          }
+
           // Vocals failed to load.
           NotificationManager.instance.addNotification(
             {
               title: 'Failure',
-              body: 'Failed to load vocal track (${path.file}.${path.ext})',
+              body: message,
               type: NotificationType.Error,
               expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
             });
@@ -550,9 +678,287 @@ class ChartEditorDialogHandler
     return dialog;
   }
 
+  /**
+   * Builds and opens a dialog where the user upload the JSON files for a song.
+   * @param state The current chart editor state.
+   * @param closable Whether the dialog can be closed by the user.
+   * @return The dialog that was opened.
+   */
+  @:haxe.warning('-WVarInit')
+  public static function openChartDialog(state:ChartEditorState, ?closable:Bool = true):Dialog
+  {
+    var dialog:Dialog = openDialog(state, CHART_EDITOR_DIALOG_OPEN_CHART_LAYOUT, true, closable);
+
+    var buttonCancel:Button = dialog.findComponent('dialogCancel', Button);
+    buttonCancel.onClick = function(_event) {
+      dialog.hideDialog(DialogButton.CANCEL);
+    }
+
+    var chartContainerA:Component = dialog.findComponent('chartContainerA');
+    var chartContainerB:Component = dialog.findComponent('chartContainerB');
+
+    var songMetadata:Map<String, SongMetadata> = [];
+    var songChartData:Map<String, SongChartData> = [];
+
+    var buttonContinue:Button = dialog.findComponent('dialogContinue', Button);
+    buttonContinue.onClick = function(_event) {
+      state.loadSong(songMetadata, songChartData);
+
+      dialog.hideDialog(DialogButton.APPLY);
+    }
+
+    var onDropFileMetadataVariation:String->Label->String->Void;
+    var onClickMetadataVariation:String->Label->UIEvent->Void;
+    var onDropFileChartDataVariation:String->Label->String->Void;
+    var onClickChartDataVariation:String->Label->UIEvent->Void;
+
+    var constructVariationEntries:Array<String>->Void = function(variations:Array<String>) {
+      // Clear the chart container.
+      while (chartContainerB.getComponentAt(0) != null)
+      {
+        chartContainerB.removeComponent(chartContainerB.getComponentAt(0));
+      }
+
+      // Build an entry for -chart.json.
+      var songDefaultChartDataEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_OPEN_CHART_ENTRY_LAYOUT);
+      var songDefaultChartDataEntryLabel:Label = songDefaultChartDataEntry.findComponent('chartEntryLabel', Label);
+      songDefaultChartDataEntryLabel.text = 'Drag and drop <song>-chart.json file, or click to browse.';
+
+      songDefaultChartDataEntry.onClick = onClickChartDataVariation.bind(Constants.DEFAULT_VARIATION).bind(songDefaultChartDataEntryLabel);
+      addDropHandler(songDefaultChartDataEntry, onDropFileChartDataVariation.bind(Constants.DEFAULT_VARIATION).bind(songDefaultChartDataEntryLabel));
+      chartContainerB.addComponent(songDefaultChartDataEntry);
+
+      for (variation in variations)
+      {
+        // Build entries for -metadata-<variation>.json.
+        var songVariationMetadataEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_OPEN_CHART_ENTRY_LAYOUT);
+        var songVariationMetadataEntryLabel:Label = songVariationMetadataEntry.findComponent('chartEntryLabel', Label);
+        songVariationMetadataEntryLabel.text = 'Drag and drop <song>-metadata-${variation}.json file, or click to browse.';
+
+        songVariationMetadataEntry.onClick = onClickMetadataVariation.bind(variation).bind(songVariationMetadataEntryLabel);
+        addDropHandler(songVariationMetadataEntry, onDropFileMetadataVariation.bind(variation).bind(songVariationMetadataEntryLabel));
+        chartContainerB.addComponent(songVariationMetadataEntry);
+
+        // Build entries for -chart-<variation>.json.
+        var songVariationChartDataEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_OPEN_CHART_ENTRY_LAYOUT);
+        var songVariationChartDataEntryLabel:Label = songVariationChartDataEntry.findComponent('chartEntryLabel', Label);
+        songVariationChartDataEntryLabel.text = 'Drag and drop <song>-chart-${variation}.json file, or click to browse.';
+
+        songVariationChartDataEntry.onClick = onClickChartDataVariation.bind(variation).bind(songVariationChartDataEntryLabel);
+        addDropHandler(songVariationChartDataEntry, onDropFileChartDataVariation.bind(variation).bind(songVariationChartDataEntryLabel));
+        chartContainerB.addComponent(songVariationChartDataEntry);
+      }
+    }
+
+    onDropFileMetadataVariation = function(variation:String, label:Label, pathStr:String) {
+      var path:Path = new Path(pathStr);
+      trace('Dropped JSON file (${path})');
+
+      var songMetadataJson:Dynamic = FileUtil.readJSONFromPath(path.toString());
+      var songMetadataVariation:SongMetadata = SongMigrator.migrateSongMetadata(songMetadataJson, 'import');
+      songMetadataVariation = SongValidator.validateSongMetadata(songMetadataVariation, 'import');
+
+      songMetadata.set(variation, songMetadataVariation);
+
+      // Tell the user the load was successful.
+      NotificationManager.instance.addNotification(
+        {
+          title: 'Success',
+          body: 'Loaded metadata file (${path.file}.${path.ext})',
+          type: NotificationType.Success,
+          expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
+        });
+
+      label.text = 'Metadata file (drag and drop, or click to browse)\nSelected file: ${path.file}.${path.ext}';
+
+      if (variation == Constants.DEFAULT_VARIATION) constructVariationEntries(songMetadataVariation.playData.songVariations);
+    };
+
+    onClickMetadataVariation = function(variation:String, label:Label, _event:UIEvent) {
+      Dialogs.openBinaryFile('Open Chart ($variation) Metadata', [
+        {label: 'JSON File (.json)', extension: 'json'}], function(selectedFile) {
+          if (selectedFile != null)
+          {
+            trace('Selected file: ' + selectedFile.name);
+
+            var songMetadataJson:Dynamic = SerializerUtil.fromJSONBytes(selectedFile.bytes);
+            var songMetadataVariation:SongMetadata = SongMigrator.migrateSongMetadata(songMetadataJson, 'import');
+            songMetadataVariation = SongValidator.validateSongMetadata(songMetadataVariation, 'import');
+            songMetadataVariation.variation = variation;
+
+            songMetadata.set(variation, songMetadataVariation);
+
+            // Tell the user the load was successful.
+            NotificationManager.instance.addNotification(
+              {
+                title: 'Success',
+                body: 'Loaded metadata file (${selectedFile.name})',
+                type: NotificationType.Success,
+                expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
+              });
+
+            label.text = 'Metadata file (drag and drop, or click to browse)\nSelected file: ${selectedFile.name}';
+
+            if (variation == Constants.DEFAULT_VARIATION) constructVariationEntries(songMetadataVariation.playData.songVariations);
+          }
+      });
+    }
+
+    onDropFileChartDataVariation = function(variation:String, label:Label, pathStr:String) {
+      var path:Path = new Path(pathStr);
+      trace('Dropped JSON file (${path})');
+
+      var songChartDataJson:Dynamic = FileUtil.readJSONFromPath(path.toString());
+      var songChartDataVariation:SongChartData = SongMigrator.migrateSongChartData(songChartDataJson, 'import');
+      songChartDataVariation = SongValidator.validateSongChartData(songChartDataVariation, 'import');
+
+      songChartData.set(variation, songChartDataVariation);
+
+      // Tell the user the load was successful.
+      NotificationManager.instance.addNotification(
+        {
+          title: 'Success',
+          body: 'Loaded chart data file (${path.file}.${path.ext})',
+          type: NotificationType.Success,
+          expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
+        });
+
+      label.text = 'Chart data file (drag and drop, or click to browse)\nSelected file: ${path.file}.${path.ext}';
+    };
+
+    onClickChartDataVariation = function(variation:String, label:Label, _event:UIEvent) {
+      Dialogs.openBinaryFile('Open Chart ($variation) Metadata', [
+        {label: 'JSON File (.json)', extension: 'json'}], function(selectedFile) {
+          if (selectedFile != null)
+          {
+            trace('Selected file: ' + selectedFile.name);
+
+            var songChartDataJson:Dynamic = SerializerUtil.fromJSONBytes(selectedFile.bytes);
+            var songChartDataVariation:SongChartData = SongMigrator.migrateSongChartData(songChartDataJson, 'import');
+            songChartDataVariation = SongValidator.validateSongChartData(songChartDataVariation, 'import');
+
+            songChartData.set(variation, songChartDataVariation);
+
+            // Tell the user the load was successful.
+            NotificationManager.instance.addNotification(
+              {
+                title: 'Success',
+                body: 'Loaded chart data file (${selectedFile.name})',
+                type: NotificationType.Success,
+                expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
+              });
+
+            label.text = 'Chart data file (drag and drop, or click to browse)\nSelected file: ${selectedFile.name}';
+          }
+      });
+    }
+
+    var metadataEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_OPEN_CHART_ENTRY_LAYOUT);
+    var metadataEntryLabel:Label = metadataEntry.findComponent('chartEntryLabel', Label);
+    metadataEntryLabel.text = 'Drag and drop <song>-metadata.json file, or click to browse.';
+
+    metadataEntry.onClick = onClickMetadataVariation.bind(Constants.DEFAULT_VARIATION).bind(metadataEntryLabel);
+    addDropHandler(metadataEntry, onDropFileMetadataVariation.bind(Constants.DEFAULT_VARIATION).bind(metadataEntryLabel));
+
+    chartContainerA.addComponent(metadataEntry);
+
+    return dialog;
+  }
+
+  /**
+   * Builds and opens a dialog where the user can import a chart from an existing file format.
+   * @param state The current chart editor state.
+   * @param format The format to import from.
+   * @param closable
+   * @return Dialog
+   */
+  public static function openImportChartDialog(state:ChartEditorState, format:String, ?closable:Bool = true):Dialog
+  {
+    var dialog:Dialog = openDialog(state, CHART_EDITOR_DIALOG_IMPORT_CHART_LAYOUT, true, closable);
+
+    var prettyFormat:String = switch (format)
+    {
+      case 'legacy': 'FNF Legacy';
+      default: 'Unknown';
+    }
+
+    var fileFilter = switch (format)
+    {
+      case 'legacy': {label: 'JSON Data File (.json)', extension: 'json'};
+      default: null;
+    }
+
+    dialog.title = 'Import Chart - ${prettyFormat}';
+
+    var buttonCancel:Button = dialog.findComponent('dialogCancel', Button);
+
+    buttonCancel.onClick = function(_event) {
+      dialog.hideDialog(DialogButton.CANCEL);
+    }
+
+    var importBox:Box = dialog.findComponent('importBox', Box);
+
+    importBox.onMouseOver = function(_event) {
+      importBox.swapClass('upload-bg', 'upload-bg-hover');
+      Cursor.cursorMode = Pointer;
+    }
+
+    importBox.onMouseOut = function(_event) {
+      importBox.swapClass('upload-bg-hover', 'upload-bg');
+      Cursor.cursorMode = Default;
+    }
+
+    var onDropFile:String->Void;
+
+    importBox.onClick = function(_event) {
+      Dialogs.openBinaryFile('Import Chart - ${prettyFormat}', [fileFilter], function(selectedFile:SelectedFileInfo) {
+        if (selectedFile != null)
+        {
+          trace('Selected file: ' + selectedFile.fullPath);
+          var selectedFileJson:Dynamic = SerializerUtil.fromJSONBytes(selectedFile.bytes);
+          var songMetadata:SongMetadata = SongMigrator.migrateSongMetadataFromLegacy(selectedFileJson);
+          var songChartData:SongChartData = SongMigrator.migrateSongChartDataFromLegacy(selectedFileJson);
+
+          state.loadSong([Constants.DEFAULT_VARIATION => songMetadata], [Constants.DEFAULT_VARIATION => songChartData]);
+
+          dialog.hideDialog(DialogButton.APPLY);
+          NotificationManager.instance.addNotification(
+            {
+              title: 'Success',
+              body: 'Loaded chart file (${selectedFile.name})',
+              type: NotificationType.Success,
+              expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
+            });
+        }
+      });
+    }
+
+    onDropFile = function(pathStr:String) {
+      var path:Path = new Path(pathStr);
+      var selectedFileJson:Dynamic = FileUtil.readJSONFromPath(path.toString());
+      var songMetadata:SongMetadata = SongMigrator.migrateSongMetadataFromLegacy(selectedFileJson);
+      var songChartData:SongChartData = SongMigrator.migrateSongChartDataFromLegacy(selectedFileJson);
+
+      state.loadSong([Constants.DEFAULT_VARIATION => songMetadata], [Constants.DEFAULT_VARIATION => songChartData]);
+
+      dialog.hideDialog(DialogButton.APPLY);
+      NotificationManager.instance.addNotification(
+        {
+          title: 'Success',
+          body: 'Loaded chart file (${path.file}.${path.ext})',
+          type: NotificationType.Success,
+          expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
+        });
+    };
+
+    addDropHandler(importBox, onDropFile);
+
+    return dialog;
+  }
+
   /**
    * Builds and opens a dialog displaying the user guide, providing guidance and help on how to use the chart editor.
-   * 
+   *
    * @param state The current chart editor state.
    * @return The dialog that was opened.
    */
@@ -569,6 +975,8 @@ class ChartEditorDialogHandler
   static function openDialog(state:ChartEditorState, key:String, modal:Bool = true, closable:Bool = true):Dialog
   {
     var dialog:Dialog = cast state.buildComponent(key);
+    if (dialog == null) return null;
+
     dialog.destroyOnClose = true;
     dialog.closable = closable;
     dialog.showDialog(modal);
diff --git a/source/funkin/ui/debug/charting/ChartEditorEventSprite.hx b/source/funkin/ui/debug/charting/ChartEditorEventSprite.hx
index 323d11abd..2016e4ccc 100644
--- a/source/funkin/ui/debug/charting/ChartEditorEventSprite.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorEventSprite.hx
@@ -27,7 +27,7 @@ class ChartEditorEventSprite extends FlxSprite
   /**
    * The image used for all song events. Cached for performance.
    */
-  var eventGraphic:BitmapData;
+  static var eventSpriteBasic:BitmapData;
 
   public function new(parent:ChartEditorState)
   {
@@ -40,12 +40,12 @@ class ChartEditorEventSprite extends FlxSprite
 
   function buildGraphic():Void
   {
-    if (eventGraphic == null)
+    if (eventSpriteBasic == null)
     {
-      eventGraphic = Assets.getBitmapData(Paths.image('ui/chart-editor/event'));
+      eventSpriteBasic = Assets.getBitmapData(Paths.image('ui/chart-editor/event'));
     }
 
-    loadGraphic(eventGraphic);
+    loadGraphic(eventSpriteBasic);
     setGraphicSize(ChartEditorState.GRID_SIZE);
     this.updateHitbox();
   }
diff --git a/source/funkin/ui/debug/charting/ChartEditorThemeHandler.hx b/source/funkin/ui/debug/charting/ChartEditorThemeHandler.hx
index 7bdf366bf..40c797169 100644
--- a/source/funkin/ui/debug/charting/ChartEditorThemeHandler.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorThemeHandler.hx
@@ -26,7 +26,7 @@ class ChartEditorThemeHandler
   // An enum of typedefs or something?
   // ================================
   static final BACKGROUND_COLOR_LIGHT:FlxColor = 0xFF673AB7;
-  static final BACKGROUND_COLOR_DARK:FlxColor = 0xFF673AB7;
+  static final BACKGROUND_COLOR_DARK:FlxColor = 0xFF361E60;
 
   // Color 1 of the grid pattern. Alternates with Color 2.
   static final GRID_COLOR_1_LIGHT:FlxColor = 0xFFE7E6E6;
@@ -43,13 +43,11 @@ class ChartEditorThemeHandler
   // Vertical divider between characters.
   static final GRID_STRUMLINE_DIVIDER_COLOR_LIGHT:FlxColor = 0xFF111111;
   static final GRID_STRUMLINE_DIVIDER_COLOR_DARK:FlxColor = 0xFFC4C4C4;
-  // static final GRID_STRUMLINE_DIVIDER_WIDTH:Float = 2;
   static final GRID_STRUMLINE_DIVIDER_WIDTH:Float = ChartEditorState.GRID_SELECTION_BORDER_WIDTH;
 
   // Horizontal divider between measures.
   static final GRID_MEASURE_DIVIDER_COLOR_LIGHT:FlxColor = 0xFF111111;
   static final GRID_MEASURE_DIVIDER_COLOR_DARK:FlxColor = 0xFFC4C4C4;
-  // static final GRID_MEASURE_DIVIDER_WIDTH:Float = 2;
   static final GRID_MEASURE_DIVIDER_WIDTH:Float = ChartEditorState.GRID_SELECTION_BORDER_WIDTH;
 
   // Border on the square highlighting selected notes.
@@ -66,6 +64,12 @@ class ChartEditorThemeHandler
   static final PLAYHEAD_BLOCK_BORDER_COLOR:FlxColor = 0xFF9D0011;
   static final PLAYHEAD_BLOCK_FILL_COLOR:FlxColor = 0xFFBD0231;
 
+  static final TOTAL_COLUMN_COUNT:Int = ChartEditorState.STRUMLINE_SIZE * 2 + 1;
+
+  /**
+   * When the theme is changed, this function updates all of the UI elements to match the new theme.
+   * @param state The ChartEditorState to update.
+   */
   public static function updateTheme(state:ChartEditorState):Void
   {
     updateBackground(state);
@@ -73,6 +77,10 @@ class ChartEditorThemeHandler
     updateSelectionSquare(state);
   }
 
+  /**
+   * Updates the tint of the background sprite to match the current theme.
+   * @param state The ChartEditorState to update.
+   */
   static function updateBackground(state:ChartEditorState):Void
   {
     state.menuBG.color = switch (state.currentTheme)
@@ -85,7 +93,7 @@ class ChartEditorThemeHandler
 
   /**
    * Builds the checkerboard background image of the chart editor, and adds dividing lines to it.
-   * @param dark Whether to draw the grid in a dark color instead of a light one.
+   * @param state The ChartEditorState to update.
    */
   static function updateGridBitmap(state:ChartEditorState):Void
   {
@@ -107,8 +115,8 @@ class ChartEditorThemeHandler
 
     // 2 * (Strumline Size) + 1 grid squares wide, by (4 * quarter notes per measure) grid squares tall.
     // This gets reused to fill the screen.
-    var gridWidth:Int = Std.int(ChartEditorState.GRID_SIZE * (ChartEditorState.STRUMLINE_SIZE * 2 + 1));
-    var gridHeight:Int = Std.int(ChartEditorState.GRID_SIZE * (Conductor.stepsPerMeasure));
+    var gridWidth:Int = Std.int(ChartEditorState.GRID_SIZE * TOTAL_COLUMN_COUNT);
+    var gridHeight:Int = Std.int(ChartEditorState.GRID_SIZE * Conductor.stepsPerMeasure);
     state.gridBitmap = FlxGridOverlay.createGrid(ChartEditorState.GRID_SIZE, ChartEditorState.GRID_SIZE, gridWidth, gridHeight, true, gridColor1, gridColor2);
 
     // Selection borders
@@ -143,7 +151,7 @@ class ChartEditorThemeHandler
       selectionBorderColor);
 
     // Selection borders across the middle.
-    for (i in 1...(ChartEditorState.STRUMLINE_SIZE * 2 + 1))
+    for (i in 1...TOTAL_COLUMN_COUNT)
     {
       state.gridBitmap.fillRect(new Rectangle((ChartEditorState.GRID_SIZE * i) - (ChartEditorState.GRID_SELECTION_BORDER_WIDTH / 2), 0,
         ChartEditorState.GRID_SELECTION_BORDER_WIDTH, state.gridBitmap.height),
@@ -167,7 +175,7 @@ class ChartEditorThemeHandler
     // Divider at top
     state.gridBitmap.fillRect(new Rectangle(0, 0, state.gridBitmap.width, GRID_MEASURE_DIVIDER_WIDTH / 2), gridMeasureDividerColor);
     // Divider at bottom
-    var dividerLineBY = state.gridBitmap.height - (GRID_MEASURE_DIVIDER_WIDTH / 2);
+    var dividerLineBY:Float = state.gridBitmap.height - (GRID_MEASURE_DIVIDER_WIDTH / 2);
     state.gridBitmap.fillRect(new Rectangle(0, dividerLineBY, state.gridBitmap.width, GRID_MEASURE_DIVIDER_WIDTH / 2), gridMeasureDividerColor);
 
     // Draw dividers between the strumlines.
@@ -180,10 +188,10 @@ class ChartEditorThemeHandler
     };
 
     // Divider at 1 * (Strumline Size)
-    var dividerLineAX = ChartEditorState.GRID_SIZE * (ChartEditorState.STRUMLINE_SIZE) - (GRID_STRUMLINE_DIVIDER_WIDTH / 2);
+    var dividerLineAX:Float = ChartEditorState.GRID_SIZE * (ChartEditorState.STRUMLINE_SIZE) - (GRID_STRUMLINE_DIVIDER_WIDTH / 2);
     state.gridBitmap.fillRect(new Rectangle(dividerLineAX, 0, GRID_STRUMLINE_DIVIDER_WIDTH, state.gridBitmap.height), gridStrumlineDividerColor);
     // Divider at 2 * (Strumline Size)
-    var dividerLineBX = ChartEditorState.GRID_SIZE * (ChartEditorState.STRUMLINE_SIZE * 2) - (GRID_STRUMLINE_DIVIDER_WIDTH / 2);
+    var dividerLineBX:Float = ChartEditorState.GRID_SIZE * (ChartEditorState.STRUMLINE_SIZE * 2) - (GRID_STRUMLINE_DIVIDER_WIDTH / 2);
     state.gridBitmap.fillRect(new Rectangle(dividerLineBX, 0, GRID_STRUMLINE_DIVIDER_WIDTH, state.gridBitmap.height), gridStrumlineDividerColor);
 
     if (state.gridTiledSprite != null)
diff --git a/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx b/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx
index 5a903481e..d849fa894 100644
--- a/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx
@@ -1,6 +1,5 @@
 package funkin.ui.debug.charting;
 
-import haxe.ui.data.ArrayDataSource;
 import funkin.play.character.BaseCharacter.CharacterType;
 import funkin.play.event.SongEvent;
 import funkin.play.event.SongEventData;
@@ -12,15 +11,17 @@ import haxe.ui.components.CheckBox;
 import haxe.ui.components.DropDown;
 import haxe.ui.components.Label;
 import haxe.ui.components.NumberStepper;
-import haxe.ui.components.NumberStepper;
 import haxe.ui.components.Slider;
 import haxe.ui.components.TextField;
-import haxe.ui.containers.dialogs.Dialog;
 import haxe.ui.containers.Box;
-import haxe.ui.containers.Frame;
 import haxe.ui.containers.Grid;
 import haxe.ui.containers.Group;
+import haxe.ui.containers.VBox;
+import haxe.ui.containers.dialogs.CollapsibleDialog;
+import haxe.ui.containers.dialogs.Dialog.DialogButton;
+import haxe.ui.containers.dialogs.Dialog.DialogEvent;
 import haxe.ui.core.Component;
+import haxe.ui.data.ArrayDataSource;
 import haxe.ui.events.UIEvent;
 
 /**
@@ -32,18 +33,26 @@ enum ChartEditorToolMode
   Place;
 }
 
+/**
+ * Static functions which handle building themed UI elements for a provided ChartEditorState.
+ */
 class ChartEditorToolboxHandler
 {
   public static function setToolboxState(state:ChartEditorState, id:String, shown:Bool):Void
   {
-    if (shown) showToolbox(state, id);
+    if (shown)
+    {
+      showToolbox(state, id);
+    }
     else
+    {
       hideToolbox(state, id);
+    }
   }
 
-  public static function showToolbox(state:ChartEditorState, id:String)
+  public static function showToolbox(state:ChartEditorState, id:String):Void
   {
-    var toolbox:Dialog = state.activeToolboxes.get(id);
+    var toolbox:CollapsibleDialog = state.activeToolboxes.get(id);
 
     if (toolbox == null) toolbox = initToolbox(state, id);
 
@@ -59,7 +68,7 @@ class ChartEditorToolboxHandler
 
   public static function hideToolbox(state:ChartEditorState, id:String):Void
   {
-    var toolbox:Dialog = state.activeToolboxes.get(id);
+    var toolbox:CollapsibleDialog = state.activeToolboxes.get(id);
 
     if (toolbox == null) toolbox = initToolbox(state, id);
 
@@ -73,13 +82,27 @@ class ChartEditorToolboxHandler
     }
   }
 
-  public static function minimizeToolbox(state:ChartEditorState, id:String):Void {}
-
-  public static function maximizeToolbox(state:ChartEditorState, id:String):Void {}
-
-  public static function initToolbox(state:ChartEditorState, id:String):Dialog
+  public static function minimizeToolbox(state:ChartEditorState, id:String):Void
   {
-    var toolbox:Dialog = null;
+    var toolbox:CollapsibleDialog = state.activeToolboxes.get(id);
+
+    if (toolbox == null) return;
+
+    toolbox.minimized = true;
+  }
+
+  public static function maximizeToolbox(state:ChartEditorState, id:String):Void
+  {
+    var toolbox:CollapsibleDialog = state.activeToolboxes.get(id);
+
+    if (toolbox == null) return;
+
+    toolbox.minimized = false;
+  }
+
+  public static function initToolbox(state:ChartEditorState, id:String):CollapsibleDialog
+  {
+    var toolbox:CollapsibleDialog = null;
     switch (id)
     {
       case ChartEditorState.CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT:
@@ -95,9 +118,9 @@ class ChartEditorToolboxHandler
       case ChartEditorState.CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT:
         toolbox = buildToolboxCharactersLayout(state);
       case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT:
-        toolbox = buildToolboxPlayerPreviewLayout(state);
+        toolbox = null; // buildToolboxPlayerPreviewLayout(state);
       case ChartEditorState.CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT:
-        toolbox = buildToolboxOpponentPreviewLayout(state);
+        toolbox = null; // buildToolboxOpponentPreviewLayout(state);
       default:
         // This happens if you try to load an unknown layout.
         trace('ChartEditorToolboxHandler.initToolbox() - Unknown toolbox ID: $id');
@@ -114,9 +137,15 @@ class ChartEditorToolboxHandler
     return toolbox;
   }
 
-  public static function getToolbox(state:ChartEditorState, id:String):Dialog
+  /**
+   * Retrieve a toolbox by its layout's asset ID.
+   * @param state The ChartEditorState instance.
+   * @param id The asset ID of the toolbox layout.
+   * @return The toolbox.
+   */
+  public static function getToolbox(state:ChartEditorState, id:String):CollapsibleDialog
   {
-    var toolbox:Dialog = state.activeToolboxes.get(id);
+    var toolbox:CollapsibleDialog = state.activeToolboxes.get(id);
 
     // Initialize the toolbox without showing it.
     if (toolbox == null) toolbox = initToolbox(state, id);
@@ -124,9 +153,9 @@ class ChartEditorToolboxHandler
     return toolbox;
   }
 
-  static function buildToolboxToolsLayout(state:ChartEditorState):Dialog
+  static function buildToolboxToolsLayout(state:ChartEditorState):CollapsibleDialog
   {
-    var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT);
+    var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT);
 
     if (toolbox == null) return null;
 
@@ -134,15 +163,15 @@ class ChartEditorToolboxHandler
     toolbox.x = 50;
     toolbox.y = 50;
 
-    toolbox.onDialogClosed = (event:DialogEvent) -> {
+    toolbox.onDialogClosed = function(event:DialogEvent) {
       state.setUICheckboxSelected('menubarItemToggleToolboxTools', false);
     }
 
-    var toolsGroup:Group = toolbox.findComponent("toolboxToolsGroup", Group);
+    var toolsGroup:Group = toolbox.findComponent('toolboxToolsGroup', Group);
 
     if (toolsGroup == null) return null;
 
-    toolsGroup.onChange = (event:UIEvent) -> {
+    toolsGroup.onChange = function(event:UIEvent) {
       switch (event.target.id)
       {
         case 'toolboxToolsGroupSelect':
@@ -157,9 +186,9 @@ class ChartEditorToolboxHandler
     return toolbox;
   }
 
-  static function buildToolboxNoteDataLayout(state:ChartEditorState):Dialog
+  static function buildToolboxNoteDataLayout(state:ChartEditorState):CollapsibleDialog
   {
-    var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT);
+    var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT);
 
     if (toolbox == null) return null;
 
@@ -167,16 +196,16 @@ class ChartEditorToolboxHandler
     toolbox.x = 75;
     toolbox.y = 100;
 
-    toolbox.onDialogClosed = (event:DialogEvent) -> {
+    toolbox.onDialogClosed = function(event:DialogEvent) {
       state.setUICheckboxSelected('menubarItemToggleToolboxNotes', false);
     }
 
-    var toolboxNotesNoteKind:DropDown = toolbox.findComponent("toolboxNotesNoteKind", DropDown);
-    var toolboxNotesCustomKindLabel:Label = toolbox.findComponent("toolboxNotesCustomKindLabel", Label);
-    var toolboxNotesCustomKind:TextField = toolbox.findComponent("toolboxNotesCustomKind", TextField);
+    var toolboxNotesNoteKind:DropDown = toolbox.findComponent('toolboxNotesNoteKind', DropDown);
+    var toolboxNotesCustomKindLabel:Label = toolbox.findComponent('toolboxNotesCustomKindLabel', Label);
+    var toolboxNotesCustomKind:TextField = toolbox.findComponent('toolboxNotesCustomKind', TextField);
 
-    toolboxNotesNoteKind.onChange = (event:UIEvent) -> {
-      var isCustom = (event.data.id == '~CUSTOM~');
+    toolboxNotesNoteKind.onChange = function(event:UIEvent) {
+      var isCustom:Bool = (event.data.id == '~CUSTOM~');
 
       if (isCustom)
       {
@@ -194,16 +223,16 @@ class ChartEditorToolboxHandler
       }
     }
 
-    toolboxNotesCustomKind.onChange = (event:UIEvent) -> {
+    toolboxNotesCustomKind.onChange = function(event:UIEvent) {
       state.selectedNoteKind = toolboxNotesCustomKind.text;
     }
 
     return toolbox;
   }
 
-  static function buildToolboxEventDataLayout(state:ChartEditorState):Dialog
+  static function buildToolboxEventDataLayout(state:ChartEditorState):CollapsibleDialog
   {
-    var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT);
+    var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT);
 
     if (toolbox == null) return null;
 
@@ -211,12 +240,12 @@ class ChartEditorToolboxHandler
     toolbox.x = 100;
     toolbox.y = 150;
 
-    toolbox.onDialogClosed = (event:DialogEvent) -> {
+    toolbox.onDialogClosed = function(event:DialogEvent) {
       state.setUICheckboxSelected('menubarItemToggleToolboxEvents', false);
     }
 
-    var toolboxEventsEventKind:DropDown = toolbox.findComponent("toolboxEventsEventKind", DropDown);
-    var toolboxEventsDataGrid:Grid = toolbox.findComponent("toolboxEventsDataGrid", Grid);
+    var toolboxEventsEventKind:DropDown = toolbox.findComponent('toolboxEventsEventKind', DropDown);
+    var toolboxEventsDataGrid:Grid = toolbox.findComponent('toolboxEventsDataGrid', Grid);
 
     toolboxEventsEventKind.dataSource = new ArrayDataSource();
 
@@ -227,7 +256,7 @@ class ChartEditorToolboxHandler
       toolboxEventsEventKind.dataSource.add({text: event.getTitle(), value: event.id});
     }
 
-    toolboxEventsEventKind.onChange = (event:UIEvent) -> {
+    toolboxEventsEventKind.onChange = function(event:UIEvent) {
       var eventType:String = event.data.value;
 
       trace('ChartEditorToolboxHandler.buildToolboxEventDataLayout() - Event type changed: $eventType');
@@ -281,9 +310,9 @@ class ChartEditorToolboxHandler
           numberStepper.value = field.defaultValue;
           input = numberStepper;
         case BOOL:
-          var checkBox = new CheckBox();
+          var checkBox:CheckBox = new CheckBox();
           checkBox.id = field.name;
-          checkBox.selected = field.defaultValue == true;
+          checkBox.selected = field.defaultValue;
           input = checkBox;
         case ENUM:
           var dropDown:DropDown = new DropDown();
@@ -293,7 +322,7 @@ class ChartEditorToolboxHandler
           // Add entries to the dropdown.
           for (optionName in field.keys.keys())
           {
-            var optionValue = field.keys.get(optionName);
+            var optionValue:String = field.keys.get(optionName);
             trace('$optionName : $optionValue');
             dropDown.dataSource.add({value: optionValue, text: optionName});
           }
@@ -314,7 +343,7 @@ class ChartEditorToolboxHandler
 
       target.addComponent(input);
 
-      input.onChange = (event:UIEvent) -> {
+      input.onChange = function(event:UIEvent) {
         trace('ChartEditorToolboxHandler.buildEventDataFormFromSchema() - ${event.target.id} = ${event.target.value}');
 
         if (event.target.value == null) state.selectedEventData.remove(event.target.id);
@@ -324,9 +353,9 @@ class ChartEditorToolboxHandler
     }
   }
 
-  static function buildToolboxDifficultyLayout(state:ChartEditorState):Dialog
+  static function buildToolboxDifficultyLayout(state:ChartEditorState):CollapsibleDialog
   {
-    var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT);
+    var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT);
 
     if (toolbox == null) return null;
 
@@ -334,36 +363,36 @@ class ChartEditorToolboxHandler
     toolbox.x = 125;
     toolbox.y = 200;
 
-    toolbox.onDialogClosed = (event:DialogEvent) -> {
+    toolbox.onDialogClosed = function(event:UIEvent) {
       state.setUICheckboxSelected('menubarItemToggleToolboxDifficulty', false);
     }
 
-    var difficultyToolboxSaveMetadata:Button = toolbox.findComponent("difficultyToolboxSaveMetadata", Button);
-    var difficultyToolboxSaveChart:Button = toolbox.findComponent("difficultyToolboxSaveChart", Button);
-    var difficultyToolboxSaveAll:Button = toolbox.findComponent("difficultyToolboxSaveAll", Button);
-    var difficultyToolboxLoadMetadata:Button = toolbox.findComponent("difficultyToolboxLoadMetadata", Button);
-    var difficultyToolboxLoadChart:Button = toolbox.findComponent("difficultyToolboxLoadChart", Button);
+    var difficultyToolboxSaveMetadata:Button = toolbox.findComponent('difficultyToolboxSaveMetadata', Button);
+    var difficultyToolboxSaveChart:Button = toolbox.findComponent('difficultyToolboxSaveChart', Button);
+    var difficultyToolboxSaveAll:Button = toolbox.findComponent('difficultyToolboxSaveAll', Button);
+    var difficultyToolboxLoadMetadata:Button = toolbox.findComponent('difficultyToolboxLoadMetadata', Button);
+    var difficultyToolboxLoadChart:Button = toolbox.findComponent('difficultyToolboxLoadChart', Button);
 
-    difficultyToolboxSaveMetadata.onClick = (event:UIEvent) -> {
+    difficultyToolboxSaveMetadata.onClick = function(event:UIEvent) {
       SongSerializer.exportSongMetadata(state.currentSongMetadata);
     };
 
-    difficultyToolboxSaveChart.onClick = (event:UIEvent) -> {
+    difficultyToolboxSaveChart.onClick = function(event:UIEvent) {
       SongSerializer.exportSongChartData(state.currentSongChartData);
     };
 
-    difficultyToolboxSaveAll.onClick = (event:UIEvent) -> {
+    difficultyToolboxSaveAll.onClick = function(event:UIEvent) {
       state.exportAllSongData();
     };
 
-    difficultyToolboxLoadMetadata.onClick = (event:UIEvent) -> {
+    difficultyToolboxLoadMetadata.onClick = function(event:UIEvent) {
       // Replace metadata for current variation.
       SongSerializer.importSongMetadataAsync(function(songMetadata) {
         state.currentSongMetadata = songMetadata;
       });
     };
 
-    difficultyToolboxLoadChart.onClick = (event:UIEvent) -> {
+    difficultyToolboxLoadChart.onClick = function(event:UIEvent) {
       // Replace chart data for current variation.
       SongSerializer.importSongChartDataAsync(function(songChartData) {
         state.currentSongChartData = songChartData;
@@ -376,9 +405,9 @@ class ChartEditorToolboxHandler
     return toolbox;
   }
 
-  static function buildToolboxMetadataLayout(state:ChartEditorState):Dialog
+  static function buildToolboxMetadataLayout(state:ChartEditorState):CollapsibleDialog
   {
-    var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
+    var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
 
     if (toolbox == null) return null;
 
@@ -386,13 +415,13 @@ class ChartEditorToolboxHandler
     toolbox.x = 150;
     toolbox.y = 250;
 
-    toolbox.onDialogClosed = (event:DialogEvent) -> {
+    toolbox.onDialogClosed = function(event:UIEvent) {
       state.setUICheckboxSelected('menubarItemToggleToolboxMetadata', false);
     }
 
     var inputSongName:TextField = toolbox.findComponent('inputSongName', TextField);
-    inputSongName.onChange = (event:UIEvent) -> {
-      var valid = event.target.text != null && event.target.text != "";
+    inputSongName.onChange = function(event:UIEvent) {
+      var valid:Bool = event.target.text != null && event.target.text != '';
 
       if (valid)
       {
@@ -404,10 +433,11 @@ class ChartEditorToolboxHandler
         state.currentSongMetadata.songName = null;
       }
     };
+    inputSongName.value = state.currentSongMetadata.songName;
 
     var inputSongArtist:TextField = toolbox.findComponent('inputSongArtist', TextField);
-    inputSongArtist.onChange = (event:UIEvent) -> {
-      var valid = event.target.text != null && event.target.text != "";
+    inputSongArtist.onChange = function(event:UIEvent) {
+      var valid:Bool = event.target.text != null && event.target.text != '';
 
       if (valid)
       {
@@ -419,28 +449,31 @@ class ChartEditorToolboxHandler
         state.currentSongMetadata.artist = null;
       }
     };
+    inputSongArtist.value = state.currentSongMetadata.artist;
 
     var inputStage:DropDown = toolbox.findComponent('inputStage', DropDown);
-    inputStage.onChange = (event:UIEvent) -> {
-      var valid = event.data != null && event.data.id != null;
+    inputStage.onChange = function(event:UIEvent) {
+      var valid:Bool = event.data != null && event.data.id != null;
 
       if (valid)
       {
         state.currentSongMetadata.playData.stage = event.data.id;
       }
     };
+    inputStage.value = state.currentSongMetadata.playData.stage;
 
     var inputNoteSkin:DropDown = toolbox.findComponent('inputNoteSkin', DropDown);
-    inputNoteSkin.onChange = (event:UIEvent) -> {
+    inputNoteSkin.onChange = function(event:UIEvent) {
       if (event.data.id == null) return;
       state.currentSongMetadata.playData.noteSkin = event.data.id;
     };
+    inputNoteSkin.value = state.currentSongMetadata.playData.noteSkin;
 
     var inputBPM:NumberStepper = toolbox.findComponent('inputBPM', NumberStepper);
-    inputBPM.onChange = (event:UIEvent) -> {
+    inputBPM.onChange = function(event:UIEvent) {
       if (event.value == null || event.value <= 0) return;
 
-      var timeChanges = state.currentSongMetadata.timeChanges;
+      var timeChanges:Array<SongTimeChange> = state.currentSongMetadata.timeChanges;
       if (timeChanges == null || timeChanges.length == 0)
       {
         timeChanges = [new SongTimeChange(-1, 0, event.value, 4, 4, [4, 4, 4, 4])];
@@ -454,28 +487,30 @@ class ChartEditorToolboxHandler
 
       state.currentSongMetadata.timeChanges = timeChanges;
     };
+    inputBPM.value = state.currentSongMetadata.timeChanges[0].bpm;
 
     var inputScrollSpeed:Slider = toolbox.findComponent('inputScrollSpeed', Slider);
-    inputScrollSpeed.onChange = (event:UIEvent) -> {
-      var valid = event.target.value != null && event.target.value > 0;
+    inputScrollSpeed.onChange = function(event:UIEvent) {
+      var valid:Bool = event.target.value != null && event.target.value > 0;
 
       if (valid)
       {
         inputScrollSpeed.removeClass('invalid-value');
-        state.currentSongChartData.scrollSpeed = event.target.value;
+        state.currentSongChartScrollSpeed = event.target.value;
       }
       else
       {
-        state.currentSongChartData.scrollSpeed = null;
+        state.currentSongChartScrollSpeed = 1.0;
       }
     };
+    inputScrollSpeed.value = state.currentSongChartData.scrollSpeed;
 
     return toolbox;
   }
 
-  static function buildToolboxCharactersLayout(state:ChartEditorState):Dialog
+  static function buildToolboxCharactersLayout(state:ChartEditorState):CollapsibleDialog
   {
-    var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT);
+    var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT);
 
     if (toolbox == null) return null;
 
@@ -483,16 +518,16 @@ class ChartEditorToolboxHandler
     toolbox.x = 175;
     toolbox.y = 300;
 
-    toolbox.onDialogClosed = (event:DialogEvent) -> {
+    toolbox.onDialogClosed = function(event:DialogEvent) {
       state.setUICheckboxSelected('menubarItemToggleToolboxCharacters', false);
     }
 
     return toolbox;
   }
 
-  static function buildToolboxPlayerPreviewLayout(state:ChartEditorState):Dialog
+  static function buildToolboxPlayerPreviewLayout(state:ChartEditorState):CollapsibleDialog
   {
-    var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT);
+    var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT);
 
     if (toolbox == null) return null;
 
@@ -500,23 +535,23 @@ class ChartEditorToolboxHandler
     toolbox.x = 200;
     toolbox.y = 350;
 
-    toolbox.onDialogClosed = (event:DialogEvent) -> {
+    toolbox.onDialogClosed = function(event:DialogEvent) {
       state.setUICheckboxSelected('menubarItemToggleToolboxPlayerPreview', false);
     }
 
     var charPlayer:CharacterPlayer = toolbox.findComponent('charPlayer');
     // TODO: We need to implement character swapping in ChartEditorState.
     charPlayer.loadCharacter('bf');
-    // charPlayer.setScale(0.5);
-    charPlayer.setCharacterType(CharacterType.BF);
+    charPlayer.characterType = CharacterType.BF;
     charPlayer.flip = true;
+    charPlayer.targetScale = 0.5;
 
     return toolbox;
   }
 
-  static function buildToolboxOpponentPreviewLayout(state:ChartEditorState):Dialog
+  static function buildToolboxOpponentPreviewLayout(state:ChartEditorState):CollapsibleDialog
   {
-    var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT);
+    var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT);
 
     if (toolbox == null) return null;
 
@@ -524,16 +559,18 @@ class ChartEditorToolboxHandler
     toolbox.x = 200;
     toolbox.y = 350;
 
-    toolbox.onDialogClosed = (event:DialogEvent) -> {
+    var container:VBox = toolbox.findComponent('charPlayerContainer', VBox);
+
+    toolbox.onDialogClosed = function(event:DialogEvent) {
       state.setUICheckboxSelected('menubarItemToggleToolboxOpponentPreview', false);
     }
 
     var charPlayer:CharacterPlayer = toolbox.findComponent('charPlayer');
     // TODO: We need to implement character swapping in ChartEditorState.
     charPlayer.loadCharacter('dad');
-    // charPlayer.setScale(0.5);
-    charPlayer.setCharacterType(CharacterType.DAD);
+    charPlayer.characterType = CharacterType.DAD;
     charPlayer.flip = false;
+    charPlayer.targetScale = 0.5;
 
     return toolbox;
   }