From a3123cd64c00ee1c617328f3dd46bc6be7681de1 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Fri, 5 Jan 2024 02:35:41 -0500
Subject: [PATCH] Measure numbers next to ticks and support for initial time
 signature.

---
 assets                                        |  2 +-
 source/Main.hx                                |  1 +
 .../ui/debug/charting/ChartEditorState.hx     | 82 ++++++++++++-------
 .../components/ChartEditorMeasureTicks.hx     | 71 ++++++++++++++++
 .../handlers/ChartEditorAudioHandler.hx       |  2 +-
 .../handlers/ChartEditorDialogHandler.hx      |  1 +
 .../ChartEditorImportExportHandler.hx         |  1 +
 .../handlers/ChartEditorThemeHandler.hx       | 82 ++++++++++++++++---
 .../toolboxes/ChartEditorMetadataToolbox.hx   | 24 ++++++
 9 files changed, 225 insertions(+), 41 deletions(-)
 create mode 100644 source/funkin/ui/debug/charting/components/ChartEditorMeasureTicks.hx

diff --git a/assets b/assets
index 9ecc4d26f..d768f62af 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 9ecc4d26fe6b26f31782cccfcd7331bd8a318ce1
+Subproject commit d768f62af4966066ebe123ea511046d90692248d
diff --git a/source/Main.hx b/source/Main.hx
index 5fbb6747b..86e520e69 100644
--- a/source/Main.hx
+++ b/source/Main.hx
@@ -112,5 +112,6 @@ class Main extends Sprite
     Toolkit.theme = 'dark'; // don't be cringe
     Toolkit.autoScale = false;
     funkin.input.Cursor.registerHaxeUICursors();
+    haxe.ui.tooltips.ToolTipManager.defaultDelay = 200;
   }
 }
diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index 4f96fad69..569311e43 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -15,6 +15,7 @@ import flixel.group.FlxSpriteGroup;
 import flixel.input.keyboard.FlxKey;
 import flixel.math.FlxMath;
 import flixel.math.FlxPoint;
+import flixel.graphics.FlxGraphic;
 import flixel.math.FlxRect;
 import flixel.sound.FlxSound;
 import flixel.system.FlxAssets.FlxSoundAsset;
@@ -80,6 +81,7 @@ import funkin.ui.debug.charting.components.ChartEditorEventSprite;
 import funkin.ui.debug.charting.components.ChartEditorHoldNoteSprite;
 import funkin.ui.debug.charting.components.ChartEditorNotePreview;
 import funkin.ui.debug.charting.components.ChartEditorNoteSprite;
+import funkin.ui.debug.charting.components.ChartEditorMeasureTicks;
 import funkin.ui.debug.charting.components.ChartEditorPlaybarHead;
 import funkin.ui.debug.charting.components.ChartEditorSelectionSquareSprite;
 import funkin.ui.debug.charting.handlers.ChartEditorShortcutHandler;
@@ -168,7 +170,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
   /**
    * The width of the scroll area.
    */
-  public static final PLAYHEAD_SCROLL_AREA_WIDTH:Int = 12;
+  public static final PLAYHEAD_SCROLL_AREA_WIDTH:Int = Std.int(GRID_SIZE);
 
   /**
    * The height of the playhead, in pixels.
@@ -334,17 +336,17 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
     this.scrollPositionInPixels = value;
 
     // Move the grid sprite to the correct position.
-    if (gridTiledSprite != null && gridPlayheadScrollArea != null)
+    if (gridTiledSprite != null && measureTicks != null)
     {
       if (isViewDownscroll)
       {
         gridTiledSprite.y = -scrollPositionInPixels + (MENU_BAR_HEIGHT + GRID_TOP_PAD);
-        gridPlayheadScrollArea.y = gridTiledSprite.y;
+        measureTicks.y = gridTiledSprite.y;
       }
       else
       {
         gridTiledSprite.y = -scrollPositionInPixels + (MENU_BAR_HEIGHT + GRID_TOP_PAD);
-        gridPlayheadScrollArea.y = gridTiledSprite.y;
+        measureTicks.y = gridTiledSprite.y;
 
         if (audioVisGroup != null && audioVisGroup.playerVis != null)
         {
@@ -366,6 +368,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
     if (selectionBoxStartPos != null) selectionBoxStartPos.y -= diff;
     // Update the note preview viewport box.
     setNotePreviewViewportBounds(calculateNotePreviewViewportBounds());
+    // Update the measure tick display.
+    if (measureTicks != null) measureTicks.y = gridTiledSprite?.y ?? 0.0;
     return this.scrollPositionInPixels;
   }
 
@@ -1630,23 +1634,28 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
    */
   var notePreviewViewportBitmap:Null<BitmapData> = null;
 
+  /**r
+   * The IMAGE used for the measure ticks. Updated by ChartEditorThemeHandler.
+   */
+  var measureTickBitmap:Null<BitmapData> = null;
+
   /**
    * The tiled sprite used to display the grid.
    * The height is the length of the song, and scrolling is done by simply the sprite.
    */
   var gridTiledSprite:Null<FlxSprite> = null;
 
+  /**
+   * The measure ticks area. Includes the numbers and the background sprite.
+   */
+  var measureTicks:Null<ChartEditorMeasureTicks> = null;
+
   /**
    * The playhead representing the current position in the song.
    * Can move around on the grid independently of the view.
    */
   var gridPlayhead:FlxSpriteGroup = new FlxSpriteGroup();
 
-  /**
-   * The sprite for the scroll area under
-   */
-  var gridPlayheadScrollArea:Null<FlxSprite> = null;
-
   /**
    * A sprite used to indicate the note that will be placed on click.
    */
@@ -1868,6 +1877,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
     this.updateTheme();
 
     buildGrid();
+    buildMeasureTicks();
     buildNotePreview();
     buildSelectionBox();
 
@@ -2122,20 +2132,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
 
     buildNoteGroup();
 
-    gridPlayheadScrollArea = new FlxSprite(0, 0);
-    gridPlayheadScrollArea.makeGraphic(10, 10, PLAYHEAD_SCROLL_AREA_COLOR); // Make it 10x10px and then scale it as needed.
-    add(gridPlayheadScrollArea);
-    gridPlayheadScrollArea.setGraphicSize(PLAYHEAD_SCROLL_AREA_WIDTH, 3000);
-    gridPlayheadScrollArea.updateHitbox();
-    gridPlayheadScrollArea.x = gridTiledSprite.x - PLAYHEAD_SCROLL_AREA_WIDTH;
-    gridPlayheadScrollArea.y = MENU_BAR_HEIGHT + GRID_TOP_PAD;
-    gridPlayheadScrollArea.zIndex = 25;
-
     // The playhead that show the current position in the song.
     add(gridPlayhead);
     gridPlayhead.zIndex = 30;
 
-    var playheadWidth:Int = GRID_SIZE * (STRUMLINE_SIZE * 2 + 1) + (PLAYHEAD_SCROLL_AREA_WIDTH * 2);
+    var playheadWidth:Int = GRID_SIZE * (STRUMLINE_SIZE * 2 + 1) + (PLAYHEAD_SCROLL_AREA_WIDTH);
     var playheadBaseYPos:Float = MENU_BAR_HEIGHT + GRID_TOP_PAD;
     gridPlayhead.setPosition(gridTiledSprite.x, playheadBaseYPos);
     var playheadSprite:FlxSprite = new FlxSprite().makeGraphic(playheadWidth, PLAYHEAD_HEIGHT, PLAYHEAD_COLOR);
@@ -2166,11 +2167,22 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
     add(audioVisGroup);
   }
 
+  function buildMeasureTicks():Void
+  {
+    measureTicks = new ChartEditorMeasureTicks(this);
+    var measureTicksWidth = (GRID_SIZE);
+    measureTicks.x = gridTiledSprite.x - measureTicksWidth;
+    measureTicks.y = MENU_BAR_HEIGHT + GRID_TOP_PAD;
+    measureTicks.zIndex = 20;
+
+    add(measureTicks);
+  }
+
   function buildNotePreview():Void
   {
     var height:Int = FlxG.height - MENU_BAR_HEIGHT - GRID_TOP_PAD - PLAYBAR_HEIGHT - GRID_TOP_PAD - GRID_TOP_PAD;
     notePreview = new ChartEditorNotePreview(height);
-    notePreview.x = 350;
+    notePreview.x = 320;
     notePreview.y = MENU_BAR_HEIGHT + GRID_TOP_PAD;
     add(notePreview);
 
@@ -2250,6 +2262,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
       bounds.height = MIN_HEIGHT;
     }
 
+    trace('Note preview viewport bounds: ' + bounds.toString());
+
     return bounds;
   }
 
@@ -2828,7 +2842,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
 
     if (metronomeVolume > 0.0 && this.subState == null && (audioInstTrack != null && audioInstTrack.isPlaying))
     {
-      playMetronomeTick(Conductor.currentBeat % 4 == 0);
+      playMetronomeTick(Conductor.currentBeat % Conductor.beatsPerMeasure == 0);
     }
 
     return true;
@@ -3533,7 +3547,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
         {
           scrollAnchorScreenPos = null;
         }
-        else if (gridPlayheadScrollArea != null && FlxG.mouse.overlaps(gridPlayheadScrollArea) && !isCursorOverHaxeUI)
+        else if (measureTicks != null && FlxG.mouse.overlaps(measureTicks) && !isCursorOverHaxeUI)
         {
           gridPlayheadScrollAreaPressed = true;
         }
@@ -4211,7 +4225,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
             {
               targetCursorMode = Pointer;
             }
-            else if (gridPlayheadScrollArea != null && FlxG.mouse.overlaps(gridPlayheadScrollArea))
+            else if (measureTicks != null && FlxG.mouse.overlaps(measureTicks))
             {
               targetCursorMode = Pointer;
             }
@@ -4505,7 +4519,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
     // Visibly center the Dad health icon.
     if (healthIconDad != null)
     {
-      healthIconDad.x = (gridTiledSprite == null) ? (0) : (gridTiledSprite.x - 45 - (healthIconDad.width / 2));
+      healthIconDad.x = (gridTiledSprite == null) ? (0) : (gridTiledSprite.x - 75 - (healthIconDad.width / 2));
       healthIconDad.y = (gridTiledSprite == null) ? (0) : (MENU_BAR_HEIGHT + GRID_TOP_PAD + 30 - (healthIconDad.height / 2));
     }
   }
@@ -4991,11 +5005,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
 
   function onSongLengthChanged():Void
   {
-    if (gridTiledSprite != null) gridTiledSprite.height = songLengthInPixels;
-    if (gridPlayheadScrollArea != null)
+    if (gridTiledSprite != null)
     {
-      gridPlayheadScrollArea.setGraphicSize(Std.int(gridPlayheadScrollArea.width), songLengthInPixels);
-      gridPlayheadScrollArea.updateHitbox();
+      gridTiledSprite.height = songLengthInPixels;
+    }
+    if (measureTicks != null)
+    {
+      measureTicks.setHeight(songLengthInPixels);
     }
 
     // Remove any notes past the end of the song.
@@ -5103,6 +5119,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
         selectedDifficulty = prevDifficulty;
 
         Conductor.mapTimeChanges(this.currentSongMetadata.timeChanges);
+        updateTimeSignature();
 
         refreshDifficultyTreeSelection();
         this.refreshToolbox(CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
@@ -5242,6 +5259,14 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
     if (audioVocalTrackGroup != null) audioVocalTrackGroup.volume = vocalTargetVolume;
   }
 
+  function updateTimeSignature():Void
+  {
+    // Redo the grid bitmap to be 4/4.
+    this.updateTheme();
+    gridTiledSprite.loadGraphic(gridBitmap);
+    measureTicks.reloadTickBitmap();
+  }
+
   /**
    * HAXEUI FUNCTIONS
    */
@@ -5354,6 +5379,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
     if (notePreviewViewportBoundsDirty)
     {
       setNotePreviewViewportBounds(calculateNotePreviewViewportBounds());
+      notePreviewViewportBoundsDirty = false;
     }
   }
 
diff --git a/source/funkin/ui/debug/charting/components/ChartEditorMeasureTicks.hx b/source/funkin/ui/debug/charting/components/ChartEditorMeasureTicks.hx
new file mode 100644
index 000000000..38ac30236
--- /dev/null
+++ b/source/funkin/ui/debug/charting/components/ChartEditorMeasureTicks.hx
@@ -0,0 +1,71 @@
+package funkin.ui.debug.charting.components;
+
+import flixel.FlxSprite;
+import flixel.addons.display.FlxTiledSprite;
+import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
+import flixel.text.FlxText;
+import flixel.util.FlxColor;
+
+@:access(funkin.ui.debug.charting.ChartEditorState)
+class ChartEditorMeasureTicks extends FlxTypedSpriteGroup<FlxSprite>
+{
+  var chartEditorState:ChartEditorState;
+
+  var tickTiledSprite:FlxTiledSprite;
+  var measureNumber:FlxText;
+
+  override function set_y(value:Float):Float
+  {
+    var result = super.set_y(value);
+
+    updateMeasureNumber();
+
+    return result;
+  }
+
+  public function new(chartEditorState:ChartEditorState)
+  {
+    super();
+
+    this.chartEditorState = chartEditorState;
+
+    tickTiledSprite = new FlxTiledSprite(chartEditorState.measureTickBitmap, chartEditorState.measureTickBitmap.width, 1000, false, true);
+    add(tickTiledSprite);
+
+    measureNumber = new FlxText(0, 0, ChartEditorState.GRID_SIZE, "1");
+    measureNumber.setFormat(Paths.font('vcr.ttf'), 20, FlxColor.WHITE);
+    measureNumber.borderStyle = FlxTextBorderStyle.OUTLINE;
+    measureNumber.borderColor = FlxColor.BLACK;
+    add(measureNumber);
+  }
+
+  public function reloadTickBitmap():Void
+  {
+    tickTiledSprite.loadGraphic(chartEditorState.measureTickBitmap);
+  }
+
+  /**
+   * At time of writing, we only have to manipulate one measure number because we can only see one measure at a time.
+   */
+  function updateMeasureNumber()
+  {
+    if (measureNumber == null) return;
+
+    var viewTopPosition = 0 - this.y;
+    var viewHeight = FlxG.height - ChartEditorState.MENU_BAR_HEIGHT - ChartEditorState.PLAYBAR_HEIGHT;
+    var viewBottomPosition = viewTopPosition + viewHeight;
+
+    var measureNumberInViewport = Math.floor(viewTopPosition / ChartEditorState.GRID_SIZE / Conductor.stepsPerMeasure) + 1;
+    var measureNumberPosition = measureNumberInViewport * ChartEditorState.GRID_SIZE * Conductor.stepsPerMeasure;
+
+    measureNumber.text = '${measureNumberInViewport + 1}';
+    measureNumber.y = measureNumberPosition + this.y;
+
+    // trace(measureNumber.text + ' at ' + measureNumber.y);
+  }
+
+  public function setHeight(songLengthInPixels:Float):Void
+  {
+    tickTiledSprite.height = songLengthInPixels;
+  }
+}
diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx
index 990ab41ae..4a45d454c 100644
--- a/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx
+++ b/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx
@@ -194,7 +194,7 @@ class ChartEditorAudioHandler
         case DAD:
           state.audioVocalTrackGroup.addOpponentVoice(vocalTrack);
           state.audioVisGroup.addOpponentVis(vocalTrack);
-          state.audioVisGroup.opponentVis.x = 435;
+          state.audioVisGroup.opponentVis.x = 405;
 
           state.audioVisGroup.opponentVis.realtimeVisLenght = Conductor.getStepTimeInMs(16) * 0.00195;
           state.audioVisGroup.opponentVis.daHeight = (ChartEditorState.GRID_SIZE) * 16;
diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx
index 2ede1a39f..175b0460a 100644
--- a/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx
+++ b/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx
@@ -686,6 +686,7 @@ class ChartEditorDialogHandler
 
       Conductor.instrumentalOffset = state.currentInstrumentalOffset; // Loads from the metadata.
       Conductor.mapTimeChanges(state.currentSongMetadata.timeChanges);
+      state.updateTimeSignature();
 
       state.selectedVariation = Constants.DEFAULT_VARIATION;
       state.selectedDifficulty = state.availableDifficulties[0];
diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx
index 267d2208a..63f8e8c71 100644
--- a/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx
+++ b/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx
@@ -117,6 +117,7 @@ class ChartEditorImportExportHandler
     Conductor.forceBPM(null); // Disable the forced BPM.
     Conductor.instrumentalOffset = state.currentInstrumentalOffset; // Loads from the metadata.
     Conductor.mapTimeChanges(state.currentSongMetadata.timeChanges);
+    state.updateTimeSignature();
 
     state.notePreviewDirty = true;
     state.notePreviewViewportBoundsDirty = true;
diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorThemeHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorThemeHandler.hx
index 4197ebdd3..020df566c 100644
--- a/source/funkin/ui/debug/charting/handlers/ChartEditorThemeHandler.hx
+++ b/source/funkin/ui/debug/charting/handlers/ChartEditorThemeHandler.hx
@@ -81,6 +81,7 @@ class ChartEditorThemeHandler
   {
     updateBackground(state);
     updateGridBitmap(state);
+    updateMeasureTicks(state);
     updateSelectionSquare(state);
     updateNotePreview(state);
   }
@@ -207,9 +208,6 @@ class ChartEditorThemeHandler
       }
     }
 
-    // Divider at top
-    state.gridBitmap.fillRect(new Rectangle(0, 0, state.gridBitmap.width, GRID_MEASURE_DIVIDER_WIDTH / 2), gridMeasureDividerColor);
-
     // Draw vertical dividers between the strumlines.
 
     var gridStrumlineDividerColor:FlxColor = switch (state.currentTheme)
@@ -233,6 +231,61 @@ class ChartEditorThemeHandler
     // Else, gridTiledSprite will be built later.
   }
 
+  static function updateMeasureTicks(state:ChartEditorState):Void
+  {
+    var measureTickWidth:Int = 6;
+    var beatTickWidth:Int = 4;
+    var stepTickWidth:Int = 2;
+
+    // Draw the measure ticks.
+    var ticksWidth:Int = Std.int(ChartEditorState.GRID_SIZE); // 1 grid squares wide.
+    var ticksHeight:Int = Std.int(ChartEditorState.GRID_SIZE * Conductor.stepsPerMeasure); // 1 measure tall.
+    state.measureTickBitmap = new BitmapData(ticksWidth, ticksHeight, true);
+    state.measureTickBitmap.fillRect(new Rectangle(0, 0, ticksWidth, ticksHeight), GRID_BEAT_DIVIDER_COLOR_DARK);
+
+    // Draw the measure ticks.
+    state.measureTickBitmap.fillRect(new Rectangle(0, 0, state.measureTickBitmap.width, measureTickWidth / 2), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
+    var bottomTickY:Float = state.measureTickBitmap.height - (measureTickWidth / 2);
+    state.measureTickBitmap.fillRect(new Rectangle(0, bottomTickY, state.measureTickBitmap.width, measureTickWidth / 2), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
+
+    // Draw the beat ticks.
+    var beatTick2Y:Float = state.measureTickBitmap.height * 1 / Conductor.beatsPerMeasure - (beatTickWidth / 2);
+    var beatTick3Y:Float = state.measureTickBitmap.height * 2 / Conductor.beatsPerMeasure - (beatTickWidth / 2);
+    var beatTick4Y:Float = state.measureTickBitmap.height * 3 / Conductor.beatsPerMeasure - (beatTickWidth / 2);
+    var beatTickLength:Float = state.measureTickBitmap.width * 2 / 3;
+    state.measureTickBitmap.fillRect(new Rectangle(0, beatTick2Y, beatTickLength, beatTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
+    state.measureTickBitmap.fillRect(new Rectangle(0, beatTick3Y, beatTickLength, beatTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
+    state.measureTickBitmap.fillRect(new Rectangle(0, beatTick4Y, beatTickLength, beatTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
+
+    // Draw the step ticks.
+    // TODO: Make this a loop or something.
+    var stepTick2Y:Float = state.measureTickBitmap.height * 1 / Conductor.stepsPerMeasure - (stepTickWidth / 2);
+    var stepTick3Y:Float = state.measureTickBitmap.height * 2 / Conductor.stepsPerMeasure - (stepTickWidth / 2);
+    var stepTick4Y:Float = state.measureTickBitmap.height * 3 / Conductor.stepsPerMeasure - (stepTickWidth / 2);
+    var stepTick6Y:Float = state.measureTickBitmap.height * 5 / Conductor.stepsPerMeasure - (stepTickWidth / 2);
+    var stepTick7Y:Float = state.measureTickBitmap.height * 6 / Conductor.stepsPerMeasure - (stepTickWidth / 2);
+    var stepTick8Y:Float = state.measureTickBitmap.height * 7 / Conductor.stepsPerMeasure - (stepTickWidth / 2);
+    var stepTick10Y:Float = state.measureTickBitmap.height * 9 / Conductor.stepsPerMeasure - (stepTickWidth / 2);
+    var stepTick11Y:Float = state.measureTickBitmap.height * 10 / Conductor.stepsPerMeasure - (stepTickWidth / 2);
+    var stepTick12Y:Float = state.measureTickBitmap.height * 11 / Conductor.stepsPerMeasure - (stepTickWidth / 2);
+    var stepTick14Y:Float = state.measureTickBitmap.height * 13 / Conductor.stepsPerMeasure - (stepTickWidth / 2);
+    var stepTick15Y:Float = state.measureTickBitmap.height * 14 / Conductor.stepsPerMeasure - (stepTickWidth / 2);
+    var stepTick16Y:Float = state.measureTickBitmap.height * 15 / Conductor.stepsPerMeasure - (stepTickWidth / 2);
+    var stepTickLength:Float = state.measureTickBitmap.width * 1 / 3;
+    state.measureTickBitmap.fillRect(new Rectangle(0, stepTick2Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
+    state.measureTickBitmap.fillRect(new Rectangle(0, stepTick3Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
+    state.measureTickBitmap.fillRect(new Rectangle(0, stepTick4Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
+    state.measureTickBitmap.fillRect(new Rectangle(0, stepTick6Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
+    state.measureTickBitmap.fillRect(new Rectangle(0, stepTick7Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
+    state.measureTickBitmap.fillRect(new Rectangle(0, stepTick8Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
+    state.measureTickBitmap.fillRect(new Rectangle(0, stepTick10Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
+    state.measureTickBitmap.fillRect(new Rectangle(0, stepTick11Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
+    state.measureTickBitmap.fillRect(new Rectangle(0, stepTick12Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
+    state.measureTickBitmap.fillRect(new Rectangle(0, stepTick14Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
+    state.measureTickBitmap.fillRect(new Rectangle(0, stepTick15Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
+    state.measureTickBitmap.fillRect(new Rectangle(0, stepTick16Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
+  }
+
   static function updateSelectionSquare(state:ChartEditorState):Void
   {
     var selectionSquareBorderColor:FlxColor = switch (state.currentTheme)
@@ -289,14 +342,21 @@ class ChartEditorThemeHandler
       ChartEditorState.GRID_SIZE - (SELECTION_SQUARE_BORDER_WIDTH * 2), ChartEditorState.GRID_SIZE - (SELECTION_SQUARE_BORDER_WIDTH * 2)),
       viewportFillColor);
 
-    state.notePreviewViewport = new FlxSliceSprite(state.notePreviewViewportBitmap,
-      new FlxRect(SELECTION_SQUARE_BORDER_WIDTH
-        + 1, SELECTION_SQUARE_BORDER_WIDTH
-        + 1, ChartEditorState.GRID_SIZE
-        - (2 * SELECTION_SQUARE_BORDER_WIDTH + 2),
-        ChartEditorState.GRID_SIZE
-        - (2 * SELECTION_SQUARE_BORDER_WIDTH + 2)),
-      32, 32);
+    if (state.notePreviewViewport != null)
+    {
+      state.notePreviewViewport.loadGraphic(state.notePreviewViewportBitmap);
+    }
+    else
+    {
+      state.notePreviewViewport = new FlxSliceSprite(state.notePreviewViewportBitmap,
+        new FlxRect(SELECTION_SQUARE_BORDER_WIDTH
+          + 1, SELECTION_SQUARE_BORDER_WIDTH
+          + 1,
+          ChartEditorState.GRID_SIZE
+          - (2 * SELECTION_SQUARE_BORDER_WIDTH + 2), ChartEditorState.GRID_SIZE
+          - (2 * SELECTION_SQUARE_BORDER_WIDTH + 2)),
+        32, 32);
+    }
   }
 
   public static function buildPlayheadBlock():FlxSprite
diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx
index bc9384cf3..3535f5113 100644
--- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx
+++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx
@@ -116,6 +116,26 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox
       }
     };
 
+    inputTimeSignature.onChange = function(event:UIEvent) {
+      var timeSignatureStr:String = event.data.text;
+      var timeSignature = timeSignatureStr.split('/');
+      if (timeSignature.length != 2) return;
+
+      var timeSignatureNum:Int = Std.parseInt(timeSignature[0]);
+      var timeSignatureDen:Int = Std.parseInt(timeSignature[1]);
+
+      var previousTimeSignatureNum:Int = chartEditorState.currentSongMetadata.timeChanges[0].timeSignatureNum;
+      var previousTimeSignatureDen:Int = chartEditorState.currentSongMetadata.timeChanges[0].timeSignatureDen;
+      if (timeSignatureNum == previousTimeSignatureNum && timeSignatureDen == previousTimeSignatureDen) return;
+
+      chartEditorState.currentSongMetadata.timeChanges[0].timeSignatureNum = timeSignatureNum;
+      chartEditorState.currentSongMetadata.timeChanges[0].timeSignatureDen = timeSignatureDen;
+
+      trace('Time signature changed to ${timeSignatureNum}/${timeSignatureDen}');
+
+      chartEditorState.updateTimeSignature();
+    };
+
     inputOffsetInst.onChange = function(event:UIEvent) {
       if (event.value == null) return;
 
@@ -172,6 +192,10 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox
     frameVariation.text = 'Variation: ${chartEditorState.selectedVariation.toTitleCase()}';
     frameDifficulty.text = 'Difficulty: ${chartEditorState.selectedDifficulty.toTitleCase()}';
 
+    var currentTimeSignature = '${chartEditorState.currentSongMetadata.timeChanges[0].timeSignatureNum}/${chartEditorState.currentSongMetadata.timeChanges[0].timeSignatureDen}';
+    trace('Setting time signature to ${currentTimeSignature}');
+    inputTimeSignature.value = {id: currentTimeSignature, text: currentTimeSignature};
+
     var stageId:String = chartEditorState.currentSongMetadata.playData.stage;
     var stageData:Null<StageData> = StageDataParser.parseStageData(stageId);
     if (inputStage != null)