From d03a2f015795ef01155b861ba47a5a6387fe2fdf Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Mon, 14 Aug 2023 23:13:12 -0400
Subject: [PATCH] WIP on improving the difficulty toolbox

---
 .../ui/debug/charting/ChartEditorState.hx     | 172 ++++++++++++++++--
 .../charting/ChartEditorToolboxHandler.hx     |  89 +++++++++
 source/funkin/util/SortUtil.hx                |  10 +-
 3 files changed, 256 insertions(+), 15 deletions(-)

diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index da8dd2d86..f925d58ca 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -722,11 +722,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 +1003,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 +1279,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 +1469,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 +1618,7 @@ class ChartEditorState extends HaxeUIState
     handleToolboxes();
     handlePlaybar();
     handlePlayhead();
+    // handleNotePreview();
 
     handleFileKeybinds();
     handleEditKeybinds();
@@ -2755,7 +2790,95 @@ 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)
+    {
+      // 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();
+      }
+      else
+      {
+        // Go to previous difficulty in this variation.
+        var prevDifficulty = availableDifficulties[currentDifficultyIndex - 1];
+        selectedDifficulty = prevDifficulty;
+
+        refreshDifficultyTreeSelection();
+      }
+    }
+    else
+    {
+      // 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;
+      }
+      else
+      {
+        // Go to next difficulty in this variation.
+        var nextDifficulty = availableDifficulties[currentDifficultyIndex + 1];
+        selectedDifficulty = nextDifficulty;
+      }
+    }
+  }
 
   /**
    * Handle keybinds for the Test menu items.
@@ -2801,7 +2924,8 @@ 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;
 
       var variations = Reflect.copy(availableVariations);
@@ -2831,10 +2955,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.
@@ -2941,13 +3081,16 @@ class ChartEditorState extends HaxeUIState
     }
   }
 
-  function getCurrentTreeDifficultyNode():TreeViewNode
+  function getCurrentTreeDifficultyNode(?treeView:TreeView = null):TreeViewNode
   {
-    var difficultyToolbox:CollapsibleDialog = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT);
-    if (difficultyToolbox == null) return null;
+    if (treeView == null)
+    {
+      var difficultyToolbox:CollapsibleDialog = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT);
+      if (difficultyToolbox == null) return null;
 
-    var treeView:TreeView = difficultyToolbox.findComponent('difficultyToolboxTree');
-    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');
@@ -2956,6 +3099,10 @@ class ChartEditorState extends HaxeUIState
     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.
@@ -2966,7 +3113,7 @@ class ChartEditorState extends HaxeUIState
     {
       trace('No target node!');
       // Reset the user's selection.
-      treeView.selectedNode = getCurrentTreeDifficultyNode();
+      treeView.selectedNode = getCurrentTreeDifficultyNode(treeView);
       return;
     }
 
@@ -2981,13 +3128,14 @@ class ChartEditorState extends HaxeUIState
           trace('Changing difficulty to $variation:$difficulty');
           selectedVariation = variation;
           selectedDifficulty = difficulty;
+          // refreshDifficultyTreeSelection(treeView);
         }
       // case 'song':
       // case 'variation':
       default:
         // Reset the user's selection.
         trace('Selected wrong node type, resetting selection.');
-        treeView.selectedNode = getCurrentTreeDifficultyNode();
+        treeView.selectedNode = getCurrentTreeDifficultyNode(treeView);
     }
   }
 
diff --git a/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx b/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx
index d35323f1b..cd3172ebf 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;
@@ -36,6 +38,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 +62,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 +101,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 +235,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 +283,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 +332,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 +466,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);
@@ -512,6 +585,10 @@ class ChartEditorToolboxHandler
     return toolbox;
   }
 
+  static function onShowToolboxMetadata(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
+
+  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);
@@ -529,6 +606,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);
@@ -553,6 +634,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);
@@ -576,4 +661,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/SortUtil.hx b/source/funkin/util/SortUtil.hx
index 082de4b41..6f3b9c0fb 100644
--- a/source/funkin/util/SortUtil.hx
+++ b/source/funkin/util/SortUtil.hx
@@ -47,7 +47,7 @@ class SortUtil
    * 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. Defaults to `default`.
+   * @param defaultValue The value to prioritize.
    */
   public static function defaultThenAlphabetically(a:String, b:String, defaultValue:String):Int
   {
@@ -62,12 +62,16 @@ class SortUtil
    * 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 defaultValue The value to prioritize. Defaults to `default`.
+   * @param defaultValues The values to prioritize.
    */
   public static function defaultsThenAlphabetically(a:String, b:String, defaultValues:Array<String>):Int
   {
     if (a == b) return 0;
-    if (defaultValues.contains(a) && defaultValues.contains(b)) return alphabetically(a, b);
+    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);