diff --git a/assets b/assets
index 6f17eb051..23f85072c 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 6f17eb051e2609d59a591d4e6eb78e37c6e90adb
+Subproject commit 23f85072c190373592a30aed137b034715623f28
diff --git a/hmm.json b/hmm.json
index 57fbbb555..d461edd24 100644
--- a/hmm.json
+++ b/hmm.json
@@ -54,14 +54,14 @@
       "name": "haxeui-core",
       "type": "git",
       "dir": null,
-      "ref": "e765a3e0b7a653823e8dec765e04623f27f573f8",
+      "ref": "5086e59e7551d775ed4d1fb0188e31de22d1312b",
       "url": "https://github.com/haxeui/haxeui-core"
     },
     {
       "name": "haxeui-flixel",
       "type": "git",
       "dir": null,
-      "ref": "7a517d561eff49d8123c128bf9f5c1123b84d014",
+      "ref": "2b9cff727999b53ed292b1675ac1c9089ac77600",
       "url": "https://github.com/haxeui/haxeui-flixel"
     },
     {
diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx
index 13bcd306e..6c00b6f68 100644
--- a/source/funkin/InitState.hx
+++ b/source/funkin/InitState.hx
@@ -197,6 +197,13 @@ class InitState extends FlxState
     FlxG.android.preventDefaultKeys = [flixel.input.android.FlxAndroidKey.BACK];
     #end
 
+    //
+    // FLIXEL PLUGINS
+    //
+    funkin.util.plugins.EvacuateDebugPlugin.initialize();
+    funkin.util.plugins.ReloadAssetsDebugPlugin.initialize();
+    funkin.util.plugins.WatchPlugin.initialize();
+
     //
     // GAME DATA PARSING
     //
diff --git a/source/funkin/data/event/SongEventData.hx b/source/funkin/data/event/SongEventData.hx
index 7a167b031..522b9314e 100644
--- a/source/funkin/data/event/SongEventData.hx
+++ b/source/funkin/data/event/SongEventData.hx
@@ -240,4 +240,28 @@ typedef SongEventSchemaField =
   ?defaultValue:Dynamic,
 }
 
-typedef SongEventSchema = Array<SongEventSchemaField>;
+@:forward
+abstract SongEventSchema(SongEventSchemaRaw)
+{
+  public function new(?fields:Array<SongEventSchemaField>)
+  {
+    this = fields;
+  }
+
+  public function getByName(name:String):SongEventSchemaField
+  {
+    for (field in this)
+    {
+      if (field.name == name) return field;
+    }
+
+    return null;
+  }
+
+  public function getFirstField():SongEventSchemaField
+  {
+    return this[0];
+  }
+}
+
+typedef SongEventSchemaRaw = Array<SongEventSchemaField>;
diff --git a/source/funkin/data/song/SongData.hx b/source/funkin/data/song/SongData.hx
index 600871e2f..90a5f47c3 100644
--- a/source/funkin/data/song/SongData.hx
+++ b/source/funkin/data/song/SongData.hx
@@ -617,6 +617,23 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
     this = new SongEventDataRaw(time, event, value);
   }
 
+  public inline function valueAsStruct(?defaultKey:String = "key"):Dynamic
+  {
+    if (this.value == null) return {};
+    // TODO: How to check if it's a dynamic struct?
+    if (Std.isOfType(this.value, Int) || Std.isOfType(this.value, String) || Std.isOfType(this.value, Float) || Std.isOfType(this.value, Bool)
+      || Std.isOfType(this.value, Array))
+    {
+      var result:haxe.DynamicAccess<Dynamic> = {};
+      result.set(defaultKey, this.value);
+      return cast result;
+    }
+    else
+    {
+      return cast this.value;
+    }
+  }
+
   public inline function getDynamic(key:String):Null<Dynamic>
   {
     return this.value == null ? null : Reflect.field(this.value, key);
diff --git a/source/funkin/ui/MusicBeatState.hx b/source/funkin/ui/MusicBeatState.hx
index 077e9e495..f207d48c8 100644
--- a/source/funkin/ui/MusicBeatState.hx
+++ b/source/funkin/ui/MusicBeatState.hx
@@ -71,34 +71,11 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler
     }
   }
 
-  function handleFunctionControls():Void
-  {
-    // Emergency exit button.
-    if (FlxG.keys.justPressed.F4) FlxG.switchState(new MainMenuState());
-
-    // This can now be used in EVERY STATE YAY!
-    if (FlxG.keys.justPressed.F5) debug_refreshModules();
-  }
-
-  function handleQuickWatch():Void
-  {
-    // Display Conductor info in the watch window.
-    FlxG.watch.addQuick("songPosition", Conductor.songPosition);
-    FlxG.watch.addQuick("songPositionNoOffset", Conductor.songPosition + Conductor.instrumentalOffset);
-    FlxG.watch.addQuick("musicTime", FlxG.sound.music?.time ?? 0.0);
-    FlxG.watch.addQuick("bpm", Conductor.bpm);
-    FlxG.watch.addQuick("currentMeasureTime", Conductor.currentBeatTime);
-    FlxG.watch.addQuick("currentBeatTime", Conductor.currentBeatTime);
-    FlxG.watch.addQuick("currentStepTime", Conductor.currentStepTime);
-  }
-
   override function update(elapsed:Float)
   {
     super.update(elapsed);
 
     handleControls();
-    handleFunctionControls();
-    handleQuickWatch();
 
     dispatchEvent(new UpdateScriptEvent(elapsed));
   }
@@ -127,16 +104,6 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler
     ModuleHandler.callEvent(event);
   }
 
-  function debug_refreshModules()
-  {
-    PolymodHandler.forceReloadAssets();
-
-    this.destroy();
-
-    // Create a new instance of the current state, so old data is cleared.
-    FlxG.resetState();
-  }
-
   public function stepHit():Bool
   {
     var event = new SongTimeScriptEvent(SONG_STEP_HIT, Conductor.currentBeat, Conductor.currentStep);
diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index bf1b70d55..294c230d5 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -147,7 +147,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
   // Layouts
   public static final CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/notedata');
 
-  public static final CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/eventdata');
+  public static final CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/eventdata');
   public static final CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT:String = Paths.ui('chart-editor/toolbox/playtest-properties');
   public static final CHART_EDITOR_TOOLBOX_METADATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/metadata');
   public static final CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT:String = Paths.ui('chart-editor/toolbox/difficulty');
@@ -489,17 +489,17 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
   /**
    * The note kind to use for notes being placed in the chart. Defaults to `''`.
    */
-  var selectedNoteKind:String = '';
+  var noteKindToPlace:String = '';
 
   /**
    * The event type to use for events being placed in the chart. Defaults to `''`.
    */
-  var selectedEventKind:String = 'FocusCamera';
+  var eventKindToPlace:String = 'FocusCamera';
 
   /**
    * The event data to use for events being placed in the chart.
    */
-  var selectedEventData:DynamicAccess<Dynamic> = {};
+  var eventDataToPlace:DynamicAccess<Dynamic> = {};
 
   /**
    * The internal index of what note snapping value is in use.
@@ -1871,6 +1871,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
 
     // Setup the onClick listeners for the UI after it's been created.
     setupUIListeners();
+    setupContextMenu();
     setupTurboKeyHandlers();
 
     setupAutoSave();
@@ -2444,23 +2445,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
     menubarItemUndo.onClick = _ -> undoLastCommand();
     menubarItemRedo.onClick = _ -> redoLastCommand();
     menubarItemCopy.onClick = function(_) {
-      // Doesn't use a command because it's not undoable.
-
-      // Calculate a single time offset for all the notes and events.
-      var timeOffset:Null<Int> = currentNoteSelection.length > 0 ? Std.int(currentNoteSelection[0].time) : null;
-      if (currentEventSelection.length > 0)
-      {
-        if (timeOffset == null || currentEventSelection[0].time < timeOffset)
-        {
-          timeOffset = Std.int(currentEventSelection[0].time);
-        }
-      }
-
-      SongDataUtils.writeItemsToClipboard(
-        {
-          notes: SongDataUtils.buildNoteClipboard(currentNoteSelection, timeOffset),
-          events: SongDataUtils.buildEventClipboard(currentEventSelection, timeOffset),
-        });
+      copySelection();
     };
     menubarItemCut.onClick = _ -> performCommand(new CutItemsCommand(currentNoteSelection, currentEventSelection));
 
@@ -2626,7 +2611,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
     menubarItemToggleToolboxDifficulty.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT, event.value);
     menubarItemToggleToolboxMetadata.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_METADATA_LAYOUT, event.value);
     menubarItemToggleToolboxNotes.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT, event.value);
-    menubarItemToggleToolboxEvents.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT, event.value);
+    menubarItemToggleToolboxEventData.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT, event.value);
     menubarItemToggleToolboxPlaytestProperties.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT, event.value);
     menubarItemToggleToolboxPlayerPreview.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT, event.value);
     menubarItemToggleToolboxOpponentPreview.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT, event.value);
@@ -2635,6 +2620,42 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
     // registerContextMenu(null, Paths.ui('chart-editor/context/test'));
   }
 
+  function setupContextMenu():Void
+  {
+    Screen.instance.registerEvent(MouseEvent.RIGHT_MOUSE_UP, function(e:MouseEvent) {
+      var xPos = e.screenX;
+      var yPos = e.screenY;
+      onContextMenu(xPos, yPos);
+    });
+  }
+
+  function onContextMenu(xPos:Float, yPos:Float)
+  {
+    trace('User right clicked to open menu at (${xPos}, ${yPos})');
+    // this.openDefaultContextMenu(xPos, yPos);
+  }
+
+  function copySelection():Void
+  {
+    // Doesn't use a command because it's not undoable.
+
+    // Calculate a single time offset for all the notes and events.
+    var timeOffset:Null<Int> = currentNoteSelection.length > 0 ? Std.int(currentNoteSelection[0].time) : null;
+    if (currentEventSelection.length > 0)
+    {
+      if (timeOffset == null || currentEventSelection[0].time < timeOffset)
+      {
+        timeOffset = Std.int(currentEventSelection[0].time);
+      }
+    }
+
+    SongDataUtils.writeItemsToClipboard(
+      {
+        notes: SongDataUtils.buildNoteClipboard(currentNoteSelection, timeOffset),
+        events: SongDataUtils.buildEventClipboard(currentEventSelection, timeOffset),
+      });
+  }
+
   /**
    * Initialize TurboKeyHandlers and add them to the state (so `update()` is called)
    * We can then probe `keyHandler.activated` to see if the key combo's action should be taken.
@@ -2812,6 +2833,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
       playMetronomeTick(Conductor.currentBeat % 4 == 0);
     }
 
+    // Show the mouse cursor.
+    // Just throwing this somewhere convenient and infrequently called because sometimes Flixel's debug thing hides the cursor.
+    Cursor.show();
+
     return true;
   }
 
@@ -3015,6 +3040,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
 
           // Update the event sprite's position.
           eventSprite.updateEventPosition(renderedEvents);
+          // Update the sprite's graphic. TODO: Is this inefficient?
+          eventSprite.playAnimation(eventSprite.eventData.event);
         }
         else
         {
@@ -3471,6 +3498,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
 
     // trace('shouldHandleCursor: $shouldHandleCursor');
 
+    // TODO: TBH some of this should be using FlxMouseEventManager...
+
     if (shouldHandleCursor)
     {
       // Over the course of this big conditional block,
@@ -4054,14 +4083,14 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
                 {
                   // Create an event and place it in the chart.
                   // TODO: Figure out configuring event data.
-                  var newEventData:SongEventData = new SongEventData(cursorSnappedMs, selectedEventKind, selectedEventData.clone());
+                  var newEventData:SongEventData = new SongEventData(cursorSnappedMs, eventKindToPlace, eventDataToPlace.clone());
 
                   performCommand(new AddEventsCommand([newEventData], FlxG.keys.pressed.CONTROL));
                 }
                 else
                 {
                   // Create a note and place it in the chart.
-                  var newNoteData:SongNoteData = new SongNoteData(cursorSnappedMs, cursorColumn, 0, selectedNoteKind.clone());
+                  var newNoteData:SongNoteData = new SongNoteData(cursorSnappedMs, cursorColumn, 0, noteKindToPlace.clone());
 
                   performCommand(new AddNotesCommand([newNoteData], FlxG.keys.pressed.CONTROL));
 
@@ -4099,13 +4128,52 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
           if (highlightedNote != null && highlightedNote.noteData != null)
           {
             // TODO: Handle the case of clicking on a sustain piece.
-            // Remove the note.
-            performCommand(new RemoveNotesCommand([highlightedNote.noteData]));
+            if (FlxG.keys.pressed.SHIFT)
+            {
+              // Shift + Right click opens the context menu.
+              // If we are clicking a large selection, open the Selection context menu, otherwise open the Note context menu.
+              var isHighlightedNoteSelected:Bool = isNoteSelected(highlightedNote.noteData);
+              var useSingleNoteContextMenu:Bool = (!isHighlightedNoteSelected)
+                || (isHighlightedNoteSelected && currentNoteSelection.length == 1);
+              // Show the context menu connected to the note.
+              if (useSingleNoteContextMenu)
+              {
+                this.openNoteContextMenu(FlxG.mouse.screenX, FlxG.mouse.screenY, highlightedNote.noteData);
+              }
+              else
+              {
+                this.openSelectionContextMenu(FlxG.mouse.screenX, FlxG.mouse.screenY);
+              }
+            }
+            else
+            {
+              // Right click removes the note.
+              performCommand(new RemoveNotesCommand([highlightedNote.noteData]));
+            }
           }
           else if (highlightedEvent != null && highlightedEvent.eventData != null)
           {
-            // Remove the event.
-            performCommand(new RemoveEventsCommand([highlightedEvent.eventData]));
+            if (FlxG.keys.pressed.SHIFT)
+            {
+              // Shift + Right click opens the context menu.
+              // If we are clicking a large selection, open the Selection context menu, otherwise open the Event context menu.
+              var isHighlightedEventSelected:Bool = isEventSelected(highlightedEvent.eventData);
+              var useSingleEventContextMenu:Bool = (!isHighlightedEventSelected)
+                || (isHighlightedEventSelected && currentEventSelection.length == 1);
+              if (useSingleEventContextMenu)
+              {
+                this.openEventContextMenu(FlxG.mouse.screenX, FlxG.mouse.screenY, highlightedEvent.eventData);
+              }
+              else
+              {
+                this.openSelectionContextMenu(FlxG.mouse.screenX, FlxG.mouse.screenY);
+              }
+            }
+            else
+            {
+              // Right click removes the event.
+              performCommand(new RemoveEventsCommand([highlightedEvent.eventData]));
+            }
           }
           else
           {
@@ -4126,11 +4194,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
 
             if (gridGhostEvent == null) throw "ERROR: Tried to handle cursor, but gridGhostEvent is null! Check ChartEditorState.buildGrid()";
 
-            var eventData:SongEventData = gridGhostEvent.eventData != null ? gridGhostEvent.eventData : new SongEventData(cursorMs, selectedEventKind, null);
+            var eventData:SongEventData = gridGhostEvent.eventData != null ? gridGhostEvent.eventData : new SongEventData(cursorMs, eventKindToPlace, null);
 
-            if (selectedEventKind != eventData.event)
+            if (eventKindToPlace != eventData.event)
             {
-              eventData.event = selectedEventKind;
+              eventData.event = eventKindToPlace;
             }
             eventData.time = cursorSnappedMs;
 
@@ -4146,11 +4214,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
 
             if (gridGhostNote == null) throw "ERROR: Tried to handle cursor, but gridGhostNote is null! Check ChartEditorState.buildGrid()";
 
-            var noteData:SongNoteData = gridGhostNote.noteData != null ? gridGhostNote.noteData : new SongNoteData(cursorMs, cursorColumn, 0, selectedNoteKind);
+            var noteData:SongNoteData = gridGhostNote.noteData != null ? gridGhostNote.noteData : new SongNoteData(cursorMs, cursorColumn, 0, noteKindToPlace);
 
-            if (cursorColumn != noteData.data || selectedNoteKind != noteData.kind)
+            if (cursorColumn != noteData.data || noteKindToPlace != noteData.kind)
             {
-              noteData.kind = selectedNoteKind;
+              noteData.kind = noteKindToPlace;
               noteData.data = cursorColumn;
               gridGhostNote.playNoteAnimation();
             }
@@ -4443,7 +4511,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
 
     if (notesAtPos.length == 0)
     {
-      var newNoteData:SongNoteData = new SongNoteData(playheadPosSnappedMs, column, 0, selectedNoteKind);
+      var newNoteData:SongNoteData = new SongNoteData(playheadPosSnappedMs, column, 0, noteKindToPlace);
       performCommand(new AddNotesCommand([newNoteData], FlxG.keys.pressed.CONTROL));
     }
     else
@@ -4746,10 +4814,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
     #end
   }
 
-  override function handleQuickWatch():Void
+  function handleQuickWatch():Void
   {
-    super.handleQuickWatch();
-
     FlxG.watch.addQuick('musicTime', audioInstTrack?.time ?? 0.0);
 
     FlxG.watch.addQuick('scrollPosInPixels', scrollPositionInPixels);
@@ -5487,6 +5553,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
 
     cleanupAutoSave();
 
+    this.closeAllMenus();
+
     // Hide the mouse cursor on other states.
     Cursor.hide();
 
diff --git a/source/funkin/ui/debug/charting/commands/SelectItemsCommand.hx b/source/funkin/ui/debug/charting/commands/SelectItemsCommand.hx
index abe8b9e35..f59672646 100644
--- a/source/funkin/ui/debug/charting/commands/SelectItemsCommand.hx
+++ b/source/funkin/ui/debug/charting/commands/SelectItemsCommand.hx
@@ -33,6 +33,16 @@ class SelectItemsCommand implements ChartEditorCommand
       state.currentEventSelection.push(event);
     }
 
+    // If we just selected one or more events (and no notes), then we should make the event data toolbox display the event data for the selected event.
+    if (this.notes.length == 0 && this.events.length >= 1)
+    {
+      var eventSelected = this.events[0];
+      state.eventKindToPlace = eventSelected.event;
+      var eventData = eventSelected.valueAsStruct();
+      state.eventDataToPlace = eventData;
+      state.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT);
+    }
+
     state.noteDisplayDirty = true;
     state.notePreviewDirty = true;
   }
diff --git a/source/funkin/ui/debug/charting/commands/SetItemSelectionCommand.hx b/source/funkin/ui/debug/charting/commands/SetItemSelectionCommand.hx
index a06aefabc..7dc344835 100644
--- a/source/funkin/ui/debug/charting/commands/SetItemSelectionCommand.hx
+++ b/source/funkin/ui/debug/charting/commands/SetItemSelectionCommand.hx
@@ -30,6 +30,16 @@ class SetItemSelectionCommand implements ChartEditorCommand
     state.currentNoteSelection = notes;
     state.currentEventSelection = events;
 
+    // If we just selected one or more events (and no notes), then we should make the event data toolbox display the event data for the selected event.
+    if (this.notes.length == 0 && this.events.length >= 1)
+    {
+      var eventSelected = this.events[0];
+      state.eventKindToPlace = eventSelected.event;
+      var eventData = eventSelected.valueAsStruct();
+      state.eventDataToPlace = eventData;
+      state.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT);
+    }
+
     state.noteDisplayDirty = true;
   }
 
diff --git a/source/funkin/ui/debug/charting/components/ChartEditorEventSprite.hx b/source/funkin/ui/debug/charting/components/ChartEditorEventSprite.hx
index 4c9d91407..e5bbf0807 100644
--- a/source/funkin/ui/debug/charting/components/ChartEditorEventSprite.hx
+++ b/source/funkin/ui/debug/charting/components/ChartEditorEventSprite.hx
@@ -145,8 +145,6 @@ class ChartEditorEventSprite extends FlxSprite
     else
     {
       this.visible = true;
-      // Only play the animation if the event type has changed.
-      // if (this.eventData == null || this.eventData.event != value.event)
       playAnimation(value.event);
       this.eventData = value;
       // Update the position to match the note data.
diff --git a/source/funkin/ui/debug/charting/contextmenus/ChartEditorBaseContextMenu.hx b/source/funkin/ui/debug/charting/contextmenus/ChartEditorBaseContextMenu.hx
new file mode 100644
index 000000000..f25f3ebb3
--- /dev/null
+++ b/source/funkin/ui/debug/charting/contextmenus/ChartEditorBaseContextMenu.hx
@@ -0,0 +1,19 @@
+package funkin.ui.debug.charting.contextmenus;
+
+import haxe.ui.containers.menus.Menu;
+
+@:access(funkin.ui.debug.charting.ChartEditorState)
+class ChartEditorBaseContextMenu extends Menu
+{
+  var chartEditorState:ChartEditorState;
+
+  public function new(chartEditorState:ChartEditorState, xPos:Float = 0, yPos:Float = 0)
+  {
+    super();
+
+    this.chartEditorState = chartEditorState;
+
+    this.left = xPos;
+    this.top = yPos;
+  }
+}
diff --git a/source/funkin/ui/debug/charting/contextmenus/ChartEditorDefaultContextMenu.hx b/source/funkin/ui/debug/charting/contextmenus/ChartEditorDefaultContextMenu.hx
new file mode 100644
index 000000000..9529cc2fd
--- /dev/null
+++ b/source/funkin/ui/debug/charting/contextmenus/ChartEditorDefaultContextMenu.hx
@@ -0,0 +1,14 @@
+package funkin.ui.debug.charting.contextmenus;
+
+import haxe.ui.containers.menus.Menu;
+import haxe.ui.core.Screen;
+
+@:access(funkin.ui.debug.charting.ChartEditorState)
+@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/context-menus/default.xml"))
+class ChartEditorDefaultContextMenu extends ChartEditorBaseContextMenu
+{
+  public function new(chartEditorState2:ChartEditorState, xPos2:Float = 0, yPos2:Float = 0)
+  {
+    super(chartEditorState2, xPos2, yPos2);
+  }
+}
diff --git a/source/funkin/ui/debug/charting/contextmenus/ChartEditorEventContextMenu.hx b/source/funkin/ui/debug/charting/contextmenus/ChartEditorEventContextMenu.hx
new file mode 100644
index 000000000..a79125b21
--- /dev/null
+++ b/source/funkin/ui/debug/charting/contextmenus/ChartEditorEventContextMenu.hx
@@ -0,0 +1,32 @@
+package funkin.ui.debug.charting.contextmenus;
+
+import haxe.ui.containers.menus.Menu;
+import haxe.ui.containers.menus.MenuItem;
+import haxe.ui.core.Screen;
+import funkin.data.song.SongData.SongEventData;
+import funkin.ui.debug.charting.commands.RemoveEventsCommand;
+
+@:access(funkin.ui.debug.charting.ChartEditorState)
+@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/context-menus/event.xml"))
+class ChartEditorEventContextMenu extends ChartEditorBaseContextMenu
+{
+  var contextmenuEdit:MenuItem;
+  var contextmenuDelete:MenuItem;
+
+  var data:SongEventData;
+
+  public function new(chartEditorState2:ChartEditorState, xPos2:Float = 0, yPos2:Float = 0, data:SongEventData)
+  {
+    super(chartEditorState2, xPos2, yPos2);
+    this.data = data;
+
+    initialize();
+  }
+
+  function initialize()
+  {
+    contextmenuDelete.onClick = function(_) {
+      chartEditorState.performCommand(new RemoveEventsCommand([data]));
+    }
+  }
+}
diff --git a/source/funkin/ui/debug/charting/contextmenus/ChartEditorNoteContextMenu.hx b/source/funkin/ui/debug/charting/contextmenus/ChartEditorNoteContextMenu.hx
new file mode 100644
index 000000000..4bfab27e8
--- /dev/null
+++ b/source/funkin/ui/debug/charting/contextmenus/ChartEditorNoteContextMenu.hx
@@ -0,0 +1,38 @@
+package funkin.ui.debug.charting.contextmenus;
+
+import haxe.ui.containers.menus.Menu;
+import haxe.ui.containers.menus.MenuItem;
+import haxe.ui.core.Screen;
+import funkin.data.song.SongData.SongNoteData;
+import funkin.ui.debug.charting.commands.FlipNotesCommand;
+import funkin.ui.debug.charting.commands.RemoveNotesCommand;
+
+@:access(funkin.ui.debug.charting.ChartEditorState)
+@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/context-menus/note.xml"))
+class ChartEditorNoteContextMenu extends ChartEditorBaseContextMenu
+{
+  var contextmenuFlip:MenuItem;
+  var contextmenuDelete:MenuItem;
+
+  var data:SongNoteData;
+
+  public function new(chartEditorState2:ChartEditorState, xPos2:Float = 0, yPos2:Float = 0, data:SongNoteData)
+  {
+    super(chartEditorState2, xPos2, yPos2);
+    this.data = data;
+
+    initialize();
+  }
+
+  function initialize():Void
+  {
+    // NOTE: Remember to use commands here to ensure undo/redo works properly
+    contextmenuFlip.onClick = function(_) {
+      chartEditorState.performCommand(new FlipNotesCommand([data]));
+    }
+
+    contextmenuDelete.onClick = function(_) {
+      chartEditorState.performCommand(new RemoveNotesCommand([data]));
+    }
+  }
+}
diff --git a/source/funkin/ui/debug/charting/contextmenus/ChartEditorSelectionContextMenu.hx b/source/funkin/ui/debug/charting/contextmenus/ChartEditorSelectionContextMenu.hx
new file mode 100644
index 000000000..feed9b689
--- /dev/null
+++ b/source/funkin/ui/debug/charting/contextmenus/ChartEditorSelectionContextMenu.hx
@@ -0,0 +1,58 @@
+package funkin.ui.debug.charting.contextmenus;
+
+import haxe.ui.containers.menus.Menu;
+import haxe.ui.containers.menus.MenuItem;
+import haxe.ui.core.Screen;
+import funkin.ui.debug.charting.commands.CutItemsCommand;
+import funkin.ui.debug.charting.commands.RemoveEventsCommand;
+import funkin.ui.debug.charting.commands.RemoveItemsCommand;
+import funkin.ui.debug.charting.commands.RemoveNotesCommand;
+
+@:access(funkin.ui.debug.charting.ChartEditorState)
+@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/context-menus/selection.xml"))
+class ChartEditorSelectionContextMenu extends ChartEditorBaseContextMenu
+{
+  var contextmenuCut:MenuItem;
+  var contextmenuCopy:MenuItem;
+  var contextmenuPaste:MenuItem;
+  var contextmenuDelete:MenuItem;
+  var contextmenuFlip:MenuItem;
+  var contextmenuSelectAll:MenuItem;
+  var contextmenuSelectInverse:MenuItem;
+  var contextmenuSelectNone:MenuItem;
+
+  public function new(chartEditorState2:ChartEditorState, xPos2:Float = 0, yPos2:Float = 0)
+  {
+    super(chartEditorState2, xPos2, yPos2);
+
+    initialize();
+  }
+
+  function initialize():Void
+  {
+    contextmenuCut.onClick = (_) -> {
+      chartEditorState.performCommand(new CutItemsCommand(chartEditorState.currentNoteSelection, chartEditorState.currentEventSelection));
+    };
+    contextmenuCopy.onClick = (_) -> {
+      chartEditorState.copySelection();
+    };
+    contextmenuFlip.onClick = (_) -> {
+      if (chartEditorState.currentNoteSelection.length > 0 && chartEditorState.currentEventSelection.length > 0)
+      {
+        chartEditorState.performCommand(new RemoveItemsCommand(chartEditorState.currentNoteSelection, chartEditorState.currentEventSelection));
+      }
+      else if (chartEditorState.currentNoteSelection.length > 0)
+      {
+        chartEditorState.performCommand(new RemoveNotesCommand(chartEditorState.currentNoteSelection));
+      }
+      else if (chartEditorState.currentEventSelection.length > 0)
+      {
+        chartEditorState.performCommand(new RemoveEventsCommand(chartEditorState.currentEventSelection));
+      }
+      else
+      {
+        // Do nothing???
+      }
+    };
+  }
+}
diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorContextMenuHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorContextMenuHandler.hx
new file mode 100644
index 000000000..b914f4149
--- /dev/null
+++ b/source/funkin/ui/debug/charting/handlers/ChartEditorContextMenuHandler.hx
@@ -0,0 +1,64 @@
+package funkin.ui.debug.charting.handlers;
+
+import funkin.ui.debug.charting.contextmenus.ChartEditorDefaultContextMenu;
+import funkin.ui.debug.charting.contextmenus.ChartEditorEventContextMenu;
+import funkin.ui.debug.charting.contextmenus.ChartEditorNoteContextMenu;
+import funkin.ui.debug.charting.contextmenus.ChartEditorSelectionContextMenu;
+import haxe.ui.containers.menus.Menu;
+import haxe.ui.core.Screen;
+import funkin.data.song.SongData.SongNoteData;
+import funkin.data.song.SongData.SongEventData;
+
+/**
+ * Handles context menus (the little menus that appear when you right click on stuff) for the new Chart Editor.
+ */
+@:nullSafety
+@:access(funkin.ui.debug.charting.ChartEditorState)
+class ChartEditorContextMenuHandler
+{
+  static var existingMenus:Array<Menu> = [];
+
+  public static function openDefaultContextMenu(state:ChartEditorState, xPos:Float, yPos:Float)
+  {
+    displayMenu(state, new ChartEditorDefaultContextMenu(state, xPos, yPos));
+  }
+
+  public static function openSelectionContextMenu(state:ChartEditorState, xPos:Float, yPos:Float)
+  {
+    displayMenu(state, new ChartEditorSelectionContextMenu(state, xPos, yPos));
+  }
+
+  public static function openNoteContextMenu(state:ChartEditorState, xPos:Float, yPos:Float, data:SongNoteData)
+  {
+    displayMenu(state, new ChartEditorNoteContextMenu(state, xPos, yPos, data));
+  }
+
+  public static function openEventContextMenu(state:ChartEditorState, xPos:Float, yPos:Float, data:SongEventData)
+  {
+    displayMenu(state, new ChartEditorEventContextMenu(state, xPos, yPos, data));
+  }
+
+  static function displayMenu(state:ChartEditorState, targetMenu:Menu)
+  {
+    // Close any existing menus
+    closeAllMenus(state);
+
+    // Show the new menu
+    Screen.instance.addComponent(targetMenu);
+    existingMenus.push(targetMenu);
+  }
+
+  public static function closeMenu(state:ChartEditorState, targetMenu:Menu)
+  {
+    // targetMenu.close();
+    existingMenus.remove(targetMenu);
+  }
+
+  public static function closeAllMenus(state:ChartEditorState)
+  {
+    for (existingMenu in existingMenus)
+    {
+      closeMenu(state, existingMenu);
+    }
+  }
+}
diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx
index a9a9c375d..08298eb66 100644
--- a/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx
+++ b/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx
@@ -23,6 +23,7 @@ import funkin.ui.debug.charting.util.ChartEditorDropdowns;
 import funkin.ui.haxeui.components.CharacterPlayer;
 import funkin.util.FileUtil;
 import haxe.ui.components.Button;
+import haxe.ui.data.ArrayDataSource;
 import haxe.ui.components.CheckBox;
 import haxe.ui.components.DropDown;
 import haxe.ui.components.HorizontalSlider;
@@ -36,12 +37,12 @@ import haxe.ui.containers.dialogs.Dialog.DialogButton;
 import haxe.ui.containers.dialogs.Dialog.DialogEvent;
 import funkin.ui.debug.charting.toolboxes.ChartEditorBaseToolbox;
 import funkin.ui.debug.charting.toolboxes.ChartEditorMetadataToolbox;
+import funkin.ui.debug.charting.toolboxes.ChartEditorEventDataToolbox;
 import haxe.ui.containers.Frame;
 import haxe.ui.containers.Grid;
 import haxe.ui.containers.TreeView;
 import haxe.ui.containers.TreeViewNode;
 import haxe.ui.core.Component;
-import haxe.ui.data.ArrayDataSource;
 import haxe.ui.events.UIEvent;
 
 /**
@@ -79,8 +80,9 @@ class ChartEditorToolboxHandler
       {
         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_EVENT_DATA_LAYOUT:
+          // TODO: Fix this.
+          cast(toolbox, ChartEditorBaseToolbox).refresh();
         case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT:
           onShowToolboxPlaytestProperties(state, toolbox);
         case ChartEditorState.CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT:
@@ -119,7 +121,7 @@ class ChartEditorToolboxHandler
       {
         case ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT:
           onHideToolboxNoteData(state, toolbox);
-        case ChartEditorState.CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT:
+        case ChartEditorState.CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT:
           onHideToolboxEventData(state, toolbox);
         case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT:
           onHideToolboxPlaytestProperties(state, toolbox);
@@ -195,7 +197,7 @@ class ChartEditorToolboxHandler
     {
       case ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT:
         toolbox = buildToolboxNoteDataLayout(state);
-      case ChartEditorState.CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT:
+      case ChartEditorState.CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT:
         toolbox = buildToolboxEventDataLayout(state);
       case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT:
         toolbox = buildToolboxPlaytestPropertiesLayout(state);
@@ -283,19 +285,19 @@ class ChartEditorToolboxHandler
         toolboxNotesCustomKindLabel.hidden = false;
         toolboxNotesCustomKind.hidden = false;
 
-        state.selectedNoteKind = toolboxNotesCustomKind.text;
+        state.noteKindToPlace = toolboxNotesCustomKind.text;
       }
       else
       {
         toolboxNotesCustomKindLabel.hidden = true;
         toolboxNotesCustomKind.hidden = true;
 
-        state.selectedNoteKind = event.data.id;
+        state.noteKindToPlace = event.data.id;
       }
     }
 
     toolboxNotesCustomKind.onChange = function(event:UIEvent) {
-      state.selectedNoteKind = toolboxNotesCustomKind.text;
+      state.noteKindToPlace = toolboxNotesCustomKind.text;
     }
 
     return toolbox;
@@ -305,159 +307,16 @@ class ChartEditorToolboxHandler
 
   static function onHideToolboxNoteData(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
 
-  static function buildToolboxEventDataLayout(state:ChartEditorState):Null<CollapsibleDialog>
-  {
-    var toolbox:CollapsibleDialog = cast RuntimeComponentBuilder.fromAsset(ChartEditorState.CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT);
-
-    if (toolbox == null) return null;
-
-    // Starting position.
-    toolbox.x = 100;
-    toolbox.y = 150;
-
-    toolbox.onDialogClosed = function(event:DialogEvent) {
-      state.menubarItemToggleToolboxEvents.selected = false;
-    }
-
-    var toolboxEventsEventKind:Null<DropDown> = toolbox.findComponent('toolboxEventsEventKind', DropDown);
-    if (toolboxEventsEventKind == null) throw 'ChartEditorToolboxHandler.buildToolboxEventDataLayout() - Could not find toolboxEventsEventKind component.';
-    var toolboxEventsDataGrid:Null<Grid> = toolbox.findComponent('toolboxEventsDataGrid', Grid);
-    if (toolboxEventsDataGrid == null) throw 'ChartEditorToolboxHandler.buildToolboxEventDataLayout() - Could not find toolboxEventsDataGrid component.';
-
-    toolboxEventsEventKind.dataSource = new ArrayDataSource();
-
-    var songEvents:Array<SongEvent> = SongEventParser.listEvents();
-
-    for (event in songEvents)
-    {
-      toolboxEventsEventKind.dataSource.add({text: event.getTitle(), value: event.id});
-    }
-
-    toolboxEventsEventKind.onChange = function(event:UIEvent) {
-      var eventType:String = event.data.value;
-
-      trace('ChartEditorToolboxHandler.buildToolboxEventDataLayout() - Event type changed: $eventType');
-
-      state.selectedEventKind = eventType;
-
-      var schema:SongEventSchema = SongEventParser.getEventSchema(eventType);
-
-      if (schema == null)
-      {
-        trace('ChartEditorToolboxHandler.buildToolboxEventDataLayout() - Unknown event kind: $eventType');
-        return;
-      }
-
-      buildEventDataFormFromSchema(state, toolboxEventsDataGrid, schema);
-    }
-    toolboxEventsEventKind.value = state.selectedEventKind;
-
-    return toolbox;
-  }
-
-  static function onShowToolboxEventData(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
-
-  static function onShowToolboxPlaytestProperties(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
+  static function onHideToolboxMetadata(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
 
   static function onHideToolboxEventData(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
 
+  static function onHideToolboxDifficulty(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
+
+  static function onShowToolboxPlaytestProperties(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
+
   static function onHideToolboxPlaytestProperties(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
 
-  static function buildEventDataFormFromSchema(state:ChartEditorState, target:Box, schema:SongEventSchema):Void
-  {
-    trace(schema);
-    // Clear the frame.
-    target.removeAllComponents();
-
-    state.selectedEventData = {};
-
-    for (field in schema)
-    {
-      if (field == null) continue;
-
-      // Add a label.
-      var label:Label = new Label();
-      label.text = field.title;
-      label.verticalAlign = "center";
-      target.addComponent(label);
-
-      var input:Component;
-      switch (field.type)
-      {
-        case INTEGER:
-          var numberStepper:NumberStepper = new NumberStepper();
-          numberStepper.id = field.name;
-          numberStepper.step = field.step ?? 1.0;
-          numberStepper.min = field.min ?? 0.0;
-          numberStepper.max = field.max ?? 10.0;
-          if (field.defaultValue != null) numberStepper.value = field.defaultValue;
-          input = numberStepper;
-        case FLOAT:
-          var numberStepper:NumberStepper = new NumberStepper();
-          numberStepper.id = field.name;
-          numberStepper.step = field.step ?? 0.1;
-          if (field.min != null) numberStepper.min = field.min;
-          if (field.max != null) numberStepper.max = field.max;
-          if (field.defaultValue != null) numberStepper.value = field.defaultValue;
-          input = numberStepper;
-        case BOOL:
-          var checkBox:CheckBox = new CheckBox();
-          checkBox.id = field.name;
-          if (field.defaultValue != null) checkBox.selected = field.defaultValue;
-          input = checkBox;
-        case ENUM:
-          var dropDown:DropDown = new DropDown();
-          dropDown.id = field.name;
-          dropDown.width = 200.0;
-          dropDown.dataSource = new ArrayDataSource();
-
-          if (field.keys == null) throw 'Field "${field.name}" is of Enum type but has no keys.';
-
-          // Add entries to the dropdown.
-
-          for (optionName in field.keys.keys())
-          {
-            var optionValue:Null<Dynamic> = field.keys.get(optionName);
-            trace('$optionName : $optionValue');
-            dropDown.dataSource.add({value: optionValue, text: optionName});
-          }
-
-          dropDown.value = field.defaultValue;
-
-          input = dropDown;
-        case STRING:
-          input = new TextField();
-          input.id = field.name;
-          if (field.defaultValue != null) input.text = field.defaultValue;
-        default:
-          // Unknown type. Display a label so we know what it is.
-          input = new Label();
-          input.id = field.name;
-          input.text = field.type;
-      }
-
-      target.addComponent(input);
-
-      input.onChange = function(event:UIEvent) {
-        var value = event.target.value;
-        if (field.type == ENUM)
-        {
-          value = event.target.value.value;
-        }
-        trace('ChartEditorToolboxHandler.buildEventDataFormFromSchema() - ${event.target.id} = ${value}');
-
-        if (value == null)
-        {
-          state.selectedEventData.remove(event.target.id);
-        }
-        else
-        {
-          state.selectedEventData.set(event.target.id, value);
-        }
-      }
-    }
-  }
-
   static function buildToolboxPlaytestPropertiesLayout(state:ChartEditorState):Null<CollapsibleDialog>
   {
     // fill with playtest properties
@@ -576,8 +435,6 @@ class ChartEditorToolboxHandler
     trace('selected node: ${treeView.selectedNode}');
   }
 
-  static function onHideToolboxDifficulty(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
-
   static function buildToolboxMetadataLayout(state:ChartEditorState):Null<ChartEditorBaseToolbox>
   {
     var toolbox:ChartEditorBaseToolbox = ChartEditorMetadataToolbox.build(state);
@@ -587,7 +444,14 @@ class ChartEditorToolboxHandler
     return toolbox;
   }
 
-  static function onHideToolboxMetadata(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
+  static function buildToolboxEventDataLayout(state:ChartEditorState):Null<ChartEditorBaseToolbox>
+  {
+    var toolbox:ChartEditorBaseToolbox = ChartEditorEventDataToolbox.build(state);
+
+    if (toolbox == null) return null;
+
+    return toolbox;
+  }
 
   static function buildToolboxPlayerPreviewLayout(state:ChartEditorState):Null<CollapsibleDialog>
   {
diff --git a/source/funkin/ui/debug/charting/import.hx b/source/funkin/ui/debug/charting/import.hx
index 933eaa3a5..b0569e3bb 100644
--- a/source/funkin/ui/debug/charting/import.hx
+++ b/source/funkin/ui/debug/charting/import.hx
@@ -3,6 +3,7 @@ package funkin.ui.debug.charting;
 #if !macro
 // Apply handlers so they can be called as though they were functions in ChartEditorState
 using funkin.ui.debug.charting.handlers.ChartEditorAudioHandler;
+using funkin.ui.debug.charting.handlers.ChartEditorContextMenuHandler;
 using funkin.ui.debug.charting.handlers.ChartEditorDialogHandler;
 using funkin.ui.debug.charting.handlers.ChartEditorImportExportHandler;
 using funkin.ui.debug.charting.handlers.ChartEditorNotificationHandler;
diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorEventDataToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorEventDataToolbox.hx
new file mode 100644
index 000000000..7066fc913
--- /dev/null
+++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorEventDataToolbox.hx
@@ -0,0 +1,259 @@
+package funkin.ui.debug.charting.toolboxes;
+
+import funkin.play.character.BaseCharacter.CharacterType;
+import funkin.play.character.CharacterData;
+import funkin.play.stage.StageData;
+import funkin.play.event.SongEvent;
+import funkin.data.event.SongEventData.SongEventSchema;
+import funkin.ui.debug.charting.commands.ChangeStartingBPMCommand;
+import funkin.ui.debug.charting.util.ChartEditorDropdowns;
+import haxe.ui.components.Button;
+import haxe.ui.components.CheckBox;
+import haxe.ui.components.DropDown;
+import haxe.ui.components.HorizontalSlider;
+import haxe.ui.components.Label;
+import haxe.ui.components.NumberStepper;
+import haxe.ui.components.Slider;
+import haxe.ui.core.Component;
+import funkin.data.event.SongEventData.SongEventParser;
+import haxe.ui.components.TextField;
+import haxe.ui.containers.Box;
+import haxe.ui.containers.Frame;
+import haxe.ui.events.UIEvent;
+import haxe.ui.data.ArrayDataSource;
+import haxe.ui.containers.Grid;
+import haxe.ui.components.DropDown;
+import haxe.ui.containers.Frame;
+
+/**
+ * The toolbox which allows modifying information like Song Title, Scroll Speed, Characters/Stages, and starting BPM.
+ */
+// @:nullSafety // TODO: Fix null safety when used with HaxeUI build macros.
+@:access(funkin.ui.debug.charting.ChartEditorState)
+@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/toolboxes/event-data.xml"))
+class ChartEditorEventDataToolbox extends ChartEditorBaseToolbox
+{
+  var toolboxEventsEventKind:DropDown;
+  var toolboxEventsDataFrame:Frame;
+  var toolboxEventsDataGrid:Grid;
+
+  var _initializing:Bool = true;
+
+  public function new(chartEditorState2:ChartEditorState)
+  {
+    super(chartEditorState2);
+
+    initialize();
+
+    this.onDialogClosed = onClose;
+
+    this._initializing = false;
+  }
+
+  function onClose(event:UIEvent)
+  {
+    chartEditorState.menubarItemToggleToolboxEventData.selected = false;
+  }
+
+  function initialize():Void
+  {
+    toolboxEventsEventKind.dataSource = new ArrayDataSource();
+
+    var songEvents:Array<SongEvent> = SongEventParser.listEvents();
+
+    for (event in songEvents)
+    {
+      toolboxEventsEventKind.dataSource.add({text: event.getTitle(), value: event.id});
+    }
+
+    toolboxEventsEventKind.onChange = function(event:UIEvent) {
+      var eventType:String = event.data.value;
+
+      trace('ChartEditorToolboxHandler.buildToolboxEventDataLayout() - Event type changed: $eventType');
+
+      // Edit the event data to place.
+      chartEditorState.eventKindToPlace = eventType;
+
+      var schema:SongEventSchema = SongEventParser.getEventSchema(eventType);
+
+      if (schema == null)
+      {
+        trace('ChartEditorToolboxHandler.buildToolboxEventDataLayout() - Unknown event kind: $eventType');
+        return;
+      }
+
+      buildEventDataFormFromSchema(toolboxEventsDataGrid, schema);
+
+      if (!_initializing && chartEditorState.currentEventSelection.length > 0)
+      {
+        // Edit the event data of any selected events.
+        for (event in chartEditorState.currentEventSelection)
+        {
+          event.event = chartEditorState.eventKindToPlace;
+          event.value = chartEditorState.eventDataToPlace;
+        }
+        chartEditorState.saveDataDirty = true;
+        chartEditorState.noteDisplayDirty = true;
+        chartEditorState.notePreviewDirty = true;
+      }
+    }
+    toolboxEventsEventKind.value = chartEditorState.eventKindToPlace;
+  }
+
+  public override function refresh():Void
+  {
+    super.refresh();
+
+    toolboxEventsEventKind.value = chartEditorState.eventKindToPlace;
+
+    for (pair in chartEditorState.eventDataToPlace.keyValueIterator())
+    {
+      var fieldId:String = pair.key;
+      var value:Null<Dynamic> = pair.value;
+
+      var field:Component = toolboxEventsDataGrid.findComponent(fieldId);
+
+      if (field == null)
+      {
+        throw 'ChartEditorToolboxHandler.refresh() - Field "${fieldId}" does not exist in the event data form.';
+      }
+      else
+      {
+        switch (field)
+        {
+          case Std.isOfType(_, NumberStepper) => true:
+            var numberStepper:NumberStepper = cast field;
+            numberStepper.value = value;
+          case Std.isOfType(_, CheckBox) => true:
+            var checkBox:CheckBox = cast field;
+            checkBox.selected = value;
+          case Std.isOfType(_, DropDown) => true:
+            var dropDown:DropDown = cast field;
+            dropDown.value = value;
+          case Std.isOfType(_, TextField) => true:
+            var textField:TextField = cast field;
+            textField.text = value;
+          default:
+            throw 'ChartEditorToolboxHandler.refresh() - Field "${fieldId}" is of unknown type "${Type.getClassName(Type.getClass(field))}".';
+        }
+      }
+    }
+  }
+
+  function buildEventDataFormFromSchema(target:Box, schema:SongEventSchema):Void
+  {
+    trace(schema);
+    // Clear the frame.
+    target.removeAllComponents();
+
+    chartEditorState.eventDataToPlace = {};
+
+    for (field in schema)
+    {
+      if (field == null) continue;
+
+      // Add a label for the data field.
+      var label:Label = new Label();
+      label.text = field.title;
+      label.verticalAlign = "center";
+      target.addComponent(label);
+
+      // Add an input field for the data field.
+      var input:Component;
+      switch (field.type)
+      {
+        case INTEGER:
+          var numberStepper:NumberStepper = new NumberStepper();
+          numberStepper.id = field.name;
+          numberStepper.step = field.step ?? 1.0;
+          numberStepper.min = field.min ?? 0.0;
+          numberStepper.max = field.max ?? 10.0;
+          if (field.defaultValue != null) numberStepper.value = field.defaultValue;
+          input = numberStepper;
+        case FLOAT:
+          var numberStepper:NumberStepper = new NumberStepper();
+          numberStepper.id = field.name;
+          numberStepper.step = field.step ?? 0.1;
+          if (field.min != null) numberStepper.min = field.min;
+          if (field.max != null) numberStepper.max = field.max;
+          if (field.defaultValue != null) numberStepper.value = field.defaultValue;
+          input = numberStepper;
+        case BOOL:
+          var checkBox:CheckBox = new CheckBox();
+          checkBox.id = field.name;
+          if (field.defaultValue != null) checkBox.selected = field.defaultValue;
+          input = checkBox;
+        case ENUM:
+          var dropDown:DropDown = new DropDown();
+          dropDown.id = field.name;
+          dropDown.width = 200.0;
+          dropDown.dataSource = new ArrayDataSource();
+
+          if (field.keys == null) throw 'Field "${field.name}" is of Enum type but has no keys.';
+
+          // Add entries to the dropdown.
+
+          for (optionName in field.keys.keys())
+          {
+            var optionValue:Null<Dynamic> = field.keys.get(optionName);
+            trace('$optionName : $optionValue');
+            dropDown.dataSource.add({value: optionValue, text: optionName});
+          }
+
+          dropDown.value = field.defaultValue;
+
+          input = dropDown;
+        case STRING:
+          input = new TextField();
+          input.id = field.name;
+          if (field.defaultValue != null) input.text = field.defaultValue;
+        default:
+          // Unknown type. Display a label that proclaims the type so we can debug it.
+          input = new Label();
+          input.id = field.name;
+          input.text = field.type;
+      }
+
+      target.addComponent(input);
+
+      // Update the value of the event data.
+      input.onChange = function(event:UIEvent) {
+        var value = event.target.value;
+        if (field.type == ENUM)
+        {
+          value = event.target.value.value;
+        }
+
+        trace('ChartEditorToolboxHandler.buildEventDataFormFromSchema() - ${event.target.id} = ${value}');
+
+        // Edit the event data to place.
+        if (value == null)
+        {
+          chartEditorState.eventDataToPlace.remove(event.target.id);
+        }
+        else
+        {
+          chartEditorState.eventDataToPlace.set(event.target.id, value);
+        }
+
+        // Edit the event data of any existing events.
+        if (!_initializing && chartEditorState.currentEventSelection.length > 0)
+        {
+          for (event in chartEditorState.currentEventSelection)
+          {
+            event.event = chartEditorState.eventKindToPlace;
+            event.value = chartEditorState.eventDataToPlace;
+          }
+          chartEditorState.saveDataDirty = true;
+          chartEditorState.noteDisplayDirty = true;
+          chartEditorState.notePreviewDirty = true;
+        }
+      }
+    }
+  }
+
+  public static function build(chartEditorState:ChartEditorState):ChartEditorEventDataToolbox
+  {
+    return new ChartEditorEventDataToolbox(chartEditorState);
+  }
+}
diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx
index bc9384cf3..3f3c825ce 100644
--- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx
+++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx
@@ -162,6 +162,8 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox
 
   public override function refresh():Void
   {
+    super.refresh();
+
     inputSongName.value = chartEditorState.currentSongMetadata.songName;
     inputSongArtist.value = chartEditorState.currentSongMetadata.artist;
     inputStage.value = chartEditorState.currentSongMetadata.playData.stage;
diff --git a/source/funkin/util/plugins/EvacuateDebugPlugin.hx b/source/funkin/util/plugins/EvacuateDebugPlugin.hx
new file mode 100644
index 000000000..1803c25ba
--- /dev/null
+++ b/source/funkin/util/plugins/EvacuateDebugPlugin.hx
@@ -0,0 +1,35 @@
+package funkin.util.plugins;
+
+import flixel.FlxBasic;
+
+/**
+ * A plugin which adds functionality to press `F4` to immediately transition to the main menu.
+ * This is useful for debugging or if you get softlocked or something.
+ */
+class EvacuateDebugPlugin extends FlxBasic
+{
+  public function new()
+  {
+    super();
+  }
+
+  public static function initialize():Void
+  {
+    FlxG.plugins.addPlugin(new EvacuateDebugPlugin());
+  }
+
+  public override function update(elapsed:Float):Void
+  {
+    super.update(elapsed);
+
+    if (FlxG.keys.justPressed.F4)
+    {
+      FlxG.switchState(new funkin.ui.mainmenu.MainMenuState());
+    }
+  }
+
+  public override function destroy():Void
+  {
+    super.destroy();
+  }
+}
diff --git a/source/funkin/util/plugins/README.md b/source/funkin/util/plugins/README.md
new file mode 100644
index 000000000..fe87d36e5
--- /dev/null
+++ b/source/funkin/util/plugins/README.md
@@ -0,0 +1,5 @@
+# funkin.util.plugins
+
+Flixel plugins are objects with `update()` functions that are called from every state.
+
+See: https://github.com/HaxeFlixel/flixel/blob/dev/flixel/system/frontEnds/PluginFrontEnd.hx
diff --git a/source/funkin/util/plugins/ReloadAssetsDebugPlugin.hx b/source/funkin/util/plugins/ReloadAssetsDebugPlugin.hx
new file mode 100644
index 000000000..a43317cce
--- /dev/null
+++ b/source/funkin/util/plugins/ReloadAssetsDebugPlugin.hx
@@ -0,0 +1,38 @@
+package funkin.util.plugins;
+
+import flixel.FlxBasic;
+
+/**
+ * A plugin which adds functionality to press `F5` to reload all game assets, then reload the current state.
+ * This is useful for hot reloading assets during development.
+ */
+class ReloadAssetsDebugPlugin extends FlxBasic
+{
+  public function new()
+  {
+    super();
+  }
+
+  public static function initialize():Void
+  {
+    FlxG.plugins.addPlugin(new ReloadAssetsDebugPlugin());
+  }
+
+  public override function update(elapsed:Float):Void
+  {
+    super.update(elapsed);
+
+    if (FlxG.keys.justPressed.F5)
+    {
+      funkin.modding.PolymodHandler.forceReloadAssets();
+
+      // Create a new instance of the current state, so old data is cleared.
+      FlxG.resetState();
+    }
+  }
+
+  public override function destroy():Void
+  {
+    super.destroy();
+  }
+}
diff --git a/source/funkin/util/plugins/WatchPlugin.hx b/source/funkin/util/plugins/WatchPlugin.hx
new file mode 100644
index 000000000..5d718c2e0
--- /dev/null
+++ b/source/funkin/util/plugins/WatchPlugin.hx
@@ -0,0 +1,38 @@
+package funkin.util.plugins;
+
+import flixel.FlxBasic;
+
+/**
+ * A plugin which adds functionality to display several universally important values
+ * in the Flixel variable watch window.
+ */
+class WatchPlugin extends FlxBasic
+{
+  public function new()
+  {
+    super();
+  }
+
+  public static function initialize():Void
+  {
+    FlxG.plugins.addPlugin(new WatchPlugin());
+  }
+
+  public override function update(elapsed:Float):Void
+  {
+    super.update(elapsed);
+
+    FlxG.watch.addQuick("songPosition", Conductor.songPosition);
+    FlxG.watch.addQuick("songPositionNoOffset", Conductor.songPosition + Conductor.instrumentalOffset);
+    FlxG.watch.addQuick("musicTime", FlxG.sound?.music?.time ?? 0.0);
+    FlxG.watch.addQuick("bpm", Conductor.bpm);
+    FlxG.watch.addQuick("currentMeasureTime", Conductor.currentMeasureTime);
+    FlxG.watch.addQuick("currentBeatTime", Conductor.currentBeatTime);
+    FlxG.watch.addQuick("currentStepTime", Conductor.currentStepTime);
+  }
+
+  public override function destroy():Void
+  {
+    super.destroy();
+  }
+}