diff --git a/source/funkin/MainMenuState.hx b/source/funkin/MainMenuState.hx
index 0f594ffd9..7c54357bb 100644
--- a/source/funkin/MainMenuState.hx
+++ b/source/funkin/MainMenuState.hx
@@ -55,7 +55,7 @@ class MainMenuState extends MusicBeatState
     transIn = FlxTransitionableState.defaultTransIn;
     transOut = FlxTransitionableState.defaultTransOut;
 
-    if (!FlxG.sound.music.playing)
+    if (!(FlxG?.sound?.music?.playing ?? false))
     {
       FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'));
     }
diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx
index 398c28753..ec89d8706 100644
--- a/source/funkin/play/song/Song.hx
+++ b/source/funkin/play/song/Song.hx
@@ -297,7 +297,7 @@ class SongDifficulty
   public var songName:String = SongValidator.DEFAULT_SONGNAME;
   public var songArtist:String = SongValidator.DEFAULT_ARTIST;
   public var timeFormat:SongTimeFormat = SongValidator.DEFAULT_TIMEFORMAT;
-  public var divisions:Int = SongValidator.DEFAULT_DIVISIONS;
+  public var divisions:Null<Int> = SongValidator.DEFAULT_DIVISIONS;
   public var looped:Bool = SongValidator.DEFAULT_LOOPED;
   public var generatedBy:String = SongValidator.DEFAULT_GENERATEDBY;
 
diff --git a/source/funkin/play/song/SongData.hx b/source/funkin/play/song/SongData.hx
index 6f2475cf9..938ee0708 100644
--- a/source/funkin/play/song/SongData.hx
+++ b/source/funkin/play/song/SongData.hx
@@ -11,6 +11,7 @@ import haxe.DynamicAccess;
 import haxe.Json;
 import openfl.utils.Assets;
 import thx.semver.Version;
+import funkin.util.SerializerUtil;
 
 /**
  * Contains utilities for loading and parsing stage data.
@@ -138,13 +139,8 @@ class SongDataParser
   {
     var result:Array<SongMetadata> = [];
 
-    var rawJson:String = loadSongMetadataFile(songId);
-    var jsonData:Dynamic = null;
-    try
-    {
-      jsonData = Json.parse(rawJson);
-    }
-    catch (e) {}
+    var jsonStr:String = loadSongMetadataFile(songId);
+    var jsonData:Dynamic = SerializerUtil.fromJSON(jsonStr);
 
     var songMetadata:SongMetadata = SongMigrator.migrateSongMetadata(jsonData, songId);
     songMetadata = SongValidator.validateSongMetadata(songMetadata, songId);
@@ -160,9 +156,10 @@ class SongDataParser
 
     for (variation in variations)
     {
-      var variationRawJson:String = loadSongMetadataFile(songId, variation);
-      var variationSongMetadata:SongMetadata = SongMigrator.migrateSongMetadata(variationRawJson, '${songId}_${variation}');
-      variationSongMetadata = SongValidator.validateSongMetadata(variationSongMetadata, '${songId}_${variation}');
+      var variationJsonStr:String = loadSongMetadataFile(songId, variation);
+      var variationJsonData:Dynamic = SerializerUtil.fromJSON(variationJsonStr);
+      var variationSongMetadata:SongMetadata = SongMigrator.migrateSongMetadata(variationJsonData, '${songId}:${variation}');
+      variationSongMetadata = SongValidator.validateSongMetadata(variationSongMetadata, '${songId}:${variation}');
       if (variationSongMetadata != null)
       {
         variationSongMetadata.variation = variation;
@@ -269,7 +266,7 @@ typedef RawSongMetadata =
   var songName:String;
   var artist:String;
   var timeFormat:SongTimeFormat;
-  var divisions:Int;
+  var divisions:Null<Int>; // Optional field
   var timeChanges:Array<SongTimeChange>;
   var looped:Bool;
   var playData:SongPlayData;
@@ -292,7 +289,7 @@ abstract SongMetadata(RawSongMetadata)
         songName: songName,
         artist: artist,
         timeFormat: 'ms',
-        divisions: 96,
+        divisions: null,
         timeChanges: [new SongTimeChange(-1, 0, 100, 4, 4, [4, 4, 4, 4])],
         looped: false,
         playData:
diff --git a/source/funkin/play/song/SongValidator.hx b/source/funkin/play/song/SongValidator.hx
index 936ad46f7..d91dda1d9 100644
--- a/source/funkin/play/song/SongValidator.hx
+++ b/source/funkin/play/song/SongValidator.hx
@@ -15,7 +15,7 @@ class SongValidator
   public static final DEFAULT_SONGNAME:String = "Unknown";
   public static final DEFAULT_ARTIST:String = "Unknown";
   public static final DEFAULT_TIMEFORMAT:SongTimeFormat = SongTimeFormat.MILLISECONDS;
-  public static final DEFAULT_DIVISIONS:Int = -1;
+  public static final DEFAULT_DIVISIONS:Null<Int> = null;
   public static final DEFAULT_LOOPED:Bool = false;
   public static final DEFAULT_STAGE:String = "mainStage";
   public static final DEFAULT_SCROLLSPEED:Float = 1.0;
diff --git a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx
index 6641a16c0..43e8ac96c 100644
--- a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx
@@ -115,7 +115,12 @@ class ChartEditorDialogHandler
 
       if (songData == null) continue;
 
-      var songName:String = songData.getDifficulty().songName;
+      var songName:Null<String> = songData.getDifficulty('normal') ?.songName;
+      if (songName == null) songName = songData.getDifficulty() ?.songName;
+      if (songName == null)
+      {
+        trace('[WARN] Could not fetch song name for ${targetSongId}');
+      }
 
       var linkTemplateSong:Link = new Link();
       linkTemplateSong.text = songName;
diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index 26b001d7e..c26fb8998 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -1,5 +1,9 @@
 package funkin.ui.debug.charting;
 
+import haxe.ui.components.TextField;
+import haxe.ui.components.DropDown;
+import haxe.ui.components.NumberStepper;
+import haxe.ui.containers.Frame;
 import flixel.addons.display.FlxSliceSprite;
 import flixel.addons.display.FlxTiledSprite;
 import flixel.FlxCamera;
@@ -722,11 +726,38 @@ class ChartEditorState extends HaxeUIState
    */
   var songMetadata:Map<String, SongMetadata>;
 
+  /**
+   * Retrieves the list of variations for the current song.
+   */
   var availableVariations(get, null):Array<String>;
 
   function get_availableVariations():Array<String>
   {
-    return [for (x in songMetadata.keys()) x];
+    var variations:Array<String> = [for (x in songMetadata.keys()) x];
+    variations.sort(SortUtil.defaultThenAlphabetically.bind('default'));
+    return variations;
+  }
+
+  /**
+   * Retrieves the list of difficulties for the current variation of the current song.
+   * ONLY CONTAINS DIFFICULTIES FOR THE CURRENT VARIATION so if on the default variation, erect/nightmare won't be included.
+   */
+  var availableDifficulties(get, null):Array<String>;
+
+  function get_availableDifficulties():Array<String>
+  {
+    return songMetadata.get(selectedVariation).playData.difficulties;
+  }
+
+  /**
+   * Retrieves the list of difficulties for ALL variations of the current song.
+   */
+  var allDifficulties(get, null):Array<String>;
+
+  function get_allDifficulties():Array<String>
+  {
+    var result:Array<Array<String>> = [for (x in availableVariations) songMetadata.get(x).playData.difficulties];
+    return result.flatten();
   }
 
   /**
@@ -976,6 +1007,11 @@ class ChartEditorState extends HaxeUIState
     return playableCharData.opponent = value;
   }
 
+  /**
+   * SIGNALS
+   */
+  // ==============================
+  // public var onDifficultyChange(default, null):FlxTypedSignal<ChartEditorState->Void> = new FlxTypedSignal<ChartEditorState->Void>();
   /**
    * RENDER OBJECTS
    */
@@ -1247,7 +1283,6 @@ class ChartEditorState extends HaxeUIState
     var height:Int = FlxG.height - MENU_BAR_HEIGHT - GRID_TOP_PAD - 200;
     notePreview = new ChartEditorNotePreview(height);
     notePreview.y = MENU_BAR_HEIGHT + GRID_TOP_PAD;
-    // TODO: Re-enable.
     // add(notePreview);
   }
 
@@ -1438,6 +1473,9 @@ class ChartEditorState extends HaxeUIState
     addUIChangeListener('menubarItemDownscroll', event -> isViewDownscroll = event.value);
     setUICheckboxSelected('menubarItemDownscroll', isViewDownscroll);
 
+    addUIClickListener('menubarItemDifficultyUp', _ -> incrementDifficulty(1));
+    addUIClickListener('menubarItemDifficultyDown', _ -> incrementDifficulty(-1));
+
     addUIChangeListener('menubarItemPlaytestStartTime', event -> playtestStartTime = event.value);
     setUICheckboxSelected('menubarItemPlaytestStartTime', playtestStartTime);
 
@@ -1584,6 +1622,7 @@ class ChartEditorState extends HaxeUIState
     handleToolboxes();
     handlePlaybar();
     handlePlayhead();
+    // handleNotePreview();
 
     handleFileKeybinds();
     handleEditKeybinds();
@@ -2755,7 +2794,105 @@ class ChartEditorState extends HaxeUIState
   /**
    * Handle keybinds for View menu items.
    */
-  function handleViewKeybinds():Void {}
+  function handleViewKeybinds():Void
+  {
+    if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.LEFT)
+    {
+      incrementDifficulty(-1);
+    }
+    if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.RIGHT)
+    {
+      incrementDifficulty(1);
+    }
+  }
+
+  function incrementDifficulty(change:Int):Void
+  {
+    var currentDifficultyIndex:Int = availableDifficulties.indexOf(selectedDifficulty);
+    var currentAllDifficultyIndex:Int = allDifficulties.indexOf(selectedDifficulty);
+
+    if (currentDifficultyIndex == -1 || currentAllDifficultyIndex == -1)
+    {
+      trace('ERROR determining difficulty index!');
+    }
+
+    var isFirstDiff:Bool = currentAllDifficultyIndex == 0;
+    var isLastDiff:Bool = (currentAllDifficultyIndex == allDifficulties.length - 1);
+
+    var isFirstDiffInVariation:Bool = currentDifficultyIndex == 0;
+    var isLastDiffInVariation:Bool = (currentDifficultyIndex == availableDifficulties.length - 1);
+
+    trace(allDifficulties);
+
+    if (change < 0 && isFirstDiff)
+    {
+      trace('At lowest difficulty! Do nothing.');
+      return;
+    }
+
+    if (change > 0 && isLastDiff)
+    {
+      trace('At highest difficulty! Do nothing.');
+      return;
+    }
+
+    if (change < 0)
+    {
+      trace('Decrement difficulty.');
+
+      // If we reached this point, we are not at the lowest difficulty.
+      if (isFirstDiffInVariation)
+      {
+        // Go to the previous variation, then last difficulty in that variation.
+        var currentVariationIndex:Int = availableVariations.indexOf(selectedVariation);
+        var prevVariation = availableVariations[currentVariationIndex - 1];
+        selectedVariation = prevVariation;
+
+        var prevDifficulty = availableDifficulties[availableDifficulties.length - 1];
+        selectedDifficulty = prevDifficulty;
+
+        refreshDifficultyTreeSelection();
+        refreshSongMetadataToolbox();
+      }
+      else
+      {
+        // Go to previous difficulty in this variation.
+        var prevDifficulty = availableDifficulties[currentDifficultyIndex - 1];
+        selectedDifficulty = prevDifficulty;
+
+        refreshDifficultyTreeSelection();
+        refreshSongMetadataToolbox();
+      }
+    }
+    else
+    {
+      trace('Increment difficulty.');
+
+      // If we reached this point, we are not at the highest difficulty.
+      if (isLastDiffInVariation)
+      {
+        // Go to next variation, then first difficulty in that variation.
+        var currentVariationIndex:Int = availableVariations.indexOf(selectedVariation);
+        var nextVariation = availableVariations[currentVariationIndex + 1];
+        selectedVariation = nextVariation;
+
+        var nextDifficulty = availableDifficulties[0];
+        selectedDifficulty = nextDifficulty;
+
+        refreshDifficultyTreeSelection();
+        refreshSongMetadataToolbox();
+      }
+      else
+      {
+        // Go to next difficulty in this variation.
+        var nextDifficulty = availableDifficulties[currentDifficultyIndex + 1];
+        selectedDifficulty = nextDifficulty;
+
+        refreshDifficultyTreeSelection();
+        refreshSongMetadataToolbox();
+      }
+    }
+  }
 
   /**
    * Handle keybinds for the Test menu items.
@@ -2801,10 +2938,14 @@ class ChartEditorState extends HaxeUIState
       // Clear the tree view so we can rebuild it.
       treeView.clearNodes();
 
-      var treeSong:TreeViewNode = treeView.addNode({id: 'stv_song', text: 'S: $currentSongName', icon: 'haxeui-core/styles/default/haxeui_tiny.png'});
+      // , icon: 'haxeui-core/styles/default/haxeui_tiny.png'
+      var treeSong:TreeViewNode = treeView.addNode({id: 'stv_song', text: 'S: $currentSongName'});
       treeSong.expanded = true;
 
-      for (curVariation in availableVariations)
+      var variations = Reflect.copy(availableVariations);
+      variations.sort(SortUtil.alphabetically);
+
+      for (curVariation in variations)
       {
         var variationMetadata:SongMetadata = songMetadata.get(curVariation);
 
@@ -2828,10 +2969,26 @@ class ChartEditorState extends HaxeUIState
       }
 
       treeView.onChange = onChangeTreeDifficulty;
-      treeView.selectedNode = getCurrentTreeDifficultyNode();
+      refreshDifficultyTreeSelection(treeView);
     }
   }
 
+  function refreshDifficultyTreeSelection(?treeView:TreeView):Void
+  {
+    if (treeView == null)
+    {
+      // Manage the Select Difficulty tree view.
+      var difficultyToolbox:CollapsibleDialog = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT);
+      if (difficultyToolbox == null) return;
+
+      treeView = difficultyToolbox.findComponent('difficultyToolboxTree');
+      if (treeView == null) return;
+    }
+
+    trace(treeView);
+    treeView.selectedNode = getCurrentTreeDifficultyNode(treeView);
+  }
+
   function handlePlayerPreviewToolbox():Void
   {
     // Manage the Select Difficulty tree view.
@@ -2938,20 +3095,28 @@ class ChartEditorState extends HaxeUIState
     }
   }
 
-  function getCurrentTreeDifficultyNode():TreeViewNode
+  function getCurrentTreeDifficultyNode(?treeView:TreeView = null):TreeViewNode
   {
-    var treeView:TreeView = findComponent('difficultyToolboxTree');
+    if (treeView == null)
+    {
+      var difficultyToolbox:CollapsibleDialog = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT);
+      if (difficultyToolbox == null) return null;
 
-    if (treeView == null) return null;
+      treeView = difficultyToolbox.findComponent('difficultyToolboxTree');
+      if (treeView == null) return null;
+    }
 
     var result:TreeViewNode = treeView.findNodeByPath('stv_song/stv_variation_$selectedVariation/stv_difficulty_${selectedVariation}_$selectedDifficulty',
       'id');
-
     if (result == null) return null;
 
     return result;
   }
 
+  /**
+   * Called when selecting a tree element in the Difficulty toolbox.
+   * @param event The click event.
+   */
   function onChangeTreeDifficulty(event:UIEvent):Void
   {
     // Get the newly selected node.
@@ -2962,7 +3127,7 @@ class ChartEditorState extends HaxeUIState
     {
       trace('No target node!');
       // Reset the user's selection.
-      treeView.selectedNode = getCurrentTreeDifficultyNode();
+      treeView.selectedNode = getCurrentTreeDifficultyNode(treeView);
       return;
     }
 
@@ -2974,19 +3139,56 @@ class ChartEditorState extends HaxeUIState
 
         if (variation != null && difficulty != null)
         {
-          trace('Changing difficulty to $variation:$difficulty');
+          trace('Changing difficulty to "$variation:$difficulty"');
           selectedVariation = variation;
           selectedDifficulty = difficulty;
+          // refreshDifficultyTreeSelection(treeView);
+          refreshSongMetadataToolbox();
         }
       // case 'song':
       // case 'variation':
       default:
         // Reset the user's selection.
         trace('Selected wrong node type, resetting selection.');
-        treeView.selectedNode = getCurrentTreeDifficultyNode();
+        treeView.selectedNode = getCurrentTreeDifficultyNode(treeView);
+        refreshSongMetadataToolbox();
     }
   }
 
+  /**
+   * When the difficulty changes, update the song metadata toolbox to reflect the new data.
+   */
+  function refreshSongMetadataToolbox():Void
+  {
+    var toolbox:CollapsibleDialog = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
+
+    var inputSongName:TextField = toolbox.findComponent('inputSongName', TextField);
+    inputSongName.value = currentSongMetadata.songName;
+
+    var inputSongArtist:TextField = toolbox.findComponent('inputSongArtist', TextField);
+    inputSongArtist.value = currentSongMetadata.artist;
+
+    var inputStage:DropDown = toolbox.findComponent('inputStage', DropDown);
+    inputStage.value = currentSongMetadata.playData.stage;
+
+    var inputNoteSkin:DropDown = toolbox.findComponent('inputNoteSkin', DropDown);
+    inputNoteSkin.value = currentSongMetadata.playData.noteSkin;
+
+    var inputBPM:NumberStepper = toolbox.findComponent('inputBPM', NumberStepper);
+    inputBPM.value = currentSongMetadata.timeChanges[0].bpm;
+
+    var labelScrollSpeed:Label = toolbox.findComponent('labelScrollSpeed', Label);
+    labelScrollSpeed.text = 'Scroll Speed: ${currentSongChartScrollSpeed}x';
+
+    var inputScrollSpeed:Slider = toolbox.findComponent('inputScrollSpeed', Slider);
+    inputScrollSpeed.value = currentSongChartScrollSpeed;
+
+    var frameVariation:Frame = toolbox.findComponent('frameVariation', Frame);
+    frameVariation.text = 'Variation: ${selectedVariation.toTitleCase()}';
+    var frameDifficulty:Frame = toolbox.findComponent('frameDifficulty', Frame);
+    frameDifficulty.text = 'Difficulty: ${selectedDifficulty.toTitleCase()}';
+  }
+
   function addDifficulty(variation:String):Void {}
 
   function addVariation(variationId:String):Void
diff --git a/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx b/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx
index 5cace2ff6..a6e230f6e 100644
--- a/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx
@@ -1,5 +1,7 @@
 package funkin.ui.debug.charting;
 
+import haxe.ui.containers.TreeView;
+import haxe.ui.containers.TreeViewNode;
 import funkin.play.character.BaseCharacter.CharacterType;
 import funkin.play.event.SongEvent;
 import funkin.play.event.SongEventData;
@@ -17,6 +19,7 @@ import haxe.ui.containers.Box;
 import haxe.ui.containers.Grid;
 import haxe.ui.containers.Group;
 import haxe.ui.containers.VBox;
+import haxe.ui.containers.Frame;
 import haxe.ui.containers.dialogs.CollapsibleDialog;
 import haxe.ui.containers.dialogs.Dialog.DialogButton;
 import haxe.ui.containers.dialogs.Dialog.DialogEvent;
@@ -36,6 +39,7 @@ enum ChartEditorToolMode
 /**
  * Static functions which handle building themed UI elements for a provided ChartEditorState.
  */
+@:allow(funkin.ui.debug.charting.ChartEditorState)
 class ChartEditorToolboxHandler
 {
   public static function setToolboxState(state:ChartEditorState, id:String, shown:Bool):Void
@@ -59,6 +63,29 @@ class ChartEditorToolboxHandler
     if (toolbox != null)
     {
       toolbox.showDialog(false);
+
+      switch (id)
+      {
+        case ChartEditorState.CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT:
+          onShowToolboxTools(state, toolbox);
+        case ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT:
+          onShowToolboxNoteData(state, toolbox);
+        case ChartEditorState.CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT:
+          onShowToolboxEventData(state, toolbox);
+        case ChartEditorState.CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT:
+          onShowToolboxDifficulty(state, toolbox);
+        case ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT:
+          onShowToolboxMetadata(state, toolbox);
+        case ChartEditorState.CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT:
+          onShowToolboxCharacters(state, toolbox);
+        case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT:
+          onShowToolboxPlayerPreview(state, toolbox);
+        case ChartEditorState.CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT:
+          onShowToolboxOpponentPreview(state, toolbox);
+        default:
+          // This happens if you try to load an unknown layout.
+          trace('ChartEditorToolboxHandler.showToolbox() - Unknown toolbox ID: $id');
+      }
     }
     else
     {
@@ -75,6 +102,29 @@ class ChartEditorToolboxHandler
     if (toolbox != null)
     {
       toolbox.hideDialog(DialogButton.CANCEL);
+
+      switch (id)
+      {
+        case ChartEditorState.CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT:
+          onHideToolboxTools(state, toolbox);
+        case ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT:
+          onHideToolboxNoteData(state, toolbox);
+        case ChartEditorState.CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT:
+          onHideToolboxEventData(state, toolbox);
+        case ChartEditorState.CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT:
+          onHideToolboxDifficulty(state, toolbox);
+        case ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT:
+          onHideToolboxMetadata(state, toolbox);
+        case ChartEditorState.CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT:
+          onHideToolboxCharacters(state, toolbox);
+        case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT:
+          onHideToolboxPlayerPreview(state, toolbox);
+        case ChartEditorState.CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT:
+          onHideToolboxOpponentPreview(state, toolbox);
+        default:
+          // This happens if you try to load an unknown layout.
+          trace('ChartEditorToolboxHandler.hideToolbox() - Unknown toolbox ID: $id');
+      }
     }
     else
     {
@@ -186,6 +236,10 @@ class ChartEditorToolboxHandler
     return toolbox;
   }
 
+  static function onShowToolboxTools(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
+
+  static function onHideToolboxTools(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
+
   static function buildToolboxNoteDataLayout(state:ChartEditorState):CollapsibleDialog
   {
     var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT);
@@ -230,6 +284,10 @@ class ChartEditorToolboxHandler
     return toolbox;
   }
 
+  static function onShowToolboxNoteData(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
+
+  static function onHideToolboxNoteData(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
+
   static function buildToolboxEventDataLayout(state:ChartEditorState):CollapsibleDialog
   {
     var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT);
@@ -275,6 +333,10 @@ class ChartEditorToolboxHandler
     return toolbox;
   }
 
+  static function onShowToolboxEventData(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
+
+  static function onHideToolboxEventData(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
+
   static function buildEventDataFormFromSchema(state:ChartEditorState, target:Box, schema:SongEventSchema):Void
   {
     trace(schema);
@@ -405,6 +467,18 @@ class ChartEditorToolboxHandler
     return toolbox;
   }
 
+  static function onShowToolboxDifficulty(state:ChartEditorState, toolbox:CollapsibleDialog):Void
+  {
+    // Update the selected difficulty when reopening the toolbox.
+    var treeView:TreeView = toolbox.findComponent('difficultyToolboxTree');
+    if (treeView == null) return;
+
+    treeView.selectedNode = state.getCurrentTreeDifficultyNode(treeView);
+    trace('selected node: ${treeView.selectedNode}');
+  }
+
+  static function onHideToolboxDifficulty(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
+
   static function buildToolboxMetadataLayout(state:ChartEditorState):CollapsibleDialog
   {
     var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
@@ -464,7 +538,7 @@ class ChartEditorToolboxHandler
 
     var inputNoteSkin:DropDown = toolbox.findComponent('inputNoteSkin', DropDown);
     inputNoteSkin.onChange = function(event:UIEvent) {
-      if (event.data.id == null) return;
+      if ((event?.data?.id ?? null) == null) return;
       state.currentSongMetadata.playData.noteSkin = event.data.id;
     };
     inputNoteSkin.value = state.currentSongMetadata.playData.noteSkin;
@@ -489,6 +563,8 @@ class ChartEditorToolboxHandler
     };
     inputBPM.value = state.currentSongMetadata.timeChanges[0].bpm;
 
+    var labelScrollSpeed:Label = toolbox.findComponent('labelScrollSpeed', Label);
+
     var inputScrollSpeed:Slider = toolbox.findComponent('inputScrollSpeed', Slider);
     inputScrollSpeed.onChange = function(event:UIEvent) {
       var valid:Bool = event.target.value != null && event.target.value > 0;
@@ -502,12 +578,27 @@ class ChartEditorToolboxHandler
       {
         state.currentSongChartScrollSpeed = 1.0;
       }
+      labelScrollSpeed.text = 'Scroll Speed: ${state.currentSongChartScrollSpeed}x';
     };
-    inputScrollSpeed.value = state.currentSongChartData.scrollSpeed;
+    inputScrollSpeed.value = state.currentSongChartScrollSpeed;
+    labelScrollSpeed.text = 'Scroll Speed: ${state.currentSongChartScrollSpeed}x';
+
+    var frameVariation:Frame = toolbox.findComponent('frameVariation', Frame);
+    frameVariation.text = 'Variation: ${state.selectedVariation.toTitleCase()}';
+
+    var frameDifficulty:Frame = toolbox.findComponent('frameDifficulty', Frame);
+    frameDifficulty.text = 'Difficulty: ${state.selectedDifficulty.toTitleCase()}';
 
     return toolbox;
   }
 
+  static function onShowToolboxMetadata(state:ChartEditorState, toolbox:CollapsibleDialog):Void
+  {
+    state.refreshSongMetadataToolbox();
+  }
+
+  static function onHideToolboxMetadata(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
+
   static function buildToolboxCharactersLayout(state:ChartEditorState):CollapsibleDialog
   {
     var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT);
@@ -525,6 +616,10 @@ class ChartEditorToolboxHandler
     return toolbox;
   }
 
+  static function onShowToolboxCharacters(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
+
+  static function onHideToolboxCharacters(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
+
   static function buildToolboxPlayerPreviewLayout(state:ChartEditorState):CollapsibleDialog
   {
     var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT);
@@ -549,6 +644,10 @@ class ChartEditorToolboxHandler
     return toolbox;
   }
 
+  static function onShowToolboxPlayerPreview(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
+
+  static function onHideToolboxPlayerPreview(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
+
   static function buildToolboxOpponentPreviewLayout(state:ChartEditorState):CollapsibleDialog
   {
     var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT);
@@ -572,4 +671,8 @@ class ChartEditorToolboxHandler
 
     return toolbox;
   }
+
+  static function onShowToolboxOpponentPreview(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
+
+  static function onHideToolboxOpponentPreview(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
 }
diff --git a/source/funkin/util/SerializerUtil.hx b/source/funkin/util/SerializerUtil.hx
index 9452b7785..d731aee8f 100644
--- a/source/funkin/util/SerializerUtil.hx
+++ b/source/funkin/util/SerializerUtil.hx
@@ -36,7 +36,16 @@ class SerializerUtil
    */
   public static function fromJSON(input:String):Dynamic
   {
-    return Json.parse(input);
+    try
+    {
+      return Json.parse(input);
+    }
+    catch (e)
+    {
+      trace('An error occurred while parsing JSON from string data');
+      trace(e);
+      return null;
+    }
   }
 
   /**
diff --git a/source/funkin/util/SortUtil.hx b/source/funkin/util/SortUtil.hx
index 61418c299..df45e0717 100644
--- a/source/funkin/util/SortUtil.hx
+++ b/source/funkin/util/SortUtil.hx
@@ -6,6 +6,19 @@ import flixel.util.FlxSort;
 #end
 import funkin.play.notes.NoteSprite;
 
+/**
+ * A set of functions related to sorting.
+ *
+ * NOTE: `Array.sort()` takes a function `(x, y) -> Int`.
+ * If the objects are in the correct order (x before y), return a negative value.
+ * If the objects need to be swapped (y before x), return a negative value.
+ * If the objects are equal, return 0.
+ *
+ * NOTE: `Array.sort()` does NOT guarantee that the order of equal elements. `haxe.ds.ArraySort.sort()` does guarantee this.
+ * NOTE: `Array.sort()` may not be the most efficient sorting algorithm for all use cases (especially if the array is known to be mostly sorted).
+ *    You may consider using one of the functions in `funkin.util.tools.ArraySortTools` instead.
+ * NOTE: Both sort functions modify the array in-place. You may consider using `Reflect.copy()` to make a copy of the array before sorting.
+ */
 class SortUtil
 {
   /**
@@ -30,8 +43,10 @@ class SortUtil
 
   /**
    * Sort predicate for sorting strings alphabetically.
+   * @param a The first string to compare.
+   * @param b The second string to compare.
    */
-  public static function alphabetically(a:String, b:String)
+  public static function alphabetically(a:String, b:String):Int
   {
     a = a.toUpperCase();
     b = b.toUpperCase();
@@ -39,4 +54,39 @@ class SortUtil
     // Sort alphabetically. Yes that's how this works.
     return a == b ? 0 : a > b ? 1 : -1;
   }
+
+  /**
+   * Sort predicate which sorts two strings alphabetically, but prioritizes a specific string first.
+   * Example usage: `array.sort(defaultThenAlphabetical.bind('test'))` will sort the array so that the string 'test' is first.
+   * @param a The first string to compare.
+   * @param b The second string to compare.
+   * @param defaultValue The value to prioritize.
+   */
+  public static function defaultThenAlphabetically(defaultValue:String, a:String, b:String):Int
+  {
+    if (a == b) return 0;
+    if (a == defaultValue) return -1;
+    if (b == defaultValue) return 1;
+    return alphabetically(a, b);
+  }
+
+  /**
+   * Sort predicate which sorts two strings alphabetically, but prioritizes a specific string first.
+   * Example usage: `array.sort(defaultsThenAlphabetical.bind(['test']))` will sort the array so that the string 'test' is first.
+   * @param a The first string to compare.
+   * @param b The second string to compare.
+   * @param defaultValues The values to prioritize.
+   */
+  public static function defaultsThenAlphabetically(defaultValues:Array<String>, a:String, b:String):Int
+  {
+    if (a == b) return 0;
+    if (defaultValues.contains(a) && defaultValues.contains(b))
+    {
+      // Sort by index in defaultValues
+      return defaultValues.indexOf(a) - defaultValues.indexOf(b);
+    };
+    if (defaultValues.contains(a)) return -1;
+    if (defaultValues.contains(b)) return 1;
+    return alphabetically(a, b);
+  }
 }