[
  {
    "path": ".config/dotnet-tools.json",
    "content": "{\n  \"version\": 1,\n  \"isRoot\": true,\n  \"tools\": {\n    \"ppy.localisationanalyser.tools\": {\n      \"version\": \"2024.802.0\",\n      \"commands\": [\n        \"localisation\"\n      ]\n    },\n    \"jetbrains.resharper.globaltools\": {\n      \"version\": \"2025.1.3\",\n      \"commands\": [\n        \"jb\"\n      ]\n    },\n    \"nvika\": {\n      \"version\": \"4.0.0\",\n      \"commands\": [\n        \"nvika\"\n      ]\n    },\n    \"codefilesanity\": {\n      \"version\": \"0.0.37\",\n      \"commands\": [\n        \"CodeFileSanity\"\n      ]\n    }\n  }\n}"
  },
  {
    "path": ".editorconfig",
    "content": "# EditorConfig is awesome: http://editorconfig.org\nroot = true\n\n[*.{csproj,props,targets}]\ncharset = utf-8-bom\nend_of_line = crlf\ninsert_final_newline = true\nindent_style = space\nindent_size = 2\ntrim_trailing_whitespace = true\n\n[g_*.cs]\ngenerated_code = true\n\n[*.cs]\nend_of_line = crlf\ninsert_final_newline = true\nindent_style = space\nindent_size = 4\ntrim_trailing_whitespace = true\n\n#license header\nfile_header_template = Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\\nSee the LICENCE file in the repository root for full licence text.\n\n#Roslyn naming styles\n\n#PascalCase for public and protected members\ndotnet_naming_style.pascalcase.capitalization = pascal_case\ndotnet_naming_symbols.public_members.applicable_accessibilities = public,internal,protected,protected_internal,private_protected\ndotnet_naming_symbols.public_members.applicable_kinds = property,method,field,event\ndotnet_naming_rule.public_members_pascalcase.severity = error\ndotnet_naming_rule.public_members_pascalcase.symbols = public_members\ndotnet_naming_rule.public_members_pascalcase.style = pascalcase\n\n#camelCase for private members\ndotnet_naming_style.camelcase.capitalization = camel_case\n\ndotnet_naming_symbols.private_members.applicable_accessibilities = private\ndotnet_naming_symbols.private_members.applicable_kinds = property,method,field,event\ndotnet_naming_rule.private_members_camelcase.severity = warning\ndotnet_naming_rule.private_members_camelcase.symbols = private_members\ndotnet_naming_rule.private_members_camelcase.style = camelcase\n\ndotnet_naming_symbols.local_function.applicable_kinds = local_function\ndotnet_naming_rule.local_function_camelcase.severity = warning\ndotnet_naming_rule.local_function_camelcase.symbols = local_function\ndotnet_naming_rule.local_function_camelcase.style = camelcase\n\n#all_lower for private and local constants/static readonlys\ndotnet_naming_style.all_lower.capitalization = all_lower\ndotnet_naming_style.all_lower.word_separator = _\n\ndotnet_naming_symbols.private_constants.applicable_accessibilities = private\ndotnet_naming_symbols.private_constants.required_modifiers = const\ndotnet_naming_symbols.private_constants.applicable_kinds = field\ndotnet_naming_rule.private_const_all_lower.severity = warning\ndotnet_naming_rule.private_const_all_lower.symbols = private_constants\ndotnet_naming_rule.private_const_all_lower.style = all_lower\n\ndotnet_naming_symbols.private_static_readonly.applicable_accessibilities = private\ndotnet_naming_symbols.private_static_readonly.required_modifiers = static,readonly\ndotnet_naming_symbols.private_static_readonly.applicable_kinds = field\ndotnet_naming_rule.private_static_readonly_all_lower.severity = warning\ndotnet_naming_rule.private_static_readonly_all_lower.symbols = private_static_readonly\ndotnet_naming_rule.private_static_readonly_all_lower.style = all_lower\n\ndotnet_naming_symbols.local_constants.applicable_kinds = local\ndotnet_naming_symbols.local_constants.required_modifiers = const\ndotnet_naming_rule.local_const_all_lower.severity = warning\ndotnet_naming_rule.local_const_all_lower.symbols = local_constants\ndotnet_naming_rule.local_const_all_lower.style = all_lower\n\n#ALL_UPPER for non private constants/static readonlys\ndotnet_naming_style.all_upper.capitalization = all_upper\ndotnet_naming_style.all_upper.word_separator = _\n\ndotnet_naming_symbols.public_constants.applicable_accessibilities = public,internal,protected,protected_internal,private_protected\ndotnet_naming_symbols.public_constants.required_modifiers = const\ndotnet_naming_symbols.public_constants.applicable_kinds = field\ndotnet_naming_rule.public_const_all_upper.severity = warning\ndotnet_naming_rule.public_const_all_upper.symbols = public_constants\ndotnet_naming_rule.public_const_all_upper.style = all_upper\n\ndotnet_naming_symbols.public_static_readonly.applicable_accessibilities = public,internal,protected,protected_internal,private_protected\ndotnet_naming_symbols.public_static_readonly.required_modifiers = static,readonly\ndotnet_naming_symbols.public_static_readonly.applicable_kinds = field\ndotnet_naming_rule.public_static_readonly_all_upper.severity = warning\ndotnet_naming_rule.public_static_readonly_all_upper.symbols = public_static_readonly\ndotnet_naming_rule.public_static_readonly_all_upper.style = all_upper\n\n#Roslyn formating options\n\n#Formatting - indentation options\ncsharp_indent_case_contents = true\ncsharp_indent_case_contents_when_block = false\ncsharp_indent_labels = one_less_than_current\ncsharp_indent_switch_labels = true\n\n#Formatting - new line options\ncsharp_new_line_before_catch = true\ncsharp_new_line_before_else = true\ncsharp_new_line_before_finally = true\ncsharp_new_line_before_open_brace = all\n#csharp_new_line_before_members_in_anonymous_types = true\n#csharp_new_line_before_members_in_object_initializers = true # Currently no effect in VS/dotnet format (16.4), and makes Rider confusing\ncsharp_new_line_between_query_expression_clauses = true\n\n#Formatting - organize using options\ndotnet_sort_system_directives_first = true\n\n#Formatting - spacing options\ncsharp_space_after_cast = false\ncsharp_space_after_colon_in_inheritance_clause = true\ncsharp_space_after_keywords_in_control_flow_statements = true\ncsharp_space_before_colon_in_inheritance_clause = true\ncsharp_space_between_method_call_empty_parameter_list_parentheses = false\ncsharp_space_between_method_call_name_and_opening_parenthesis = false\ncsharp_space_between_method_call_parameter_list_parentheses = false\ncsharp_space_between_method_declaration_empty_parameter_list_parentheses = false\ncsharp_space_between_method_declaration_parameter_list_parentheses = false\n\n#Formatting - wrapping options\ncsharp_preserve_single_line_blocks = true\ncsharp_preserve_single_line_statements = true\n\n#Roslyn language styles\n\n#Style - this. qualification\ndotnet_style_qualification_for_field = false:warning\ndotnet_style_qualification_for_property = false:warning\ndotnet_style_qualification_for_method = false:warning\ndotnet_style_qualification_for_event = false:warning\n\n#Style - type names\ndotnet_style_predefined_type_for_locals_parameters_members = true:warning\ndotnet_style_predefined_type_for_member_access = true:warning\ncsharp_style_var_when_type_is_apparent = true:none\ncsharp_style_var_for_built_in_types = false:warning\ncsharp_style_var_elsewhere = true:silent\n\n#Style - modifiers\ndotnet_style_require_accessibility_modifiers = for_non_interface_members:warning\ncsharp_preferred_modifier_order = public,private,protected,internal,new,abstract,virtual,sealed,override,static,readonly,extern,unsafe,volatile,async:warning\n\n#Style - parentheses\n# Skipped because roslyn cannot separate +-*/ with << >>\n\n#Style - expression bodies\ncsharp_style_expression_bodied_accessors = true:warning\ncsharp_style_expression_bodied_constructors = false:none\ncsharp_style_expression_bodied_indexers = true:warning\ncsharp_style_expression_bodied_methods = false:silent\ncsharp_style_expression_bodied_operators = true:warning\ncsharp_style_expression_bodied_properties = true:warning\ncsharp_style_expression_bodied_local_functions = true:silent\n\n#Style - expression preferences\ndotnet_style_object_initializer = true:warning\ndotnet_style_collection_initializer = true:warning\ndotnet_style_prefer_inferred_anonymous_type_member_names = true:warning\ndotnet_style_prefer_auto_properties = true:warning\ndotnet_style_prefer_conditional_expression_over_assignment = true:silent\ndotnet_style_prefer_conditional_expression_over_return = true:silent\ndotnet_style_prefer_compound_assignment = true:warning\n\n#Style - null/type checks\ndotnet_style_coalesce_expression = true:warning\ndotnet_style_null_propagation = true:warning\ncsharp_style_pattern_matching_over_is_with_cast_check = true:warning\ncsharp_style_pattern_matching_over_as_with_null_check = true:warning\ncsharp_style_throw_expression = true:silent\ncsharp_style_conditional_delegate_call = true:warning\n\n#Style - unused\ndotnet_style_readonly_field = true:silent\ndotnet_code_quality_unused_parameters = non_public:silent\ncsharp_style_unused_value_expression_statement_preference = discard_variable:silent\ncsharp_style_unused_value_assignment_preference = discard_variable:warning\n\n#Style - variable declaration\ncsharp_style_inlined_variable_declaration = true:warning\ncsharp_style_deconstructed_variable_declaration = false:silent\n\n#Style - other C# 7.x features\ndotnet_style_prefer_inferred_tuple_names = true:warning\ncsharp_prefer_simple_default_expression = true:warning\ncsharp_style_pattern_local_over_anonymous_function = true:warning\ndotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent\n\n#Style - C# 8 features\ncsharp_prefer_static_local_function = true:warning\ncsharp_prefer_simple_using_statement = true:silent\ncsharp_style_prefer_index_operator = false:silent\ncsharp_style_prefer_range_operator = false:silent\ncsharp_style_prefer_switch_expression = false:none\n\n#Style - C# 10 features\ncsharp_style_namespace_declarations = file_scoped:suggestion\n\n#Style - C# 12 features\ncsharp_style_prefer_primary_constructors = false\n\n[*.{yaml,yml}]\ninsert_final_newline = true\nindent_style = space\nindent_size = 2\ntrim_trailing_whitespace = true\n\ndotnet_diagnostic.OLOC001.words_in_name = 5\ndotnet_diagnostic.OLOC001.license_header = // Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\\n// See the LICENCE file in the repository root for full licence text.\n"
  },
  {
    "path": ".git-blame-ignore-revs",
    "content": "# Partial everything\nc3d76bde9f3d78b420ab6a18b8191c00a503b2be\n# Make test project to the file-scope namespace\n3546a366f24d23dee1188758df0f8c6744dd1001\n# Make main project to the file-scope namespace\n5597711949c30a9699c4ab3957c0d98e6ff2212c\n# Apply the trailing comma in the whole solution.\n70fa847a40aa6a6c8ea3f1b277e4c2a23c8484a4"
  },
  {
    "path": ".gitattributes",
    "content": "# Autodetect text files and ensure that we normalise their\n# line endings to lf internally. When checked out they may\n# use different line endings.\n* text=auto\n\n# Check out with crlf (Windows) line endings\n*.sln text eol=crlf\n*.csproj text eol=crlf\n*.cs text diff=csharp eol=crlf\n*.resx text eol=crlf\n*.vsixmanifest text eol=crlf\npackages.config text eol=crlf\nApp.config text eol=crlf\n*.bat text eol=crlf\n*.cmd text eol=crlf\n*.snippet text eol=crlf\n*.manifest text eol=crlf\n*.licenseheader text eol=crlf\n\n# Check out with lf (UNIX) line endings\n*.sh text eol=lf\n.gitignore text eol=lf\n.gitattributes text eol=lf\n*.md text eol=lf\n.travis.yml text eol=lf"
  },
  {
    "path": ".github/CODEOWNERS",
    "content": "# License related\n/CodeAnalysis/      @andy840119\n/LICENSE            @andy840119\n/README.md          @andy840119\n/osu.licenseheader  @andy840119\n\n\n# CI config\n/.github/               @andy840119\n/Directory.Build.props  @andy840119\n/appveyor.yml           @andy840119\n/cake.config            @andy840119\n\n# Resource related\n/assets/                                      @andy840119\n/osu.Game.Rulesets.Karaoke/Resources/         @andy840119\n/osu.Game.Rulesets.Karaoke.Tests/Resources/   @andy840119\n\n# Editor or git config\n/.config/dotnet-tools.json                    @andy840119\n/.editorconfig                                @andy840119\n/.gitattributes                               @andy840119\n/.gitignore                                   @andy840119\n/osu.Game.Rulesets.Karaoke.sln                @andy840119\n/osu.Game.Rulesets.Karaoke.sln.DotSettings    @andy840119\n"
  },
  {
    "path": ".github/labeler.yml",
    "content": "localization:\n  - osu.Game.Rulesets.Karaoke/Localisation/*\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: .NET Core\n\non:\n  push:\n    branches: [ master ]\n  pull_request:\n    branches: [ master ]\n\njobs:\n  build:\n    name: Build and Test\n    runs-on: ubuntu-latest\n\n    steps:\n    - name: Checkout repository\n      uses: actions/checkout@v2\n    - name: Install dependencies\n      run: dotnet restore\n    - name: Build with .NET\n      run: dotnet build --no-restore --configuration Release\n    - name: Unit Tests\n      run: dotnet test --no-build --no-restore --configuration Release\n"
  },
  {
    "path": ".github/workflows/crowdin.yml",
    "content": "name: Crowdin Action\n\non:\n  push:\n    branches: [ master ]\n    paths:\n      - 'osu.Game.Rulesets.Karaoke/Localisation/**'\n\njobs:\n  generate-localization-file:\n    name: Generate the localization file\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v2\n      - name: Install dependencies\n        run: dotnet restore\n      - name: Install the localization tools.\n        run: dotnet tool restore\n      - name: Generate the localization file\n        run: dotnet localisation to-resx ./osu.Game.Rulesets.Karaoke/osu.Game.Rulesets.Karaoke.csproj --output ./crowdin\n\n      - name: Upload the localization into crowdin\n        uses: crowdin/github-action@1.4.9\n        with:\n          # upload the source to the target path of the https://github.com/karaoke-dev/karaoke-resources\n          # see the document in the https://support.crowdin.com/configuration-file/?q=dest\n          upload_sources: true\n          upload_sources_args: '--dest master/osu.Game.Rulesets.Karaoke.Resources/Localisation/%file_name%.%file_extension%'\n          source: crowdin/*.resx\n          # there's no translation can be uploaded in this repo, but we still need to give it a value.\n          translation: crowdin/%file_name%.%locale%.%file_extension%\n          # This is a numeric id, not to be confused with Crowdin API v1 \"project identifier\" string.\n          # See \"API v2\" on https://crowdin.com/project/<your-project>/settings#api\n          project_id: ${{ secrets.CROWDIN_PROJECT_ID }}\n          # A personal access token, not to be confused with Crowdin API v1 \"API key\".\n          # See https://crowdin.com/settings#api-key to generate a token.\n          token: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}\n          CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}\n\n      - name: Send notification after upload success or failed.\n        uses: mshick/add-pr-comment@v2\n        with:\n          if: always()\n          message: |\n            🚀🚀🚀 New localization string has been successfully uploaded to the crowdin 🚀🚀🚀\n            Go to https://crowdin.com/project/karaoke-dev to fill the translation.\n          message-failure: |\n            💥💥💥 New localization string uploaded failed to the crowdin. 💥💥💥\n            You can ignore this message or contact to the developer if you want to translate it now.\n"
  },
  {
    "path": ".github/workflows/dotnet-format.yml",
    "content": "name: Format check on pull request\non: pull_request\n\njobs:\n  dotnet-format:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout repo\n        uses: actions/checkout@v4\n\n      - name: Install .NET 8.0.x\n        uses: actions/setup-dotnet@v4\n        with:\n          dotnet-version: \"8.0.x\"\n\n      - name: Restore Tools\n        run: dotnet tool restore\n\n      - name: Restore Packages\n        run: dotnet restore osu.Game.Rulesets.Karaoke.sln\n\n      - name: Restore inspectcode cache\n        uses: actions/cache@v4\n        with:\n          path: ${{ github.workspace }}/inspectcode\n          key: inspectcode-${{ hashFiles('.config/dotnet-tools.json', '.github/workflows/ci.yml', 'osu*.sln', '.editorconfig', '.globalconfig', 'CodeAnalysis/*', '**/*.csproj', '**/*.props') }}\n\n      - name: Dotnet code style\n        # The EnforceCodeStyleInBuild might cause false positive errors, disabling.\n        # run: dotnet build -c Debug -warnaserror osu.Game.Rulesets.Karaoke.sln -p:EnforceCodeStyleInBuild=true\n        run: dotnet build -c Debug -warnaserror osu.Game.Rulesets.Karaoke.sln\n\n      - name: CodeFileSanity\n        run: |\n          exit_code=0\n          while read -r line; do\n            if [[ ! -z \"$line\" ]]; then\n              echo \"::error::$line\"\n              exit_code=1\n            fi\n          done <<< $(dotnet codefilesanity)\n          exit $exit_code\n\n      - name: InspectCode\n        # Still use XML output since vika's poor support for new formats\n        run: dotnet jb inspectcode $(pwd)/osu.Game.Rulesets.Karaoke.sln --no-build -f=\"xml\" --output=\"inspectcodereport.xml\" --caches-home=\"inspectcode\" --verbosity=WARN\n\n      - name: NVika\n        run: dotnet nvika parsereport \"${{github.workspace}}/inspectcodereport.xml\" --treatwarningsaserrors\n"
  },
  {
    "path": ".github/workflows/labeler.yml",
    "content": "name: Pull Request Labeler\n\non:\n  pull_request_target:\n    paths:\n      # we only add use this action for add the localization label for now, so run this action if localization changed.\n      - 'osu.Game.Rulesets.Karaoke/Localisation/**'\n\njobs:\n  triage:\n    permissions:\n      contents: read\n      pull-requests: write\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/labeler@v4\n        with:\n          repo-token: \"${{ secrets.GITHUB_TOKEN }}\"\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Tagged Release\non:\n  push:\n    tags: ['*']\n\njobs:\n  build:\n    name: Build and Create Release\n    runs-on: ubuntu-latest\n\n    steps:\n    - name: Checkout repository\n      uses: actions/checkout@v2\n    - name: Setup .NET 8.0.x\n      uses: actions/setup-dotnet@v3\n      with:\n        dotnet-version: \"8.0.x\"\n    - name: Fetch all tags\n      run: git fetch --depth=1 origin +refs/tags/*:refs/tags/*\n    - name: Get current tag\n      run: echo \"CURRENT_TAG=$(git describe --abbrev=0 --tags $(git rev-list --tags --max-count=1))\" >> $GITHUB_ENV\n    - name: Install dependencies\n      run: dotnet restore\n    - name: Build\n      run: dotnet build osu.Game.Rulesets.Karaoke --configuration Release -p:version=${{env.CURRENT_TAG}} --no-restore\n    - name: Create Release\n      id: create_release\n      uses: actions/create-release@latest\n      env:\n        GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}\n      with:\n        tag_name: ${{ github.ref }}\n        release_name: ${{ github.ref }}\n    - name: Zip the dlls\n      run: |\n        cd osu.Game.Rulesets.Karaoke/bin/Release/net8.0/DLLs\n        zip -r ../osu.Game.Rulesets.Karaoke.zip ./*\n    - name: Upload Release Asset\n      uses: softprops/action-gh-release@v1\n      with:\n        token: ${{ secrets.RELEASE_TOKEN }}\n        files: |\n          osu.Game.Rulesets.Karaoke/bin/Release/net8.0/osu.Game.Rulesets.Karaoke.zip\n        draft: true\n        body: |\n          Thank you for showing interest in this ruleset. This is a tagged release (${{ env.CURRENT_TAG }}).\n    - name: Generate changelog\n      run: |\n        sudo npm install github-release-notes -g\n        gren release -T ${{secrets.RELEASE_TOKEN}} --tags=${{env.CURRENT_TAG}} --override\n"
  },
  {
    "path": ".gitignore",
    "content": "## Ignore Visual Studio temporary files, build results, and\n## files generated by popular Visual Studio add-ons.\n\n# User-specific files\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# User-specific files (MonoDevelop/Xamarin Studio)\n*.userprefs\n\n# Build results\n[Dd]ebug/\n[Dd]ebugPublic/\n[Rr]elease/\n[Rr]eleases/\nbld/\n[Bb]in/\n[Oo]bj/\n[Ll]og/\n\n# Visual Studio 2015 cache/options directory\n.vs/\n# Uncomment if you have tasks that create the project's static files in wwwroot\n#wwwroot/\n\n# MSTest test Results\n[Tt]est[Rr]esult*/\n[Bb]uild[Ll]og.*\n\n# NUNIT\n*.VisualState.xml\nTestResult.xml\n\n# Build Results of an ATL Project\n[Dd]ebugPS/\n[Rr]eleasePS/\ndlldata.c\n\n# DNX\nproject.lock.json\nproject.fragment.lock.json\nartifacts/\n\n*_i.c\n*_p.c\n*_i.h\n*.ilk\n*.meta\n*.obj\n*.pch\n*.pdb\n*.pgc\n*.pgd\n*.rsp\n*.sbr\n*.tlb\n*.tli\n*.tlh\n*.tmp\n*.tmp_proj\n*.log\n*.vspscc\n*.vssscc\n.builds\n*.pidb\n*.svclog\n*.scc\n\n# Chutzpah Test files\n_Chutzpah*\n\n# Visual C++ cache files\nipch/\n*.aps\n*.ncb\n*.opendb\n*.opensdf\n*.sdf\n*.cachefile\n*.VC.db\n*.VC.VC.opendb\n\n# Visual Studio profiler\n*.psess\n*.vsp\n*.vspx\n*.sap\n\n# TFS 2012 Local Workspace\n$tf/\n\n# Guidance Automation Toolkit\n*.gpState\n\n# ReSharper is a .NET coding add-in\n_ReSharper*/\n*.[Rr]e[Ss]harper\n*.DotSettings.user\n\n# JustCode is a .NET coding add-in\n.JustCode\n\n# TeamCity is a build add-in\n_TeamCity*\n\n# DotCover is a Code Coverage Tool\n*.dotCover\n\n# NCrunch\n_NCrunch_*\n.*crunch*.local.xml\nnCrunchTemp_*\n\n# MightyMoose\n*.mm.*\nAutoTest.Net/\n\n# Web workbench (sass)\n.sass-cache/\n\n# Installshield output folder\n[Ee]xpress/\n\n# DocProject is a documentation generator add-in\nDocProject/buildhelp/\nDocProject/Help/*.HxT\nDocProject/Help/*.HxC\nDocProject/Help/*.hhc\nDocProject/Help/*.hhk\nDocProject/Help/*.hhp\nDocProject/Help/Html2\nDocProject/Help/html\n\n# Click-Once directory\npublish/\n\n# Publish Web Output\n*.[Pp]ublish.xml\n*.azurePubxml\n# TODO: Comment the next line if you want to checkin your web deploy settings\n# but database connection strings (with potential passwords) will be unencrypted\n*.pubxml\n*.publishproj\n\n# Microsoft Azure Web App publish settings. Comment the next line if you want to\n# checkin your Azure Web App publish settings, but sensitive information contained\n# in these scripts will be unencrypted\nPublishScripts/\n\n# NuGet Packages\n*.nupkg\n# The packages folder can be ignored because of Package Restore\n**/packages/*\n# except build/, which is used as an MSBuild target.\n!**/packages/build/\n# Uncomment if necessary however generally it will be regenerated when needed\n#!**/packages/repositories.config\n# NuGet v3's project.json files produces more ignoreable files\n*.nuget.props\n*.nuget.targets\n\n# Microsoft Azure Build Output\ncsx/\n*.build.csdef\n\n# Microsoft Azure Emulator\necf/\nrcf/\n\n# Windows Store app package directories and files\nAppPackages/\nBundleArtifacts/\nPackage.StoreAssociation.xml\n_pkginfo.txt\n\n# Visual Studio cache files\n# files ending in .cache can be ignored\n*.[Cc]ache\n# but keep track of directories ending in .cache\n!*.[Cc]ache/\n\n# Others\nClientBin/\n~$*\n*~\n*.dbmdl\n*.dbproj.schemaview\n*.pfx\n*.publishsettings\nnode_modules/\norleans.codegen.cs\nResource.designer.cs\n\n# Since there are multiple workflows, uncomment next line to ignore bower_components\n# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)\n#bower_components/\n\n# RIA/Silverlight projects\nGenerated_Code/\n\n# Backup & report files from converting an old project file\n# to a newer Visual Studio version. Backup files are not needed,\n# because we have git ;-)\n_UpgradeReport_Files/\nBackup*/\nUpgradeLog*.XML\nUpgradeLog*.htm\n\n# SQL Server files\n*.mdf\n*.ldf\n\n# Business Intelligence projects\n*.rdl.data\n*.bim.layout\n*.bim_*.settings\n\n# Microsoft Fakes\nFakesAssemblies/\n\n# GhostDoc plugin setting file\n*.GhostDoc.xml\n\n# Node.js Tools for Visual Studio\n.ntvs_analysis.dat\n\n# Visual Studio 6 build log\n*.plg\n\n# Visual Studio 6 workspace options file\n*.opt\n\n# Visual Studio LightSwitch build output\n**/*.HTMLClient/GeneratedArtifacts\n**/*.DesktopClient/GeneratedArtifacts\n**/*.DesktopClient/ModelManifest.xml\n**/*.Server/GeneratedArtifacts\n**/*.Server/ModelManifest.xml\n_Pvt_Extensions\n\n# Paket dependency manager\n.paket/paket.exe\npaket-files/\n\n# FAKE - F# Make\n.fake/\n\n# JetBrains Rider\n.idea/\n*.sln.iml\n\n# Python Tools for Visual Studio (PTVS)\n__pycache__/\n*.pyc\n\n# Cake #\n/tools/**\n/build/tools/**\n/build/temp/**\n\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/**/usage.statistics.xml\n.idea/**/dictionaries\n.idea/**/shelf\n\n# Generated files\n.idea/**/contentModel.xml\n\n# Sensitive or high-churn files\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n.idea/**/dbnavigator.xml\n\n# Gradle\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# Gradle and Maven with auto-import\n# When using Gradle or Maven with auto-import, you should exclude module files,\n# since they will be recreated, and may cause churn.  Uncomment if using\n# auto-import.\n.idea/modules.xml\n.idea/*.iml\n.idea/modules\n*.iml\n*.ipr\n\n# CMake\ncmake-build-*/\n\n# Mongo Explorer plugin\n.idea/**/mongoSettings.xml\n\n# File-based project format\n*.iws\n\n# IntelliJ\nout/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Cursive Clojure plugin\n.idea/replstate.xml\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n# Editor-based Rest Client\n.idea/httpRequests\n\n# Android studio 3.1+ serialized cache file\n.idea/caches/build_file_checksums.ser\n\n# fastlane\nfastlane/report.xml\n\n# inspectcode\ninspectcodereport.xml\ninspectcode\n\n# BenchmarkDotNet\n/BenchmarkDotNet.Artifacts\n\n*.GeneratedMSBuildEditorConfig.editorconfig\n"
  },
  {
    "path": ".globalconfig",
    "content": "is_global = true\n\n# .NET Code Style\n# IDE styles reference: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/\n\n# IDE0001: Simplify names\ndotnet_diagnostic.IDE0001.severity = warning\n\n# IDE0002: Simplify member access\ndotnet_diagnostic.IDE0002.severity = warning\n\n# IDE0003: Remove qualification\ndotnet_diagnostic.IDE0003.severity = warning\n\n# IDE0004: Remove unnecessary cast\ndotnet_diagnostic.IDE0004.severity = warning\n\n# IDE0005: Remove unnecessary imports\ndotnet_diagnostic.IDE0005.severity = warning\n\n# IDE0034: Simplify default literal\ndotnet_diagnostic.IDE0034.severity = warning\n\n# IDE0036: Sort modifiers\ndotnet_diagnostic.IDE0036.severity = warning\n\n# IDE0040: Add accessibility modifier\ndotnet_diagnostic.IDE0040.severity = warning\n\n# IDE0049: Use keyword for type name\ndotnet_diagnostic.IDE0040.severity = warning\n\n# IDE0055: Fix formatting\ndotnet_diagnostic.IDE0055.severity = warning\n\n# IDE0051: Private method is unused\ndotnet_diagnostic.IDE0051.severity = silent\n\n# IDE0052: Private member is unused\ndotnet_diagnostic.IDE0052.severity = silent\n\n# IDE0073: File header\ndotnet_diagnostic.IDE0073.severity = warning\n\n# IDE0130: Namespace mismatch with folder\ndotnet_diagnostic.IDE0130.severity = warning\n\n# IDE1006: Naming style\ndotnet_diagnostic.IDE1006.severity = warning\n\n#Disable operator overloads requiring alternate named methods\ndotnet_diagnostic.CA2225.severity = none\n\n# Banned APIs\ndotnet_diagnostic.RS0030.severity = error\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing Guidelines\n\nThank you for showing interest in the development of karaoke ruleset. We aim to provide a good collaborating environment for everyone involved, and as such have decided to list some of the most important things to keep in mind in the process. The guidelines below have been chosen based on past experience.\n\n## Table of contents\n\n1. [Reporting bugs](#reporting-bugs)\n2. [Providing general feedback](#providing-general-feedback)\n3. [Issue or discussion?](#issue-or-discussion)\n4. [Submitting pull requests](#submitting-pull-requests)\n5. [Resources](#resources)\n\n## Reporting bugs\n\nA **bug** is a situation in which there is something clearly *and objectively* wrong with the ruleset. Examples of applicable bug reports are:\n\n- The ruleset crashes to desktop when I start a beatmap\n- Cannot load karoake beatmap that edited before.\n- The ruleset slows down a lot when I play this specific map.\n- Text effect looks weird if using directX renderer.\n\nTo track bug reports, we primarily use GitHub **issues**. When opening an issue, please keep in mind the following:\n\n- Before opening the issue, please search for any similar existing issues using the text search bar and the issue labels. This includes both open and closed issues (we may have already fixed something, but the fix hasn't yet been released).\n- When opening the issue, please fill out as much of the issue template as you can. In particular, please make sure to include logs and screenshots as much as possible. The instructions on how to find the log files are included in the issue template.\n- We may ask you for follow-up information to reproduce or debug the problem. Please look out for this and provide follow-up info if we request it.\n\nIf we cannot reproduce the issue, it is deemed low priority, or it is deemed to be specific to your setup in some way, the issue may be downgraded to a discussion. This will be done by a maintainer for you.\n\n## Providing general feedback\n\nIf you wish to:\n\n- Provide *subjective* feedback on the ruleset (about how the UI looks, about how the default skin works, about ruleset mechanics, about how the PP and scoring systems work, etc.),\n- Suggest a new feature to be added to the ruleset.\n- Report a non-specific problem with the ruleset that you think may be connected to your hardware or operating system specifically.\n\nthen it is generally best to start with a **discussion** first. Discussions are a good avenue to group subjective feedback on a single topic, or gauge interest in a particular feature request.\n\nWhen opening a discussion, please keep in mind the following:\n\n- Use the search function to see if your idea has been proposed before, or if there is already a thread about a particular issue you wish to raise.\n- If proposing a feature, please try to explain the feature in as much detail as possible.\n- If you're reporting a non-specific problem, please provide applicable logs, screenshots, or video that illustrate the issue.\n\nIf a discussion gathers enough traction, then it may be converted into an issue. This will be done by a maintainer for you.\n\n## Issue or discussion?\n\nWe realise that the line between an issue and a discussion may be fuzzy, so while we ask you to use your best judgement based on the description above, please don't think about it too hard either. Feedback in a slightly wrong place is better than no feedback at all.\n\nWhen in doubt, it's probably best to start with a discussion first. We will escalate to issues as needed.\n\n## Submitting pull requests\n\nThe [issue tracker](https://github.com/karaoke-dev/karaoke/issues) should provide plenty of issues to start with. We also have a [`Good for contributor`](https://github.com/karaoke-dev/karaoke/issues?q=is%3Aissue+is%3Aopen+label%3A\"Good+for+contributor\") label.\n\nIn the case of simple issues, a direct PR is okay. However, if you decide to work on an existing issue which doesn't seem trivial, **please ask us first**. This way we can try to estimate if it is a good fit for you and provide the correct direction on how to address it. In addition, note that while we do not rule out external contributors from working on roadmapped issues, we will generally prefer to handle them ourselves unless they're not very time sensitive.\n\nIf you'd like to propose a subjective change to one of the visual aspects of the ruleset, or there is a bigger task you'd like to work on, but there is no corresponding issue or discussion thread yet for it, **please open a discussion or issue first** to avoid wasted effort.\n\nAside from the above, below is a brief checklist of things to watch out when you're preparing your code changes:\n\n- Make sure you're comfortable with the principles of object-oriented programming, the syntax of `C\\#` and your `development environment`.\n- Make sure you are familiar with [git](https://git-scm.com/) and [the pull request workflow](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/proposing-changes-to-your-work-with-pull-requests).\n- Discuss with us first if you're planning to work on a `bigger change`, or `it's UI/UX related`.\n- Please do not make code changes via the GitHub web interface.\n- Please add tests for your changes. We expect most new features and bugfixes to have test coverage, unless the effort of adding them is prohibitive. The visual testing methodology we use is described in more detail [here](https://github.com/ppy/osu-framework/wiki/Development-and-Testing).\n\nAfter you're done with your changes and you wish to open the PR, please observe the following recommendations:\n\n- Please submit the pull request from a [topic branch](https://git-scm.com/book/en/v2/Git-Branching-Branching-Workflows#_topic_branch) (not `master`), and keep the *Allow edits from maintainers* check box selected, so that we can push fixes to your PR if necessary.\n- Please write the commit messages with useful information in mind. We recommend reading [this article](https://chris.beams.io/posts/git-commit/) for some good tips.\n- Unlike osu! project, `force-push` is allowed in here. Use it if you need to.\n- Rebase the PR branch to the `master` if PR is too old or has conflicts.\n\nWe are highly committed to quality when it comes to the karaoke ruleset project.\nThis means that contributions from less experienced community members can take multiple rounds of review to get to a mergeable state.\n\nIf you're uncertain about some part of the codebase or some inner workings of the karaoke ruleset, please reach out either by leaving a comment in the relevant issue, discussion, or PR thread, or by posting a message in the [development Discord server](https://discord.gg/ga2xZXk).\nWe will try to help you as much as we can.\n\n## Resources\n\n- [`ppy/osu`](https://github.com/ppy/osu): The game that karaoke ruleset is running on.\n- [`ppy/osu` wiki](https://github.com/ppy/osu/wiki): Contains articles about various technical aspects of the game\n- [`ppy/osu-framework`](https://github.com/ppy/osu-framework): The game framework that karaoke ruleset is running on.\n- [`ppy/osu-framework` wiki](https://github.com/ppy/osu-framework/wiki): Contains introductory information about osu!framework, the bespoke 2D game framework we use for the game.\n.\n"
  },
  {
    "path": "CodeAnalysis/BannedSymbols.txt",
    "content": "M:System.Object.Equals(System.Object,System.Object)~System.Boolean;Don't use object.Equals. Use IEquatable<T> or EqualityComparer<T>.Default instead.\nM:System.Object.Equals(System.Object)~System.Boolean;Don't use object.Equals. Use IEquatable<T> or EqualityComparer<T>.Default instead.\nM:System.ValueType.Equals(System.Object)~System.Boolean;Don't use object.Equals(Fallbacks to ValueType). Use IEquatable<T> or EqualityComparer<T>.Default instead.\nM:System.Nullable`1.Equals(System.Object)~System.Boolean;Use == instead.\nT:System.IComparable;Don't use non-generic IComparable. Use generic version instead.\nT:SixLabors.ImageSharp.IDeepCloneable`1;Use osu.Game.Utils.IDeepCloneable<T> instead.\nM:osu.Framework.Graphics.Sprites.SpriteText.#ctor;Use OsuSpriteText.\nM:osu.Framework.Bindables.IBindableList`1.GetBoundCopy();Fails on iOS. Use manual ctor + BindTo instead. (see https://github.com/mono/mono/issues/19900)\nT:Microsoft.EntityFrameworkCore.Internal.EnumerableExtensions;Don't use internal extension methods.\nT:Microsoft.EntityFrameworkCore.Internal.TypeExtensions;Don't use internal extension methods.\nT:NuGet.Packaging.CollectionExtensions;Don't use internal extension methods.\nM:Realms.IRealmCollection`1.SubscribeForNotifications`1(Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IRealmCollection<T>,NotificationCallbackDelegate<T>) instead.\nM:System.Guid.#ctor;Probably meaning to use Guid.NewGuid() instead. If actually wanting empty, use Guid.Empty.\nM:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Linq.IQueryable{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IQueryable<T>,NotificationCallbackDelegate<T>) instead.\nM:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Collections.Generic.IList{``0},Realms.NotificationCallbackDelegate{``0});Use osu.Game.Database.RealmObjectExtensions.QueryAsyncWithNotifications(IList<T>,NotificationCallbackDelegate<T>) instead.\nM:System.Threading.Tasks.Task.Wait();Don't use Task.Wait. Use Task.WaitSafely() to ensure we avoid deadlocks.\nP:System.Threading.Tasks.Task`1.Result;Don't use Task.Result. Use Task.GetResultSafely() to ensure we avoid deadlocks.\n"
  },
  {
    "path": "CodeAnalysis/osu.ruleset",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RuleSet Name=\"osu!karaoke Rule Set\" Description=\" \" ToolsVersion=\"16.0\">\n  <Rules AnalyzerId=\"Microsoft.CodeQuality.Analyzers\" RuleNamespace=\"Microsoft.CodeQuality.Analyzers\">\n    <Rule Id=\"CA1016\" Action=\"None\" />\n    <Rule Id=\"CA1028\" Action=\"None\" />\n    <Rule Id=\"CA1031\" Action=\"None\" />\n    <Rule Id=\"CA1034\" Action=\"None\" />\n    <Rule Id=\"CA1036\" Action=\"None\" />\n    <Rule Id=\"CA1040\" Action=\"None\" />\n    <Rule Id=\"CA1044\" Action=\"None\" />\n    <Rule Id=\"CA1051\" Action=\"None\" />\n    <Rule Id=\"CA1054\" Action=\"None\" />\n    <Rule Id=\"CA1056\" Action=\"None\" />\n    <Rule Id=\"CA1062\" Action=\"None\" />\n    <Rule Id=\"CA1063\" Action=\"None\" />\n    <Rule Id=\"CA1067\" Action=\"None\" />\n    <Rule Id=\"CA1707\" Action=\"None\" />\n    <Rule Id=\"CA1710\" Action=\"None\" />\n    <Rule Id=\"CA1714\" Action=\"None\" />\n    <Rule Id=\"CA1716\" Action=\"None\" />\n    <Rule Id=\"CA1717\" Action=\"None\" />\n    <Rule Id=\"CA1720\" Action=\"None\" />\n    <Rule Id=\"CA1721\" Action=\"None\" />\n    <Rule Id=\"CA1724\" Action=\"None\" />\n    <Rule Id=\"CA1801\" Action=\"None\" />\n    <Rule Id=\"CA1806\" Action=\"None\" />\n    <Rule Id=\"CA1812\" Action=\"None\" />\n    <Rule Id=\"CA1814\" Action=\"None\" />\n    <Rule Id=\"CA1815\" Action=\"None\" />\n    <Rule Id=\"CA1819\" Action=\"None\" />\n    <Rule Id=\"CA1822\" Action=\"None\" />\n    <Rule Id=\"CA1823\" Action=\"None\" />\n    <Rule Id=\"CA2007\" Action=\"Warning\" />\n    <Rule Id=\"CA2214\" Action=\"None\" />\n    <Rule Id=\"CA2227\" Action=\"None\" />\n  </Rules>\n  <Rules AnalyzerId=\"Microsoft.CodeQuality.CSharp.Analyzers\" RuleNamespace=\"Microsoft.CodeQuality.CSharp.Analyzers\">\n    <Rule Id=\"CA1001\" Action=\"None\" />\n    <Rule Id=\"CA1032\" Action=\"None\" />\n  </Rules>\n  <Rules AnalyzerId=\"Microsoft.NetCore.Analyzers\" RuleNamespace=\"Microsoft.NetCore.Analyzers\">\n    <Rule Id=\"CA1303\" Action=\"None\" />\n    <Rule Id=\"CA1304\" Action=\"None\" />\n    <Rule Id=\"CA1305\" Action=\"None\" />\n    <Rule Id=\"CA1307\" Action=\"None\" />\n    <Rule Id=\"CA1308\" Action=\"None\" />\n    <Rule Id=\"CA1816\" Action=\"None\" />\n    <Rule Id=\"CA1826\" Action=\"None\" />\n    <Rule Id=\"CA2000\" Action=\"None\" />\n    <Rule Id=\"CA2008\" Action=\"None\" />\n    <Rule Id=\"CA2213\" Action=\"None\" />\n    <Rule Id=\"CA2235\" Action=\"None\" />\n  </Rules>\n  <Rules AnalyzerId=\"Microsoft.NetCore.CSharp.Analyzers\" RuleNamespace=\"Microsoft.NetCore.CSharp.Analyzers\">\n    <Rule Id=\"CA1309\" Action=\"Warning\" />\n    <Rule Id=\"CA2201\" Action=\"Warning\" />\n  </Rules>\n</RuleSet>"
  },
  {
    "path": "Directory.Build.props",
    "content": "<!-- Contains required properties for osu!framework projects. -->\n<Project>\n  <PropertyGroup Label=\"C#\">\n    <LangVersion>12.0</LangVersion>\n    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>\n    <Nullable>enable</Nullable>\n  </PropertyGroup>\n  <ItemGroup Label=\"License\">\n    <None Include=\"$(MSBuildThisFileDirectory)osu.licenseheader\">\n      <Link>osu.licenseheader</Link>\n    </None>\n  </ItemGroup>\n  <ItemGroup Label=\"Resources\">\n    <EmbeddedResource Include=\"Resources\\**\\*.*\"/>\n  </ItemGroup>\n  <ItemGroup Label=\"Code Analysis\">\n    <PackageReference Include=\"Microsoft.CodeAnalysis.BannedApiAnalyzers\" Version=\"3.3.3\" PrivateAssets=\"All\"/>\n    <AdditionalFiles Include=\"$(MSBuildThisFileDirectory)CodeAnalysis\\BannedSymbols.txt\"/>\n  </ItemGroup>\n  <PropertyGroup Label=\"Code Analysis\">\n    <CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)CodeAnalysis\\osu.ruleset</CodeAnalysisRuleSet>\n  </PropertyGroup>\n  <PropertyGroup Label=\"Documentation\">\n    <GenerateDocumentationFile>true</GenerateDocumentationFile>\n    <NoWarn>$(NoWarn);CS1591</NoWarn>\n  </PropertyGroup>\n</Project>\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<https://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<https://www.gnu.org/licenses/why-not-lgpl.html>.\n"
  },
  {
    "path": "README.md",
    "content": "![](assets/logo.png)\n\n# karaoke --dev\n[![CodeFactor](https://www.codefactor.io/repository/github/karaoke-dev/karaoke/badge)](https://www.codefactor.io/repository/github/karaoke-dev/karaoke)\n[![Build status](https://ci.appveyor.com/api/projects/status/07ytm0sei6l5oy08?svg=true)](https://ci.appveyor.com/project/andy840119/karaoke)\n[![Crowdin](https://badges.crowdin.net/karaoke-dev/localized.svg)](https://crowdin.com/project/karaoke-dev)\n[![Waifu](https://img.shields.io/badge/月子我婆-passed-ff69b4.svg)](https://github.com/karaoke-dev/karaoke)\n[![GitHub last commit](https://img.shields.io/github/last-commit/karaoke-dev/karaoke)](https://github.com/karaoke-dev/karaoke/releases)\n[![Tagged Release](https://github.com/karaoke-dev/karaoke/workflows/Tagged%20Release/badge.svg)](https://github.com/karaoke-dev/karaoke/releases)\n[![dev chat](https://discordapp.com/api/guilds/299006062323826688/widget.png?style=shield)](https://discord.gg/ga2xZXk)\n[![Total lines](https://tokei.rs/b1/github/karaoke-dev/karaoke)](https://ghloc.vercel.app/karaoke-dev/karaoke?branch=master)\n[![Dashboard](https://img.shields.io/badge/Dashboard-stonks!-informational)](https://www.repotrends.com/karaoke-dev/karaoke)\n[![Star History Chart](https://img.shields.io/github/stars/karaoke-dev/karaoke?style=flat&label=Stars&color=yellow&cacheSeconds=86000)](https://seladb.github.io/StarTrack-js/#/preload?r=karaoke-dev,karaoke)\n[![airpods pro](https://img.shields.io/badge/Andy's%20airpods%20pro-missing-red.svg)](https://github.com/karaoke-dev/karaoke/issues/1514)\n[![airpods 2](https://img.shields.io/badge/Andy's%20airpods%202-missing-red)](https://github.com/karaoke-dev/karaoke/issues/1513)\n\n\nThe source code of the `karaoke` ruleset, running on [osu!lazer](https://github.com/ppy/osu).\n\n## Status\n\nThis project is still not very stable, `so we recommend looking around this project to find new features instead of actually using it`. \n\nAlso:\n- This project doesn't have much of a [demo](https://github.com/karaoke-dev/sample-beatmap) currently available. And most demos are Japanese only.\n- This project is not very stable, especially in the editor.\n- Beatmap does not support save feature.\n\nReccommend using this ruleset until [support batch import song](https://github.com/karaoke-dev/karaoke/issues/2144).\n\nIf you run into any problems, you can shoot us an email (support@karaoke.dev) or send me a message on [Discord](https://discord.gg/ga2xZXk). I will typically reply faster on Discord.\n\nAnd feel free to report any bugs if found.\n\n## How to run this project\n\nSee [this tutorial](https://karaoke-dev.github.io/how-to-install/) to get the ruleset from the existing build.\n\nOr you can compile it yourself: `release build` then copy `Packed/osu.Game.Rulesets.Karaoke.dll` into your [ruleset folder](https://github.com/LumpBloom7/sentakki/wiki/Ruleset-installation-guide)\n\n## License\n\nThis repo is covered under the [GPL V3](LICENSE) license.\nIf you plan on using this repo for commercial purposes, please contact us at (support@karaoke.dev) to get permission first.\n\nUsing this repo to create, use or using the beatmap format to distribution any `PIRATED`, `unauthorized` karaoke songs/beatmaps is absolutely forbidden.\nThis repo is trying to make song author(or people has copyright) ability to distribution their songs with karaoke format without any restriction, not for copycat to make thing with copyright issue.\n\n## Thanks to\n\n- [osu!](https://github.com/ppy/osu) and it's [framework](https://github.com/ppy/osu-framework) for karaoke!\n\n- [RhythmKaTTE](http://juna-idler.blogspot.com/2016/05/rhythmkatte-version-01.html), [RhythmicaLyrics](http://suwa.pupu.jp/RhythmicaLyrics.html) and [Aegisub](https://github.com/Aegisub/Aegisub), an open-source software used to create lyrics with time tags.\nParts of the lyrics editor in this ruleset were inspired by them.\n\n- [ニコカラメーカー](http://shinta0806be.ldblog.jp/tag/%E3%83%8B%E3%82%B3%E3%82%AB%E3%83%A9%E3%83%A1%E3%83%BC%E3%82%AB%E3%83%BC), a software to convert `.lrc` files into karaoke video with beautiful text effects.\n\n- [JetBrains](https://www.jetbrains.com/?from=osu-karaoke), for contributing a free [Rider](https://www.jetbrains.com/rider/) license used to developing.\n\n- [Appveyor](https://www.appveyor.com/), [CodeFactor](https://www.codefactor.io/) and [Github action](https://github.com/features/actions) for providing free `CI`/`CD` service.\n\n- [Figma](https://www.figma.com/), for quick creation of assets like logos or icon.\n\n- [Miro](https://miro.com/). Used for flow-charts and deciding how to structure some parts.\n"
  },
  {
    "path": "appveyor.yml",
    "content": "clone_depth: 1\nversion: '{branch}-{build}'\nimage: Visual Studio 2022\nbranches:\n  only:\n    - master\ndotnet_csproj:\n  patch: true\n  file: 'osu.Game.Rulesets.Karaoke\\osu.Game.Rulesets.Karaoke.csproj' # Use wildcard when it's able to exclude Xamarin projects\n  version: '0.0.{build}'\ncache:\n  - '%LOCALAPPDATA%\\NuGet\\v3-cache -> appveyor.yml'\nbefore_build:\n  - ps: dotnet --info # Useful when version mismatch between CI and local\n  - ps: nuget restore -verbosity quiet # Only nuget.exe knows both new (.NET Core) and old (Xamarin) projects\nbuild:\n  project: osu.Game.Rulesets.Karaoke.sln\n  parallel: true\n  verbosity: minimal\nafter_build:\n  - ps: dotnet tool restore\ntest:\n  assemblies:\n    except:\n      - '**\\*Android*'\n      - '**\\*iOS*'\n      - 'build\\**\\*'\n"
  },
  {
    "path": "cake.config",
    "content": "\n[Nuget]\nSource=https://api.nuget.org/v3/index.json\nUseInProcessClient=true\nLoadDependencies=true\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Beatmaps/ElementId.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Beatmaps;\r\n\r\n/// <summary>\r\n/// As unique identifier for the elements in the <see cref=\"KaraokeBeatmap\"/>\r\n/// Like how <see cref=\"Guid\"/> works.\r\n/// </summary>\r\npublic readonly struct ElementId : IComparable, IComparable<ElementId>, IEquatable<ElementId>\r\n{\r\n    public static ElementId Empty => default;\r\n\r\n    private const int length = 7;\r\n\r\n    private readonly string? id;\r\n\r\n    public ElementId(string id)\r\n    {\r\n        if (string.IsNullOrEmpty(id))\r\n        {\r\n            throw new ArgumentException(\"id should not be empty\", nameof(id));\r\n        }\r\n\r\n        if (id.Length != length)\r\n        {\r\n            throw new ArgumentException($\"id length must be {length}.\", nameof(id));\r\n        }\r\n\r\n        if (!checkFormat(id))\r\n        {\r\n            throw new ArgumentException(\"id format is not correct\", nameof(id));\r\n        }\r\n\r\n        this.id = id;\r\n    }\r\n\r\n    // char should be 0~9 and a~f\r\n    private static bool checkFormat(string id)\r\n        => id.Where(c => c is < '0' or > '9').All(c => c >= 'a' && c <= 'f');\r\n\r\n    public static ElementId NewElementId()\r\n    {\r\n        // take 7 digits\r\n        string str = Guid.NewGuid().ToString(\"N\");\r\n        string id = str[..length];\r\n        return new ElementId(id);\r\n    }\r\n\r\n    public int CompareTo(ElementId other)\r\n    {\r\n        return string.Compare(id, other.id, StringComparison.Ordinal);\r\n    }\r\n\r\n    public int CompareTo(object? obj)\r\n    {\r\n        if (obj == null)\r\n        {\r\n            return 1;\r\n        }\r\n\r\n        if (obj is not ElementId elementId)\r\n        {\r\n            throw new ArgumentException(\"Compared object should be the same type.\", nameof(obj));\r\n        }\r\n\r\n        return CompareTo(elementId);\r\n    }\r\n\r\n    public bool Equals(ElementId other)\r\n    {\r\n        return id == other.id;\r\n    }\r\n\r\n    public override bool Equals(object? obj)\r\n    {\r\n        return obj is ElementId other && Equals(other);\r\n    }\r\n\r\n    public static bool operator ==(ElementId id1, ElementId id2) => id1.Equals(id2);\r\n\r\n    public static bool operator !=(ElementId id1, ElementId id2) => !id1.Equals(id2);\r\n\r\n    public override int GetHashCode()\r\n    {\r\n        return (id ?? string.Empty).GetHashCode();\r\n    }\r\n\r\n    public override string ToString()\r\n    {\r\n        return id ?? string.Empty;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Beatmaps/Formats/KaraokeJsonBeatmapDecoder.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing Newtonsoft.Json;\r\nusing Newtonsoft.Json.Serialization;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Beatmaps.Formats;\r\nusing osu.Game.IO;\r\nusing osu.Game.IO.Serialization;\r\nusing osu.Game.Rulesets.Karaoke.IO.Serialization;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Beatmaps.Formats;\r\n\r\npublic class KaraokeJsonBeatmapDecoder : JsonBeatmapDecoder\r\n{\r\n    public new static void Register()\r\n    {\r\n        AddDecoder<Beatmap>(\"// karaoke json file format v\", m => new KaraokeJsonBeatmapDecoder());\r\n\r\n        // use this weird way to let all the fall-back beatmap(include karaoke beatmap) become karaoke beatmap.\r\n        SetFallbackDecoder<Beatmap>(() => new KaraokeJsonBeatmapDecoder());\r\n    }\r\n\r\n    protected override void ParseStreamInto(LineBufferedReader stream, Beatmap output)\r\n    {\r\n        var globalSetting = KaraokeJsonSerializableExtensions.CreateGlobalSettings();\r\n        globalSetting.ContractResolver = new KaraokeBeatmapContractResolver();\r\n\r\n        // create id if object is by reference.\r\n        globalSetting.PreserveReferencesHandling = PreserveReferencesHandling.Objects;\r\n\r\n        // should not let json decoder to read this line.\r\n        if (stream.PeekLine()?.Contains(\"// karaoke json file format v\") ?? false)\r\n        {\r\n            stream.ReadLine();\r\n        }\r\n\r\n        // equal to stream.ReadToEnd().DeserializeInto(output); in the base class.\r\n        JsonConvert.PopulateObject(stream.ReadToEnd(), output, globalSetting);\r\n    }\r\n\r\n    private class KaraokeBeatmapContractResolver : SnakeCaseKeyContractResolver\r\n    {\r\n        protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)\r\n        {\r\n            var props = base.CreateProperties(type, memberSerialization);\r\n\r\n            return type == typeof(BeatmapInfo)\r\n                ? props.Where(p => p.PropertyName != \"ruleset_id\").ToList()\r\n                : props;\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Beatmaps/Formats/KaraokeJsonBeatmapEncoder.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing Newtonsoft.Json;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.IO.Serialization;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Beatmaps.Formats;\r\n\r\npublic class KaraokeJsonBeatmapEncoder\r\n{\r\n    public string Encode(Beatmap output)\r\n    {\r\n        var globalSetting = KaraokeJsonSerializableExtensions.CreateGlobalSettings();\r\n        string json = JsonConvert.SerializeObject(output, globalSetting);\r\n        return \"// karaoke json file format v1\" + '\\n' + json;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Beatmaps/Formats/KaraokeLegacyBeatmapDecoder.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Globalization;\r\nusing System.Linq;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Beatmaps.Formats;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.Notes;\r\nusing osu.Game.Rulesets.Karaoke.Integration.Formats;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Beatmaps.Formats;\r\n\r\npublic class KaraokeLegacyBeatmapDecoder : LegacyBeatmapDecoder\r\n{\r\n    public new const int LATEST_VERSION = 1;\r\n\r\n    public new static void Register()\r\n    {\r\n        AddDecoder<Beatmap>(\"karaoke file format v\", m => new KaraokeLegacyBeatmapDecoder(Parsing.ParseInt(m.Split('v').Last())));\r\n\r\n        // use this weird way to let all the fall-back beatmap(include karaoke beatmap) become karaoke beatmap.\r\n        SetFallbackDecoder<Beatmap>(() => new KaraokeLegacyBeatmapDecoder());\r\n    }\r\n\r\n    public KaraokeLegacyBeatmapDecoder(int version = LATEST_VERSION)\r\n        : base(version)\r\n    {\r\n    }\r\n\r\n    private readonly IList<string> karFormatLines = new List<string>();\r\n    private readonly IList<string> noteLines = new List<string>();\r\n    private readonly IList<string> translations = new List<string>();\r\n\r\n    protected override void ParseLine(Beatmap beatmap, Section section, string line)\r\n    {\r\n        if (section != Section.HitObjects)\r\n        {\r\n            // should not let base.ParseLine read the line like \"Mode: 111\"\r\n            if (line.StartsWith(\"Mode\", StringComparison.Ordinal))\r\n            {\r\n                beatmap.BeatmapInfo.Ruleset = new KaraokeRuleset().RulesetInfo;\r\n                return;\r\n            }\r\n\r\n            base.ParseLine(beatmap, section, line);\r\n            return;\r\n        }\r\n\r\n        if (line.ToLower().StartsWith(\"@ruby\", StringComparison.Ordinal))\r\n        {\r\n            // kar format queue\r\n            karFormatLines.Add(line);\r\n        }\r\n        else if (line.ToLower().StartsWith(\"@note\", StringComparison.Ordinal))\r\n        {\r\n            // add tone line queue\r\n            noteLines.Add(line);\r\n        }\r\n        else if (line.ToLower().StartsWith(\"@tr\", StringComparison.Ordinal))\r\n        {\r\n            // add translation queue\r\n            translations.Add(line);\r\n        }\r\n        else if (line.StartsWith('@'))\r\n        {\r\n            // Remove @ in time tag and add into kar queue\r\n            karFormatLines.Add(line[1..]);\r\n        }\r\n        else if (line.ToLower() == \"end\")\r\n        {\r\n            string content = string.Join(\"\\n\", karFormatLines);\r\n\r\n            // Create decoder\r\n            var decoder = new KarDecoder();\r\n            var lyrics = decoder.Decode(content);\r\n\r\n            // Apply hitobjects\r\n            beatmap.HitObjects = lyrics.OfType<HitObject>().ToList();\r\n\r\n            processNotes(beatmap, noteLines);\r\n            processTranslations(beatmap, translations);\r\n        }\r\n    }\r\n\r\n    private void processNotes(Beatmap beatmap, IList<string> lines)\r\n    {\r\n        var noteGenerator = new NoteGenerator(new NoteGeneratorConfig());\r\n\r\n        // Remove all karaoke note\r\n        beatmap.HitObjects.RemoveAll(x => x is Note);\r\n\r\n        var lyrics = beatmap.HitObjects.OfType<Lyric>().ToList();\r\n\r\n        for (int l = 0; l < lyrics.Count; l++)\r\n        {\r\n            var lyric = lyrics[l];\r\n            string? line = lines.ElementAtOrDefault(l)?.Split('=').Last();\r\n\r\n            // Create default note if not exist\r\n            if (string.IsNullOrEmpty(line))\r\n            {\r\n                beatmap.HitObjects.AddRange(noteGenerator.Generate(lyric));\r\n                continue;\r\n            }\r\n\r\n            string[] notes = line.Split(',');\r\n            var defaultNotes = noteGenerator.Generate(lyric).ToList();\r\n            int minNoteNumber = Math.Min(notes.Length, defaultNotes.Count);\r\n\r\n            // Process each note\r\n            for (int i = 0; i < minNoteNumber; i++)\r\n            {\r\n                string note = notes[i];\r\n                var defaultNote = defaultNotes[i];\r\n\r\n                // Support multi note in one time tag, format like ([1;0.5;か]|1#|...)\r\n                if (!note.StartsWith('(') || !note.EndsWith(')'))\r\n                {\r\n                    // Process and add note\r\n                    applyNote(defaultNote, note);\r\n                    beatmap.HitObjects.Add(defaultNote);\r\n                }\r\n                else\r\n                {\r\n                    float startPercentage = 0;\r\n                    string[] rubyNotes = note.Replace(\"(\", string.Empty).Replace(\")\", string.Empty).Split('|');\r\n\r\n                    for (int j = 0; j < rubyNotes.Length; j++)\r\n                    {\r\n                        string rubyNote = rubyNotes[j];\r\n\r\n                        string tone;\r\n                        float percentage = (float)Math.Round((float)1 / rubyNotes.Length, 2, MidpointRounding.AwayFromZero);\r\n                        string? ruby = defaultNote.RubyText?.ElementAtOrDefault(j).ToString();\r\n\r\n                        // Format like [1;0.5;か]\r\n                        if (note.StartsWith('[') && note.EndsWith(']'))\r\n                        {\r\n                            string[] rubyNoteProperty = note.Replace(\"[\", string.Empty).Replace(\"]\", string.Empty).Split(';');\r\n\r\n                            // Copy tome property\r\n                            tone = rubyNoteProperty[0];\r\n\r\n                            // Copy percentage property\r\n                            if (rubyNoteProperty.Length >= 2)\r\n                                float.TryParse(rubyNoteProperty[1], out percentage);\r\n\r\n                            // Copy text property\r\n                            if (rubyNoteProperty.Length >= 3)\r\n                                ruby = rubyNoteProperty[2];\r\n                        }\r\n                        else\r\n                        {\r\n                            tone = rubyNote;\r\n                        }\r\n\r\n                        // Split note and apply them\r\n                        var splitDefaultNote = SliceNote(defaultNote, startPercentage, percentage);\r\n                        startPercentage += percentage;\r\n                        if (!string.IsNullOrEmpty(ruby))\r\n                            splitDefaultNote.Text = ruby;\r\n\r\n                        // Process and add note\r\n                        applyNote(splitDefaultNote, tone);\r\n                        beatmap.HitObjects.Add(splitDefaultNote);\r\n                    }\r\n                }\r\n            }\r\n        }\r\n\r\n        static void applyNote(Note note, string noteStr, string? ruby = null, double? duration = null)\r\n        {\r\n            if (noteStr == \"-\")\r\n                note.Display = false;\r\n            else\r\n            {\r\n                note.Display = true;\r\n                note.Tone = convertTone(noteStr);\r\n            }\r\n\r\n            if (!string.IsNullOrEmpty(ruby))\r\n                note.Text = ruby;\r\n\r\n            if (duration != null)\r\n                note.Duration = duration.Value;\r\n\r\n            //Support format : 1  1.  1.5  1+  1#\r\n            static Tone convertTone(string tone)\r\n            {\r\n                bool half = false;\r\n\r\n                if (tone.Contains('.') || tone.Contains('#'))\r\n                {\r\n                    half = true;\r\n\r\n                    // only get digit part\r\n                    tone = tone.Split('.').FirstOrDefault()?.Split('#').FirstOrDefault() ?? string.Empty;\r\n                }\r\n\r\n                if (!int.TryParse(tone, out int scale))\r\n                    throw new InvalidCastException($\"{tone} does not support in {nameof(KaraokeLegacyBeatmapDecoder)}\");\r\n\r\n                return new Tone\r\n                {\r\n                    Scale = scale,\r\n                    Half = half,\r\n                };\r\n            }\r\n        }\r\n    }\r\n\r\n    private void processTranslations(Beatmap beatmap, IEnumerable<string> translationLines)\r\n    {\r\n        var availableTranslations = new List<CultureInfo>();\r\n\r\n        var lyrics = beatmap.HitObjects.OfType<Lyric>().ToList();\r\n        var translations = translationLines.Select(translation => new\r\n        {\r\n            key = translation.Split('=').FirstOrDefault()?.Split('[').LastOrDefault()?.Split(']').FirstOrDefault(),\r\n            value = translation.Split('=').LastOrDefault() ?? string.Empty,\r\n        }).GroupBy(x => x.key, y => y.value).ToList();\r\n\r\n        foreach (var translation in translations)\r\n        {\r\n            // get culture and translation\r\n            string? languageCode = translation.Key;\r\n            if (string.IsNullOrEmpty(languageCode))\r\n                continue;\r\n\r\n            var cultureInfo = new CultureInfo(languageCode);\r\n            var values = translation.ToList();\r\n\r\n            int size = Math.Min(lyrics.Count, translation.Count());\r\n\r\n            for (int j = 0; j < size; j++)\r\n            {\r\n                lyrics[j].Translations.Add(cultureInfo, values[j]);\r\n            }\r\n\r\n            availableTranslations.Add(cultureInfo);\r\n        }\r\n\r\n        var dictionary = new LegacyProperties\r\n        {\r\n            AvailableTranslationLanguages = availableTranslations,\r\n        };\r\n\r\n        beatmap.HitObjects.Add(dictionary);\r\n    }\r\n\r\n    internal static Note SliceNote(Note note, double startPercentage, double durationPercentage)\r\n    {\r\n        if (startPercentage < 0 || startPercentage + durationPercentage > 1)\r\n            throw new ArgumentOutOfRangeException($\"{nameof(Note)} cannot assign split range of start from {startPercentage} and duration {durationPercentage}\");\r\n\r\n        double durationFromStartTime = note.Duration * startPercentage;\r\n        double secondNoteDuration = note.Duration * (1 - startPercentage - durationPercentage);\r\n\r\n        // todo: there's no need to create the new note.\r\n        var newNote = note.DeepClone();\r\n        newNote.StartTimeOffset = note.StartTimeOffset + durationFromStartTime;\r\n        newNote.EndTimeOffset = note.EndTimeOffset - secondNoteDuration;\r\n\r\n        return newNote;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Beatmaps/Formats/KaraokeLegacyBeatmapEncoder.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Integration.Formats;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Beatmaps.Formats;\r\n\r\npublic class KaraokeLegacyBeatmapEncoder\r\n{\r\n    public string Encode(Beatmap output)\r\n    {\r\n        var encoder = new KarEncoder();\r\n        var results = new List<string>\r\n        {\r\n            encoder.Encode(output),\r\n            string.Join(\"\\n\", encodeNotes(output)),\r\n            string.Join(\"\\n\", encodeTranslations(output)),\r\n        };\r\n\r\n        return string.Join(\"\\n\\n\", results.Where(x => !string.IsNullOrEmpty(x)));\r\n    }\r\n\r\n    private IEnumerable<string> encodeNotes(Beatmap output)\r\n    {\r\n        var notes = output.HitObjects.OfType<Note>().ToList();\r\n        var lyrics = output.HitObjects.OfType<Lyric>().ToList();\r\n        return notes.GroupBy(x => x.ReferenceLyric).Select(g =>\r\n        {\r\n            var lyric = g.Key;\r\n            if (lyric == null)\r\n                throw new ArgumentNullException();\r\n\r\n            // Get note group\r\n            var noteGroup = g.ToList().GroupBy(n => n.ReferenceTimeTagIndex);\r\n\r\n            // Convert to group format\r\n            string noteGroupStr = string.Join(\",\", noteGroup.Select(x =>\r\n            {\r\n                if (x.Count() == 1)\r\n                    return convertNote(x.First());\r\n\r\n                return \"(\" + string.Join(\"|\", x.Select(convertNote)) + \")\";\r\n            }));\r\n\r\n            return $\"note{lyrics.IndexOf(lyric) + 1}={noteGroupStr}\";\r\n        }).ToList();\r\n\r\n        // Convert single note\r\n        static string convertNote(Note note)\r\n        {\r\n            return !note.Display\r\n                ? \"-\"\r\n                : convertTone(note.Tone);\r\n\r\n            // Convert tone to string\r\n            static string convertTone(Tone tone) => tone.Scale + (tone.Half ? \"#\" : string.Empty);\r\n        }\r\n    }\r\n\r\n    private IEnumerable<string> encodeTranslations(Beatmap output)\r\n    {\r\n        if (!output.AnyTranslation())\r\n            yield break;\r\n\r\n        var lyrics = output.HitObjects.OfType<Lyric>().ToList();\r\n        var availableTranslations = output.AvailableTranslationLanguages();\r\n\r\n        foreach (var translation in availableTranslations)\r\n        {\r\n            foreach (var lyric in lyrics)\r\n            {\r\n                string translationString = lyric.Translations.TryGetValue(translation, out string? value) ? value : string.Empty;\r\n                yield return $\"@tr[{translation.Name}]={translationString}\";\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Beatmaps/IHasPrimaryKey.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Beatmaps;\r\n\r\npublic interface IHasPrimaryKey\r\n{\r\n    ElementId ID { get; }\r\n}\r\n\r\npublic static class PrimaryKeyObjectExtension\r\n{\r\n    public static TObject ChangeId<TObject>(this TObject obj, ElementId id)\r\n        where TObject : IHasPrimaryKey\r\n    {\r\n        // get id from the obj and override the id.\r\n        var propertyInfo = obj.GetType().GetProperty(nameof(IHasPrimaryKey.ID));\r\n        if (propertyInfo == null)\r\n            throw new InvalidOperationException();\r\n\r\n        propertyInfo.SetValue(obj, id);\r\n\r\n        return obj;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Beatmaps/IKaraokeBeatmapResourcesProvider.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics.Textures;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas.Types;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Beatmaps;\r\n\r\npublic interface IKaraokeBeatmapResourcesProvider\r\n{\r\n    Texture? GetSingerAvatar(ISinger singer);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Beatmaps/KaraokeBeatmap.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Globalization;\r\nusing System.Linq;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Beatmaps;\r\n\r\npublic class KaraokeBeatmap : Beatmap<KaraokeHitObject>\r\n{\r\n    public IList<CultureInfo> AvailableTranslationLanguages { get; set; } = new List<CultureInfo>();\r\n\r\n    public SingerInfo SingerInfo { get; set; } = new();\r\n\r\n    public PageInfo PageInfo { get; set; } = new();\r\n\r\n    public NoteInfo NoteInfo { get; set; } = new();\r\n\r\n    public bool Scorable { get; set; }\r\n\r\n    public override IEnumerable<BeatmapStatistic> GetStatistics()\r\n    {\r\n        int singers = SingerInfo.GetAllSingers().Count();\r\n        int lyrics = HitObjects.Count(s => s is Lyric);\r\n\r\n        var defaultStatistic = new List<BeatmapStatistic>\r\n        {\r\n            new()\r\n            {\r\n                Name = \"Singer\",\r\n                Content = singers.ToString(),\r\n                CreateIcon = () => new SpriteIcon { Icon = FontAwesome.Solid.User },\r\n            },\r\n            new()\r\n            {\r\n                Name = \"Lyric\",\r\n                Content = lyrics.ToString(),\r\n                CreateIcon = () => new SpriteIcon { Icon = FontAwesome.Solid.AlignLeft },\r\n            },\r\n        };\r\n\r\n        if (Scorable)\r\n        {\r\n            int notes = HitObjects.Count(s => s is Note { Display: true });\r\n            defaultStatistic.Add(new BeatmapStatistic\r\n            {\r\n                Name = \"Note\",\r\n                Content = notes.ToString(),\r\n                CreateIcon = () => new SpriteIcon { Icon = FontAwesome.Solid.Music },\r\n            });\r\n        }\r\n        else\r\n        {\r\n            defaultStatistic.Add(new BeatmapStatistic\r\n            {\r\n                Name = \"This beatmap is not scorable.\",\r\n                Content = \"This beatmap is not scorable.\",\r\n                CreateIcon = () => new SpriteIcon { Icon = FontAwesome.Solid.Times },\r\n            });\r\n        }\r\n\r\n        return defaultStatistic.ToArray();\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Beatmaps/KaraokeBeatmapConverter.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing System.Threading;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Beatmaps;\r\n\r\npublic class KaraokeBeatmapConverter : BeatmapConverter<KaraokeHitObject>\r\n{\r\n    public KaraokeBeatmapConverter(IBeatmap beatmap, Ruleset ruleset)\r\n        : base(beatmap, ruleset)\r\n    {\r\n    }\r\n\r\n    public override bool CanConvert() => Beatmap.HitObjects.All(h => h is KaraokeHitObject);\r\n\r\n    protected override Beatmap<KaraokeHitObject> ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken)\r\n    {\r\n        var beatmap = base.ConvertBeatmap(original, cancellationToken);\r\n\r\n        // Apply property created from legacy decoder\r\n        var propertyDictionary = beatmap.HitObjects.OfType<LegacyProperties>().FirstOrDefault();\r\n\r\n        if (propertyDictionary == null)\r\n            return beatmap;\r\n\r\n        if (beatmap is not KaraokeBeatmap karaokeBeatmap)\r\n            throw new InvalidCastException(nameof(beatmap));\r\n\r\n        karaokeBeatmap.AvailableTranslationLanguages = propertyDictionary.AvailableTranslationLanguages;\r\n        beatmap.HitObjects.Remove(propertyDictionary);\r\n\r\n        return beatmap;\r\n    }\r\n\r\n    protected override IEnumerable<KaraokeHitObject> ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken)\r\n        => throw new NotImplementedException();\r\n\r\n    protected override Beatmap<KaraokeHitObject> CreateBeatmap() => new KaraokeBeatmap();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Beatmaps/KaraokeBeatmapExtension.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Globalization;\r\nusing System.Linq;\r\nusing osu.Game.Beatmaps;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Beatmaps;\r\n\r\npublic static class KaraokeBeatmapExtension\r\n{\r\n    public static bool IsScorable(this IBeatmap beatmap)\r\n    {\r\n        // we should throw invalidate exception here but it will cause test case failed.\r\n        // because beatmap in the working beatmap in test case not always be karaoke beatmap class.\r\n        return beatmap is KaraokeBeatmap karaokeBeatmap && karaokeBeatmap.Scorable;\r\n    }\r\n\r\n    public static IList<CultureInfo> AvailableTranslationLanguages(this IBeatmap beatmap) => (beatmap as KaraokeBeatmap)?.AvailableTranslationLanguages ?? new List<CultureInfo>();\r\n\r\n    public static bool AnyTranslation(this IBeatmap beatmap) => beatmap.AvailableTranslationLanguages().Any();\r\n\r\n    public static float PitchToScale(this IBeatmap beatmap, float pitch)\r\n    {\r\n        return pitch / 20 - 7;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Beatmaps/KaraokeBeatmapProcessor.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Types;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Beatmaps;\r\n\r\npublic class KaraokeBeatmapProcessor : BeatmapProcessor\r\n{\r\n    public KaraokeBeatmapProcessor(IBeatmap beatmap)\r\n        : base(beatmap)\r\n    {\r\n    }\r\n\r\n    public override void PreProcess()\r\n    {\r\n        var karaokeBeatmap = getKaraokeBeatmap(Beatmap);\r\n\r\n        base.PreProcess();\r\n        applyInvalidProperty(karaokeBeatmap);\r\n        return;\r\n\r\n        static KaraokeBeatmap getKaraokeBeatmap(IBeatmap beatmap) =>\r\n            beatmap switch\r\n            {\r\n                // goes to there while parsing the beatmap.\r\n                KaraokeBeatmap karaokeBeatmap => karaokeBeatmap,\r\n                // goes to there while editing the beatmap.\r\n                EditorBeatmap editorBeatmap => getKaraokeBeatmap(editorBeatmap.PlayableBeatmap),\r\n                _ => throw new InvalidCastException($\"The beatmap is not a {nameof(KaraokeBeatmap)}\"),\r\n            };\r\n    }\r\n\r\n    private void applyInvalidProperty(KaraokeBeatmap beatmap)\r\n    {\r\n        // should convert to array here because validate the working property might change the start-time and the end time.\r\n        // which will cause got the wrong item in the array.\r\n        foreach (var hitObject in beatmap.HitObjects.OfType<IHasWorkingProperty>().ToArray())\r\n        {\r\n            hitObject.ValidateWorkingProperty(beatmap);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Beatmaps/KaraokeBeatmapResourcesProvider.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Textures;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas.Types;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Beatmaps;\r\n\r\npublic partial class KaraokeBeatmapResourcesProvider : Component, IKaraokeBeatmapResourcesProvider\r\n{\r\n    [Resolved]\r\n    private BeatmapManager beatmapManager { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private IBindable<WorkingBeatmap> working { get; set; } = null!;\r\n\r\n    public Texture? GetSingerAvatar(ISinger singer)\r\n    {\r\n        return null;\r\n    }\r\n\r\n    /*\r\n    public Texture? GetSingerAvatar(ISinger singer)\r\n    {\r\n        var provider = getBeatmapResourceProvider();\r\n        if (provider == null)\r\n            return null;\r\n\r\n        if (singer is not Singer s)\r\n            return null;\r\n\r\n        var beatmapSetInfo = working.Value.BeatmapSetInfo;\r\n        if (beatmapSetInfo == null)\r\n            return null;\r\n\r\n        string? path = beatmapSetInfo.GetPathForFile($\"assets/singers/{s.AvatarFile}\");\r\n        return provider.LargeTextureStore.Get(path);\r\n    }\r\n\r\n    private IBeatmapResourceProvider? getBeatmapResourceProvider()\r\n    {\r\n        // todo : use better way to get the resource provider.\r\n        var prop = typeof(BeatmapManager).GetField(\"workingBeatmapCache\", BindingFlags.Instance | BindingFlags.NonPublic);\r\n        if (prop == null)\r\n            throw new ArgumentNullException();\r\n\r\n        return prop.GetValue(beatmapManager) as WorkingBeatmapCache;\r\n    }\r\n    */\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Beatmaps/Metadatas/NoteInfo.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\n\r\npublic class NoteInfo\r\n{\r\n    public int Columns { get; set; } = 9;\r\n\r\n    public Tone MaxTone =>\r\n        new()\r\n        {\r\n            Scale = Columns / 2,\r\n        };\r\n\r\n    public Tone MinTone => -MaxTone;\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Beatmaps/Metadatas/Page.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing Newtonsoft.Json;\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\n\r\npublic class Page : IDeepCloneable<Page>, IComparable<Page>\r\n{\r\n    [JsonIgnore]\r\n    public readonly Bindable<double> TimeBindable = new();\r\n\r\n    public double Time\r\n    {\r\n        get => TimeBindable.Value;\r\n        set => TimeBindable.Value = value;\r\n    }\r\n\r\n    public Page DeepClone()\r\n    {\r\n        return new Page\r\n        {\r\n            Time = Time,\r\n        };\r\n    }\r\n\r\n    public int CompareTo(Page? other) => Time.CompareTo(other?.Time);\r\n\r\n    public override int GetHashCode() => Time.GetHashCode();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Beatmaps/Metadatas/PageInfo.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Collections.Specialized;\r\nusing System.Diagnostics;\r\nusing System.Linq;\r\nusing Newtonsoft.Json;\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\n\r\npublic class PageInfo : IDeepCloneable<PageInfo>\r\n{\r\n    [JsonIgnore]\r\n    public IBindable<int> PagesVersion => pagesVersion;\r\n\r\n    private readonly Bindable<int> pagesVersion = new();\r\n\r\n    public BindableList<Page> Pages = new();\r\n\r\n    [JsonIgnore]\r\n    public List<Page> SortedPages { get; private set; } = new();\r\n\r\n    public PageInfo()\r\n    {\r\n        Pages.CollectionChanged += (_, args) =>\r\n        {\r\n            switch (args.Action)\r\n            {\r\n                case NotifyCollectionChangedAction.Add:\r\n                    Debug.Assert(args.NewItems != null);\r\n\r\n                    foreach (var c in args.NewItems.Cast<Page>())\r\n                        c.TimeBindable.ValueChanged += timeValueChanged;\r\n                    break;\r\n\r\n                case NotifyCollectionChangedAction.Reset:\r\n                case NotifyCollectionChangedAction.Remove:\r\n                    Debug.Assert(args.OldItems != null);\r\n\r\n                    foreach (var c in args.OldItems.Cast<Page>())\r\n                        c.TimeBindable.ValueChanged -= timeValueChanged;\r\n                    break;\r\n            }\r\n\r\n            onPageChanged();\r\n\r\n            void timeValueChanged(ValueChangedEvent<double> e) => onPageChanged();\r\n        };\r\n\r\n        void onPageChanged()\r\n        {\r\n            SortedPages = Pages.OrderBy(x => x.Time).ToList();\r\n            pagesVersion.Value++;\r\n        }\r\n    }\r\n\r\n    public Page? GetPageAt(double time)\r\n    {\r\n        if (SortedPages.Count < 2)\r\n            return null;\r\n\r\n        var page = SortedPages.LastOrDefault(x => x.Time <= time);\r\n\r\n        // should not be able to get the page if time exceed the last page.\r\n        var lastPage = SortedPages.LastOrDefault();\r\n        if (page == lastPage && page?.Time < time)\r\n            return null;\r\n\r\n        return page;\r\n    }\r\n\r\n    public int? GetPageIndexAt(double time)\r\n    {\r\n        var page = GetPageAt(time);\r\n        if (page == null)\r\n            return null;\r\n\r\n        return SortedPages.IndexOf(page);\r\n    }\r\n\r\n    public int? GetPageOrder(Page page)\r\n    {\r\n        int index = SortedPages.IndexOf(page);\r\n        return index == -1 ? null : index + 1;\r\n    }\r\n\r\n    public PageInfo DeepClone()\r\n    {\r\n        var controlPointInfo = Activator.CreateInstance<PageInfo>();\r\n\r\n        return controlPointInfo;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Beatmaps/Metadatas/Singer.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing Newtonsoft.Json;\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas.Types;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\n\r\npublic class Singer : ISinger\r\n{\r\n    [JsonProperty]\r\n    public ElementId ID { get; private set; } = ElementId.NewElementId();\r\n\r\n    [JsonIgnore]\r\n    public readonly Bindable<int> OrderBindable = new();\r\n\r\n    /// <summary>\r\n    /// Order\r\n    /// </summary>\r\n    public int Order\r\n    {\r\n        get => OrderBindable.Value;\r\n        set => OrderBindable.Value = value;\r\n    }\r\n\r\n    [JsonIgnore]\r\n    public readonly Bindable<string> AvatarFileBindable = new();\r\n\r\n    public string AvatarFile\r\n    {\r\n        get => AvatarFileBindable.Value;\r\n        set => AvatarFileBindable.Value = value;\r\n    }\r\n\r\n    [JsonIgnore]\r\n    public Bindable<float> HueBindable = new BindableFloat\r\n    {\r\n        MinValue = 0,\r\n        MaxValue = 1,\r\n    };\r\n\r\n    public float Hue\r\n    {\r\n        get => HueBindable.Value;\r\n        set => HueBindable.Value = value;\r\n    }\r\n\r\n    [JsonIgnore]\r\n    public readonly Bindable<string> NameBindable = new();\r\n\r\n    public string Name\r\n    {\r\n        get => NameBindable.Value;\r\n        set => NameBindable.Value = value;\r\n    }\r\n\r\n    [JsonIgnore]\r\n    public readonly Bindable<string> RomanisationBindable = new();\r\n\r\n    public string Romanisation\r\n    {\r\n        get => RomanisationBindable.Value;\r\n        set => RomanisationBindable.Value = value;\r\n    }\r\n\r\n    [JsonIgnore]\r\n    public readonly Bindable<string> EnglishNameBindable = new();\r\n\r\n    public string EnglishName\r\n    {\r\n        get => EnglishNameBindable.Value;\r\n        set => EnglishNameBindable.Value = value;\r\n    }\r\n\r\n    [JsonIgnore]\r\n    public readonly Bindable<string> DescriptionBindable = new();\r\n\r\n    public string Description\r\n    {\r\n        get => DescriptionBindable.Value;\r\n        set => DescriptionBindable.Value = value;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Beatmaps/Metadatas/SingerInfo.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas.Types;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\n\r\npublic class SingerInfo\r\n{\r\n    public bool SupportSingerState { get; set; }\r\n\r\n    // todo: should make the property as readonly.\r\n    public BindableList<Singer> Singers { get; set; } = new();\r\n\r\n    // todo: should make the property as readonly.\r\n    public BindableList<SingerState> SingerState { get; set; } = new();\r\n\r\n    public IEnumerable<Singer> GetAllSingers() =>\r\n        Singers.OrderBy(x => x.Order);\r\n\r\n    public IEnumerable<SingerState> GetAllAvailableSingerStates(Singer singer) =>\r\n        SingerState.Where(x => x.MainSingerId == singer.ID).OrderBy(x => x.Order);\r\n\r\n    public IDictionary<Singer, SingerState[]> GetSingerByIds(ElementId[] singerIds)\r\n    {\r\n        var matchedMainSingers = GetAllSingers().Where(x => singerIds.Contains(x.ID));\r\n        return matchedMainSingers.ToDictionary(k => k, v =>\r\n        {\r\n            var matchedSingerStates = GetAllAvailableSingerStates(v);\r\n\r\n            return matchedSingerStates.Where(x => singerIds.Contains(x.ID)).ToArray();\r\n        });\r\n    }\r\n\r\n    public IDictionary<Singer, SingerState[]> GetSingerMap()\r\n    {\r\n        var matchedMainSingers = GetAllSingers();\r\n        return matchedMainSingers.ToDictionary(k => k, v => GetAllAvailableSingerStates(v).ToArray());\r\n    }\r\n\r\n    public Singer AddSinger(Action<Singer>? action = null)\r\n    {\r\n        var singer = new Singer();\r\n        action?.Invoke(singer);\r\n\r\n        Singers.Add(singer);\r\n\r\n        return singer;\r\n    }\r\n\r\n    public SingerState AddSingerState(Singer singer, Action<SingerState>? action = null)\r\n    {\r\n        if (!Singers.Contains(singer))\r\n            throw new InvalidOperationException(\"Main singer must in the singer info.\");\r\n\r\n        var mainSingerId = singer.ID;\r\n        var singerState = new SingerState(mainSingerId);\r\n        action?.Invoke(singerState);\r\n\r\n        SingerState.Add(singerState);\r\n\r\n        return singerState;\r\n    }\r\n\r\n    public bool RemoveSinger(ISinger singer)\r\n    {\r\n        switch (singer)\r\n        {\r\n            case Singer mainSinger:\r\n            {\r\n                var singerStates = GetAllAvailableSingerStates(mainSinger);\r\n\r\n                foreach (var singerState in singerStates)\r\n                {\r\n                    RemoveSinger(singerState);\r\n                }\r\n\r\n                return Singers.Remove(mainSinger);\r\n            }\r\n\r\n            case SingerState singerState:\r\n                return SingerState.Remove(singerState);\r\n\r\n            default:\r\n                throw new InvalidCastException();\r\n        }\r\n    }\r\n\r\n    public bool HasSinger(ISinger singer)\r\n    {\r\n        return singer switch\r\n        {\r\n            Singer mainSinger => Singers.Contains(mainSinger),\r\n            SingerState singerState => SingerState.Contains(singerState),\r\n            _ => throw new InvalidCastException(),\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Beatmaps/Metadatas/SingerState.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing Newtonsoft.Json;\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas.Types;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\n\r\npublic class SingerState : ISinger\r\n{\r\n    public SingerState()\r\n    {\r\n    }\r\n\r\n    public SingerState(ElementId mainSingerId)\r\n    {\r\n        MainSingerId = mainSingerId;\r\n    }\r\n\r\n    [JsonProperty]\r\n    public ElementId ID { get; private set; } = ElementId.NewElementId();\r\n\r\n    [JsonProperty]\r\n    public ElementId MainSingerId { get; private set; }\r\n\r\n    [JsonIgnore]\r\n    public readonly Bindable<int> OrderBindable = new();\r\n\r\n    /// <summary>\r\n    /// Order\r\n    /// </summary>\r\n    public int Order\r\n    {\r\n        get => OrderBindable.Value;\r\n        set => OrderBindable.Value = value;\r\n    }\r\n\r\n    [JsonIgnore]\r\n    public Bindable<float> HueBindable = new BindableFloat\r\n    {\r\n        MinValue = 0,\r\n        MaxValue = 1,\r\n    };\r\n\r\n    public float Hue\r\n    {\r\n        get => HueBindable.Value;\r\n        set => HueBindable.Value = value;\r\n    }\r\n\r\n    [JsonIgnore]\r\n    public readonly Bindable<string> DescriptionBindable = new();\r\n\r\n    public string Description\r\n    {\r\n        get => DescriptionBindable.Value;\r\n        set => DescriptionBindable.Value = value;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Beatmaps/Metadatas/Types/ISinger.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Objects.Types;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas.Types;\r\n\r\npublic interface ISinger : IHasOrder, IHasPrimaryKey\r\n{\r\n    float Hue { get; }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Beatmaps/Utils/SingerUtils.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas.Types;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Beatmaps.Utils;\r\n\r\npublic static class SingerUtils\r\n{\r\n    public static int GetShiftingStyleIndex(IEnumerable<int> singerIds)\r\n        => singerIds.Sum(x => (int)Math.Pow(2, x - 1));\r\n\r\n    public static int[] GetSingersIndex(int styleIndex)\r\n    {\r\n        if (styleIndex < 1)\r\n            return Array.Empty<int>();\r\n\r\n        string binary = Convert.ToString(styleIndex, 2);\r\n\r\n        return binary.Select((v, i) => new { value = v, singer = binary.Length - i })\r\n                     .Where(x => x.value == '1').Select(x => x.singer).OrderBy(x => x).ToArray();\r\n    }\r\n\r\n    public static Color4 GetContentColour(ISinger singer)\r\n        => Colour4.FromHSL(singer.Hue, 0.4f, 0.6f);\r\n\r\n    public static Color4 GetBackgroundColour(ISinger singer)\r\n        => Colour4.FromHSL(singer.Hue, 0.1f, 0.4f);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Bindables/BindableCultureInfo.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Globalization;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Logging;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Bindables;\r\n\r\npublic class BindableCultureInfo : Bindable<CultureInfo?>\r\n{\r\n    public BindableCultureInfo(CultureInfo? value = default)\r\n        : base(value)\r\n    {\r\n    }\r\n\r\n    public override void Parse(object? input, IFormatProvider provider)\r\n    {\r\n        if (input == null)\r\n        {\r\n            Value = null;\r\n            return;\r\n        }\r\n\r\n        try\r\n        {\r\n            switch (input)\r\n            {\r\n                case string str:\r\n                    Value = CultureInfoUtils.CreateLoadCultureInfoByCode(str);\r\n                    break;\r\n\r\n                case int lcid:\r\n                    Value = CultureInfoUtils.CreateLoadCultureInfoById(lcid);\r\n                    break;\r\n\r\n                case CultureInfo cultureInfo:\r\n                    Value = cultureInfo;\r\n                    break;\r\n\r\n                default:\r\n                    base.Parse(input, provider);\r\n                    break;\r\n            }\r\n        }\r\n        catch (Exception ex)\r\n        {\r\n            Value = null;\r\n\r\n            // It might have issue that the culture info is not available in the system.\r\n            // Log it instead of throw exception.\r\n            Logger.Error(ex, $\"Failed to parse {input} into {typeof(CultureInfo)}\");\r\n        }\r\n    }\r\n\r\n    protected override Bindable<CultureInfo?> CreateInstance() => new BindableCultureInfo();\r\n\r\n    public override string ToString(string? format, IFormatProvider? formatProvider)\r\n        => Value != null ? CultureInfoUtils.GetSaveCultureInfoCode(Value) : string.Empty;\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Bindables/BindableFontUsage.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing System.Text.RegularExpressions;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Extensions;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Bindables;\r\n\r\npublic class BindableFontUsage : RangeConstrainedBindable<FontUsage>\r\n{\r\n    private const int default_min_font_size = 1;\r\n    private const int default_max_font_size = 72;\r\n\r\n    protected override FontUsage DefaultMinValue => Default.With(size: default_min_font_size);\r\n    protected override FontUsage DefaultMaxValue => Default.With(size: default_max_font_size);\r\n\r\n    public BindableFontUsage(FontUsage value = default)\r\n        : base(value)\r\n    {\r\n    }\r\n\r\n    public float MinFontSize\r\n    {\r\n        get => MinValue.Size;\r\n        set => MinValue = MinValue.With(size: value);\r\n    }\r\n\r\n    public float MaxFontSize\r\n    {\r\n        get => MaxValue.Size;\r\n        set => MaxValue = MaxValue.With(size: value);\r\n    }\r\n\r\n    // IDK why not being called in here while saving.\r\n    public override string ToString(string? format, IFormatProvider? formatProvider)\r\n        => $\"family={Value.Family} weight={Value.Weight} size={Value.Size} italics={Value.Italics} fixedWidth={Value.FixedWidth}\";\r\n\r\n    public override void Parse(object? input, IFormatProvider provider)\r\n    {\r\n        if (input is not string str || string.IsNullOrEmpty(str))\r\n        {\r\n            Value = default;\r\n            return;\r\n        }\r\n\r\n        // because FontUsage.ToString() will have \",\" symbol.\r\n        str = str.Replace(\",\", string.Empty);\r\n        var regex = new Regex(@\"\\b(?<key>font|family|weight|size|italics|fixedWidth)(?<op>[=]+)(?<value>(\"\".*\"\")|(\\S*))\", RegexOptions.Compiled | RegexOptions.IgnoreCase);\r\n        var dictionary = regex.Matches(str).ToDictionary(k => k.GetGroupValue<string>(\"key\"), v => v.GetGroupValue<string>(\"value\"));\r\n\r\n        if (dictionary.TryGetValue(\"Font\", out string? font))\r\n        {\r\n            string? family = font.Contains('-') ? font.Split('-').FirstOrDefault() : font;\r\n            string? weight = font.Contains('-') ? font.Split('-').LastOrDefault() : string.Empty;\r\n            float size = float.Parse(dictionary[\"Size\"]);\r\n            bool italics = dictionary[\"Italics\"].ToLower() == \"true\";\r\n            bool fixedWidth = dictionary[\"FixedWidth\"].ToLower() == \"true\";\r\n            Value = new FontUsage(family, size, weight, italics, fixedWidth);\r\n        }\r\n        else\r\n        {\r\n            string family = dictionary[\"family\"];\r\n            string weight = dictionary[\"weight\"];\r\n            float size = float.Parse(dictionary[\"size\"]);\r\n            bool italics = dictionary[\"italics\"].ToLower() == \"true\";\r\n            bool fixedWidth = dictionary[\"fixedWidth\"].ToLower() == \"true\";\r\n            Value = new FontUsage(family, size, weight, italics, fixedWidth);\r\n        }\r\n    }\r\n\r\n    protected override Bindable<FontUsage> CreateInstance() => new BindableFontUsage();\r\n\r\n    protected sealed override FontUsage ClampValue(FontUsage value, FontUsage minValue, FontUsage maxValue)\r\n    {\r\n        return value.With(size: Math.Clamp(value.Size, minValue.Size, maxValue.Size));\r\n    }\r\n\r\n    protected sealed override bool IsValidRange(FontUsage min, FontUsage max)\r\n        => min.Size <= max.Size;\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Configuration/KaraokeRulesetConfigManager.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Configuration.Tracking;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Configuration;\r\nusing osu.Game.Rulesets.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.UI;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Configuration;\r\n\r\npublic class KaraokeRulesetConfigManager : RulesetConfigManager<KaraokeRulesetSetting>\r\n{\r\n    public KaraokeRulesetConfigManager(SettingsStore? settings, RulesetInfo ruleset, int? variant = null)\r\n        : base(settings, ruleset, variant)\r\n    {\r\n    }\r\n\r\n    protected override void InitialiseDefaults()\r\n    {\r\n        base.InitialiseDefaults();\r\n\r\n        // Visual\r\n        SetDefault(KaraokeRulesetSetting.ScrollTime, 5000.0, 1000.0, 10000.0, 100.0);\r\n        SetDefault(KaraokeRulesetSetting.ScrollDirection, KaraokeScrollingDirection.Left);\r\n        SetDefault(KaraokeRulesetSetting.DisplayNoteRubyText, false);\r\n        SetDefault(KaraokeRulesetSetting.ShowCursor, true);\r\n        SetDefault(KaraokeRulesetSetting.NoteAlpha, 1, 0.2, 1, 0.01);\r\n        SetDefault(KaraokeRulesetSetting.LyricAlpha, 1, 0.2, 1, 0.01);\r\n\r\n        // Pitch\r\n        SetDefault(KaraokeRulesetSetting.OverridePitchAtGameplay, false);\r\n        SetDefault(KaraokeRulesetSetting.Pitch, 0, -10, 10);\r\n        SetDefault(KaraokeRulesetSetting.OverrideVocalPitchAtGameplay, false);\r\n        SetDefault(KaraokeRulesetSetting.VocalPitch, 0, -10, 10);\r\n        SetDefault(KaraokeRulesetSetting.OverrideScoringPitchAtGameplay, false);\r\n        SetDefault(KaraokeRulesetSetting.ScoringPitch, 0, -10, 10);\r\n\r\n        // Playback\r\n        SetDefault(KaraokeRulesetSetting.OverridePlaybackSpeedAtGameplay, false);\r\n        SetDefault(KaraokeRulesetSetting.PlaybackSpeed, 0, -10, 10);\r\n\r\n        // Device\r\n        SetDefault(KaraokeRulesetSetting.MicrophoneDevice, string.Empty);\r\n\r\n        // Font\r\n        SetDefault(KaraokeRulesetSetting.MainFont, new FontUsage(\"Torus\", 48, \"Bold\"), 48f, 48f);\r\n        SetDefault(KaraokeRulesetSetting.RubyFont, new FontUsage(\"Torus\", 20, \"Bold\"), 8f, 48f);\r\n        SetDefault(KaraokeRulesetSetting.RubyMargin, 5, 0, 20);\r\n        SetDefault(KaraokeRulesetSetting.RomanisationFont, new FontUsage(\"Torus\", 20, \"Bold\"), 8f, 48f);\r\n        SetDefault(KaraokeRulesetSetting.RomanisationMargin, 0, 0, 20);\r\n        SetDefault(KaraokeRulesetSetting.ForceUseDefaultFont, false);\r\n        SetDefault(KaraokeRulesetSetting.TranslationFont, new FontUsage(\"Torus\", 18, \"Bold\"), 10f, 48f);\r\n        SetDefault(KaraokeRulesetSetting.ForceUseDefaultTranslationFont, false);\r\n        SetDefault(KaraokeRulesetSetting.NoteFont, new FontUsage(\"Torus\", 12, \"Bold\"), 10f, 32f);\r\n        SetDefault(KaraokeRulesetSetting.ForceUseDefaultNoteFont, false);\r\n    }\r\n\r\n    protected override void AddBindable<TBindable>(KaraokeRulesetSetting lookup, Bindable<TBindable> bindable)\r\n    {\r\n        switch (lookup)\r\n        {\r\n            case KaraokeRulesetSetting.MainFont:\r\n            case KaraokeRulesetSetting.RubyFont:\r\n            case KaraokeRulesetSetting.RomanisationFont:\r\n            case KaraokeRulesetSetting.TranslationFont:\r\n            case KaraokeRulesetSetting.NoteFont:\r\n                base.AddBindable(lookup, new BindableFontUsage(TypeUtils.ChangeType<FontUsage>(bindable.Value)));\r\n                break;\r\n\r\n            default:\r\n                base.AddBindable(lookup, bindable);\r\n                break;\r\n        }\r\n    }\r\n\r\n    protected BindableFontUsage SetDefault(KaraokeRulesetSetting setting, FontUsage fontUsage, float? minFontSize = null, float? maxFontSize = null)\r\n    {\r\n        base.SetDefault(setting, fontUsage);\r\n\r\n        // Should not use base.setDefault's value because it will return Bindable<FontUsage>, not BindableFontUsage\r\n        var bindable = GetOriginalBindable<FontUsage>(setting);\r\n        if (bindable is not BindableFontUsage bindableFontUsage)\r\n            throw new InvalidCastException(nameof(bindable));\r\n\r\n        // Assign size restriction in here.\r\n        if (minFontSize.HasValue) bindableFontUsage.MinFontSize = minFontSize.Value;\r\n        if (maxFontSize.HasValue) bindableFontUsage.MaxFontSize = maxFontSize.Value;\r\n\r\n        return bindableFontUsage;\r\n    }\r\n\r\n    public override TrackedSettings CreateTrackedSettings() => new()\r\n    {\r\n        new TrackedSetting<double>(KaraokeRulesetSetting.ScrollTime, v => new SettingDescription(v, \"Scroll Time\", $\"{v}ms\")),\r\n        new TrackedSetting<bool>(KaraokeRulesetSetting.DisplayNoteRubyText, b => new SettingDescription(b, \"Toggle display\", b ? \"Show\" : \"Hide\")),\r\n        new TrackedSetting<bool>(KaraokeRulesetSetting.ShowCursor, b => new SettingDescription(b, \"Cursor display\", b ? \"Show\" : \"Hide\")),\r\n        new TrackedSetting<string>(KaraokeRulesetSetting.MicrophoneDevice, d => new SettingDescription(d, \"Change to the new microphone device\", d)),\r\n    };\r\n}\r\n\r\npublic enum KaraokeRulesetSetting\r\n{\r\n    // Visual\r\n    ScrollTime,\r\n    ScrollDirection,\r\n    DisplayNoteRubyText,\r\n    ShowCursor,\r\n    NoteAlpha,\r\n    LyricAlpha,\r\n\r\n    // Pitch\r\n    OverridePitchAtGameplay,\r\n    Pitch,\r\n    OverrideVocalPitchAtGameplay,\r\n    VocalPitch,\r\n    OverrideScoringPitchAtGameplay,\r\n    ScoringPitch,\r\n\r\n    // Playback\r\n    OverridePlaybackSpeedAtGameplay,\r\n    PlaybackSpeed,\r\n\r\n    // Device\r\n    MicrophoneDevice,\r\n\r\n    // Font\r\n    MainFont,\r\n    RubyFont,\r\n    RubyMargin,\r\n    RomanisationFont,\r\n    RomanisationMargin,\r\n    ForceUseDefaultFont,\r\n    TranslationFont,\r\n    ForceUseDefaultTranslationFont,\r\n    NoteFont,\r\n    ForceUseDefaultNoteFont,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Configuration/KaraokeRulesetEditCheckerConfigManager.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Configuration;\r\n\r\npublic class KaraokeRulesetEditCheckerConfigManager : InMemoryConfigManager<KaraokeRulesetEditCheckerSetting>\r\n{\r\n    protected override void InitialiseDefaults()\r\n    {\r\n        base.InitialiseDefaults();\r\n\r\n        // Lyric\r\n        SetDefault(KaraokeRulesetEditCheckerSetting.LyricRubyPositionSorting, RubyTagsUtils.Sorting.Asc);\r\n        SetDefault(KaraokeRulesetEditCheckerSetting.LyricTimeTagTimeSelfCheck, SelfCheck.BasedOnStart);\r\n        SetDefault(KaraokeRulesetEditCheckerSetting.LyricTimeTagTimeGroupCheck, GroupCheck.Asc);\r\n    }\r\n}\r\n\r\npublic enum KaraokeRulesetEditCheckerSetting\r\n{\r\n    LyricRubyPositionSorting,\r\n    LyricTimeTagTimeSelfCheck,\r\n    LyricTimeTagTimeGroupCheck,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Configuration/KaraokeRulesetEditConfigManager.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Configuration;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Configuration;\r\n\r\npublic class KaraokeRulesetEditConfigManager : InMemoryConfigManager<KaraokeRulesetEditSetting>;\r\n\r\npublic enum KaraokeRulesetEditSetting;\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Configuration/KaraokeRulesetEditGeneratorConfigManager.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Beatmaps.Pages;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.Language;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.Notes;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.ReferenceLyric;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.Romanisation.Ja;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.RubyTags.Ja;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.TimeTags.Ja;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.TimeTags.Zh;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Stages.Classic;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Stages.Preview;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Configuration;\r\n\r\npublic class KaraokeRulesetEditGeneratorConfigManager : InMemoryConfigManager<KaraokeRulesetEditGeneratorSetting>\r\n{\r\n    protected override void InitialiseDefaults()\r\n    {\r\n        base.InitialiseDefaults();\r\n\r\n        // Beatmap page\r\n        SetDefault<PageGeneratorConfig>();\r\n\r\n        // Classic stage.\r\n        SetDefault<ClassicLyricLayoutCategoryGeneratorConfig>();\r\n        SetDefault<ClassicLyricTimingInfoGeneratorConfig>();\r\n        SetDefault<ClassicStageInfoGeneratorConfig>();\r\n\r\n        // Preview stage.\r\n        SetDefault<PreviewStageInfoGeneratorConfig>();\r\n\r\n        // Language detection\r\n        SetDefault<ReferenceLyricDetectorConfig>();\r\n\r\n        // Language detection\r\n        SetDefault<LanguageDetectorConfig>();\r\n\r\n        // Ruby generator\r\n        SetDefault<JaRubyTagGeneratorConfig>();\r\n\r\n        // Time tag generator\r\n        SetDefault<JaTimeTagGeneratorConfig>();\r\n        SetDefault<ZhTimeTagGeneratorConfig>();\r\n\r\n        // Romanisation generator\r\n        SetDefault<JaRomanisationGeneratorConfig>();\r\n\r\n        // Note generator\r\n        SetDefault<NoteGeneratorConfig>();\r\n    }\r\n\r\n    protected void SetDefault<T>() where T : GeneratorConfig, new()\r\n    {\r\n        var defaultValue = CreateDefaultConfig<T>();\r\n        var setting = GetSettingByType<T>();\r\n\r\n        SetDefault(setting, defaultValue);\r\n    }\r\n\r\n    protected static T CreateDefaultConfig<T>() where T : GeneratorConfig, new() => new();\r\n\r\n    protected static KaraokeRulesetEditGeneratorSetting GetSettingByType<TValue>() =>\r\n        typeof(TValue) switch\r\n        {\r\n            Type t when t == typeof(PageGeneratorConfig) => KaraokeRulesetEditGeneratorSetting.BeatmapPageGeneratorConfig,\r\n            Type t when t == typeof(ClassicLyricLayoutCategoryGeneratorConfig) => KaraokeRulesetEditGeneratorSetting.ClassicLyricLayoutCategoryGeneratorConfig,\r\n            Type t when t == typeof(ClassicLyricTimingInfoGeneratorConfig) => KaraokeRulesetEditGeneratorSetting.ClassicLyricTimingInfoGeneratorConfig,\r\n            Type t when t == typeof(ClassicStageInfoGeneratorConfig) => KaraokeRulesetEditGeneratorSetting.ClassicStageInfoGeneratorConfig,\r\n            Type t when t == typeof(PreviewStageInfoGeneratorConfig) => KaraokeRulesetEditGeneratorSetting.PreviewStageInfoGeneratorConfig,\r\n            Type t when t == typeof(ReferenceLyricDetectorConfig) => KaraokeRulesetEditGeneratorSetting.ReferenceLyricDetectorConfig,\r\n            Type t when t == typeof(LanguageDetectorConfig) => KaraokeRulesetEditGeneratorSetting.LanguageDetectorConfig,\r\n            Type t when t == typeof(JaRubyTagGeneratorConfig) => KaraokeRulesetEditGeneratorSetting.JaRubyTagGeneratorConfig,\r\n            Type t when t == typeof(JaTimeTagGeneratorConfig) => KaraokeRulesetEditGeneratorSetting.JaTimeTagGeneratorConfig,\r\n            Type t when t == typeof(ZhTimeTagGeneratorConfig) => KaraokeRulesetEditGeneratorSetting.ZhTimeTagGeneratorConfig,\r\n            Type t when t == typeof(JaRomanisationGeneratorConfig) => KaraokeRulesetEditGeneratorSetting.JaRomanisationGeneratorConfig,\r\n            Type t when t == typeof(NoteGeneratorConfig) => KaraokeRulesetEditGeneratorSetting.NoteGeneratorConfig,\r\n            _ => throw new NotSupportedException(),\r\n        };\r\n\r\n    public TValue Get<TValue>() where TValue : GeneratorConfig, new()\r\n    {\r\n        var lookup = GetSettingByType<TValue>();\r\n        return Get<TValue>(lookup);\r\n    }\r\n\r\n    public GeneratorConfig GetGeneratorConfig(KaraokeRulesetEditGeneratorSetting lookup)\r\n    {\r\n        if (!ConfigStore.TryGetValue(lookup, out IBindable? obj))\r\n            throw new KeyNotFoundException();\r\n\r\n        var prop = obj.GetType().GetProperty(\"Value\");\r\n        if (prop?.GetValue(obj) is not GeneratorConfig generatorConfig)\r\n            throw new InvalidCastException();\r\n\r\n        return generatorConfig;\r\n    }\r\n}\r\n\r\npublic enum KaraokeRulesetEditGeneratorSetting\r\n{\r\n    // Beatmap\r\n    BeatmapPageGeneratorConfig,\r\n\r\n    // Classic stage.\r\n    ClassicLyricLayoutCategoryGeneratorConfig,\r\n    ClassicLyricTimingInfoGeneratorConfig,\r\n    ClassicStageInfoGeneratorConfig,\r\n\r\n    // Preview stage.\r\n    PreviewStageInfoGeneratorConfig,\r\n\r\n    // Reference lyric detection.\r\n    ReferenceLyricDetectorConfig,\r\n\r\n    // Language detection\r\n    LanguageDetectorConfig,\r\n\r\n    // Ruby generator\r\n    JaRubyTagGeneratorConfig,\r\n\r\n    // Time tag generator\r\n    JaTimeTagGeneratorConfig,\r\n    ZhTimeTagGeneratorConfig,\r\n\r\n    // Romanisation generator.\r\n    JaRomanisationGeneratorConfig,\r\n\r\n    // Note generator\r\n    NoteGeneratorConfig,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Configuration/KaraokeRulesetLyricEditorConfigManager.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Types;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition.Algorithms;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Configuration;\r\n\r\npublic class KaraokeRulesetLyricEditorConfigManager : InMemoryConfigManager<KaraokeRulesetLyricEditorSetting>\r\n{\r\n    protected override void InitialiseDefaults()\r\n    {\r\n        base.InitialiseDefaults();\r\n\r\n        // General\r\n        SetDefault(KaraokeRulesetLyricEditorSetting.LyricEditorPreferLayout, LyricEditorLayout.List);\r\n        SetDefault(KaraokeRulesetLyricEditorSetting.LyricEditorFontSize, FontUtils.DEFAULT_FONT_SIZE);\r\n        SetDefault(KaraokeRulesetLyricEditorSetting.AutoFocusToEditLyric, true);\r\n        SetDefault(KaraokeRulesetLyricEditorSetting.AutoFocusToEditLyricSkipRows, 1, 0, 4);\r\n        SetDefault(KaraokeRulesetLyricEditorSetting.ClickToLockLyricState, LockState.Partial);\r\n\r\n        // Composer\r\n        SetDefault(KaraokeRulesetLyricEditorSetting.ShowPropertyPanelInComposer, true);\r\n        SetDefault(KaraokeRulesetLyricEditorSetting.ShowInvalidInfoInComposer, true);\r\n        SetDefault(KaraokeRulesetLyricEditorSetting.FontSizeInComposer, FontUtils.DEFAULT_FONT_SIZE_IN_COMPOSER);\r\n\r\n        // Recording\r\n        SetDefault(KaraokeRulesetLyricEditorSetting.RecordingTimeTagMovingCaretMode, RecordingTimeTagCaretMoveMode.None);\r\n        SetDefault(KaraokeRulesetLyricEditorSetting.RecordingAutoMoveToNextTimeTag, true);\r\n        SetDefault(KaraokeRulesetLyricEditorSetting.RecordingChangeTimeWhileMovingTheCaret, true);\r\n        SetDefault(KaraokeRulesetLyricEditorSetting.RecordingTimeTagShowWaveform, true);\r\n        SetDefault(KaraokeRulesetLyricEditorSetting.RecordingTimeTagWaveformOpacity, 0.5f, 0, 1, 0.01f);\r\n        SetDefault(KaraokeRulesetLyricEditorSetting.RecordingTimeTagShowTick, true);\r\n        SetDefault(KaraokeRulesetLyricEditorSetting.RecordingTimeTagTickOpacity, 0.5f, 0, 1, 0.01f);\r\n\r\n        // Adjust\r\n        SetDefault(KaraokeRulesetLyricEditorSetting.AdjustTimeTagShowWaveform, true);\r\n        SetDefault(KaraokeRulesetLyricEditorSetting.AdjustTimeTagWaveformOpacity, 0.5f, 0, 1, 0.01f);\r\n        SetDefault(KaraokeRulesetLyricEditorSetting.AdjustTimeTagShowTick, true);\r\n        SetDefault(KaraokeRulesetLyricEditorSetting.AdjustTimeTagTickOpacity, 0.5f, 0, 1, 0.01f);\r\n    }\r\n\r\n    /// <summary>\r\n    /// Binds a local bindable with a configuration-backed bindable.\r\n    /// </summary>\r\n    public void BindWith<TValue>(KaraokeRulesetLyricEditorSetting lookup, IBindable<TValue> bindable) => bindable.BindTo(GetOriginalBindable<TValue>(lookup));\r\n}\r\n\r\npublic enum KaraokeRulesetLyricEditorSetting\r\n{\r\n    // General\r\n    LyricEditorPreferLayout,\r\n    LyricEditorFontSize,\r\n    AutoFocusToEditLyric,\r\n    AutoFocusToEditLyricSkipRows,\r\n    ClickToLockLyricState,\r\n\r\n    // Composer\r\n    ShowPropertyPanelInComposer,\r\n    ShowInvalidInfoInComposer,\r\n    FontSizeInComposer,\r\n\r\n    // Recording\r\n    RecordingTimeTagMovingCaretMode,\r\n    RecordingAutoMoveToNextTimeTag,\r\n    RecordingChangeTimeWhileMovingTheCaret,\r\n    RecordingTimeTagShowWaveform,\r\n    RecordingTimeTagWaveformOpacity,\r\n    RecordingTimeTagShowTick,\r\n    RecordingTimeTagTickOpacity,\r\n\r\n    // Adjust\r\n    AdjustTimeTagShowWaveform,\r\n    AdjustTimeTagWaveformOpacity,\r\n    AdjustTimeTagShowTick,\r\n    AdjustTimeTagTickOpacity,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Configuration/KaraokeSessionStatics.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.UI.Components;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Configuration;\r\n\r\npublic class KaraokeSessionStatics : InMemoryConfigManager<KaraokeRulesetSession>\r\n{\r\n    private readonly KaraokeRulesetConfigManager rulesetConfigManager;\r\n\r\n    public KaraokeSessionStatics(KaraokeRulesetConfigManager config, IBeatmap? beatmap)\r\n    {\r\n        rulesetConfigManager = config;\r\n\r\n        // Pitch\r\n        bool overridePitch = getValue<bool>(KaraokeRulesetSetting.OverridePitchAtGameplay);\r\n        int pitchValue = getValue<int>(KaraokeRulesetSetting.Pitch);\r\n        SetDefault(KaraokeRulesetSession.Pitch, overridePitch ? pitchValue : 0, -10, 10);\r\n\r\n        bool overrideVocalPitch = getValue<bool>(KaraokeRulesetSetting.OverrideVocalPitchAtGameplay);\r\n        int vocalPitchValue = getValue<int>(KaraokeRulesetSetting.VocalPitch);\r\n        SetDefault(KaraokeRulesetSession.VocalPitch, overrideVocalPitch ? vocalPitchValue : 0, -10, 10);\r\n\r\n        bool overrideScoringPitch = getValue<bool>(KaraokeRulesetSetting.OverrideScoringPitchAtGameplay);\r\n        int scoringPitchValue = getValue<int>(KaraokeRulesetSetting.ScoringPitch);\r\n        SetDefault(KaraokeRulesetSession.ScoringPitch, overrideScoringPitch ? scoringPitchValue : 0, -8, 8);\r\n\r\n        // Playback\r\n        bool overridePlaybackSpeed = getValue<bool>(KaraokeRulesetSetting.OverridePlaybackSpeedAtGameplay);\r\n        int playbackSpeedValue = getValue<int>(KaraokeRulesetSetting.PlaybackSpeed);\r\n        SetDefault(KaraokeRulesetSession.PlaybackSpeed, overridePlaybackSpeed ? playbackSpeedValue : 0, -10, 10);\r\n\r\n        // Practice\r\n        SetDefault(KaraokeRulesetSession.SingingLyrics, Array.Empty<Lyric>());\r\n\r\n        // Scoring status\r\n        SetDefault(KaraokeRulesetSession.ScoringStatus, ScoringStatusMode.NotInitialized);\r\n    }\r\n\r\n    private T getValue<T>(KaraokeRulesetSetting setting) => rulesetConfigManager.Get<T>(setting);\r\n}\r\n\r\npublic enum KaraokeRulesetSession\r\n{\r\n    // Pitch\r\n    Pitch,\r\n    VocalPitch,\r\n    ScoringPitch,\r\n\r\n    // Playback\r\n    PlaybackSpeed,\r\n\r\n    // Practice\r\n    SingingLyrics,\r\n\r\n    // Scoring status\r\n    ScoringStatus,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Difficulty/KaraokeDifficultyAttributes.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing Newtonsoft.Json;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Rulesets.Difficulty;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Difficulty;\r\n\r\npublic class KaraokeDifficultyAttributes : DifficultyAttributes\r\n{\r\n    /// <summary>\r\n    /// The hit window for a GREAT hit inclusive of rate-adjusting mods (DT/HT/etc).\r\n    /// </summary>\r\n    /// <remarks>\r\n    /// Rate-adjusting mods do not affect the hit window at all in osu-stable.\r\n    /// </remarks>\r\n    [JsonProperty(\"great_hit_window\")]\r\n    public double GreatHitWindow { get; set; }\r\n\r\n    /// <summary>\r\n    /// The score multiplier applied via score-reducing mods.\r\n    /// </summary>\r\n    [JsonProperty(\"score_multiplier\")]\r\n    public double ScoreMultiplier { get; set; }\r\n\r\n    public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes()\r\n    {\r\n        foreach (var v in base.ToDatabaseAttributes())\r\n            yield return v;\r\n\r\n        yield return (ATTRIB_ID_DIFFICULTY, StarRating);\r\n    }\r\n\r\n    public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)\r\n    {\r\n        base.FromDatabaseAttributes(values, onlineInfo);\r\n\r\n        StarRating = values[ATTRIB_ID_DIFFICULTY];\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Difficulty/KaraokeDifficultyCalculator.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Extensions;\r\nusing osu.Game.Rulesets.Difficulty;\r\nusing osu.Game.Rulesets.Difficulty.Preprocessing;\r\nusing osu.Game.Rulesets.Difficulty.Skills;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Difficulty.Preprocessing;\r\nusing osu.Game.Rulesets.Karaoke.Difficulty.Skills;\r\nusing osu.Game.Rulesets.Karaoke.Mods;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Mods;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Difficulty;\r\n\r\npublic class KaraokeDifficultyCalculator : DifficultyCalculator\r\n{\r\n    private const double star_scaling_factor = 0.018;\r\n\r\n    private readonly bool isForCurrentRuleset;\r\n    private readonly double originalOverallDifficulty;\r\n\r\n    public KaraokeDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)\r\n        : base(ruleset, beatmap)\r\n    {\r\n        isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.MatchesOnlineID(ruleset);\r\n        originalOverallDifficulty = beatmap.BeatmapInfo.Difficulty.OverallDifficulty;\r\n    }\r\n\r\n    protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)\r\n    {\r\n        if (beatmap.HitObjects.Count == 0)\r\n            return new KaraokeDifficultyAttributes { Mods = mods };\r\n\r\n        return new KaraokeDifficultyAttributes\r\n        {\r\n            StarRating = skills[0].DifficultyValue() * star_scaling_factor,\r\n            Mods = mods,\r\n            // Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future\r\n            GreatHitWindow = (int)Math.Ceiling(getHitWindow300(mods) / clockRate),\r\n            MaxCombo = beatmap.HitObjects.Sum(h => h is Note ? 2 : 1),\r\n        };\r\n    }\r\n\r\n    protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)\r\n    {\r\n        var sortedObjects = beatmap.HitObjects.OfType<Note>().ToArray();\r\n\r\n        // todo : might have a sort.\r\n        // LegacySortHelper<HitObject>.Sort(sortedObjects, Comparer<HitObject>.Create((a, b) => (int)Math.Round(a.StartTime) - (int)Math.Round(b.StartTime)));\r\n\r\n        var objects = new List<DifficultyHitObject>();\r\n\r\n        for (int i = 1; i < sortedObjects.Length; i++)\r\n            objects.Add(new KaraokeDifficultyHitObject(sortedObjects[i], sortedObjects[i - 1], clockRate, objects, objects.Count));\r\n\r\n        return objects;\r\n    }\r\n\r\n    // Sorting is done in CreateDifficultyHitObjects, since the full list of hitobjects is required.\r\n    protected override IEnumerable<DifficultyHitObject> SortObjects(IEnumerable<DifficultyHitObject> input) => input;\r\n\r\n    protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[]\r\n    {\r\n        new Strain(mods, ((KaraokeBeatmap)beatmap).NoteInfo),\r\n    };\r\n\r\n    protected override Mod[] DifficultyAdjustmentMods =>\r\n        new Mod[]\r\n        {\r\n            new KaraokeModDisableNote(),\r\n            new KaraokeModHiddenNote(),\r\n        };\r\n\r\n    private int getHitWindow300(Mod[] mods)\r\n    {\r\n        if (!isForCurrentRuleset)\r\n            return applyModAdjustments(Math.Round(originalOverallDifficulty) > 4 ? 34 : 47, mods);\r\n\r\n        double od = Math.Min(10.0, Math.Max(0, 10.0 - originalOverallDifficulty));\r\n        return applyModAdjustments(34 + 3 * od, mods);\r\n\r\n        static int applyModAdjustments(double value, Mod[] mods)\r\n        {\r\n            if (mods.Any(m => m is KaraokeModDisableNote))\r\n                value /= 1.4;\r\n            else if (mods.Any(m => m is KaraokeModHiddenNote))\r\n                value *= 1.4;\r\n\r\n            return (int)value;\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Difficulty/KaraokePerformanceAttributes.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing Newtonsoft.Json;\r\nusing osu.Game.Rulesets.Difficulty;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Difficulty;\r\n\r\npublic class KaraokePerformanceAttributes : PerformanceAttributes\r\n{\r\n    [JsonProperty(\"difficulty\")]\r\n    public double Difficulty { get; set; }\r\n\r\n    [JsonProperty(\"accuracy\")]\r\n    public double Accuracy { get; set; }\r\n\r\n    [JsonProperty(\"scaled_score\")]\r\n    public double ScaledScore { get; set; }\r\n\r\n    public override IEnumerable<PerformanceDisplayAttribute> GetAttributesForDisplay()\r\n    {\r\n        foreach (var attribute in base.GetAttributesForDisplay())\r\n            yield return attribute;\r\n\r\n        yield return new PerformanceDisplayAttribute(nameof(Difficulty), \"Difficulty\", Difficulty);\r\n        yield return new PerformanceDisplayAttribute(nameof(Accuracy), \"Accuracy\", Accuracy);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Difficulty/KaraokePerformanceCalculator.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Game.Rulesets.Difficulty;\r\nusing osu.Game.Rulesets.Mods;\r\nusing osu.Game.Rulesets.Scoring;\r\nusing osu.Game.Scoring;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Difficulty;\r\n\r\npublic class KaraokePerformanceCalculator : PerformanceCalculator\r\n{\r\n    // Score after being scaled by non-difficulty-increasing mods\r\n    private double scaledScore;\r\n\r\n    private int countPerfect;\r\n    private int countGreat;\r\n    private int countGood;\r\n    private int countOk;\r\n    private int countMeh;\r\n    private int countMiss;\r\n\r\n    public KaraokePerformanceCalculator()\r\n        : base(new KaraokeRuleset())\r\n    {\r\n    }\r\n\r\n    protected override PerformanceAttributes CreatePerformanceAttributes(ScoreInfo score, DifficultyAttributes attributes)\r\n    {\r\n        var karaokeAttributes = (KaraokeDifficultyAttributes)attributes;\r\n\r\n        scaledScore = score.TotalScore;\r\n        countPerfect = score.Statistics.GetValueOrDefault(HitResult.Perfect);\r\n        countGreat = score.Statistics.GetValueOrDefault(HitResult.Great);\r\n        countGood = score.Statistics.GetValueOrDefault(HitResult.Good);\r\n        countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);\r\n        countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);\r\n        countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);\r\n\r\n        if (karaokeAttributes.ScoreMultiplier > 0)\r\n        {\r\n            // Scale score up, so it's comparable to other keymods\r\n            scaledScore *= 1.0 / karaokeAttributes.ScoreMultiplier;\r\n        }\r\n\r\n        // Arbitrary initial value for scaling pp in order to standardize distributions across game modes.\r\n        // The specific number has no intrinsic meaning and can be adjusted as needed.\r\n        double multiplier = 0.8;\r\n\r\n        if (score.Mods.Any(m => m is ModNoFail))\r\n            multiplier *= 0.9;\r\n        if (score.Mods.Any(m => m is ModEasy))\r\n            multiplier *= 0.5;\r\n\r\n        double difficultyValue = computeDifficultyValue(karaokeAttributes);\r\n        double accValue = computeAccuracyValue(difficultyValue, karaokeAttributes);\r\n        double totalValue =\r\n            Math.Pow(\r\n                Math.Pow(difficultyValue, 1.1) +\r\n                Math.Pow(accValue, 1.1), 1.0 / 1.1\r\n            ) * multiplier;\r\n\r\n        return new KaraokePerformanceAttributes\r\n        {\r\n            Difficulty = difficultyValue,\r\n            Accuracy = accValue,\r\n            ScaledScore = scaledScore,\r\n            Total = totalValue,\r\n        };\r\n    }\r\n\r\n    private double computeDifficultyValue(KaraokeDifficultyAttributes attributes)\r\n    {\r\n        double difficultyValue = Math.Pow(5 * Math.Max(1, attributes.StarRating / 0.2) - 4.0, 2.2) / 135.0;\r\n\r\n        difficultyValue *= 1.0 + 0.1 * Math.Min(1.0, totalHits / 1500.0);\r\n\r\n        switch (scaledScore)\r\n        {\r\n            case <= 500000:\r\n                difficultyValue = 0;\r\n                break;\r\n\r\n            case <= 600000:\r\n                difficultyValue *= (scaledScore - 500000) / 100000 * 0.3;\r\n                break;\r\n\r\n            case <= 700000:\r\n                difficultyValue *= 0.3 + (scaledScore - 600000) / 100000 * 0.25;\r\n                break;\r\n\r\n            case <= 800000:\r\n                difficultyValue *= 0.55 + (scaledScore - 700000) / 100000 * 0.20;\r\n                break;\r\n\r\n            case <= 900000:\r\n                difficultyValue *= 0.75 + (scaledScore - 800000) / 100000 * 0.15;\r\n                break;\r\n\r\n            default:\r\n                difficultyValue *= 0.90 + (scaledScore - 900000) / 100000 * 0.1;\r\n                break;\r\n        }\r\n\r\n        return difficultyValue;\r\n    }\r\n\r\n    private double computeAccuracyValue(double difficultyValue, KaraokeDifficultyAttributes attributes)\r\n    {\r\n        if (attributes.GreatHitWindow <= 0)\r\n            return 0;\r\n\r\n        // Lots of arbitrary values from testing.\r\n        // Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution\r\n        double accuracyValue = Math.Max(0.0, 0.2 - (attributes.GreatHitWindow - 34) * 0.006667)\r\n                               * difficultyValue\r\n                               * Math.Pow(Math.Max(0.0, scaledScore - 960000) / 40000, 1.1);\r\n\r\n        return accuracyValue;\r\n    }\r\n\r\n    private double totalHits => countPerfect + countOk + countGreat + countGood + countMeh + countMiss;\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Difficulty/Preprocessing/KaraokeDifficultyHitObject.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Game.Rulesets.Difficulty.Preprocessing;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Difficulty.Preprocessing;\r\n\r\npublic class KaraokeDifficultyHitObject : DifficultyHitObject\r\n{\r\n    public new Note BaseObject => (Note)base.BaseObject;\r\n\r\n    public KaraokeDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate, List<DifficultyHitObject> objects, int index)\r\n        : base(hitObject, lastObject, clockRate, objects, index)\r\n    {\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Difficulty/Skills/Strain.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Utils;\r\nusing osu.Game.Rulesets.Difficulty.Preprocessing;\r\nusing osu.Game.Rulesets.Difficulty.Skills;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\nusing osu.Game.Rulesets.Karaoke.Difficulty.Preprocessing;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Mods;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Difficulty.Skills;\r\n\r\npublic class Strain : StrainDecaySkill\r\n{\r\n    private const double individual_decay_base = 0.125;\r\n    private const double overall_decay_base = 0.30;\r\n\r\n    protected override double SkillMultiplier => 1;\r\n    protected override double StrainDecayBase => 1;\r\n\r\n    private readonly double[] holdEndTimes;\r\n    private readonly double[] individualStrains;\r\n\r\n    private double individualStrain;\r\n    private double overallStrain;\r\n\r\n    public Strain(Mod[] mods, NoteInfo noteInfo)\r\n        : base(mods)\r\n    {\r\n        int totalColumns = noteInfo.Columns;\r\n\r\n        holdEndTimes = new double[totalColumns * 2 - 1];\r\n        individualStrains = new double[totalColumns * 2 - 1];\r\n        overallStrain = 1;\r\n    }\r\n\r\n    protected override double StrainValueOf(DifficultyHitObject current)\r\n    {\r\n        var maniaCurrent = (KaraokeDifficultyHitObject)current;\r\n        double endTime = maniaCurrent.EndTime;\r\n        int column = getColumnIndex(maniaCurrent.BaseObject.Tone);\r\n\r\n        double holdFactor = 1.0; // Factor to all additional strains in case something else is held\r\n        double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly\r\n\r\n        // Fill up the holdEndTimes array\r\n        for (int i = 0; i < holdEndTimes.Length; ++i)\r\n        {\r\n            // If there is at least one other overlapping end or note, then we get an addition, buuuuuut...\r\n            if (Precision.DefinitelyBigger(holdEndTimes[i], maniaCurrent.StartTime, 1) && Precision.DefinitelyBigger(endTime, holdEndTimes[i], 1))\r\n                holdAddition = 1.0;\r\n\r\n            // ... this addition only is valid if there is _no_ other note with the same ending. Releasing multiple notes at the same time is just as easy as releasing 1\r\n            if (Precision.AlmostEquals(endTime, holdEndTimes[i], 1))\r\n                holdAddition = 0;\r\n\r\n            // We give a slight bonus to everything if something is held meanwhile\r\n            if (Precision.DefinitelyBigger(holdEndTimes[i], endTime, 1))\r\n                holdFactor = 1.25;\r\n\r\n            // Decay individual strains\r\n            individualStrains[i] = applyDecay(individualStrains[i], current.DeltaTime, individual_decay_base);\r\n        }\r\n\r\n        holdEndTimes[column] = endTime;\r\n\r\n        // Increase individual strain in own column\r\n        individualStrains[column] += 2.0 * holdFactor;\r\n        individualStrain = individualStrains[column];\r\n\r\n        overallStrain = applyDecay(overallStrain, current.DeltaTime, overall_decay_base) + (1 + holdAddition) * holdFactor;\r\n\r\n        return individualStrain + overallStrain - CurrentStrain;\r\n\r\n        // todo : implementation.\r\n        static int getColumnIndex(Tone tone) => 0;\r\n    }\r\n\r\n    protected override double CalculateInitialStrain(double offset, DifficultyHitObject current)\r\n        => applyDecay(individualStrain, offset - current.Previous(0).StartTime, individual_decay_base)\r\n           + applyDecay(overallStrain, offset - current.Previous(0).StartTime, overall_decay_base);\r\n\r\n    private double applyDecay(double value, double deltaTime, double decayBase)\r\n        => value * Math.Pow(decayBase, deltaTime / 1000);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Blueprints/KaraokeSelectionBlueprint.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Rulesets.Edit;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Blueprints;\r\n\r\npublic partial class KaraokeSelectionBlueprint<T> : HitObjectSelectionBlueprint<T>\r\n    where T : KaraokeHitObject\r\n{\r\n    protected KaraokeSelectionBlueprint(T hitObject)\r\n        : base(hitObject)\r\n    {\r\n        RelativeSizeAxes = Axes.None;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Blueprints/Lyrics/LyricSelectionBlueprint.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Blueprints.Lyrics;\r\n\r\npublic partial class LyricSelectionBlueprint : KaraokeSelectionBlueprint<Lyric>\r\n{\r\n    public LyricSelectionBlueprint(Lyric lyric)\r\n        : base(lyric)\r\n    {\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Blueprints/Notes/Components/EditBodyPiece.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Skinning.Default;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Blueprints.Notes.Components;\r\n\r\npublic partial class EditBodyPiece : Container\r\n{\r\n    [BackgroundDependencyLoader]\r\n    private void load(OsuColour colours)\r\n    {\r\n        Masking = true;\r\n        BorderColour = colours.Yellow;\r\n        BorderThickness = 2;\r\n        CornerRadius = DefaultBodyPiece.CORNER_RADIUS;\r\n        Child = new Box\r\n        {\r\n            RelativeSizeAxes = Axes.Both,\r\n            AlwaysPresent = true,\r\n            Alpha = 0,\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Blueprints/Notes/NoteSelectionBlueprint.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Blueprints.Notes.Components;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Notes;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.UI;\r\nusing osu.Game.Rulesets.Karaoke.UI.Position;\r\nusing osu.Game.Rulesets.UI;\r\nusing osu.Game.Rulesets.UI.Scrolling;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Blueprints.Notes;\r\n\r\npublic partial class NoteSelectionBlueprint : KaraokeSelectionBlueprint<Note>\r\n{\r\n    [Resolved]\r\n    private INotesChangeHandler notesChangeHandler { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private INotePropertyChangeHandler notePropertyChangeHandler { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private Playfield playfield { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private IScrollingInfo scrollingInfo { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private INotePositionInfo notePositionInfo { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private EditorBeatmap beatmap { get; set; } = null!;\r\n\r\n    protected ScrollingHitObjectContainer HitObjectContainer => ((KaraokePlayfield)playfield).NotePlayfield.HitObjectContainer;\r\n\r\n    public NoteSelectionBlueprint(Note note)\r\n        : base(note)\r\n    {\r\n        AddInternal(new EditBodyPiece\r\n        {\r\n            RelativeSizeAxes = Axes.Both,\r\n        });\r\n    }\r\n\r\n    protected override void Update()\r\n    {\r\n        base.Update();\r\n\r\n        var anchor = scrollingInfo.Direction.Value == ScrollingDirection.Left ? Anchor.CentreLeft : Anchor.CentreRight;\r\n        Anchor = Origin = anchor;\r\n\r\n        Position = Parent.ToLocalSpace(HitObjectContainer.ScreenSpacePositionAtTime(HitObject.StartTime)) - AnchorPosition;\r\n        Y += notePositionInfo.Calculator.YPositionAt(HitObject.Tone);\r\n\r\n        Width = HitObjectContainer.LengthAtTime(HitObject.StartTime, HitObject.EndTime);\r\n        Height = notePositionInfo.Calculator.ColumnHeight;\r\n    }\r\n\r\n    public override MenuItem[] ContextMenuItems => new MenuItem[]\r\n    {\r\n        new OsuMenuItem(HitObject.Display ? \"Hide\" : \"Show\", HitObject.Display ? MenuItemType.Destructive : MenuItemType.Standard,\r\n            () => notePropertyChangeHandler.ChangeDisplayState(!HitObject.Display)),\r\n        new OsuMenuItem(\"Split\", MenuItemType.Destructive, () => notesChangeHandler.Split()),\r\n    };\r\n\r\n    protected override bool OnClick(ClickEvent e)\r\n    {\r\n        // should only select current note before open the popover because note change handler will change property in all selected notes.\r\n        beatmap.SelectedHitObjects.Clear();\r\n        beatmap.SelectedHitObjects.Add(HitObject);\r\n\r\n        return base.OnClick(e);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/BeatmapListPropertyChangeHandler.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers;\r\n\r\n// todo: not a good design because eventually karaoke beatmap will not have the the field with list type.\r\n// it should be wrap into class (e.g. localizationInfo) with list of translation inside.\r\n// so guess this class will be removed eventually.\r\npublic abstract partial class BeatmapListPropertyChangeHandler<TItem> : Component\r\n{\r\n    [Resolved]\r\n    private EditorBeatmap beatmap { get; set; } = null!;\r\n\r\n    private KaraokeBeatmap karaokeBeatmap => EditorBeatmapUtils.GetPlayableBeatmap(beatmap);\r\n\r\n    protected IEnumerable<Lyric> Lyrics => karaokeBeatmap.HitObjects.OfType<Lyric>();\r\n\r\n    // todo: should be interface.\r\n    protected BindableList<TItem> Items = new();\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        Items.AddRange(GetItemsFromBeatmap(karaokeBeatmap));\r\n\r\n        // todo: find a better way to handle only beatmap property changed.\r\n        beatmap.TransactionEnded += syncItemsFromBeatmap;\r\n\r\n        syncItemsFromBeatmap();\r\n\r\n        void syncItemsFromBeatmap()\r\n        {\r\n            var items = GetItemsFromBeatmap(karaokeBeatmap);\r\n\r\n            if (Items.SequenceEqual(items))\r\n                return;\r\n\r\n            Items.AddRange(items.Except(Items));\r\n            Items.RemoveAll(x => !items.Contains(x));\r\n        }\r\n    }\r\n\r\n    protected void PerformObjectChanged(TItem item, Action<TItem>? action)\r\n    {\r\n        // should call change from editor beatmap because there's only way to trigger transaction ended.\r\n        beatmap.BeginChange();\r\n        action?.Invoke(item);\r\n        beatmap.EndChange();\r\n    }\r\n\r\n    protected abstract IList<TItem> GetItemsFromBeatmap(KaraokeBeatmap beatmap);\r\n\r\n    public void Add(TItem item)\r\n    {\r\n        var items = GetItemsFromBeatmap(karaokeBeatmap);\r\n        if (items.Contains(item))\r\n            throw new InvalidOperationException(nameof(item));\r\n\r\n        PerformObjectChanged(item, i =>\r\n        {\r\n            items.Add(i);\r\n            OnItemAdded(i);\r\n        });\r\n    }\r\n\r\n    public void Remove(TItem item)\r\n    {\r\n        var items = GetItemsFromBeatmap(karaokeBeatmap);\r\n        if (!items.Contains(item))\r\n            throw new InvalidOperationException($\"{nameof(item)} is not in the list\");\r\n\r\n        PerformObjectChanged(item, i =>\r\n        {\r\n            items.Remove(i);\r\n            OnItemRemoved(i);\r\n        });\r\n    }\r\n\r\n    protected abstract void OnItemAdded(TItem item);\r\n\r\n    protected abstract void OnItemRemoved(TItem item);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/BeatmapPropertyChangeHandler.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Caching;\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Types;\r\nusing osu.Game.Rulesets.Objects;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers;\r\n\r\npublic partial class BeatmapPropertyChangeHandler : Component\r\n{\r\n    private readonly Cached changingCache = new();\r\n\r\n    [Resolved]\r\n    private EditorBeatmap beatmap { get; set; } = null!;\r\n\r\n    protected KaraokeBeatmap KaraokeBeatmap => EditorBeatmapUtils.GetPlayableBeatmap(beatmap);\r\n\r\n    protected IEnumerable<Lyric> Lyrics => KaraokeBeatmap.HitObjects.OfType<Lyric>();\r\n\r\n    protected BeatmapPropertyChangeHandler()\r\n    {\r\n        changingCache.Validate();\r\n    }\r\n\r\n    protected void PerformBeatmapChanged(Action<KaraokeBeatmap> action)\r\n    {\r\n        try\r\n        {\r\n            beatmap.BeginChange();\r\n            action(KaraokeBeatmap);\r\n            beatmap.EndChange();\r\n        }\r\n        catch\r\n        {\r\n            // We should make sure that editor beatmap will end the change if still changing.\r\n            // will goes to here if have exception in the change handler.\r\n            if (beatmap.TransactionActive)\r\n                beatmap.EndChange();\r\n\r\n            throw;\r\n        }\r\n    }\r\n\r\n    protected void PerformOnSelection<T>(Action<T> action) where T : HitObject\r\n    {\r\n        if (!changingCache.IsValid)\r\n            throw new NotSupportedException(\"Cannot trigger the change while applying another change.\");\r\n\r\n        if (beatmap.SelectedHitObjects.Count == 0)\r\n            throw new NotSupportedException($\"Should contain at least one selected {nameof(T)}\");\r\n\r\n        changingCache.Invalidate();\r\n\r\n        try\r\n        {\r\n            // should trigger the UpdateState() in the editor beatmap only if there's no active state.\r\n            beatmap.PerformOnSelection(h =>\r\n            {\r\n                if (h is T tHitObject)\r\n                    action(tHitObject);\r\n            });\r\n        }\r\n        catch\r\n        {\r\n            // We should make sure that editor beatmap will end the change if still changing.\r\n            // will goes to here if have exception in the change handler.\r\n            if (beatmap.TransactionActive)\r\n                beatmap.EndChange();\r\n\r\n            throw;\r\n        }\r\n        finally\r\n        {\r\n            changingCache.Validate();\r\n        }\r\n    }\r\n\r\n    // todo: before having better solution to handle the undo/redo with better performance, we should use this to method to force invalidate all hit-object's working property.\r\n    protected void InvalidateAllHitObjectWorkingProperty<TWorkingProperty>(TWorkingProperty property)\r\n        where TWorkingProperty : struct, Enum\r\n    {\r\n        foreach (var hitObject in KaraokeBeatmap.HitObjects.OfType<IHasWorkingProperty<TWorkingProperty>>())\r\n        {\r\n            hitObject.InvalidateWorkingProperty(property);\r\n        }\r\n\r\n        beatmap.UpdateAllHitObjects();\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Beatmaps/BeatmapPagesChangeHandler.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Beatmaps.Pages;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Workings;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Beatmaps;\r\n\r\npublic partial class BeatmapPagesChangeHandler : BeatmapPropertyChangeHandler, IBeatmapPagesChangeHandler\r\n{\r\n    #region Auto-Generate\r\n\r\n    [Resolved]\r\n    private KaraokeRulesetEditGeneratorConfigManager generatorConfigManager { get; set; } = null!;\r\n\r\n    public bool CanGenerate()\r\n    {\r\n        var config = getGeneratorConfig();\r\n        var generator = new PageGenerator(config);\r\n        return generator.CanGenerate(KaraokeBeatmap);\r\n    }\r\n\r\n    public LocalisableString? GetGeneratorNotSupportedMessage()\r\n    {\r\n        var config = getGeneratorConfig();\r\n        var generator = new PageGenerator(config);\r\n        return generator.GetInvalidMessage(KaraokeBeatmap);\r\n    }\r\n\r\n    public void AutoGenerate()\r\n    {\r\n        var config = getGeneratorConfig();\r\n        var generator = new PageGenerator(config);\r\n        var pages = generator.Generate(KaraokeBeatmap);\r\n\r\n        performPageInfoChanged(pageInfo =>\r\n        {\r\n            if (config.ClearExistPages.Value)\r\n                pageInfo.Pages.Clear();\r\n\r\n            pageInfo.Pages.AddRange(pages);\r\n        });\r\n    }\r\n\r\n    private PageGeneratorConfig getGeneratorConfig()\r\n        => generatorConfigManager.Get<PageGeneratorConfig>();\r\n\r\n    #endregion\r\n\r\n    public void Add(Page page)\r\n    {\r\n        performPageInfoChanged(pageInfo =>\r\n        {\r\n            if (checkPageExist(pageInfo, page))\r\n                throw new InvalidOperationException($\"Should not add duplicated {nameof(page)} into the {nameof(pageInfo)}.\");\r\n\r\n            pageInfo.Pages.Add(page);\r\n        });\r\n    }\r\n\r\n    public void Remove(Page page)\r\n    {\r\n        performPageInfoChanged(pageInfo =>\r\n        {\r\n            if (!checkPageExist(pageInfo, page))\r\n                throw new InvalidOperationException($\"{nameof(page)} does ont in the {nameof(pageInfo)}.\");\r\n\r\n            pageInfo.Pages.Remove(page);\r\n        });\r\n    }\r\n\r\n    public void RemoveRange(IEnumerable<Page> pages)\r\n    {\r\n        performPageInfoChanged(pageInfo =>\r\n        {\r\n            foreach (var page in pages.ToArray())\r\n            {\r\n                if (!checkPageExist(pageInfo, page))\r\n                    throw new InvalidOperationException($\"{nameof(page)} does ont in the {nameof(pageInfo)}.\");\r\n\r\n                pageInfo.Pages.Remove(page);\r\n            }\r\n        });\r\n    }\r\n\r\n    public void ShiftingPageTime(IEnumerable<Page> pages, double offset)\r\n    {\r\n        performPageInfoChanged(pageInfo =>\r\n        {\r\n            foreach (var page in pages)\r\n            {\r\n                if (!checkPageExist(pageInfo, page))\r\n                    throw new InvalidOperationException($\"{nameof(page)} does ont in the {nameof(pageInfo)}.\");\r\n\r\n                page.Time += offset;\r\n            }\r\n        });\r\n    }\r\n\r\n    private static bool checkPageExist(PageInfo pageInfo, Page page)\r\n    {\r\n        return pageInfo.Pages.Contains(page);\r\n    }\r\n\r\n    private void performPageInfoChanged(Action<PageInfo> action)\r\n    {\r\n        PerformBeatmapChanged(beatmap =>\r\n        {\r\n            action(beatmap.PageInfo);\r\n\r\n            InvalidateAllHitObjectWorkingProperty(LyricWorkingProperty.Page);\r\n            InvalidateAllHitObjectWorkingProperty(NoteWorkingProperty.Page);\r\n        });\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Beatmaps/BeatmapSingersChangeHandler.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.IO;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Extensions.IEnumerableExtensions;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas.Types;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Beatmaps;\r\n\r\npublic partial class BeatmapSingersChangeHandler : BeatmapPropertyChangeHandler, IBeatmapSingersChangeHandler\r\n{\r\n    [Resolved]\r\n    private BeatmapManager? beatmapManager { get; set; }\r\n\r\n    [Resolved]\r\n    private IBindable<WorkingBeatmap>? working { get; set; }\r\n\r\n    private SingerInfo singerInfo => KaraokeBeatmap.SingerInfo;\r\n\r\n    public BindableList<Singer> Singers => singerInfo.Singers;\r\n\r\n    public void ChangeOrder(ISinger singer, int newIndex)\r\n    {\r\n        performSingerChanged(singer, s =>\r\n        {\r\n            int oldOrder = s.Order;\r\n            int newOrder = newIndex + 1; // order is start from 1\r\n            OrderUtils.ChangeOrder(Singers.ToArray(), oldOrder, newOrder, (switchSinger, oldOrder, newOrder) =>\r\n            {\r\n                // todo : not really sure should call update?\r\n            });\r\n        });\r\n    }\r\n\r\n    public bool ChangeSingerAvatar(Singer singer, FileInfo fileInfo)\r\n    {\r\n        if (beatmapManager == null || working == null)\r\n            return false;\r\n\r\n        if (!fileInfo.Exists)\r\n            throw new FileNotFoundException();\r\n\r\n        // note: follow the same logic in the ResourcesSection.ChangeBackgroundImage\r\n        var set = working.Value.BeatmapSetInfo;\r\n\r\n        // todo: we might re-format the new file name, like give it a hash name for prevent duplicated file name with other singer.\r\n        string newFileName = fileInfo.Name;\r\n\r\n        using (var stream = fileInfo.OpenRead())\r\n        {\r\n            // in the future we probably want to check if this is being used elsewhere (other difficulties?)\r\n            var oldFile = set.Files.FirstOrDefault(f => f.Filename == singer.AvatarFile);\r\n            if (oldFile != null)\r\n                beatmapManager.DeleteFile(set, oldFile);\r\n\r\n            beatmapManager.AddFile(set, stream, $\"assets/singers/{newFileName}\");\r\n        }\r\n\r\n        performSingerChanged(singer, s =>\r\n        {\r\n            // Write-back the file name.\r\n            s.AvatarFile = newFileName;\r\n        });\r\n\r\n        return true;\r\n    }\r\n\r\n    public Singer Add()\r\n    {\r\n        var newSinger = singerInfo.AddSinger(s =>\r\n        {\r\n            s.Order = getMaxSingerOrder() + 1;\r\n            s.Name = \"New singer\";\r\n        });\r\n        return newSinger;\r\n\r\n        int getMaxSingerOrder()\r\n            => OrderUtils.GetMaxOrderNumber(singerInfo.GetAllSingers());\r\n    }\r\n\r\n    public void Remove(Singer singer)\r\n    {\r\n        singerInfo.RemoveSinger(singer);\r\n\r\n        // Should re-sort the order\r\n        OrderUtils.ShiftingOrder(singerInfo.GetAllSingers().Where(x => x.Order > singer.Order), -1);\r\n\r\n        // should clear removed singer ids in singer editor.\r\n        Lyrics.ForEach(x =>\r\n        {\r\n            x.SingerIds.Remove(singer.ID);\r\n        });\r\n    }\r\n\r\n    private void performSingerInfoChanged(Action<SingerInfo> action)\r\n    {\r\n        PerformBeatmapChanged(beatmap =>\r\n        {\r\n            action(beatmap.SingerInfo);\r\n        });\r\n    }\r\n\r\n    private void performSingerChanged<TSinger>(TSinger singer, Action<TSinger> action) where TSinger : ISinger\r\n    {\r\n        performSingerInfoChanged(singerInfo =>\r\n        {\r\n            if (!singerInfo.HasSinger(singer))\r\n                throw new InvalidOperationException(\"Singer should be in the beatmap\");\r\n\r\n            action(singer);\r\n        });\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Beatmaps/BeatmapTranslationsChangeHandler.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Globalization;\r\nusing System.Linq;\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Beatmaps;\r\n\r\npublic partial class BeatmapTranslationsChangeHandler : BeatmapListPropertyChangeHandler<CultureInfo>, IBeatmapTranslationsChangeHandler\r\n{\r\n    public IBindableList<CultureInfo> Languages => Items;\r\n\r\n    protected override IList<CultureInfo> GetItemsFromBeatmap(KaraokeBeatmap beatmap)\r\n        => beatmap.AvailableTranslationLanguages;\r\n\r\n    protected override void OnItemAdded(CultureInfo item)\r\n    {\r\n        // there's no need to do anything.\r\n    }\r\n\r\n    protected override void OnItemRemoved(CultureInfo item)\r\n    {\r\n        // Delete from lyric also.\r\n        foreach (var lyric in Lyrics.Where(lyric => lyric.Translations.ContainsKey(item)))\r\n        {\r\n            lyric.Translations.Remove(item);\r\n        }\r\n    }\r\n\r\n    public bool IsLanguageContainsTranslation(CultureInfo cultureInfo)\r\n        => Lyrics.Any(x => x.Translations.ContainsKey(cultureInfo));\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Beatmaps/IBeatmapPagesChangeHandler.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Beatmaps;\r\n\r\npublic interface IBeatmapPagesChangeHandler : IAutoGenerateChangeHandler\r\n{\r\n    LocalisableString? GetGeneratorNotSupportedMessage();\r\n\r\n    void Add(Page page);\r\n\r\n    void Remove(Page page);\r\n\r\n    void RemoveRange(IEnumerable<Page> pages);\r\n\r\n    void ShiftingPageTime(IEnumerable<Page> pages, double offset);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Beatmaps/IBeatmapSingersChangeHandler.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.IO;\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas.Types;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Beatmaps;\r\n\r\npublic interface IBeatmapSingersChangeHandler\r\n{\r\n    // todo: should use IBindableList eventually, but cannot do that because it's bind to selection item.\r\n    BindableList<Singer> Singers { get; }\r\n\r\n    void ChangeOrder(ISinger singer, int newIndex);\r\n\r\n    bool ChangeSingerAvatar(Singer singer, FileInfo fileInfo);\r\n\r\n    Singer Add();\r\n\r\n    void Remove(Singer singer);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Beatmaps/IBeatmapTranslationsChangeHandler.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Globalization;\r\nusing osu.Framework.Bindables;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Beatmaps;\r\n\r\npublic interface IBeatmapTranslationsChangeHandler\r\n{\r\n    IBindableList<CultureInfo> Languages { get; }\r\n\r\n    void Add(CultureInfo culture);\r\n\r\n    void Remove(CultureInfo culture);\r\n\r\n    bool IsLanguageContainsTranslation(CultureInfo cultureInfo);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/ChangeForbiddenException.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers;\r\n\r\npublic class ChangeForbiddenException : InvalidOperationException\r\n{\r\n    public ChangeForbiddenException(string message)\r\n        : base(message)\r\n    {\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/HitObjectChangeHandler.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Caching;\r\nusing osu.Framework.Extensions.IEnumerableExtensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Objects;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers;\r\n\r\npublic abstract partial class HitObjectChangeHandler<THitObject> : Component where THitObject : HitObject\r\n{\r\n    private readonly Cached changingCache = new();\r\n\r\n    private bool triggerBeatmapSave = true;\r\n\r\n    [Resolved]\r\n    private EditorBeatmap beatmap { get; set; } = null!;\r\n\r\n    protected IEnumerable<THitObject> HitObjects => beatmap.HitObjects.OfType<THitObject>();\r\n\r\n    protected HitObjectChangeHandler()\r\n    {\r\n        changingCache.Validate();\r\n    }\r\n\r\n    protected void CheckExactlySelectedOneHitObject()\r\n    {\r\n        if (beatmap.SelectedHitObjects.OfType<THitObject>().Count() != 1)\r\n            throw new InvalidOperationException($\"Should be exactly one {nameof(THitObject)} being selected.\");\r\n    }\r\n\r\n    // Can remove this method after as TransactionalCommitComponent injection for all rulesets(means customized ruleset is able to save/load beatmap).\r\n    // we can use changeHandler.TransactionActive to check if there's any active transaction.\r\n    // e.g. : changeHandler is TransactionalCommitComponent transactionalCommitComponent && !transactionalCommitComponent.TransactionActive\r\n    protected void NotTriggerSaveStateOnThisChange()\r\n    {\r\n        triggerBeatmapSave = false;\r\n    }\r\n\r\n    protected virtual void PerformOnSelection(Action<THitObject> action)\r\n        => PerformOnSelection<THitObject>(action);\r\n\r\n    protected void PerformOnSelection<T>(Action<T> action) where T : HitObject\r\n    {\r\n        if (!changingCache.IsValid)\r\n            throw new NotSupportedException(\"Cannot trigger the change while applying another change.\");\r\n\r\n        if (beatmap.SelectedHitObjects.Count == 0)\r\n            throw new NotSupportedException($\"Should contain at least one selected {nameof(THitObject)}\");\r\n\r\n        changingCache.Invalidate();\r\n\r\n        try\r\n        {\r\n            // todo: follow-up the discussion in the https://github.com/karaoke-dev/karaoke/pull/1669 after support the change handler for customized ruleset.\r\n            if (triggerBeatmapSave)\r\n            {\r\n                // should trigger the UpdateState() in the editor beatmap only if there's no active state.\r\n                beatmap.PerformOnSelection(h =>\r\n                {\r\n                    if (h is T tHitObject)\r\n                        action(tHitObject);\r\n                });\r\n            }\r\n            else\r\n            {\r\n                // Just update the object property if already in the changing state.\r\n                // e.g. dragging.\r\n                beatmap.SelectedHitObjects.ForEach(h =>\r\n                {\r\n                    if (h is T tHitObject)\r\n                        action(tHitObject);\r\n                });\r\n            }\r\n        }\r\n        catch\r\n        {\r\n            // We should make sure that editor beatmap will end the change if still changing.\r\n            // will goes to here if have exception in the change handler.\r\n            if (beatmap.TransactionActive)\r\n                beatmap.EndChange();\r\n\r\n            throw;\r\n        }\r\n        finally\r\n        {\r\n            changingCache.Validate();\r\n            triggerBeatmapSave = true;\r\n        }\r\n    }\r\n\r\n    protected void AddRange<T>(IEnumerable<T> hitObjects) where T : HitObject => hitObjects.ForEach(Add);\r\n\r\n    protected virtual void Add<T>(T hitObject) where T : HitObject\r\n    {\r\n        bool containsInBeatmap = HitObjects.Any(x => x == hitObject);\r\n        if (containsInBeatmap)\r\n            throw new InvalidOperationException(\"Seems this hit object is already in the beatmap.\");\r\n\r\n        if (isCreateObjectLocked(hitObject))\r\n            throw new AddOrRemoveForbiddenException();\r\n\r\n        beatmap.Add(hitObject);\r\n    }\r\n\r\n    protected virtual void Insert<T>(int index, T hitObject) where T : HitObject\r\n    {\r\n        bool containsInBeatmap = HitObjects.Any(x => x == hitObject);\r\n        if (containsInBeatmap)\r\n            throw new InvalidOperationException(\"Seems this hit object is already in the beatmap.\");\r\n\r\n        if (isCreateObjectLocked(hitObject))\r\n            throw new AddOrRemoveForbiddenException();\r\n\r\n        beatmap.Insert(index, hitObject);\r\n    }\r\n\r\n    protected void RemoveRange<T>(IEnumerable<T> hitObjects) where T : HitObject => hitObjects.ForEach(Remove);\r\n\r\n    protected void Remove<T>(T hitObject) where T : HitObject\r\n    {\r\n        if (isRemoveObjectLocked(hitObject))\r\n            throw new AddOrRemoveForbiddenException();\r\n\r\n        beatmap.Remove(hitObject);\r\n    }\r\n\r\n    private bool isCreateObjectLocked<T>(T hitObject)\r\n    {\r\n        return hitObject switch\r\n        {\r\n            Lyric => false,\r\n            Note note => note.ReferenceLyric != null && HitObjectWritableUtils.IsCreateOrRemoveNoteLocked(note.ReferenceLyric),\r\n            _ => throw new InvalidCastException(),\r\n        };\r\n    }\r\n\r\n    private bool isRemoveObjectLocked<T>(T hitObject)\r\n    {\r\n        switch (hitObject)\r\n        {\r\n            case Lyric lyric:\r\n                bool hasReferenceLyric = EditorBeatmapUtils.GetAllReferenceLyrics(beatmap, lyric).Any();\r\n                return hasReferenceLyric || HitObjectWritableUtils.IsRemoveLyricLocked(lyric);\r\n\r\n            case Note note:\r\n                return note.ReferenceLyric != null && HitObjectWritableUtils.IsCreateOrRemoveNoteLocked(note.ReferenceLyric);\r\n\r\n            default:\r\n                throw new InvalidCastException();\r\n        }\r\n    }\r\n\r\n    protected void TriggerHitObjectUpdate<T>(T hitObject) where T : HitObject\r\n    {\r\n        beatmap.Update(hitObject);\r\n    }\r\n\r\n    public class AddOrRemoveForbiddenException : Exception\r\n    {\r\n        public AddOrRemoveForbiddenException()\r\n            : base(\"Should not add or remove the hit-object.\")\r\n        {\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/HitObjectPropertyChangeHandler.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Game.Rulesets.Objects;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers;\r\n\r\npublic abstract partial class HitObjectPropertyChangeHandler<THitObject> : HitObjectChangeHandler<THitObject>, IHitObjectPropertyChangeHandler\r\n    where THitObject : HitObject\r\n{\r\n    [Resolved]\r\n    private EditorBeatmap beatmap { get; set; } = null!;\r\n\r\n    protected sealed override void PerformOnSelection(Action<THitObject> action)\r\n    {\r\n        // note: should not check lyric in the perform on selection because it will let change handler in lazer broken.\r\n        if (beatmap.SelectedHitObjects.OfType<THitObject>().Any(IsWritePropertyLocked))\r\n            throw new ChangeForbiddenException(\"This property might be locked or it's a reference property.\");\r\n\r\n        base.PerformOnSelection(action);\r\n    }\r\n\r\n    protected abstract bool IsWritePropertyLocked(THitObject hitObject);\r\n\r\n    public virtual bool IsSelectionsLocked()\r\n        => beatmap.SelectedHitObjects.OfType<THitObject>().Any(IsWritePropertyLocked);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/HitObjectsChangeHandler.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers;\r\n\r\npublic abstract partial class HitObjectsChangeHandler<THitObject> : HitObjectChangeHandler<THitObject> where THitObject : HitObject;\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/IAutoGenerateChangeHandler.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers;\r\n\r\n/// <summary>\r\n/// This interface is defined checking able to generate or detect the property, and make the change for the property.\r\n/// </summary>\r\n/// <typeparam name=\"TEnum\"></typeparam>\r\npublic interface IEnumAutoGenerateChangeHandler<in TEnum> where TEnum : Enum\r\n{\r\n    bool CanGenerate(TEnum type);\r\n\r\n    void AutoGenerate(TEnum type);\r\n}\r\n\r\n/// <summary>\r\n/// This interface is defined checking able to generate or detect the property, and make the change for the property.\r\n/// </summary>\r\n/// <typeparam name=\"TType\"></typeparam>\r\npublic interface IAutoGenerateChangeHandler<in TType>\r\n{\r\n    bool CanGenerate<T>() where T : TType;\r\n\r\n    void AutoGenerate<T>() where T : TType;\r\n}\r\n\r\n/// <summary>\r\n/// This interface is defined checking able to generate or detect the property, and make the change for the property.\r\n/// </summary>\r\npublic interface IAutoGenerateChangeHandler\r\n{\r\n    bool CanGenerate();\r\n\r\n    void AutoGenerate();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/IHitObjectPropertyChangeHandler.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers;\r\n\r\npublic interface IHitObjectPropertyChangeHandler\r\n{\r\n    bool IsSelectionsLocked();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/IImportBeatmapChangeHandler.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Beatmaps;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers;\r\n\r\npublic interface IImportBeatmapChangeHandler\r\n{\r\n    void Import(IBeatmap newBeatmap);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/ILockChangeHandler.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Objects.Types;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers;\r\n\r\npublic interface ILockChangeHandler : IHitObjectPropertyChangeHandler\r\n{\r\n    void Lock(LockState lockState);\r\n\r\n    void Unlock();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/ImportBeatmapChangeHandler.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers;\r\n\r\npublic partial class ImportBeatmapChangeHandler : Component, IImportBeatmapChangeHandler\r\n{\r\n    [Resolved]\r\n    private EditorBeatmap beatmap { get; set; } = null!;\r\n\r\n    public void Import(IBeatmap newBeatmap)\r\n    {\r\n        beatmap.BeginChange();\r\n\r\n        beatmap.Clear();\r\n\r\n        var lyrics = newBeatmap.HitObjects.OfType<Lyric>().ToArray();\r\n        beatmap.AddRange(lyrics);\r\n\r\n        beatmap.EndChange();\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/LockChangeHandler.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Types;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers;\r\n\r\npublic partial class LockChangeHandler : HitObjectPropertyChangeHandler<KaraokeHitObject>, ILockChangeHandler\r\n{\r\n    public void Lock(LockState lockState)\r\n    {\r\n        PerformOnSelection(h =>\r\n        {\r\n            if (h is IHasLock hasLock)\r\n                hasLock.Lock = lockState;\r\n        });\r\n    }\r\n\r\n    public void Unlock()\r\n    {\r\n        Lock(LockState.None);\r\n    }\r\n\r\n    protected sealed override bool IsWritePropertyLocked(KaraokeHitObject hitObject)\r\n    {\r\n        return hitObject switch\r\n        {\r\n            Lyric lyric => HitObjectWritableUtils.IsWriteLyricPropertyLocked(lyric, nameof(Lyric.Lock)),\r\n            Note note => HitObjectWritableUtils.IsWriteNotePropertyLocked(note, nameof(Lyric.Lock)),\r\n            _ => throw new NotSupportedException(),\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Lyrics/ILyricLanguageChangeHandler.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Globalization;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\n\r\npublic interface ILyricLanguageChangeHandler : ILyricPropertyChangeHandler\r\n{\r\n    void SetLanguage(CultureInfo? language);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Lyrics/ILyricListPropertyChangeHandler.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\n\r\npublic interface ILyricListPropertyChangeHandler<in TItem> : ILyricPropertyChangeHandler\r\n{\r\n    void Add(TItem item);\r\n\r\n    void AddRange(IEnumerable<TItem> items);\r\n\r\n    void Remove(TItem item);\r\n\r\n    void RemoveRange(IEnumerable<TItem> items);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Lyrics/ILyricPropertyAutoGenerateChangeHandler.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\n\r\npublic interface ILyricPropertyAutoGenerateChangeHandler : ILyricPropertyChangeHandler, IEnumAutoGenerateChangeHandler<AutoGenerateType>\r\n{\r\n    IDictionary<Lyric, LocalisableString> GetGeneratorNotSupportedLyrics(AutoGenerateType type);\r\n}\r\n\r\npublic enum AutoGenerateType\r\n{\r\n    DetectReferenceLyric,\r\n\r\n    DetectLanguage,\r\n\r\n    AutoGenerateRubyTags,\r\n\r\n    AutoGenerateTimeTags,\r\n\r\n    AutoGenerateRomanisation,\r\n\r\n    AutoGenerateNotes,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Lyrics/ILyricPropertyChangeHandler.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\n\r\npublic interface ILyricPropertyChangeHandler : IHitObjectPropertyChangeHandler;\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Lyrics/ILyricReferenceChangeHandler.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Properties;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\n\r\npublic interface ILyricReferenceChangeHandler : ILyricPropertyChangeHandler\r\n{\r\n    void UpdateReferenceLyric(Lyric? referenceLyric);\r\n\r\n    void SwitchToReferenceLyricConfig();\r\n\r\n    void SwitchToSyncLyricConfig();\r\n\r\n    void AdjustLyricConfig<TConfig>(Action<TConfig> action) where TConfig : IReferenceLyricPropertyConfig;\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Lyrics/ILyricRubyTagsChangeHandler.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\n\r\npublic interface ILyricRubyTagsChangeHandler : ILyricListPropertyChangeHandler<RubyTag>\r\n{\r\n    void SetIndex(RubyTag rubyTag, int? startIndex, int? endIndex);\r\n\r\n    void ShiftingIndex(IEnumerable<RubyTag> rubyTags, int offset);\r\n\r\n    void SetText(RubyTag rubyTag, string text);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Lyrics/ILyricSingerChangeHandler.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas.Types;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\n\r\npublic interface ILyricSingerChangeHandler : ILyricListPropertyChangeHandler<ISinger>\r\n{\r\n    void Clear();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Lyrics/ILyricTextChangeHandler.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\n\r\npublic interface ILyricTextChangeHandler : ILyricPropertyChangeHandler\r\n{\r\n    void InsertText(int charGap, string text);\r\n\r\n    void DeleteLyricText(int charGap, int count = 1);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Lyrics/ILyricTimeTagsChangeHandler.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\n\r\npublic interface ILyricTimeTagsChangeHandler : ILyricListPropertyChangeHandler<TimeTag>\r\n{\r\n    void SetTimeTagTime(TimeTag timeTag, double time);\r\n\r\n    void SetTimeTagFirstSyllable(TimeTag timeTag, bool firstSyllable);\r\n\r\n    void SetTimeTagRomanisedSyllable(TimeTag timeTag, string? romanisedSyllable);\r\n\r\n    void ShiftingTimeTagTime(IEnumerable<TimeTag> timeTags, double offset);\r\n\r\n    void ClearTimeTagTime(TimeTag timeTag);\r\n\r\n    void ClearAllTimeTagTime();\r\n\r\n    void AddByPosition(TextIndex index);\r\n\r\n    void RemoveByPosition(TextIndex index);\r\n\r\n    TimeTag Shifting(TimeTag timeTag, ShiftingDirection direction, ShiftingType type);\r\n}\r\n\r\npublic enum ShiftingDirection\r\n{\r\n    Left,\r\n\r\n    Right,\r\n}\r\n\r\npublic enum ShiftingType\r\n{\r\n    State,\r\n\r\n    Index,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Lyrics/ILyricTranslationChangeHandler.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Globalization;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\n\r\npublic interface ILyricTranslationChangeHandler : ILyricPropertyChangeHandler\r\n{\r\n    void UpdateTranslation(CultureInfo cultureInfo, string translation);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Lyrics/ILyricsChangeHandler.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\n\r\npublic interface ILyricsChangeHandler\r\n{\r\n    void Split(int index);\r\n\r\n    void Combine();\r\n\r\n    void CreateAtPosition();\r\n\r\n    void CreateAtLast();\r\n\r\n    void AddBelowToSelection(Lyric newLyric);\r\n\r\n    void AddRangeBelowToSelection(IEnumerable<Lyric> newLyrics);\r\n\r\n    void Remove();\r\n\r\n    void ChangeOrder(int newOrder);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Lyrics/LyricLanguageChangeHandler.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Globalization;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\n\r\npublic partial class LyricLanguageChangeHandler : LyricPropertyChangeHandler, ILyricLanguageChangeHandler\r\n{\r\n    public void SetLanguage(CultureInfo? language)\r\n    {\r\n        PerformOnSelection(lyric =>\r\n        {\r\n            if (EqualityComparer<CultureInfo?>.Default.Equals(language, lyric.Language))\r\n                return;\r\n\r\n            lyric.Language = language;\r\n        });\r\n    }\r\n\r\n    protected override bool IsWritePropertyLocked(Lyric lyric)\r\n        => HitObjectWritableUtils.IsWriteLyricPropertyLocked(lyric, nameof(Lyric.Language));\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Lyrics/LyricPropertyAutoGenerateChangeHandler.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Globalization;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.Language;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.Notes;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.ReferenceLyric;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.Romanisation;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.RubyTags;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.TimeTags;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Properties;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\n\r\npublic partial class LyricPropertyAutoGenerateChangeHandler : LyricPropertyChangeHandler, ILyricPropertyAutoGenerateChangeHandler\r\n{\r\n    // should change this flag if wants to change property in the lyrics.\r\n    // Not a good to waite a global property for that but there's no better choice.\r\n    private AutoGenerateType? currentAutoGenerateType;\r\n\r\n    [Resolved]\r\n    private EditorBeatmap beatmap { get; set; } = null!;\r\n\r\n    public bool CanGenerate(AutoGenerateType type)\r\n    {\r\n        currentAutoGenerateType = type;\r\n\r\n        switch (type)\r\n        {\r\n            case AutoGenerateType.DetectReferenceLyric:\r\n                var referenceLyricDetector = getDetector<Lyric?, ReferenceLyricDetectorConfig>(HitObjects);\r\n                return canDetect(referenceLyricDetector);\r\n\r\n            case AutoGenerateType.DetectLanguage:\r\n                var languageDetector = getDetector<CultureInfo, LanguageDetectorConfig>();\r\n                return canDetect(languageDetector);\r\n\r\n            case AutoGenerateType.AutoGenerateRubyTags:\r\n                var rubyGenerator = getSelector<RubyTag[], RubyTagGeneratorConfig>();\r\n                return canGenerate(rubyGenerator);\r\n\r\n            case AutoGenerateType.AutoGenerateTimeTags:\r\n                var timeTagGenerator = getSelector<TimeTag[], TimeTagGeneratorConfig>();\r\n                return canGenerate(timeTagGenerator);\r\n\r\n            case AutoGenerateType.AutoGenerateRomanisation:\r\n                var romanisationGenerator = getSelector<IReadOnlyDictionary<TimeTag, RomanisationGenerateResult>, RomanisationGeneratorConfig>();\r\n                return canGenerate(romanisationGenerator);\r\n\r\n            case AutoGenerateType.AutoGenerateNotes:\r\n                var noteGenerator = getGenerator<Note[], NoteGeneratorConfig>();\r\n                return canGenerate(noteGenerator);\r\n\r\n            default:\r\n                throw new ArgumentOutOfRangeException(nameof(type));\r\n        }\r\n\r\n        bool canDetect<T>(PropertyDetector<Lyric, T> detector)\r\n            => HitObjects.Where(x => !IsWritePropertyLocked(x)).Any(detector.CanDetect);\r\n\r\n        bool canGenerate<T>(PropertyGenerator<Lyric, T> generator)\r\n            => HitObjects.Where(x => !IsWritePropertyLocked(x)).Any(generator.CanGenerate);\r\n    }\r\n\r\n    public IDictionary<Lyric, LocalisableString> GetGeneratorNotSupportedLyrics(AutoGenerateType type)\r\n    {\r\n        currentAutoGenerateType = type;\r\n\r\n        switch (type)\r\n        {\r\n            case AutoGenerateType.DetectReferenceLyric:\r\n                var referenceLyricDetector = getDetector<Lyric?, ReferenceLyricDetectorConfig>(HitObjects);\r\n                return getInvalidMessageFromDetector(referenceLyricDetector);\r\n\r\n            case AutoGenerateType.DetectLanguage:\r\n                var languageDetector = getDetector<CultureInfo, LanguageDetectorConfig>();\r\n                return getInvalidMessageFromDetector(languageDetector);\r\n\r\n            case AutoGenerateType.AutoGenerateRubyTags:\r\n                var rubyGenerator = getSelector<RubyTag[], RubyTagGeneratorConfig>();\r\n                return getInvalidMessageFromGenerator(rubyGenerator);\r\n\r\n            case AutoGenerateType.AutoGenerateTimeTags:\r\n                var timeTagGenerator = getSelector<TimeTag[], TimeTagGeneratorConfig>();\r\n                return getInvalidMessageFromGenerator(timeTagGenerator);\r\n\r\n            case AutoGenerateType.AutoGenerateRomanisation:\r\n                var romanisationGenerator = getSelector<IReadOnlyDictionary<TimeTag, RomanisationGenerateResult>, RomanisationGeneratorConfig>();\r\n                return getInvalidMessageFromGenerator(romanisationGenerator);\r\n\r\n            case AutoGenerateType.AutoGenerateNotes:\r\n                var noteGenerator = getGenerator<Note[], NoteGeneratorConfig>();\r\n                return getInvalidMessageFromGenerator(noteGenerator);\r\n\r\n            default:\r\n                throw new ArgumentOutOfRangeException(nameof(type));\r\n        }\r\n\r\n        IDictionary<Lyric, LocalisableString> getInvalidMessageFromDetector<T>(PropertyDetector<Lyric, T> detector)\r\n            => HitObjects.Select(x => new KeyValuePair<Lyric, LocalisableString?>(x, detector.GetInvalidMessage(x) ?? getReferenceLyricInvalidMessage(x)))\r\n                         .Where(x => x.Value != null)\r\n                         .ToDictionary(k => k.Key, v => v.Value!.Value);\r\n\r\n        IDictionary<Lyric, LocalisableString> getInvalidMessageFromGenerator<T>(PropertyGenerator<Lyric, T> generator)\r\n            => HitObjects.Select(x => new KeyValuePair<Lyric, LocalisableString?>(x, generator.GetInvalidMessage(x) ?? getReferenceLyricInvalidMessage(x)))\r\n                         .Where(x => x.Value != null)\r\n                         .ToDictionary(k => k.Key, v => v.Value!.Value);\r\n\r\n        LocalisableString? getReferenceLyricInvalidMessage(Lyric lyric)\r\n        {\r\n            bool locked = IsWritePropertyLocked(lyric);\r\n            return locked ? \"Cannot modify property because has reference lyric.\" : default(LocalisableString?);\r\n        }\r\n    }\r\n\r\n    public void AutoGenerate(AutoGenerateType type)\r\n    {\r\n        currentAutoGenerateType = type;\r\n\r\n        switch (type)\r\n        {\r\n            case AutoGenerateType.DetectReferenceLyric:\r\n                var referenceLyricDetector = getDetector<Lyric?, ReferenceLyricDetectorConfig>(HitObjects);\r\n                PerformOnSelection(lyric =>\r\n                {\r\n                    var referencedLyric = referenceLyricDetector.Detect(lyric);\r\n                    lyric.ReferenceLyricId = referencedLyric?.ID;\r\n\r\n                    // technically this property should be assigned by beatmap processor, but should be OK to assign here for testing purpose.\r\n                    lyric.ReferenceLyric = referencedLyric;\r\n\r\n                    if (lyric.ReferenceLyricId != null && lyric.ReferenceLyricConfig is not SyncLyricConfig)\r\n                        lyric.ReferenceLyricConfig = new SyncLyricConfig();\r\n                });\r\n                break;\r\n\r\n            case AutoGenerateType.DetectLanguage:\r\n                var languageDetector = getDetector<CultureInfo, LanguageDetectorConfig>();\r\n                PerformOnSelection(lyric =>\r\n                {\r\n                    var detectedLanguage = languageDetector.Detect(lyric);\r\n                    lyric.Language = detectedLanguage;\r\n                });\r\n                break;\r\n\r\n            case AutoGenerateType.AutoGenerateRubyTags:\r\n                var rubyGenerator = getSelector<RubyTag[], RubyTagGeneratorConfig>();\r\n                PerformOnSelection(lyric =>\r\n                {\r\n                    lyric.RubyTags = rubyGenerator.Generate(lyric);\r\n                });\r\n                break;\r\n\r\n            case AutoGenerateType.AutoGenerateTimeTags:\r\n                var timeTagGenerator = getSelector<TimeTag[], TimeTagGeneratorConfig>();\r\n                PerformOnSelection(lyric =>\r\n                {\r\n                    lyric.TimeTags = timeTagGenerator.Generate(lyric);\r\n                });\r\n                break;\r\n\r\n            case AutoGenerateType.AutoGenerateRomanisation:\r\n                var romanisationGenerator = getSelector<IReadOnlyDictionary<TimeTag, RomanisationGenerateResult>, RomanisationGeneratorConfig>();\r\n                PerformOnSelection(lyric =>\r\n                {\r\n                    var results = romanisationGenerator.Generate(lyric);\r\n\r\n                    foreach (var (key, value) in results)\r\n                    {\r\n                        var matchedTimeTag = lyric.TimeTags.Single(x => x == key);\r\n                        matchedTimeTag.FirstSyllable = value.FirstSyllable;\r\n                        matchedTimeTag.RomanisedSyllable = value.RomanisedSyllable;\r\n                    }\r\n                });\r\n                break;\r\n\r\n            case AutoGenerateType.AutoGenerateNotes:\r\n                var noteGenerator = getGenerator<Note[], NoteGeneratorConfig>();\r\n                PerformOnSelection(lyric =>\r\n                {\r\n                    // clear exist notes if from those\r\n                    var matchedNotes = EditorBeatmapUtils.GetNotesByLyric(beatmap, lyric);\r\n                    RemoveRange(matchedNotes);\r\n\r\n                    var notes = noteGenerator.Generate(lyric);\r\n                    AddRange(notes);\r\n                });\r\n                break;\r\n\r\n            default:\r\n                throw new ArgumentOutOfRangeException(nameof(type));\r\n        }\r\n    }\r\n\r\n    public override bool IsSelectionsLocked()\r\n        => throw new InvalidOperationException(\"Auto-generator does not support this check method.\");\r\n\r\n    protected override bool IsWritePropertyLocked(Lyric lyric) =>\r\n        currentAutoGenerateType switch\r\n        {\r\n            AutoGenerateType.DetectReferenceLyric => HitObjectWritableUtils.IsWriteLyricPropertyLocked(lyric, nameof(Lyric.ReferenceLyric), nameof(Lyric.ReferenceLyricConfig)),\r\n            AutoGenerateType.DetectLanguage => HitObjectWritableUtils.IsWriteLyricPropertyLocked(lyric, nameof(Lyric.Language)),\r\n            AutoGenerateType.AutoGenerateRubyTags => HitObjectWritableUtils.IsWriteLyricPropertyLocked(lyric, nameof(Lyric.RubyTags)),\r\n            AutoGenerateType.AutoGenerateTimeTags => HitObjectWritableUtils.IsWriteLyricPropertyLocked(lyric, nameof(Lyric.TimeTags)),\r\n            AutoGenerateType.AutoGenerateRomanisation => HitObjectWritableUtils.IsWriteLyricPropertyLocked(lyric, nameof(Lyric.TimeTags)),\r\n            AutoGenerateType.AutoGenerateNotes => HitObjectWritableUtils.IsCreateOrRemoveNoteLocked(lyric),\r\n            _ => throw new ArgumentOutOfRangeException(),\r\n        };\r\n\r\n    #region Utililty\r\n\r\n    [Resolved]\r\n    private KaraokeRulesetEditGeneratorConfigManager? generatorConfigManager { get; set; }\r\n\r\n    private LyricPropertyDetector<TProperty, TConfig> getDetector<TProperty, TConfig>()\r\n        where TConfig : GeneratorConfig, new()\r\n    {\r\n        var config = getGeneratorConfig<TConfig>();\r\n        return createInstance<LyricPropertyDetector<TProperty, TConfig>>(config);\r\n    }\r\n\r\n    private LyricPropertyDetector<TProperty, TConfig> getDetector<TProperty, TConfig>(IEnumerable<Lyric> lyrics)\r\n        where TConfig : GeneratorConfig, new()\r\n    {\r\n        var config = getGeneratorConfig<TConfig>();\r\n        return createInstance<LyricPropertyDetector<TProperty, TConfig>>(lyrics, config);\r\n    }\r\n\r\n    private LyricPropertyGenerator<TProperty, TConfig> getGenerator<TProperty, TConfig>()\r\n        where TConfig : GeneratorConfig, new()\r\n    {\r\n        var config = getGeneratorConfig<TConfig>();\r\n        return createInstance<LyricPropertyGenerator<TProperty, TConfig>>(config);\r\n    }\r\n\r\n    private LyricGeneratorSelector<TProperty, TBaseConfig> getSelector<TProperty, TBaseConfig>()\r\n        where TBaseConfig : GeneratorConfig\r\n    {\r\n        return createInstance<LyricGeneratorSelector<TProperty, TBaseConfig>>(generatorConfigManager);\r\n    }\r\n\r\n    private static TType createInstance<TType>(params object?[]? args)\r\n    {\r\n        var generatedType = getChildType(typeof(TType));\r\n\r\n        var instance = (TType?)Activator.CreateInstance(generatedType, args);\r\n        if (instance == null)\r\n            throw new InvalidOperationException();\r\n\r\n        return instance;\r\n\r\n        static Type getChildType(Type type)\r\n        {\r\n            // should get the assembly that the has the class GeneratorConfig.\r\n            var assembly = typeof(GeneratorConfig).Assembly;\r\n            return assembly.GetTypes()\r\n                           .Single(x => type.IsAssignableFrom(x) && !x.IsInterface && !x.IsAbstract);\r\n        }\r\n    }\r\n\r\n    private TConfig getGeneratorConfig<TConfig>()\r\n        where TConfig : GeneratorConfig, new()\r\n    {\r\n        if (generatorConfigManager == null)\r\n            throw new InvalidOperationException();\r\n\r\n        return generatorConfigManager.Get<TConfig>();\r\n    }\r\n\r\n    #endregion\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Lyrics/LyricPropertyChangeHandler.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\n\r\npublic abstract partial class LyricPropertyChangeHandler : HitObjectPropertyChangeHandler<Lyric>, ILyricPropertyChangeHandler;\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Lyrics/LyricReferenceChangeHandler.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Properties;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\n\r\npublic partial class LyricReferenceChangeHandler : LyricPropertyChangeHandler, ILyricReferenceChangeHandler\r\n{\r\n    public void UpdateReferenceLyric(Lyric? referenceLyric)\r\n    {\r\n        if (referenceLyric != null && !HitObjects.Contains(referenceLyric))\r\n            throw new InvalidOperationException($\"{nameof(referenceLyric)} should in the beatmap.\");\r\n\r\n        PerformOnSelection(lyric =>\r\n        {\r\n            if (referenceLyric == lyric)\r\n                throw new InvalidOperationException($\"{nameof(referenceLyric)} should not be the same instance as {nameof(lyric)}\");\r\n\r\n            if (referenceLyric?.ReferenceLyric != null)\r\n                throw new InvalidOperationException($\"{nameof(referenceLyric)} should not contains another reference lyric.\");\r\n\r\n            lyric.ReferenceLyricId = referenceLyric?.ID;\r\n\r\n            if (lyric.ReferenceLyricId == null)\r\n            {\r\n                lyric.ReferenceLyricConfig = null;\r\n            }\r\n            else\r\n            {\r\n                // should make sure that config will be created if have reference lyric.\r\n                // todo: not really sure should use sync config if lyric text are similar.\r\n                lyric.ReferenceLyricConfig ??= new ReferenceLyricConfig();\r\n            }\r\n\r\n            TriggerHitObjectUpdate(lyric);\r\n        });\r\n    }\r\n\r\n    public void SwitchToReferenceLyricConfig()\r\n    {\r\n        PerformOnSelection(lyric =>\r\n        {\r\n            if (lyric.ReferenceLyric == null)\r\n                throw new InvalidOperationException($\"{nameof(lyric)} must have reference lyric.\");\r\n\r\n            lyric.ReferenceLyricConfig = new ReferenceLyricConfig();\r\n        });\r\n    }\r\n\r\n    public void SwitchToSyncLyricConfig()\r\n    {\r\n        PerformOnSelection(lyric =>\r\n        {\r\n            if (lyric.ReferenceLyric == null)\r\n                throw new InvalidOperationException($\"{nameof(lyric)} must have reference lyric.\");\r\n\r\n            lyric.ReferenceLyricConfig = new SyncLyricConfig();\r\n        });\r\n    }\r\n\r\n    public void AdjustLyricConfig<TConfig>(Action<TConfig> action) where TConfig : IReferenceLyricPropertyConfig\r\n    {\r\n        PerformOnSelection(lyric =>\r\n        {\r\n            if (lyric.ReferenceLyricConfig is not TConfig config)\r\n                throw new InvalidOperationException($\"{nameof(config)} must be the type of ${typeof(TConfig)}.\");\r\n\r\n            action(config);\r\n        });\r\n    }\r\n\r\n    protected override bool IsWritePropertyLocked(Lyric lyric)\r\n        => HitObjectWritableUtils.IsWriteLyricPropertyLocked(lyric, nameof(Lyric.ReferenceLyric), nameof(Lyric.ReferenceLyricConfig));\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Lyrics/LyricRubyTagsChangeHandler.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\n\r\npublic partial class LyricRubyTagsChangeHandler : LyricPropertyChangeHandler, ILyricRubyTagsChangeHandler\r\n{\r\n    public void Add(RubyTag item)\r\n    {\r\n        CheckExactlySelectedOneHitObject();\r\n\r\n        PerformOnSelection(lyric =>\r\n        {\r\n            bool containsInLyric = this.containsInLyric(lyric, item);\r\n            if (containsInLyric)\r\n                throw new InvalidOperationException($\"{nameof(item)} already in the lyric\");\r\n\r\n            addToLyric(lyric, item);\r\n        });\r\n    }\r\n\r\n    public void AddRange(IEnumerable<RubyTag> items)\r\n    {\r\n        CheckExactlySelectedOneHitObject();\r\n\r\n        PerformOnSelection(lyric =>\r\n        {\r\n            // should convert to array because enumerable might change while deleting.\r\n            foreach (var rubyTag in items.ToArray())\r\n            {\r\n                bool containsInLyric = this.containsInLyric(lyric, rubyTag);\r\n                if (containsInLyric)\r\n                    throw new InvalidOperationException($\"{nameof(rubyTag)} already in the lyric\");\r\n\r\n                addToLyric(lyric, rubyTag);\r\n            }\r\n        });\r\n    }\r\n\r\n    public void Remove(RubyTag item)\r\n    {\r\n        CheckExactlySelectedOneHitObject();\r\n\r\n        PerformOnSelection(lyric =>\r\n        {\r\n            bool containsInLyric = this.containsInLyric(lyric, item);\r\n            if (!containsInLyric)\r\n                throw new InvalidOperationException($\"{nameof(item)} is not in the lyric\");\r\n\r\n            removeFromLyric(lyric, item);\r\n        });\r\n    }\r\n\r\n    public void RemoveRange(IEnumerable<RubyTag> items)\r\n    {\r\n        CheckExactlySelectedOneHitObject();\r\n\r\n        PerformOnSelection(lyric =>\r\n        {\r\n            // should convert to array because enumerable might change while deleting.\r\n            foreach (var rubyTag in items.ToArray())\r\n            {\r\n                bool containsInLyric = this.containsInLyric(lyric, rubyTag);\r\n                if (!containsInLyric)\r\n                    throw new InvalidOperationException($\"{nameof(rubyTag)} is not in the lyric\");\r\n\r\n                removeFromLyric(lyric, rubyTag);\r\n            }\r\n        });\r\n    }\r\n\r\n    public void SetIndex(RubyTag rubyTag, int? startIndex, int? endIndex)\r\n    {\r\n        CheckExactlySelectedOneHitObject();\r\n\r\n        // note: it's ok not sort the text tag by index.\r\n        PerformOnSelection(lyric =>\r\n        {\r\n            bool containsInLyric = this.containsInLyric(lyric, rubyTag);\r\n            if (!containsInLyric)\r\n                throw new InvalidOperationException($\"{nameof(rubyTag)} is not in the lyric\");\r\n\r\n            if (startIndex != null)\r\n                rubyTag.StartIndex = startIndex.Value;\r\n\r\n            if (endIndex != null)\r\n                rubyTag.EndIndex = endIndex.Value;\r\n\r\n            // after change the index, should check if index is valid.\r\n            if (RubyTagUtils.OutOfRange(rubyTag, lyric.Text))\r\n                throw new InvalidOperationException($\"{nameof(startIndex)} or {nameof(endIndex)} is not valid\");\r\n        });\r\n    }\r\n\r\n    public void ShiftingIndex(IEnumerable<RubyTag> rubyTags, int offset)\r\n    {\r\n        CheckExactlySelectedOneHitObject();\r\n\r\n        // note: it's ok not sort the text tag by index.\r\n        PerformOnSelection(lyric =>\r\n        {\r\n            foreach (var rubyTag in rubyTags)\r\n            {\r\n                bool containsInLyric = this.containsInLyric(lyric, rubyTag);\r\n                if (!containsInLyric)\r\n                    throw new InvalidOperationException($\"{nameof(rubyTag)} is not in the lyric\");\r\n\r\n                (int startIndex, int endIndex) = RubyTagUtils.GetShiftingIndex(rubyTag, lyric.Text, offset);\r\n                rubyTag.StartIndex = startIndex;\r\n                rubyTag.EndIndex = endIndex;\r\n            }\r\n        });\r\n    }\r\n\r\n    public void SetText(RubyTag rubyTag, string text)\r\n    {\r\n        CheckExactlySelectedOneHitObject();\r\n\r\n        PerformOnSelection(lyric =>\r\n        {\r\n            bool containsInLyric = this.containsInLyric(lyric, rubyTag);\r\n            if (!containsInLyric)\r\n                throw new InvalidOperationException($\"{nameof(rubyTag)} is not in the lyric\");\r\n\r\n            rubyTag.Text = text;\r\n        });\r\n    }\r\n\r\n    private bool containsInLyric(Lyric lyric, RubyTag rubyTag)\r\n        => lyric.RubyTags.Contains(rubyTag);\r\n\r\n    private void addToLyric(Lyric lyric, RubyTag rubyTag)\r\n        => lyric.RubyTags.Add(rubyTag);\r\n\r\n    private void removeFromLyric(Lyric lyric, RubyTag rubyTag)\r\n        => lyric.RubyTags.Remove(rubyTag);\r\n\r\n    protected override bool IsWritePropertyLocked(Lyric lyric)\r\n        => HitObjectWritableUtils.IsWriteLyricPropertyLocked(lyric, nameof(Lyric.RubyTags));\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Lyrics/LyricSingerChangeHandler.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas.Types;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\n\r\npublic partial class LyricSingerChangeHandler : LyricPropertyChangeHandler, ILyricSingerChangeHandler\r\n{\r\n    public void Add(ISinger singer)\r\n    {\r\n        PerformOnSelection(lyric =>\r\n        {\r\n            lyric.SingerIds.Add(singer.ID);\r\n\r\n            TriggerHitObjectUpdate(lyric);\r\n        });\r\n    }\r\n\r\n    public void AddRange(IEnumerable<ISinger> singers)\r\n    {\r\n        PerformOnSelection(lyric =>\r\n        {\r\n            // should convert to array because enumerable might change while deleting.\r\n            foreach (var singer in singers.ToArray())\r\n            {\r\n                lyric.SingerIds.Add(singer.ID);\r\n            }\r\n\r\n            TriggerHitObjectUpdate(lyric);\r\n        });\r\n    }\r\n\r\n    public void Remove(ISinger singer)\r\n    {\r\n        PerformOnSelection(lyric =>\r\n        {\r\n            lyric.SingerIds.Remove(singer.ID);\r\n\r\n            TriggerHitObjectUpdate(lyric);\r\n        });\r\n    }\r\n\r\n    public void RemoveRange(IEnumerable<ISinger> singers)\r\n    {\r\n        PerformOnSelection(lyric =>\r\n        {\r\n            // should convert to array because enumerable might change while deleting.\r\n            foreach (var singer in singers.ToArray())\r\n            {\r\n                lyric.SingerIds.Remove(singer.ID);\r\n            }\r\n\r\n            TriggerHitObjectUpdate(lyric);\r\n        });\r\n    }\r\n\r\n    public void Clear()\r\n    {\r\n        PerformOnSelection(lyric =>\r\n        {\r\n            lyric.SingerIds.Clear();\r\n\r\n            TriggerHitObjectUpdate(lyric);\r\n        });\r\n    }\r\n\r\n    protected override bool IsWritePropertyLocked(Lyric lyric)\r\n        => HitObjectWritableUtils.IsWriteLyricPropertyLocked(lyric, nameof(Lyric.SingerIds));\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Lyrics/LyricTextChangeHandler.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\n\r\npublic partial class LyricTextChangeHandler : LyricPropertyChangeHandler, ILyricTextChangeHandler\r\n{\r\n    public void InsertText(int charGap, string text)\r\n    {\r\n        CheckExactlySelectedOneHitObject();\r\n\r\n        PerformOnSelection(lyric =>\r\n        {\r\n            LyricUtils.AddText(lyric, charGap, text);\r\n        });\r\n    }\r\n\r\n    public void DeleteLyricText(int charGap, int count = 1)\r\n    {\r\n        CheckExactlySelectedOneHitObject();\r\n\r\n        PerformOnSelection(lyric =>\r\n        {\r\n            LyricUtils.RemoveText(lyric, charGap - count, count);\r\n\r\n            if (!string.IsNullOrEmpty(lyric.Text))\r\n                return;\r\n\r\n            if (HitObjectWritableUtils.IsRemoveLyricLocked(lyric))\r\n                return;\r\n\r\n            OrderUtils.ShiftingOrder(HitObjects.Where(x => x.Order > lyric.Order), -1);\r\n            Remove(lyric);\r\n        });\r\n    }\r\n\r\n    protected override bool IsWritePropertyLocked(Lyric lyric)\r\n        => HitObjectWritableUtils.IsWriteLyricPropertyLocked(lyric, nameof(Lyric.Text), nameof(Lyric.RubyTags), nameof(Lyric.TimeTags));\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Lyrics/LyricTimeTagsChangeHandler.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\n\r\npublic partial class LyricTimeTagsChangeHandler : LyricPropertyChangeHandler, ILyricTimeTagsChangeHandler\r\n{\r\n    public void SetTimeTagTime(TimeTag timeTag, double time)\r\n    {\r\n        CheckExactlySelectedOneHitObject();\r\n\r\n        PerformOnSelection(lyric =>\r\n        {\r\n            bool containsInLyric = lyric.TimeTags.Contains(timeTag);\r\n            if (!containsInLyric)\r\n                throw new InvalidOperationException($\"{nameof(timeTag)} is not in the lyric\");\r\n\r\n            timeTag.Time = time;\r\n        });\r\n    }\r\n\r\n    public void SetTimeTagFirstSyllable(TimeTag timeTag, bool firstSyllable)\r\n    {\r\n        CheckExactlySelectedOneHitObject();\r\n\r\n        PerformOnSelection(lyric =>\r\n        {\r\n            bool containsInLyric = lyric.TimeTags.Contains(timeTag);\r\n            if (!containsInLyric)\r\n                throw new InvalidOperationException($\"{nameof(timeTag)} is not in the lyric\");\r\n\r\n            timeTag.FirstSyllable = firstSyllable;\r\n        });\r\n    }\r\n\r\n    public void SetTimeTagRomanisedSyllable(TimeTag timeTag, string? romanisedSyllable)\r\n    {\r\n        CheckExactlySelectedOneHitObject();\r\n\r\n        PerformOnSelection(lyric =>\r\n        {\r\n            bool containsInLyric = lyric.TimeTags.Contains(timeTag);\r\n            if (!containsInLyric)\r\n                throw new InvalidOperationException($\"{nameof(timeTag)} is not in the lyric\");\r\n\r\n            timeTag.RomanisedSyllable = romanisedSyllable;\r\n\r\n            if (!string.IsNullOrWhiteSpace(romanisedSyllable))\r\n                return;\r\n\r\n            timeTag.RomanisedSyllable = string.Empty;\r\n            timeTag.FirstSyllable = false;\r\n        });\r\n    }\r\n\r\n    public void ShiftingTimeTagTime(IEnumerable<TimeTag> timeTags, double offset)\r\n    {\r\n        CheckExactlySelectedOneHitObject();\r\n        NotTriggerSaveStateOnThisChange();\r\n\r\n        PerformOnSelection(lyric =>\r\n        {\r\n            foreach (var timeTag in timeTags)\r\n            {\r\n                bool containsInLyric = lyric.TimeTags.Contains(timeTag);\r\n                if (!containsInLyric)\r\n                    throw new InvalidOperationException($\"{nameof(timeTag)} is not in the lyric\");\r\n\r\n                timeTag.Time += offset;\r\n            }\r\n        });\r\n    }\r\n\r\n    public void ClearTimeTagTime(TimeTag timeTag)\r\n    {\r\n        CheckExactlySelectedOneHitObject();\r\n\r\n        PerformOnSelection(lyric =>\r\n        {\r\n            bool containsInLyric = lyric.TimeTags.Contains(timeTag);\r\n            if (!containsInLyric)\r\n                throw new InvalidOperationException($\"{nameof(timeTag)} is not in the lyric\");\r\n\r\n            timeTag.Time = null;\r\n        });\r\n    }\r\n\r\n    public void ClearAllTimeTagTime()\r\n    {\r\n        PerformOnSelection(lyric =>\r\n        {\r\n            foreach (var timeTag in lyric.TimeTags)\r\n            {\r\n                timeTag.Time = null;\r\n            }\r\n        });\r\n    }\r\n\r\n    public void Add(TimeTag timeTag)\r\n    {\r\n        CheckExactlySelectedOneHitObject();\r\n\r\n        PerformOnSelection(lyric =>\r\n        {\r\n            bool containsInLyric = lyric.TimeTags.Contains(timeTag);\r\n            if (containsInLyric)\r\n                throw new InvalidOperationException($\"{nameof(timeTag)} already in the lyric\");\r\n\r\n            insertTimeTag(lyric, timeTag, InsertDirection.End);\r\n        });\r\n    }\r\n\r\n    public void AddRange(IEnumerable<TimeTag> timeTags)\r\n    {\r\n        CheckExactlySelectedOneHitObject();\r\n\r\n        PerformOnSelection(lyric =>\r\n        {\r\n            // should convert to array because enumerable might change while deleting.\r\n            foreach (var timeTag in timeTags.ToArray())\r\n            {\r\n                bool containsInLyric = lyric.TimeTags.Contains(timeTag);\r\n                if (containsInLyric)\r\n                    throw new InvalidOperationException($\"{nameof(timeTag)} already in the lyric\");\r\n\r\n                insertTimeTag(lyric, timeTag, InsertDirection.End);\r\n            }\r\n        });\r\n    }\r\n\r\n    public void Remove(TimeTag timeTag)\r\n    {\r\n        CheckExactlySelectedOneHitObject();\r\n\r\n        PerformOnSelection(lyric =>\r\n        {\r\n            // delete time tag from list\r\n            lyric.TimeTags.Remove(timeTag);\r\n        });\r\n    }\r\n\r\n    public void RemoveRange(IEnumerable<TimeTag> timeTags)\r\n    {\r\n        CheckExactlySelectedOneHitObject();\r\n\r\n        PerformOnSelection(lyric =>\r\n        {\r\n            // should convert to array because enumerable might change while deleting.\r\n            foreach (var timeTag in timeTags.ToArray())\r\n            {\r\n                bool containsInLyric = lyric.TimeTags.Remove(timeTag);\r\n                if (!containsInLyric)\r\n                    throw new InvalidOperationException($\"{nameof(timeTag)} is not in the lyric\");\r\n            }\r\n        });\r\n    }\r\n\r\n    public void AddByPosition(TextIndex index)\r\n    {\r\n        CheckExactlySelectedOneHitObject();\r\n\r\n        PerformOnSelection(lyric =>\r\n        {\r\n            insertTimeTag(lyric, new TimeTag(index), InsertDirection.End);\r\n        });\r\n    }\r\n\r\n    public void RemoveByPosition(TextIndex index)\r\n    {\r\n        CheckExactlySelectedOneHitObject();\r\n\r\n        PerformOnSelection(lyric =>\r\n        {\r\n            var matchedTimeTags = lyric.TimeTags.Where(x => x.Index == index).ToList();\r\n            if (!matchedTimeTags.Any())\r\n                return;\r\n\r\n            var removedTimeTag = matchedTimeTags.MinBy(x => x.Time ?? double.MinValue);\r\n            if (removedTimeTag != null)\r\n                lyric.TimeTags.Remove(removedTimeTag);\r\n        });\r\n    }\r\n\r\n    public TimeTag Shifting(TimeTag timeTag, ShiftingDirection direction, ShiftingType type)\r\n    {\r\n        CheckExactlySelectedOneHitObject();\r\n\r\n        TimeTag newTimeTag = null!;\r\n\r\n        PerformOnSelection(lyric =>\r\n        {\r\n            bool containsInLyric = lyric.TimeTags.Contains(timeTag);\r\n            if (!containsInLyric)\r\n                throw new InvalidOperationException($\"{nameof(timeTag)} is not in the lyric\");\r\n\r\n            // remove the time-tag first.\r\n            lyric.TimeTags.Remove(timeTag);\r\n\r\n            // then, create a new one and insert into the list.\r\n            var newIndex = calculateNewIndex(lyric, timeTag.Index, direction, type);\r\n            double? newTime = timeTag.Time;\r\n            newTimeTag = new TimeTag(newIndex, newTime);\r\n\r\n            switch (direction)\r\n            {\r\n                case ShiftingDirection.Left:\r\n                    insertTimeTag(lyric, newTimeTag, InsertDirection.End);\r\n                    break;\r\n\r\n                case ShiftingDirection.Right:\r\n                    insertTimeTag(lyric, newTimeTag, InsertDirection.Start);\r\n                    break;\r\n\r\n                default:\r\n                    throw new InvalidOperationException();\r\n            }\r\n        });\r\n\r\n        return newTimeTag;\r\n\r\n        static TextIndex calculateNewIndex(Lyric lyric, TextIndex originIndex, ShiftingDirection direction, ShiftingType type)\r\n        {\r\n            var newIndex = getNewIndex(originIndex, direction, type);\r\n            if (TextIndexUtils.OutOfRange(newIndex, lyric.Text))\r\n                throw new ArgumentOutOfRangeException();\r\n\r\n            return newIndex;\r\n\r\n            static TextIndex getNewIndex(TextIndex originIndex, ShiftingDirection direction, ShiftingType type) =>\r\n                type switch\r\n                {\r\n                    ShiftingType.Index => TextIndexUtils.ShiftingIndex(originIndex, direction == ShiftingDirection.Left ? -1 : 1),\r\n                    ShiftingType.State => direction == ShiftingDirection.Left ? TextIndexUtils.GetPreviousIndex(originIndex) : TextIndexUtils.GetNextIndex(originIndex),\r\n                    _ => throw new InvalidOperationException(),\r\n                };\r\n        }\r\n    }\r\n\r\n    private void insertTimeTag(Lyric lyric, TimeTag timeTag, InsertDirection direction)\r\n    {\r\n        var timeTags = lyric.TimeTags;\r\n\r\n        // just add if there's no time-tag\r\n        if (lyric.TimeTags.Count == 0)\r\n        {\r\n            timeTags.Add(timeTag);\r\n            return;\r\n        }\r\n\r\n        if (timeTags.All(x => x.Index < timeTag.Index))\r\n        {\r\n            timeTags.Add(timeTag);\r\n        }\r\n        else if (timeTags.All(x => x.Index > timeTag.Index))\r\n        {\r\n            timeTags.Insert(0, timeTag);\r\n        }\r\n        else\r\n        {\r\n            switch (direction)\r\n            {\r\n                case InsertDirection.Start:\r\n                {\r\n                    var nextTimeTag = timeTags.FirstOrDefault(x => x.Index >= timeTag.Index) ?? timeTags.Last();\r\n                    int index = timeTags.IndexOf(nextTimeTag);\r\n                    timeTags.Insert(index, timeTag);\r\n                    break;\r\n                }\r\n\r\n                case InsertDirection.End:\r\n                {\r\n                    var previousTimeTag = timeTags.Reverse().FirstOrDefault(x => x.Index <= timeTag.Index) ?? timeTags.First();\r\n                    int index = timeTags.IndexOf(previousTimeTag) + 1;\r\n                    timeTags.Insert(index, timeTag);\r\n                    break;\r\n                }\r\n\r\n                default:\r\n                    throw new InvalidOperationException();\r\n            }\r\n        }\r\n    }\r\n\r\n    protected override bool IsWritePropertyLocked(Lyric lyric)\r\n        => HitObjectWritableUtils.IsWriteLyricPropertyLocked(lyric, nameof(Lyric.TimeTags));\r\n\r\n    /// <summary>\r\n    /// Insert direction if contains the time-tag with the same index.\r\n    /// </summary>\r\n    private enum InsertDirection\r\n    {\r\n        Start,\r\n\r\n        End,\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Lyrics/LyricTranslationChangeHandler.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Globalization;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\n\r\npublic partial class LyricTranslationChangeHandler : LyricPropertyChangeHandler, ILyricTranslationChangeHandler\r\n{\r\n    public void UpdateTranslation(CultureInfo cultureInfo, string translation)\r\n    {\r\n        CheckExactlySelectedOneHitObject();\r\n\r\n        PerformOnSelection(lyric =>\r\n        {\r\n            // should not save translation if is null or empty or whitespace\r\n            if (string.IsNullOrWhiteSpace(translation))\r\n            {\r\n                if (lyric.Translations.ContainsKey(cultureInfo))\r\n                    lyric.Translations.Remove(cultureInfo);\r\n            }\r\n            else\r\n            {\r\n                if (!lyric.Translations.TryAdd(cultureInfo, translation))\r\n                    lyric.Translations[cultureInfo] = translation;\r\n            }\r\n        });\r\n    }\r\n\r\n    protected override bool IsWritePropertyLocked(Lyric lyric)\r\n        => HitObjectWritableUtils.IsWriteLyricPropertyLocked(lyric, nameof(Lyric.Translations));\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Lyrics/LyricsChangeHandler.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Extensions.IEnumerableExtensions;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\n\r\npublic partial class LyricsChangeHandler : HitObjectsChangeHandler<Lyric>, ILyricsChangeHandler\r\n{\r\n    public void Split(int index)\r\n    {\r\n        CheckExactlySelectedOneHitObject();\r\n\r\n        PerformOnSelection(lyric =>\r\n        {\r\n            // Shifting order that order is larger than current lyric.\r\n            int lyricOrder = lyric.Order;\r\n            OrderUtils.ShiftingOrder(HitObjects.Where(x => x.Order > lyricOrder), 1);\r\n\r\n            // Split lyric\r\n            var (firstLyric, secondLyric) = LyricsUtils.SplitLyric(lyric, index);\r\n            firstLyric.Order = lyric.Order;\r\n            secondLyric.Order = lyric.Order + 1;\r\n\r\n            // Add those tho lyric and remove old one.\r\n            Add(secondLyric);\r\n            Add(firstLyric);\r\n            Remove(lyric);\r\n        });\r\n    }\r\n\r\n    public void Combine()\r\n    {\r\n        CheckExactlySelectedOneHitObject();\r\n\r\n        PerformOnSelection(lyric =>\r\n        {\r\n            var previousLyric = HitObjects.GetPrevious(lyric);\r\n            if (previousLyric == null)\r\n                throw new ArgumentNullException(nameof(previousLyric));\r\n\r\n            // Shifting order that order is larger than current lyric.\r\n            int lyricOrder = previousLyric.Order;\r\n            OrderUtils.ShiftingOrder(HitObjects.Where(x => x.Order > lyricOrder), -1);\r\n\r\n            var newLyric = LyricsUtils.CombineLyric(previousLyric, lyric);\r\n            newLyric.Order = lyricOrder;\r\n\r\n            // Add created lyric and remove old two.\r\n            Add(newLyric);\r\n            Remove(previousLyric);\r\n            Remove(lyric);\r\n        });\r\n    }\r\n\r\n    public void CreateAtPosition()\r\n    {\r\n        AddBelowToSelection(new Lyric\r\n        {\r\n            Text = \"New lyric\",\r\n        });\r\n    }\r\n\r\n    public void CreateAtLast()\r\n    {\r\n        int order = OrderUtils.GetMaxOrderNumber(HitObjects.ToArray());\r\n\r\n        // Add new lyric to target order.\r\n        Add(new Lyric\r\n        {\r\n            Text = \"New lyric\",\r\n            Order = order + 1,\r\n        });\r\n    }\r\n\r\n    public void AddBelowToSelection(Lyric newLyric)\r\n    {\r\n        AddRangeBelowToSelection(new[] { newLyric });\r\n    }\r\n\r\n    public void AddRangeBelowToSelection(IEnumerable<Lyric> newLyrics)\r\n    {\r\n        CheckExactlySelectedOneHitObject();\r\n\r\n        PerformOnSelection(lyric =>\r\n        {\r\n            int order = lyric.Order;\r\n\r\n            // Shifting order that order is larger than current lyric.\r\n            OrderUtils.ShiftingOrder(HitObjects.Where(x => x.Order > order), newLyrics.Count());\r\n\r\n            foreach (var newLyric in newLyrics)\r\n            {\r\n                newLyric.Order = ++order;\r\n                Add(newLyric);\r\n            }\r\n        });\r\n    }\r\n\r\n    public void Remove()\r\n    {\r\n        PerformOnSelection(lyric =>\r\n        {\r\n            // Shifting order that order is larger than current lyric.\r\n            OrderUtils.ShiftingOrder(HitObjects.Where(x => x.Order > lyric.Order), -1);\r\n            Remove(lyric);\r\n        });\r\n    }\r\n\r\n    public void ChangeOrder(int newOrder)\r\n    {\r\n        PerformOnSelection(lyric =>\r\n        {\r\n            int oldOrder = lyric.Order;\r\n            OrderUtils.ChangeOrder(HitObjects.ToArray(), oldOrder, newOrder + 1, (switchSinger, oldOrder, newOrder) =>\r\n            {\r\n                // todo : not really sure should call update?\r\n            });\r\n        });\r\n    }\r\n\r\n    protected override void Add<T>(T hitObject)\r\n    {\r\n        if (hitObject is Lyric lyric)\r\n        {\r\n            int index = getInsertIndex(lyric.Order);\r\n            Insert(index, lyric);\r\n        }\r\n        else\r\n        {\r\n            base.Add(hitObject);\r\n        }\r\n    }\r\n\r\n    protected override void Insert<T>(int index, T hitObject)\r\n    {\r\n        if (hitObject is Lyric lyric)\r\n        {\r\n            base.Insert(index, lyric);\r\n        }\r\n        else\r\n        {\r\n            base.Add(hitObject);\r\n        }\r\n    }\r\n\r\n    private int getInsertIndex(int order)\r\n        => HitObjects.ToList().FindIndex(x => x.Order == order - 1) + 1;\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Notes/INotePropertyChangeHandler.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Notes;\r\n\r\npublic interface INotePropertyChangeHandler : IHitObjectPropertyChangeHandler\r\n{\r\n    void ChangeText(string text);\r\n\r\n    void ChangeRubyText(string? ruby);\r\n\r\n    void ChangeDisplayState(bool display);\r\n\r\n    void OffsetTone(Tone offset);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Notes/INotesChangeHandler.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Notes;\r\n\r\npublic interface INotesChangeHandler\r\n{\r\n    void Split(float percentage = 0.5f);\r\n\r\n    void Combine();\r\n\r\n    void Clear();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Notes/NotePropertyChangeHandler.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Allocation;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Notes;\r\n\r\npublic partial class NotePropertyChangeHandler : HitObjectPropertyChangeHandler<Note>, INotePropertyChangeHandler\r\n{\r\n    [Resolved]\r\n    private EditorBeatmap beatmap { get; set; } = null!;\r\n\r\n    public void ChangeText(string text)\r\n    {\r\n        CheckExactlySelectedOneHitObject();\r\n\r\n        PerformOnSelection(note =>\r\n        {\r\n            note.Text = text;\r\n        });\r\n    }\r\n\r\n    public void ChangeRubyText(string? ruby)\r\n    {\r\n        CheckExactlySelectedOneHitObject();\r\n\r\n        PerformOnSelection(note =>\r\n        {\r\n            // Should change ruby text as null if remove all words.\r\n            note.RubyText = string.IsNullOrEmpty(ruby) ? null : ruby;\r\n        });\r\n    }\r\n\r\n    public void ChangeDisplayState(bool display)\r\n    {\r\n        PerformOnSelection(note =>\r\n        {\r\n            note.Display = display;\r\n\r\n            // Move to center if note is not display\r\n            if (!note.Display)\r\n                note.Tone = new Tone();\r\n        });\r\n    }\r\n\r\n    public void OffsetTone(Tone offset)\r\n    {\r\n        if (offset == default(Tone))\r\n            throw new InvalidOperationException(\"Offset number should not be zero.\");\r\n\r\n        var noteInfo = EditorBeatmapUtils.GetPlayableBeatmap(beatmap).NoteInfo;\r\n\r\n        PerformOnSelection(note =>\r\n        {\r\n            if (note.Tone >= noteInfo.MaxTone && offset > 0)\r\n                return;\r\n\r\n            if (note.Tone <= noteInfo.MinTone && offset < 0)\r\n                return;\r\n\r\n            note.Tone += offset;\r\n\r\n            //Change all note to visible\r\n            note.Display = true;\r\n        });\r\n    }\r\n\r\n    protected sealed override bool IsWritePropertyLocked(Note note)\r\n        => HitObjectWritableUtils.IsWriteNotePropertyLocked(note, nameof(Note.Text), nameof(Note.RubyText), nameof(Note.Display));\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Notes/NotesChangeHandler.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Utils;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Notes;\r\n\r\npublic partial class NotesChangeHandler : HitObjectsChangeHandler<Note>, INotesChangeHandler\r\n{\r\n    [Resolved]\r\n    private EditorBeatmap beatmap { get; set; } = null!;\r\n\r\n    public void Split(float percentage = 0.5f)\r\n    {\r\n        CheckExactlySelectedOneHitObject();\r\n\r\n        PerformOnSelection(note =>\r\n        {\r\n            var (firstNote, secondNote) = NotesUtils.SplitNote(note);\r\n            Add(firstNote);\r\n            Add(secondNote);\r\n            Remove(note);\r\n        });\r\n    }\r\n\r\n    public void Combine()\r\n    {\r\n        PerformOnSelection<Lyric>(lyric =>\r\n        {\r\n            var notes = beatmap.SelectedHitObjects.OfType<Note>().Where(n => n.ReferenceLyric == lyric).ToList();\r\n\r\n            if (notes.Count < 2)\r\n                throw new InvalidOperationException($\"Should have select at lest two {nameof(notes)}.\");\r\n\r\n            var combinedNote = NotesUtils.CombineNote(notes[0], notes[1]);\r\n\r\n            for (int i = 2; i < notes.Count; i++)\r\n            {\r\n                combinedNote = NotesUtils.CombineNote(notes[i - 1], notes[i]);\r\n            }\r\n\r\n            RemoveRange(notes);\r\n            Add(combinedNote);\r\n        });\r\n    }\r\n\r\n    public void Clear()\r\n    {\r\n        PerformOnSelection<Lyric>(lyric =>\r\n        {\r\n            var notes = beatmap.HitObjects.OfType<Note>().Where(n => n.ReferenceLyric == lyric).ToList();\r\n            RemoveRange(notes);\r\n        });\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Stages/ClassicStageChangeHandler.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos.Classic;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Stages;\r\n\r\npublic partial class ClassicStageChangeHandler : StagePropertyChangeHandler, IClassicStageChangeHandler\r\n{\r\n    [Resolved]\r\n    private EditorBeatmap beatmap { get; set; } = null!;\r\n\r\n    #region Layout definition\r\n\r\n    public void EditLayoutDefinition(Action<ClassicStageDefinition> action)\r\n    {\r\n        performStageInfoChanged(x =>\r\n        {\r\n            action(x.StageDefinition);\r\n        });\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Timing info\r\n\r\n    public void AddTimingPoint(Action<ClassicLyricTimingPoint>? action = null)\r\n    {\r\n        performTimingInfoChanged(timingInfo =>\r\n        {\r\n            timingInfo.AddTimingPoint(action);\r\n        });\r\n    }\r\n\r\n    public void RemoveTimingPoint(ClassicLyricTimingPoint timePoint)\r\n    {\r\n        performTimingInfoChanged(timingInfo =>\r\n        {\r\n            timingInfo.RemoveTimingPoint(timePoint);\r\n        });\r\n    }\r\n\r\n    public void RemoveRangeOfTimingPoints(IEnumerable<ClassicLyricTimingPoint> timePoints)\r\n    {\r\n        performTimingInfoChanged(timingInfo =>\r\n        {\r\n            foreach (var timePoint in timePoints)\r\n            {\r\n                timingInfo.RemoveTimingPoint(timePoint);\r\n            }\r\n        });\r\n    }\r\n\r\n    public void ShiftingTimingPoints(IEnumerable<ClassicLyricTimingPoint> timePoints, double offset)\r\n    {\r\n        performTimingInfoChanged(timingInfo =>\r\n        {\r\n            foreach (var timePoint in timePoints)\r\n            {\r\n                timePoint.Time += offset;\r\n            }\r\n        });\r\n    }\r\n\r\n    public void AddLyricIntoTimingPoint(ClassicLyricTimingPoint timePoint)\r\n    {\r\n        performTimingInfoChanged(timingInfo =>\r\n        {\r\n            var selectedLyric = beatmap.SelectedHitObjects.OfType<Lyric>();\r\n\r\n            foreach (var lyric in selectedLyric)\r\n            {\r\n                timingInfo.AddToMapping(timePoint, lyric);\r\n            }\r\n        });\r\n    }\r\n\r\n    public void RemoveLyricFromTimingPoint(ClassicLyricTimingPoint timePoint)\r\n    {\r\n        performTimingInfoChanged(timingInfo =>\r\n        {\r\n            var selectedLyric = beatmap.SelectedHitObjects.OfType<Lyric>();\r\n\r\n            foreach (var lyric in selectedLyric)\r\n            {\r\n                timingInfo.RemoveFromMapping(timePoint, lyric);\r\n            }\r\n        });\r\n    }\r\n\r\n    private static bool checkTimingPointExist(ClassicLyricTimingInfo timingInfo, ClassicLyricTimingPoint timingPoint)\r\n    {\r\n        return timingInfo.Timings.Contains(timingPoint);\r\n    }\r\n\r\n    #endregion\r\n\r\n    private void performStageInfoChanged(Action<ClassicStageInfo> action)\r\n    {\r\n        throw new NotImplementedException();\r\n    }\r\n\r\n    private void performTimingInfoChanged(Action<ClassicLyricTimingInfo> action)\r\n    {\r\n        throw new NotImplementedException();\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Stages/IClassicStageChangeHandler.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos.Classic;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Stages;\r\n\r\npublic interface IClassicStageChangeHandler\r\n{\r\n    #region Layout definition\r\n\r\n    void EditLayoutDefinition(Action<ClassicStageDefinition> action);\r\n\r\n    #endregion\r\n\r\n    #region Timing info\r\n\r\n    void AddTimingPoint(Action<ClassicLyricTimingPoint>? action = null);\r\n\r\n    void RemoveTimingPoint(ClassicLyricTimingPoint timePoint);\r\n\r\n    void RemoveRangeOfTimingPoints(IEnumerable<ClassicLyricTimingPoint> timePoints);\r\n\r\n    void ShiftingTimingPoints(IEnumerable<ClassicLyricTimingPoint> timePoints, double offset);\r\n\r\n    void AddLyricIntoTimingPoint(ClassicLyricTimingPoint timePoint);\r\n\r\n    void RemoveLyricFromTimingPoint(ClassicLyricTimingPoint timePoint);\r\n\r\n    #endregion\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Stages/IStageElementCategoryChangeHandler.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Stages;\r\n\r\npublic interface IStageElementCategoryChangeHandler<TStageElement>\r\n    where TStageElement : StageElement\r\n{\r\n    void AddElement(Action<TStageElement>? action = null);\r\n\r\n    void EditElement(ElementId? id, Action<TStageElement> action);\r\n\r\n    void RemoveElement(TStageElement element);\r\n\r\n    void AddToMapping(TStageElement element);\r\n\r\n    void RemoveFromMapping();\r\n\r\n    void ClearUnusedMapping();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Stages/IStagesChangeHandler.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Stages;\r\n\r\npublic interface IStagesChangeHandler : IAutoGenerateChangeHandler<StageInfo>\r\n{\r\n    LocalisableString? GetGeneratorNotSupportedMessage<TStageInfo>() where TStageInfo : StageInfo;\r\n\r\n    void Remove<TStageInfo>() where TStageInfo : StageInfo;\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Stages/StageElementCategoryChangeHandler.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Stages;\r\n\r\npublic partial class StageElementCategoryChangeHandler<TStageElement, THitObject> : StagePropertyChangeHandler, IStageElementCategoryChangeHandler<TStageElement>\r\n    where TStageElement : StageElement, IComparable<TStageElement>, new()\r\n    where THitObject : KaraokeHitObject, IHasPrimaryKey\r\n{\r\n    private readonly Func<IEnumerable<StageInfo>, StageElementCategory<TStageElement, THitObject>> stageCategoryAction;\r\n\r\n    public StageElementCategoryChangeHandler(Func<IEnumerable<StageInfo>, StageElementCategory<TStageElement, THitObject>> stageCategoryAction)\r\n    {\r\n        this.stageCategoryAction = stageCategoryAction;\r\n    }\r\n\r\n    public void AddElement(Action<TStageElement>? action = null)\r\n    {\r\n        performStageInfoChanged(s =>\r\n        {\r\n            s.AddElement(action);\r\n        });\r\n    }\r\n\r\n    public void EditElement(ElementId? id, Action<TStageElement> action)\r\n    {\r\n        performStageInfoChanged(s =>\r\n        {\r\n            s.EditElement(id, action);\r\n        });\r\n    }\r\n\r\n    public void RemoveElement(TStageElement element)\r\n    {\r\n        performStageInfoChanged(s =>\r\n        {\r\n            s.RemoveElement(element);\r\n        });\r\n    }\r\n\r\n    public void AddToMapping(TStageElement element)\r\n    {\r\n        PerformOnSelection<THitObject>(hitObject =>\r\n        {\r\n            performStageInfoChanged(s =>\r\n            {\r\n                s.AddToMapping(element, hitObject);\r\n            });\r\n        });\r\n    }\r\n\r\n    public void RemoveFromMapping()\r\n    {\r\n        PerformOnSelection<THitObject>(hitObject =>\r\n        {\r\n            performStageInfoChanged(s =>\r\n            {\r\n                s.RemoveHitObjectFromMapping(hitObject);\r\n            });\r\n        });\r\n    }\r\n\r\n    public void ClearUnusedMapping()\r\n    {\r\n        performStageInfoChanged(s =>\r\n        {\r\n            s.ClearUnusedMapping(id => Lyrics.Any(x => x.ID == id));\r\n        });\r\n    }\r\n\r\n    private void performStageInfoChanged(Action<StageElementCategory<TStageElement, THitObject>> stageAction)\r\n    {\r\n        throw new NotImplementedException();\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Stages/StagePropertyChangeHandler.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Stages;\r\n\r\n/// <summary>\r\n/// Note: will start to implement this class after the stage info is able to edit.\r\n/// </summary>\r\npublic partial class StagePropertyChangeHandler : Component\r\n{\r\n    protected IEnumerable<Lyric> Lyrics => throw new NotImplementedException();\r\n\r\n    protected void PerformOnSelection<T>(Action<T> action) where T : HitObject\r\n    {\r\n        throw new NotImplementedException();\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/ChangeHandlers/Stages/StagesChangeHandler.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Stages;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Stages;\r\n\r\npublic partial class StagesChangeHandler : StagePropertyChangeHandler, IStagesChangeHandler\r\n{\r\n    [Resolved]\r\n    private EditorBeatmap beatmap { get; set; } = null!;\r\n\r\n    private KaraokeBeatmap karaokeBeatmap => EditorBeatmapUtils.GetPlayableBeatmap(beatmap);\r\n\r\n    [Resolved]\r\n    private KaraokeRulesetEditGeneratorConfigManager generatorConfigManager { get; set; } = null!;\r\n\r\n    bool IAutoGenerateChangeHandler<StageInfo>.CanGenerate<TStageInfo>()\r\n        => CanGenerate<TStageInfo>();\r\n\r\n    public bool CanGenerate<TStageInfo>() where TStageInfo : StageInfo\r\n    {\r\n        return GetGeneratorNotSupportedMessage<TStageInfo>() == null;\r\n    }\r\n\r\n    public LocalisableString? GetGeneratorNotSupportedMessage<TStageInfo>() where TStageInfo : StageInfo\r\n    {\r\n        var stage = getStageInfo<TStageInfo>();\r\n        if (stage != null)\r\n            return $\"{nameof(TStageInfo)} already exist in the beatmap.\";\r\n\r\n        var generator = new StageInfoGeneratorSelector<TStageInfo>(generatorConfigManager);\r\n        return generator.GetInvalidMessage(karaokeBeatmap);\r\n    }\r\n\r\n    void IAutoGenerateChangeHandler<StageInfo>.AutoGenerate<TStageInfo>()\r\n        => AutoGenerate<TStageInfo>();\r\n\r\n    public void AutoGenerate<TStageInfo>() where TStageInfo : StageInfo\r\n    {\r\n        var stage = getStageInfo<TStageInfo>();\r\n        if (stage != null)\r\n            throw new InvalidOperationException($\"{nameof(TStageInfo)} already exist in the beatmap.\");\r\n\r\n        var generator = new StageInfoGeneratorSelector<TStageInfo>(generatorConfigManager);\r\n        var stageInfo = generator.Generate(karaokeBeatmap);\r\n\r\n        getStageInfos().Add(stageInfo);\r\n    }\r\n\r\n    public void Remove<TStageInfo>() where TStageInfo : StageInfo\r\n    {\r\n        var stage = getStageInfo<TStageInfo>();\r\n        if (stage == null)\r\n            throw new InvalidOperationException($\"There's no {nameof(TStageInfo)} in the beatmap.\");\r\n\r\n        getStageInfos().Remove(stage);\r\n\r\n        // todo: maybe should update the current stage.\r\n    }\r\n\r\n    private IList<StageInfo> getStageInfos()\r\n        => throw new NotImplementedException();\r\n\r\n    private TStageInfo? getStageInfo<TStageInfo>() where TStageInfo : StageInfo\r\n        => throw new NotImplementedException();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Checks/CheckBeatmapAvailableTranslations.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Globalization;\r\nusing System.Linq;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Checks;\r\n\r\npublic class CheckBeatmapAvailableTranslations : CheckBeatmapProperty<IList<CultureInfo>, Lyric>\r\n{\r\n    protected override string Description => \"Beatmap with invalid localization info.\";\r\n\r\n    public override IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]\r\n    {\r\n        new IssueTemplateMissingTranslation(this),\r\n        new IssueTemplateMissingPartialTranslation(this),\r\n        new IssueTemplateTranslationNotInListedLanguage(this),\r\n    };\r\n\r\n    protected override IList<CultureInfo> GetPropertyFromBeatmap(KaraokeBeatmap karaokeBeatmap)\r\n        => karaokeBeatmap.AvailableTranslationLanguages;\r\n\r\n    protected override IEnumerable<Issue> CheckProperty(IList<CultureInfo> property)\r\n    {\r\n        // todo: maybe check duplicated languages?\r\n        yield break;\r\n    }\r\n\r\n    protected override IEnumerable<Issue> CheckHitObjects(IList<CultureInfo> property, IReadOnlyList<Lyric> hitObject)\r\n    {\r\n        if (hitObject.Count == 0)\r\n            yield break;\r\n\r\n        // check if some translations is missing or empty.\r\n        foreach (var language in property)\r\n        {\r\n            var missingTranslationLyrics = hitObject.Where(x => !x.Translations.ContainsKey(language) || string.IsNullOrWhiteSpace(x.Translations[language])).ToArray();\r\n\r\n            if (missingTranslationLyrics.Length == hitObject.Count)\r\n            {\r\n                yield return new IssueTemplateMissingTranslation(this).Create(missingTranslationLyrics, language);\r\n            }\r\n            else if (missingTranslationLyrics.Any())\r\n            {\r\n                yield return new IssueTemplateMissingPartialTranslation(this).Create(missingTranslationLyrics, language);\r\n            }\r\n        }\r\n\r\n        // should check is lyric contains translation that is not listed in beatmap.\r\n        // if got this issue, then it's a bug.\r\n        var allTranslationLanguageInLyric = hitObject.SelectMany(x => x.Translations.Keys).Distinct();\r\n        var languageNotListInBeatmap = allTranslationLanguageInLyric.Except(property);\r\n\r\n        foreach (var language in languageNotListInBeatmap)\r\n        {\r\n            var notContainsTranslationLyrics = hitObject.Where(x => !x.Translations.ContainsKey(language));\r\n\r\n            yield return new IssueTemplateTranslationNotInListedLanguage(this).Create(notContainsTranslationLyrics, language);\r\n        }\r\n    }\r\n\r\n    public class IssueTemplateMissingTranslation : IssueTemplate\r\n    {\r\n        public IssueTemplateMissingTranslation(ICheck check)\r\n            : base(check, IssueType.Problem, \"There are no lyric translations for this language.\")\r\n        {\r\n        }\r\n\r\n        public Issue Create(IEnumerable<HitObject> hitObjects, CultureInfo cultureInfo)\r\n            => new(hitObjects, this, cultureInfo);\r\n    }\r\n\r\n    public class IssueTemplateMissingPartialTranslation : IssueTemplate\r\n    {\r\n        public IssueTemplateMissingPartialTranslation(ICheck check)\r\n            : base(check, IssueType.Problem, \"Some lyrics in this language are missing translations.\")\r\n        {\r\n        }\r\n\r\n        public Issue Create(IEnumerable<HitObject> hitObjects, CultureInfo cultureInfo)\r\n            => new(hitObjects, this, cultureInfo);\r\n    }\r\n\r\n    public class IssueTemplateTranslationNotInListedLanguage : IssueTemplate\r\n    {\r\n        public IssueTemplateTranslationNotInListedLanguage(ICheck check)\r\n            : base(check, IssueType.Problem, \"Seems some translation language is not listed in the beatmap, please contact developer to fix that bug.\")\r\n        {\r\n        }\r\n\r\n        public Issue Create(IEnumerable<HitObject> hitObjects, CultureInfo cultureInfo)\r\n            => new(hitObjects, this, cultureInfo);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Checks/CheckBeatmapNoteInfo.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks.Issues;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Checks;\r\n\r\npublic class CheckBeatmapNoteInfo : CheckBeatmapProperty<NoteInfo, Note>\r\n{\r\n    public const int MIN_COLUMNS = 9;\r\n\r\n    public const int MAX_COLUMNS = 12;\r\n\r\n    protected override string Description => \"Check invalid note info and the note that out of range.\";\r\n\r\n    public override IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]\r\n    {\r\n        new IssueTemplateColumnNotEnough(this),\r\n        new IssueTemplateColumnExceed(this),\r\n        new IssueTemplateNoteToneTooLow(this),\r\n        new IssueTemplateNoteToneTooHigh(this),\r\n    };\r\n\r\n    protected override NoteInfo GetPropertyFromBeatmap(KaraokeBeatmap karaokeBeatmap)\r\n        => karaokeBeatmap.NoteInfo;\r\n\r\n    protected override IEnumerable<Issue> CheckProperty(NoteInfo property)\r\n    {\r\n        switch (property.Columns)\r\n        {\r\n            case < MIN_COLUMNS:\r\n                yield return new IssueTemplateColumnNotEnough(this).Create();\r\n\r\n                break;\r\n\r\n            case > MAX_COLUMNS:\r\n                yield return new IssueTemplateColumnExceed(this).Create();\r\n\r\n                break;\r\n        }\r\n    }\r\n\r\n    protected override IEnumerable<Issue> CheckHitObject(NoteInfo property, Note hitObject)\r\n    {\r\n        if (hitObject.Tone < property.MinTone)\r\n            yield return new IssueTemplateNoteToneTooLow(this).Create(hitObject);\r\n\r\n        if (hitObject.Tone > property.MaxTone)\r\n            yield return new IssueTemplateNoteToneTooHigh(this).Create(hitObject);\r\n    }\r\n\r\n    public class IssueTemplateColumnNotEnough : IssueTemplate\r\n    {\r\n        public IssueTemplateColumnNotEnough(ICheck check)\r\n            : base(check, IssueType.Warning, \"Column is not enough.\")\r\n        {\r\n        }\r\n\r\n        public Issue Create() => new(this);\r\n    }\r\n\r\n    public class IssueTemplateColumnExceed : IssueTemplate\r\n    {\r\n        public IssueTemplateColumnExceed(ICheck check)\r\n            : base(check, IssueType.Warning, \"Column exceed.\")\r\n        {\r\n        }\r\n\r\n        public Issue Create() => new(this);\r\n    }\r\n\r\n    public class IssueTemplateNoteToneTooLow : IssueTemplate\r\n    {\r\n        public IssueTemplateNoteToneTooLow(ICheck check)\r\n            : base(check, IssueType.Warning, \"Note's tone is too low.\")\r\n        {\r\n        }\r\n\r\n        public Issue Create(Note note) => new NoteIssue(note, this);\r\n    }\r\n\r\n    public class IssueTemplateNoteToneTooHigh : IssueTemplate\r\n    {\r\n        public IssueTemplateNoteToneTooHigh(ICheck check)\r\n            : base(check, IssueType.Warning, \"Note's tone is too high.\")\r\n        {\r\n        }\r\n\r\n        public Issue Create(Note note) => new NoteIssue(note, this);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Checks/CheckBeatmapPageInfo.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks.Issues;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Checks;\r\n\r\npublic class CheckBeatmapPageInfo : CheckBeatmapProperty<PageInfo, Lyric>\r\n{\r\n    public const double MIN_INTERVAL = 3000;\r\n\r\n    public const double MAX_INTERVAL = 10000;\r\n\r\n    protected override string Description => \"Check invalid page in the beatmap\";\r\n\r\n    public override IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]\r\n    {\r\n        new IssueTemplateLessThanTwoPages(this),\r\n        new IssueTemplatePageIntervalTooShort(this),\r\n        new IssueTemplatePageIntervalTooLong(this),\r\n        new IssueTemplatePageIntervalShouldHaveAtLeastOneLyric(this),\r\n        new IssueTemplateLyricNotWrapIntoTime(this),\r\n    };\r\n\r\n    protected override PageInfo GetPropertyFromBeatmap(KaraokeBeatmap karaokeBeatmap)\r\n        => karaokeBeatmap.PageInfo;\r\n\r\n    protected override IEnumerable<Issue> CheckProperty(PageInfo property)\r\n    {\r\n        var pages = property.Pages;\r\n\r\n        if (pages.Count < 2)\r\n        {\r\n            yield return new IssueTemplateLessThanTwoPages(this).Create();\r\n\r\n            yield break;\r\n        }\r\n\r\n        for (int i = 1; i < pages.Count; i++)\r\n        {\r\n            var previous = pages[i - 1];\r\n            var current = pages[i];\r\n\r\n            double previousTime = previous.Time;\r\n            double currentTime = current.Time;\r\n\r\n            if (currentTime - previousTime < MIN_INTERVAL)\r\n                yield return new IssueTemplatePageIntervalTooShort(this).Create(previous, current);\r\n\r\n            if (currentTime - previousTime > MAX_INTERVAL)\r\n                yield return new IssueTemplatePageIntervalTooLong(this).Create(previous, current);\r\n        }\r\n    }\r\n\r\n    protected override IEnumerable<Issue> CheckHitObjects(PageInfo property, IReadOnlyList<Lyric> hitObject)\r\n    {\r\n        var pages = property.Pages;\r\n        if (pages.Count < 2)\r\n            yield break;\r\n\r\n        var availablePagesInObject = hitObject.ToDictionary(k => k, v => v.TimeValid ? property.GetPageAt(v.StartTime) : null);\r\n\r\n        var missingHitObjectPages = pages.Where(page => !availablePagesInObject.ContainsValue(page)).ToArray();\r\n\r\n        for (int i = 1; i < missingHitObjectPages.Length; i++)\r\n        {\r\n            var previous = missingHitObjectPages[i - 1];\r\n            var current = missingHitObjectPages[i];\r\n\r\n            yield return new IssueTemplatePageIntervalShouldHaveAtLeastOneLyric(this).Create(previous, current);\r\n        }\r\n\r\n        foreach (var lyric in availablePagesInObject.Where(x => x.Value == null).Select(x => x.Key))\r\n        {\r\n            yield return new IssueTemplateLyricNotWrapIntoTime(this).Create(lyric);\r\n        }\r\n    }\r\n\r\n    public class IssueTemplateLessThanTwoPages : IssueTemplate\r\n    {\r\n        public IssueTemplateLessThanTwoPages(ICheck check)\r\n            : base(check, IssueType.Warning, \"Should have at least two pages.\")\r\n        {\r\n        }\r\n\r\n        public Issue Create() => new(this);\r\n    }\r\n\r\n    public class IssueTemplatePageIntervalTooShort : IssueTemplate\r\n    {\r\n        public IssueTemplatePageIntervalTooShort(ICheck check)\r\n            : base(check, IssueType.Warning, \"Interval between two pages are too short.\")\r\n        {\r\n        }\r\n\r\n        public Issue Create(Page startPage, Page endPage)\r\n            => new BeatmapPageIssue(startPage, endPage, this);\r\n    }\r\n\r\n    public class IssueTemplatePageIntervalTooLong : IssueTemplate\r\n    {\r\n        public IssueTemplatePageIntervalTooLong(ICheck check)\r\n            : base(check, IssueType.Warning, \"Interval between two pages are too long.\")\r\n        {\r\n        }\r\n\r\n        public Issue Create(Page startPage, Page endPage)\r\n            => new BeatmapPageIssue(startPage, endPage, this);\r\n    }\r\n\r\n    public class IssueTemplatePageIntervalShouldHaveAtLeastOneLyric : IssueTemplate\r\n    {\r\n        public IssueTemplatePageIntervalShouldHaveAtLeastOneLyric(ICheck check)\r\n            : base(check, IssueType.Negligible, \"Should have at least one lyric between two pages.\")\r\n        {\r\n        }\r\n\r\n        public Issue Create(Page startPage, Page endPage)\r\n            => new BeatmapPageIssue(startPage, endPage, this);\r\n    }\r\n\r\n    public class IssueTemplateLyricNotWrapIntoTime : IssueTemplate\r\n    {\r\n        public IssueTemplateLyricNotWrapIntoTime(ICheck check)\r\n            : base(check, IssueType.Negligible, \"Lyric is not wrap by the page.\")\r\n        {\r\n        }\r\n\r\n        public Issue Create(Lyric lyric)\r\n            => new LyricIssue(lyric, this);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Checks/CheckBeatmapProperty.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Game.Rulesets.Edit;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Checks;\r\n\r\npublic abstract class CheckBeatmapProperty<TProperty, THitObject> : ICheck where THitObject : KaraokeHitObject\r\n{\r\n    public CheckMetadata Metadata => new(CheckCategory.Metadata, Description);\r\n\r\n    protected abstract string Description { get; }\r\n\r\n    public abstract IEnumerable<IssueTemplate> PossibleTemplates { get; }\r\n\r\n    public IEnumerable<Issue> Run(BeatmapVerifierContext context)\r\n    {\r\n        var beatmap = getBeatmap(context);\r\n        var property = GetPropertyFromBeatmap(beatmap);\r\n        if (property == null)\r\n            return Array.Empty<Issue>();\r\n\r\n        var issues = CheckProperty(property);\r\n        var hitObjects = context.CurrentDifficulty.Playable.HitObjects;\r\n\r\n        var hitObjectIssues = hitObjects.OfType<THitObject>().SelectMany(x => CheckHitObject(property, x));\r\n        var hitObjectsIssues = CheckHitObjects(property, hitObjects.OfType<THitObject>().ToList());\r\n\r\n        return issues.Concat(hitObjectIssues).Concat(hitObjectsIssues);\r\n    }\r\n\r\n    protected abstract TProperty? GetPropertyFromBeatmap(KaraokeBeatmap karaokeBeatmap);\r\n\r\n    protected virtual IEnumerable<Issue> CheckProperty(TProperty property)\r\n    {\r\n        yield break;\r\n    }\r\n\r\n    protected virtual IEnumerable<Issue> CheckHitObject(TProperty property, THitObject hitObject)\r\n    {\r\n        yield break;\r\n    }\r\n\r\n    protected virtual IEnumerable<Issue> CheckHitObjects(TProperty property, IReadOnlyList<THitObject> hitObject)\r\n    {\r\n        yield break;\r\n    }\r\n\r\n    private static KaraokeBeatmap getBeatmap(BeatmapVerifierContext context)\r\n    {\r\n        // follow the usage in the IssueList in osu.Game\r\n        if (context.CurrentDifficulty.Playable is EditorBeatmap editorBeatmap)\r\n            return EditorBeatmapUtils.GetPlayableBeatmap(editorBeatmap);\r\n\r\n        throw new InvalidOperationException();\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Checks/CheckClassicStageInfo.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks.Issues;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos.Classic;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Checks;\r\n\r\npublic class CheckClassicStageInfo : CheckStageInfo<ClassicStageInfo>\r\n{\r\n    public const float MIN_ROW_HEIGHT = 30;\r\n    public const float MAX_ROW_HEIGHT = 200;\r\n\r\n    public const int MIN_LINE_SIZE = 0;\r\n    public const int MAX_LINE_SIZE = 4;\r\n\r\n    public const double MIN_TIMING_INTERVAL = 3000;\r\n    public const double MAX_TIMING_INTERVAL = 10000;\r\n\r\n    protected override string Description => \"Check invalid info in the classic stage info.\";\r\n\r\n    public override IEnumerable<IssueTemplate> CustomTemplates => new IssueTemplate[]\r\n    {\r\n        new IssueTemplateInvalidRowHeight(this),\r\n        new IssueTemplateLessThanTwoTimingPoints(this),\r\n        new IssueTemplateTimingIntervalTooShort(this),\r\n        new IssueTemplateTimingIntervalTooLong(this),\r\n        new IssueTemplateTimingInfoHitObjectNotExist(this),\r\n        new IssueTemplateTimingInfoMappingHasNoTiming(this),\r\n        new IssueTemplateTimingInfoTimingNotExist(this),\r\n        new IssueTemplateTimingInfoLyricNotHaveTwoTiming(this),\r\n        new IssueTemplateLyricLayoutInvalidLineNumber(this),\r\n    };\r\n\r\n    public CheckClassicStageInfo()\r\n    {\r\n        RegisterCategory(x => x.StyleCategory, 0);\r\n        RegisterCategory(x => x.LyricLayoutCategory, 2);\r\n    }\r\n\r\n    public override IEnumerable<Issue> CheckStageInfoWithHitObjects(ClassicStageInfo stageInfo, IReadOnlyList<KaraokeHitObject> hitObjects)\r\n    {\r\n        var issues = new List<Issue>();\r\n\r\n        issues.AddRange(checkLyricLayoutDefinition(stageInfo.StageDefinition));\r\n        issues.AddRange(checkLyricTimingInfo(stageInfo.LyricTimingInfo, hitObjects.OfType<Lyric>().ToArray()));\r\n\r\n        return issues;\r\n    }\r\n\r\n    private IEnumerable<Issue> checkLyricLayoutDefinition(ClassicStageDefinition layoutDefinition)\r\n    {\r\n        if (layoutDefinition.LineHeight is < MIN_ROW_HEIGHT or > MAX_ROW_HEIGHT)\r\n            yield return new IssueTemplateInvalidRowHeight(this).Create();\r\n    }\r\n\r\n    private IEnumerable<Issue> checkLyricTimingInfo(ClassicLyricTimingInfo timingInfo, IReadOnlyList<Lyric> hitObjects)\r\n    {\r\n        var timings = timingInfo.Timings;\r\n        var mappings = timingInfo.Mappings;\r\n\r\n        if (timings.Count < 2)\r\n        {\r\n            yield return new IssueTemplateLessThanTwoTimingPoints(this).Create();\r\n\r\n            yield break;\r\n        }\r\n\r\n        // check timing interval.\r\n        for (int i = 1; i < timings.Count; i++)\r\n        {\r\n            var previous = timings[i - 1];\r\n            var current = timings[i];\r\n\r\n            double previousTime = previous.Time;\r\n            double currentTime = current.Time;\r\n\r\n            if (currentTime - previousTime < MIN_TIMING_INTERVAL)\r\n                yield return new IssueTemplateTimingIntervalTooShort(this).Create(previous, current);\r\n\r\n            if (currentTime - previousTime > MAX_TIMING_INTERVAL)\r\n                yield return new IssueTemplateTimingIntervalTooLong(this).Create(previous, current);\r\n        }\r\n\r\n        // check have non-matched ids.\r\n        foreach (var mapping in mappings)\r\n        {\r\n            // mapping lyric should be exist.\r\n            if (hitObjects.All(x => x.ID != mapping.Key))\r\n                yield return new IssueTemplateTimingInfoHitObjectNotExist(this).Create();\r\n\r\n            // mapping timing should be exist.\r\n            if (mapping.Value.Length == 0)\r\n                yield return new IssueTemplateTimingInfoMappingHasNoTiming(this).Create();\r\n\r\n            // mapping timing should be exist.\r\n            if (mapping.Value.Length != 0 && timings.All(x => !mapping.Value.Contains(x.ID)))\r\n                yield return new IssueTemplateTimingInfoTimingNotExist(this).Create();\r\n        }\r\n\r\n        // check mapping roles.\r\n        foreach (var hitObject in hitObjects)\r\n        {\r\n            int timingAmounts = timingInfo.GetLyricTimingPoints(hitObject).Count();\r\n\r\n            // should have exactly 2 matched timing point in the lyric.\r\n            if (timingAmounts != 0 && timingAmounts != 2)\r\n                yield return new IssueTemplateTimingInfoLyricNotHaveTwoTiming(this).Create();\r\n        }\r\n    }\r\n\r\n    protected override IEnumerable<Issue> CheckElement<TStageElement>(TStageElement element)\r\n    {\r\n        switch (element)\r\n        {\r\n            case ClassicLyricLayout classicLyricLayout:\r\n                if (classicLyricLayout.Line is < MIN_LINE_SIZE or > MAX_LINE_SIZE)\r\n                    yield return new IssueTemplateLyricLayoutInvalidLineNumber(this).Create();\r\n\r\n                break;\r\n\r\n            case ClassicStyle:\r\n                // todo: might need to check if skin resource is exist?\r\n                break;\r\n\r\n            default:\r\n                throw new InvalidOperationException(\"Unknown stage element type.\");\r\n        }\r\n    }\r\n\r\n    public class IssueTemplateInvalidRowHeight : IssueTemplate\r\n    {\r\n        public IssueTemplateInvalidRowHeight(ICheck check)\r\n            : base(check, IssueType.Warning, $\"Row height should be in the range of {MIN_ROW_HEIGHT} and {MAX_ROW_HEIGHT}.\")\r\n        {\r\n        }\r\n\r\n        public Issue Create() => new(this);\r\n    }\r\n\r\n    #region timing info\r\n\r\n    public class IssueTemplateLessThanTwoTimingPoints : IssueTemplate\r\n    {\r\n        public IssueTemplateLessThanTwoTimingPoints(ICheck check)\r\n            : base(check, IssueType.Warning, \"Should have at least two timing points.\")\r\n        {\r\n        }\r\n\r\n        public Issue Create() => new(this);\r\n    }\r\n\r\n    public class IssueTemplateTimingIntervalTooShort : IssueTemplate\r\n    {\r\n        public IssueTemplateTimingIntervalTooShort(ICheck check)\r\n            : base(check, IssueType.Warning, \"Interval between two timing points are too short.\")\r\n        {\r\n        }\r\n\r\n        public Issue Create(ClassicLyricTimingPoint startTimingPoint, ClassicLyricTimingPoint endTimingPoint)\r\n            => new BeatmapClassicLyricTimingPointIssue(startTimingPoint, endTimingPoint, this);\r\n    }\r\n\r\n    public class IssueTemplateTimingIntervalTooLong : IssueTemplate\r\n    {\r\n        public IssueTemplateTimingIntervalTooLong(ICheck check)\r\n            : base(check, IssueType.Warning, \"Interval between two timing points are too long.\")\r\n        {\r\n        }\r\n\r\n        public Issue Create(ClassicLyricTimingPoint startTimingPoint, ClassicLyricTimingPoint endTimingPoint)\r\n            => new BeatmapClassicLyricTimingPointIssue(startTimingPoint, endTimingPoint, this);\r\n    }\r\n\r\n    public class IssueTemplateTimingInfoHitObjectNotExist : IssueTemplate\r\n    {\r\n        public IssueTemplateTimingInfoHitObjectNotExist(ICheck check)\r\n            : base(check, IssueType.Warning, \"Maybe caused by hit-object has been deleted. Don't worry, go to the stage editor and will be easy to fix them.\")\r\n        {\r\n        }\r\n\r\n        public Issue Create() => new(this);\r\n    }\r\n\r\n    public class IssueTemplateTimingInfoMappingHasNoTiming : IssueTemplate\r\n    {\r\n        public IssueTemplateTimingInfoMappingHasNoTiming(ICheck check)\r\n            : base(check, IssueType.Error, \"Mapping should have the timing in the value. Should be the internal error.\")\r\n        {\r\n        }\r\n\r\n        public Issue Create() => new(this);\r\n    }\r\n\r\n    public class IssueTemplateTimingInfoTimingNotExist : IssueTemplate\r\n    {\r\n        public IssueTemplateTimingInfoTimingNotExist(ICheck check)\r\n            : base(check, IssueType.Error, \"It's caused by stage element has been deleted, but still remain the mapping data.\")\r\n        {\r\n        }\r\n\r\n        public Issue Create() => new(this);\r\n    }\r\n\r\n    public class IssueTemplateTimingInfoLyricNotHaveTwoTiming : IssueTemplate\r\n    {\r\n        public IssueTemplateTimingInfoLyricNotHaveTwoTiming(ICheck check)\r\n            : base(check, IssueType.Warning, \"Lyric should have exactly two timing. One is for start time and another one is for end time.\")\r\n        {\r\n        }\r\n\r\n        public Issue Create() => new(this);\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region element\r\n\r\n    public class IssueTemplateLyricLayoutInvalidLineNumber : IssueTemplate\r\n    {\r\n        public IssueTemplateLyricLayoutInvalidLineNumber(ICheck check)\r\n            : base(check, IssueType.Warning, $\"Line number should be in the range of {MIN_LINE_SIZE} and {MAX_LINE_SIZE}.\")\r\n        {\r\n        }\r\n\r\n        public Issue Create() => new(this);\r\n    }\r\n\r\n    #endregion\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Checks/CheckHitObjectProperty.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Game.Rulesets.Edit;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Checks;\r\n\r\npublic abstract class CheckHitObjectProperty<THitObject> : ICheck where THitObject : KaraokeHitObject\r\n{\r\n    public CheckMetadata Metadata => new(CheckCategory.HitObjects, Description);\r\n\r\n    protected abstract string Description { get; }\r\n\r\n    public abstract IEnumerable<IssueTemplate> PossibleTemplates { get; }\r\n\r\n    public virtual IEnumerable<Issue> Run(BeatmapVerifierContext context)\r\n    {\r\n        var hitObjects = context.CurrentDifficulty.Playable.HitObjects.OfType<THitObject>();\r\n\r\n        return hitObjects.Select(Check).SelectMany(x => x);\r\n    }\r\n\r\n    protected abstract IEnumerable<Issue> Check(THitObject hitObject);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Checks/CheckHitObjectReferenceProperty.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Game.Rulesets.Edit;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Checks;\r\n\r\npublic abstract class CheckHitObjectReferenceProperty<THitObject, TReferencedHitObject> : CheckHitObjectProperty<THitObject>\r\n    where THitObject : KaraokeHitObject\r\n    where TReferencedHitObject : KaraokeHitObject\r\n{\r\n    public override IEnumerable<Issue> Run(BeatmapVerifierContext context)\r\n    {\r\n        var hitObjects = context.CurrentDifficulty.Playable.HitObjects.OfType<THitObject>();\r\n        var allAvailableReferencedHitObjects = context.CurrentDifficulty.Playable.HitObjects.OfType<TReferencedHitObject>().ToArray();\r\n\r\n        var issues = base.Run(context);\r\n        var referenceIssues = hitObjects.Select(x => CheckReferenceProperty(x, allAvailableReferencedHitObjects)).SelectMany(x => x);\r\n\r\n        return issues.Concat(referenceIssues);\r\n    }\r\n\r\n    protected sealed override IEnumerable<Issue> Check(THitObject hitObject)\r\n    {\r\n        yield break;\r\n    }\r\n\r\n    protected abstract IEnumerable<Issue> CheckReferenceProperty(THitObject hitObject, IEnumerable<TReferencedHitObject> allAvailableReferencedHitObjects);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Checks/CheckLyricLanguage.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks.Issues;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Checks;\r\n\r\npublic class CheckLyricLanguage : CheckHitObjectProperty<Lyric>\r\n{\r\n    protected override string Description => \"Lyric with invalid language.\";\r\n\r\n    public override IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]\r\n    {\r\n        new IssueTemplateNotFill(this),\r\n    };\r\n\r\n    protected override IEnumerable<Issue> Check(Lyric lyric)\r\n    {\r\n        if (lyric.Language == null)\r\n            yield return new IssueTemplateNotFill(this).Create(lyric);\r\n    }\r\n\r\n    public class IssueTemplateNotFill : IssueTemplate\r\n    {\r\n        public IssueTemplateNotFill(ICheck check)\r\n            : base(check, IssueType.Problem, \"Lyric must have assign language.\")\r\n        {\r\n        }\r\n\r\n        public Issue Create(Lyric lyric)\r\n            => new LyricIssue(lyric, this);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Checks/CheckLyricReferenceLyric.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks.Issues;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Checks;\r\n\r\npublic class CheckLyricReferenceLyric : CheckHitObjectReferenceProperty<Lyric, Lyric>\r\n{\r\n    protected override string Description => \"Lyric with invalid reference lyric.\";\r\n\r\n    public override IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]\r\n    {\r\n        new IssueTemplateSelfReference(this),\r\n        new IssueTemplateInvalidReferenceLyric(this),\r\n        new IssueTemplateNullReferenceLyricConfig(this),\r\n        new IssueTemplateHasReferenceLyricConfigWhenNoReferenceLyric(this),\r\n    };\r\n\r\n    protected override IEnumerable<Issue> CheckReferenceProperty(Lyric lyric, IEnumerable<Lyric> allAvailableReferencedHitObjects)\r\n    {\r\n        if (lyric.ReferenceLyric == lyric)\r\n            yield return new IssueTemplateSelfReference(this).Create(lyric);\r\n\r\n        if (lyric.ReferenceLyric != null && !allAvailableReferencedHitObjects.Contains(lyric.ReferenceLyric))\r\n            yield return new IssueTemplateInvalidReferenceLyric(this).Create(lyric);\r\n\r\n        if (lyric.ReferenceLyric != null && lyric.ReferenceLyricConfig == null)\r\n            yield return new IssueTemplateNullReferenceLyricConfig(this).Create(lyric);\r\n\r\n        if (lyric.ReferenceLyric == null && lyric.ReferenceLyricConfig != null)\r\n            yield return new IssueTemplateHasReferenceLyricConfigWhenNoReferenceLyric(this).Create(lyric);\r\n    }\r\n\r\n    public class IssueTemplateSelfReference : IssueTemplate\r\n    {\r\n        public IssueTemplateSelfReference(ICheck check)\r\n            : base(check, IssueType.Error, \"Lyric should not reference to itself.\")\r\n        {\r\n        }\r\n\r\n        public Issue Create(Lyric lyric)\r\n            => new LyricIssue(lyric, this);\r\n    }\r\n\r\n    public class IssueTemplateInvalidReferenceLyric : IssueTemplate\r\n    {\r\n        public IssueTemplateInvalidReferenceLyric(ICheck check)\r\n            : base(check, IssueType.Error, \"Reference lyric does not exist in the beatmap.\")\r\n        {\r\n        }\r\n\r\n        public Issue Create(Lyric lyric)\r\n            => new LyricIssue(lyric, this);\r\n    }\r\n\r\n    public class IssueTemplateNullReferenceLyricConfig : IssueTemplate\r\n    {\r\n        public IssueTemplateNullReferenceLyricConfig(ICheck check)\r\n            : base(check, IssueType.Error, \"Must have config if reference to another lyric.\")\r\n        {\r\n        }\r\n\r\n        public Issue Create(Lyric lyric)\r\n            => new LyricIssue(lyric, this);\r\n    }\r\n\r\n    public class IssueTemplateHasReferenceLyricConfigWhenNoReferenceLyric : IssueTemplate\r\n    {\r\n        public IssueTemplateHasReferenceLyricConfigWhenNoReferenceLyric(ICheck check)\r\n            : base(check, IssueType.Error, \"Should not have the reference lyric config if reference to another lyric.\")\r\n        {\r\n        }\r\n\r\n        public Issue Create(Lyric lyric)\r\n            => new LyricIssue(lyric, this);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Checks/CheckLyricRubyTag.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks.Issues;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Checks;\r\n\r\npublic class CheckLyricRubyTag : CheckHitObjectProperty<Lyric>\r\n{\r\n    protected override string Description => \"Lyric with invalid ruby tag.\";\r\n\r\n    public override IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]\r\n    {\r\n        new IssueTemplateOutOfRange(this),\r\n        new IssueTemplateOverlapping(this),\r\n        new IssueTemplateEmptyText(this),\r\n    };\r\n\r\n    protected override IEnumerable<Issue> Check(Lyric lyric)\r\n    {\r\n        string text = lyric.Text;\r\n        var rubyTags = lyric.RubyTags;\r\n\r\n        const RubyTagsUtils.Sorting sorting = RubyTagsUtils.Sorting.Asc;\r\n\r\n        var outOfRangeTags = RubyTagsUtils.FindOutOfRange(rubyTags, text);\r\n        var overlappingTags = RubyTagsUtils.FindOverlapping(rubyTags, sorting);\r\n        var emptyTags = RubyTagsUtils.FindEmptyText(rubyTags);\r\n\r\n        foreach (var rubyTag in outOfRangeTags)\r\n        {\r\n            yield return new IssueTemplateOutOfRange(this).Create(lyric, rubyTag);\r\n        }\r\n\r\n        foreach (var rubyTag in overlappingTags)\r\n        {\r\n            yield return new IssueTemplateOverlapping(this).Create(lyric, rubyTag);\r\n        }\r\n\r\n        foreach (var rubyTag in emptyTags)\r\n        {\r\n            yield return new IssueTemplateEmptyText(this).Create(lyric, rubyTag);\r\n        }\r\n    }\r\n\r\n    public abstract class IssueTemplateLyricRuby : IssueTemplate\r\n    {\r\n        protected IssueTemplateLyricRuby(ICheck check, IssueType type, string unformattedMessage)\r\n            : base(check, type, unformattedMessage)\r\n        {\r\n        }\r\n\r\n        public Issue Create(Lyric lyric, RubyTag rubyTag) => new LyricRubyTagIssue(lyric, this, rubyTag, rubyTag);\r\n    }\r\n\r\n    public class IssueTemplateOutOfRange : IssueTemplateLyricRuby\r\n    {\r\n        public IssueTemplateOutOfRange(ICheck check)\r\n            : base(check, IssueType.Error, \"Ruby tag index is out of range.\")\r\n        {\r\n        }\r\n    }\r\n\r\n    public class IssueTemplateOverlapping : IssueTemplateLyricRuby\r\n    {\r\n        public IssueTemplateOverlapping(ICheck check)\r\n            : base(check, IssueType.Problem, \"Ruby tag index is overlapping to another ruby tag.\")\r\n        {\r\n        }\r\n    }\r\n\r\n    public class IssueTemplateEmptyText : IssueTemplateLyricRuby\r\n    {\r\n        public IssueTemplateEmptyText(ICheck check)\r\n            : base(check, IssueType.Problem, \"Ruby tag's text should not be empty or white-space only.\")\r\n        {\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Checks/CheckLyricSinger.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks.Issues;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Checks;\r\n\r\npublic class CheckLyricSinger : CheckHitObjectProperty<Lyric>\r\n{\r\n    protected override string Description => \"Lyric with invalid singer.\";\r\n\r\n    public override IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]\r\n    {\r\n        new IssueTemplateNoSinger(this),\r\n    };\r\n\r\n    protected override IEnumerable<Issue> Check(Lyric lyric)\r\n    {\r\n        if (!lyric.SingerIds.Any())\r\n            yield return new IssueTemplateNoSinger(this).Create(lyric);\r\n    }\r\n\r\n    public class IssueTemplateNoSinger : IssueTemplate\r\n    {\r\n        public IssueTemplateNoSinger(ICheck check)\r\n            : base(check, IssueType.Problem, \"Lyric must have at least one singer.\")\r\n        {\r\n        }\r\n\r\n        public Issue Create(Lyric lyric)\r\n            => new LyricIssue(lyric, this);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Checks/CheckLyricText.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks.Issues;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Checks;\r\n\r\npublic class CheckLyricText : CheckHitObjectProperty<Lyric>\r\n{\r\n    protected override string Description => \"Lyric with invalid text.\";\r\n\r\n    public override IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]\r\n    {\r\n        new IssueTemplateEmptyText(this),\r\n    };\r\n\r\n    protected override IEnumerable<Issue> Check(Lyric lyric)\r\n    {\r\n        if (string.IsNullOrWhiteSpace(lyric.Text))\r\n            yield return new IssueTemplateEmptyText(this).Create(lyric);\r\n    }\r\n\r\n    public class IssueTemplateEmptyText : IssueTemplate\r\n    {\r\n        public IssueTemplateEmptyText(ICheck check)\r\n            : base(check, IssueType.Problem, \"Lyric's text should not be empty or white-space only.\")\r\n        {\r\n        }\r\n\r\n        public Issue Create(Lyric lyric)\r\n            => new LyricIssue(lyric, this);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Checks/CheckLyricTimeTag.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks.Issues;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Checks;\r\n\r\npublic class CheckLyricTimeTag : CheckHitObjectProperty<Lyric>\r\n{\r\n    protected override string Description => \"Lyric with invalid time-tag.\";\r\n\r\n    public override IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]\r\n    {\r\n        new IssueTemplateEmpty(this),\r\n        new IssueTemplateMissingStart(this),\r\n        new IssueTemplateMissingEnd(this),\r\n        new IssueTemplateOutOfRange(this),\r\n        new IssueTemplateOverlapping(this),\r\n        new IssueTemplateEmptyTime(this),\r\n        new IssueTemplateInvalidRomanisedSyllable(this),\r\n        new IssueTemplateShouldFillRomanisedSyllable(this),\r\n        new IssueTemplateShouldNotFillRomanisedSyllable(this),\r\n        new IssueTemplateShouldNotMarkFirstSyllable(this),\r\n    };\r\n\r\n    protected override IEnumerable<Issue> Check(Lyric lyric)\r\n    {\r\n        var issues = new List<Issue>();\r\n        issues.AddRange(CheckTimeTag(lyric));\r\n        issues.AddRange(CheckTimeTagRomanisedSyllable(lyric));\r\n        return issues;\r\n    }\r\n\r\n    protected IEnumerable<Issue> CheckTimeTag(Lyric lyric)\r\n    {\r\n        if (!lyric.TimeTags.Any())\r\n        {\r\n            yield return new IssueTemplateEmpty(this).Create(lyric);\r\n\r\n            yield break;\r\n        }\r\n\r\n        if (!TimeTagsUtils.HasStartTimeTagInLyric(lyric.TimeTags, lyric.Text))\r\n            yield return new IssueTemplateMissingStart(this).Create(lyric);\r\n\r\n        if (!TimeTagsUtils.HasEndTimeTagInLyric(lyric.TimeTags, lyric.Text))\r\n            yield return new IssueTemplateMissingEnd(this).Create(lyric);\r\n\r\n        // todo: maybe config?\r\n        const GroupCheck group_check = GroupCheck.Asc;\r\n        const SelfCheck self_check = SelfCheck.BasedOnStart;\r\n\r\n        var outOfRangeTags = TimeTagsUtils.FindOutOfRange(lyric.TimeTags, lyric.Text);\r\n        var overlappingTimeTags = TimeTagsUtils.FindOverlapping(lyric.TimeTags, group_check, self_check).ToArray();\r\n        var noTimeTimeTags = TimeTagsUtils.FindNoneTime(lyric.TimeTags);\r\n\r\n        foreach (var timeTag in outOfRangeTags)\r\n        {\r\n            yield return new IssueTemplateOutOfRange(this).Create(lyric, timeTag);\r\n        }\r\n\r\n        foreach (var timeTag in overlappingTimeTags)\r\n        {\r\n            yield return new IssueTemplateOverlapping(this).Create(lyric, timeTag);\r\n        }\r\n\r\n        foreach (var timeTag in noTimeTimeTags)\r\n        {\r\n            yield return new IssueTemplateEmptyTime(this).Create(lyric, timeTag);\r\n        }\r\n    }\r\n\r\n    protected IEnumerable<Issue> CheckTimeTagRomanisedSyllable(Lyric lyric)\r\n    {\r\n        if (!lyric.TimeTags.Any())\r\n        {\r\n            yield break;\r\n        }\r\n\r\n        foreach (var timeTag in lyric.TimeTags)\r\n        {\r\n            bool firstSyllable = timeTag.FirstSyllable;\r\n            string? romanisedSyllable = timeTag.RomanisedSyllable;\r\n\r\n            switch (timeTag.Index.State)\r\n            {\r\n                case TextIndex.IndexState.Start:\r\n                    // if input the romanised syllable, should be valid.\r\n                    if (romanisedSyllable != null && !isRomanisedSyllableValid(romanisedSyllable))\r\n                        yield return new IssueTemplateInvalidRomanisedSyllable(this).Create(lyric, timeTag);\r\n\r\n                    // if is first romanised syllable, should not be null.\r\n                    if (firstSyllable && romanisedSyllable == null)\r\n                        yield return new IssueTemplateShouldFillRomanisedSyllable(this).Create(lyric, timeTag);\r\n\r\n                    break;\r\n\r\n                case TextIndex.IndexState.End:\r\n                    if (romanisedSyllable != null)\r\n                        yield return new IssueTemplateShouldNotFillRomanisedSyllable(this).Create(lyric, timeTag);\r\n\r\n                    if (firstSyllable)\r\n                        yield return new IssueTemplateShouldNotMarkFirstSyllable(this).Create(lyric, timeTag);\r\n\r\n                    break;\r\n\r\n                default:\r\n                    throw new ArgumentOutOfRangeException();\r\n            }\r\n        }\r\n\r\n        yield break;\r\n\r\n        static bool isRomanisedSyllableValid(string text)\r\n        {\r\n            // should not be white-space only.\r\n            if (string.IsNullOrWhiteSpace(text))\r\n                return false;\r\n\r\n            // should be all latin text or white-space\r\n            return text.All(c => CharUtils.IsLatin(c) || CharUtils.IsSpacing(c));\r\n        }\r\n    }\r\n\r\n    public class IssueTemplateEmpty : IssueTemplate\r\n    {\r\n        public IssueTemplateEmpty(ICheck check)\r\n            : base(check, IssueType.Problem, \"This lyric has no time-tag.\")\r\n        {\r\n        }\r\n\r\n        public Issue Create(Lyric lyric) => new LyricIssue(lyric, this);\r\n    }\r\n\r\n    public class IssueTemplateMissingStart : IssueTemplate\r\n    {\r\n        public IssueTemplateMissingStart(ICheck check)\r\n            : base(check, IssueType.Problem, \"Missing first time-tag in the lyric.\")\r\n        {\r\n        }\r\n\r\n        public Issue Create(Lyric lyric) => new LyricIssue(lyric, this);\r\n    }\r\n\r\n    public class IssueTemplateMissingEnd : IssueTemplate\r\n    {\r\n        public IssueTemplateMissingEnd(ICheck check)\r\n            : base(check, IssueType.Problem, \"Missing last time-tag in the lyric.\")\r\n        {\r\n        }\r\n\r\n        public Issue Create(Lyric lyric) => new LyricIssue(lyric, this);\r\n    }\r\n\r\n    public abstract class IssueTemplateLyricTimeTag : IssueTemplate\r\n    {\r\n        protected IssueTemplateLyricTimeTag(ICheck check, IssueType type, string unformattedMessage)\r\n            : base(check, type, unformattedMessage)\r\n        {\r\n        }\r\n\r\n        public Issue Create(Lyric lyric, TimeTag timeTag) => new LyricTimeTagIssue(lyric, this, timeTag, timeTag);\r\n    }\r\n\r\n    public class IssueTemplateOutOfRange : IssueTemplateLyricTimeTag\r\n    {\r\n        public IssueTemplateOutOfRange(ICheck check)\r\n            : base(check, IssueType.Problem, \"Time-tag index is out of range.\")\r\n        {\r\n        }\r\n    }\r\n\r\n    public class IssueTemplateOverlapping : IssueTemplateLyricTimeTag\r\n    {\r\n        public IssueTemplateOverlapping(ICheck check)\r\n            : base(check, IssueType.Problem, \"Time-tag index is overlapping to another time-tag.\")\r\n        {\r\n        }\r\n    }\r\n\r\n    public class IssueTemplateEmptyTime : IssueTemplateLyricTimeTag\r\n    {\r\n        public IssueTemplateEmptyTime(ICheck check)\r\n            : base(check, IssueType.Problem, \"Time-tag has no time.\")\r\n        {\r\n        }\r\n    }\r\n\r\n    public class IssueTemplateInvalidRomanisedSyllable : IssueTemplateLyricTimeTag\r\n    {\r\n        public IssueTemplateInvalidRomanisedSyllable(ICheck check)\r\n            : base(check, IssueType.Problem, \"Romanised syllable should not be empty or white-space only.\")\r\n        {\r\n        }\r\n    }\r\n\r\n    public class IssueTemplateShouldFillRomanisedSyllable : IssueTemplateLyricTimeTag\r\n    {\r\n        public IssueTemplateShouldFillRomanisedSyllable(ICheck check)\r\n            : base(check, IssueType.Problem, \"Romanised syllable should not be empty or white-space if in the first time-tag.\")\r\n        {\r\n        }\r\n    }\r\n\r\n    public class IssueTemplateShouldNotFillRomanisedSyllable : IssueTemplateLyricTimeTag\r\n    {\r\n        public IssueTemplateShouldNotFillRomanisedSyllable(ICheck check)\r\n            : base(check, IssueType.Error, \"Should not have empty romanised syllable if time-tag is end.\")\r\n        {\r\n        }\r\n    }\r\n\r\n    public class IssueTemplateShouldNotMarkFirstSyllable : IssueTemplateLyricTimeTag\r\n    {\r\n        public IssueTemplateShouldNotMarkFirstSyllable(ICheck check)\r\n            : base(check, IssueType.Error, \"Should not have empty romanised syllable if time-tag is end.\")\r\n        {\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Checks/CheckLyricTranslations.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Globalization;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks.Issues;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Checks;\r\n\r\npublic class CheckLyricTranslations : CheckHitObjectProperty<Lyric>\r\n{\r\n    protected override string Description => \"Lyric with invalid translations.\";\r\n\r\n    public override IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]\r\n    {\r\n        new IssueTemplateEmptyText(this),\r\n    };\r\n\r\n    protected override IEnumerable<Issue> Check(Lyric lyric)\r\n    {\r\n        var translations = lyric.Translations;\r\n\r\n        foreach ((var language, string translation) in translations)\r\n        {\r\n            if (string.IsNullOrWhiteSpace(translation))\r\n                yield return new IssueTemplateEmptyText(this).Create(lyric, language);\r\n        }\r\n    }\r\n\r\n    public class IssueTemplateEmptyText : IssueTemplate\r\n    {\r\n        public IssueTemplateEmptyText(ICheck check)\r\n            : base(check, IssueType.Problem, \"Translation in the lyric should not by empty or white-space only.\")\r\n        {\r\n        }\r\n\r\n        public Issue Create(Lyric lyric, CultureInfo language)\r\n            => new LyricIssue(lyric, this, language);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Checks/CheckNoteReferenceLyric.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks.Issues;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Checks;\r\n\r\npublic class CheckNoteReferenceLyric : CheckHitObjectReferenceProperty<Note, Lyric>\r\n{\r\n    protected override string Description => \"Note with invalid reference lyric.\";\r\n\r\n    public override IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]\r\n    {\r\n        new IssueTemplateNullReferenceLyric(this),\r\n        new IssueTemplateInvalidReferenceLyric(this),\r\n        new IssueTemplateMissingReferenceTimeTag(this),\r\n        new IssueTemplateMissingStartReferenceTimeTag(this),\r\n        new IssueTemplateStartReferenceTimeTagMissingTime(this),\r\n        new IssueTemplateMissingEndReferenceTimeTag(this),\r\n        new IssueTemplateEndReferenceTimeTagMissingTime(this),\r\n    };\r\n\r\n    protected override IEnumerable<Issue> CheckReferenceProperty(Note note, IEnumerable<Lyric> allAvailableReferencedHitObjects)\r\n    {\r\n        if (note.ReferenceLyric == null)\r\n        {\r\n            yield return new IssueTemplateNullReferenceLyric(this).Create(note);\r\n\r\n            yield break;\r\n        }\r\n\r\n        if (note.ReferenceLyric != null && !allAvailableReferencedHitObjects.Contains(note.ReferenceLyric))\r\n            yield return new IssueTemplateInvalidReferenceLyric(this).Create(note);\r\n\r\n        var startTimeTag = note.StartReferenceTimeTag;\r\n        var endTimeTag = note.EndReferenceTimeTag;\r\n\r\n        if (startTimeTag == null && endTimeTag == null)\r\n        {\r\n            yield return new IssueTemplateMissingReferenceTimeTag(this).Create(note);\r\n\r\n            yield break;\r\n        }\r\n\r\n        if (startTimeTag == null)\r\n            yield return new IssueTemplateMissingStartReferenceTimeTag(this).Create(note);\r\n\r\n        if (startTimeTag != null && startTimeTag.Time == null)\r\n            yield return new IssueTemplateStartReferenceTimeTagMissingTime(this).Create(note);\r\n\r\n        if (endTimeTag == null)\r\n            yield return new IssueTemplateMissingEndReferenceTimeTag(this).Create(note);\r\n\r\n        if (endTimeTag != null && endTimeTag.Time == null)\r\n            yield return new IssueTemplateEndReferenceTimeTagMissingTime(this).Create(note);\r\n    }\r\n\r\n    public class IssueTemplateNullReferenceLyric : IssueTemplate\r\n    {\r\n        public IssueTemplateNullReferenceLyric(ICheck check)\r\n            : base(check, IssueType.Error, \"Note must have its parent lyric.\")\r\n        {\r\n        }\r\n\r\n        public Issue Create(Note note)\r\n            => new NoteIssue(note, this);\r\n    }\r\n\r\n    public class IssueTemplateInvalidReferenceLyric : IssueTemplate\r\n    {\r\n        public IssueTemplateInvalidReferenceLyric(ICheck check)\r\n            : base(check, IssueType.Error, \"Note's reference lyric must in the beatmap.\")\r\n        {\r\n        }\r\n\r\n        public Issue Create(Note note)\r\n            => new NoteIssue(note, this);\r\n    }\r\n\r\n    public class IssueTemplateMissingReferenceTimeTag : IssueTemplate\r\n    {\r\n        public IssueTemplateMissingReferenceTimeTag(ICheck check)\r\n            : base(check, IssueType.Problem, \"Note's reference time-tag is missing.\")\r\n        {\r\n        }\r\n\r\n        public Issue Create(Note note)\r\n            => new NoteIssue(note, this);\r\n    }\r\n\r\n    public class IssueTemplateMissingStartReferenceTimeTag : IssueTemplate\r\n    {\r\n        public IssueTemplateMissingStartReferenceTimeTag(ICheck check)\r\n            : base(check, IssueType.Problem, \"Note's start reference time-tag is missing.\")\r\n        {\r\n        }\r\n\r\n        public Issue Create(Note note)\r\n            => new NoteIssue(note, this);\r\n    }\r\n\r\n    public class IssueTemplateStartReferenceTimeTagMissingTime : IssueTemplate\r\n    {\r\n        public IssueTemplateStartReferenceTimeTagMissingTime(ICheck check)\r\n            : base(check, IssueType.Problem, \"Note's start reference time-tag is found but missing time.\")\r\n        {\r\n        }\r\n\r\n        public Issue Create(Note note)\r\n            => new NoteIssue(note, this);\r\n    }\r\n\r\n    public class IssueTemplateMissingEndReferenceTimeTag : IssueTemplate\r\n    {\r\n        public IssueTemplateMissingEndReferenceTimeTag(ICheck check)\r\n            : base(check, IssueType.Problem, \"Note's end reference time-tag is missing.\")\r\n        {\r\n        }\r\n\r\n        public Issue Create(Note note)\r\n            => new NoteIssue(note, this);\r\n    }\r\n\r\n    public class IssueTemplateEndReferenceTimeTagMissingTime : IssueTemplate\r\n    {\r\n        public IssueTemplateEndReferenceTimeTagMissingTime(ICheck check)\r\n            : base(check, IssueType.Problem, \"Note's end reference time-tag is found but missing time.\")\r\n        {\r\n        }\r\n\r\n        public Issue Create(Note note)\r\n            => new NoteIssue(note, this);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Checks/CheckNoteText.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks.Issues;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Checks;\r\n\r\npublic class CheckNoteText : CheckHitObjectProperty<Note>\r\n{\r\n    protected override string Description => \"Note with invalid text.\";\r\n\r\n    public override IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]\r\n    {\r\n        new IssueTemplateEmptyText(this),\r\n        new IssueTemplateEmptyRubyText(this),\r\n    };\r\n\r\n    protected override IEnumerable<Issue> Check(Note note)\r\n    {\r\n        if (string.IsNullOrWhiteSpace(note.Text))\r\n            yield return new IssueTemplateEmptyText(this).Create(note);\r\n\r\n        if (note.RubyText != null && string.IsNullOrWhiteSpace(note.RubyText))\r\n            yield return new IssueTemplateEmptyRubyText(this).Create(note);\r\n    }\r\n\r\n    public class IssueTemplateEmptyText : IssueTemplate\r\n    {\r\n        public IssueTemplateEmptyText(ICheck check)\r\n            : base(check, IssueType.Problem, \"Note must have text.\")\r\n        {\r\n        }\r\n\r\n        public Issue Create(Note note)\r\n            => new NoteIssue(note, this);\r\n    }\r\n\r\n    public class IssueTemplateEmptyRubyText : IssueTemplate\r\n    {\r\n        public IssueTemplateEmptyRubyText(ICheck check)\r\n            : base(check, IssueType.Error, \"Note's ruby text should be null or has the value.\")\r\n        {\r\n        }\r\n\r\n        public Issue Create(Note note)\r\n            => new NoteIssue(note, this);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Checks/CheckNoteTime.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks.Issues;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Checks;\r\n\r\npublic class CheckNoteTime : CheckHitObjectProperty<Note>\r\n{\r\n    public const double MIN_DURATION = 100;\r\n\r\n    public const double MAX_DURATION = 10000;\r\n\r\n    protected override string Description => \"Note with invalid timing in the note.\";\r\n\r\n    public override IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]\r\n    {\r\n        new IssueTemplateInvalidReferenceTimeTagTime(this),\r\n        new IssueTemplateDurationTooShort(this),\r\n        new IssueTemplateDurationTooLong(this),\r\n    };\r\n\r\n    protected override IEnumerable<Issue> Check(Note note)\r\n    {\r\n        // because lyric's start and end time is assigned by the reference lyric, so should skip the check if note does not contains the reference lyric.\r\n        var referenceLyric = note.ReferenceLyric;\r\n        if (referenceLyric == null)\r\n            yield break;\r\n\r\n        //  should make sure that reference time-tag has time.\r\n        // if contains no time, will reported in the CheckNoteReferenceLyric.\r\n        double? startTime = note.StartReferenceTimeTag?.Time;\r\n        double? endTime = note.EndReferenceTimeTag?.Time;\r\n        if (startTime == null || endTime == null)\r\n            yield break;\r\n\r\n        // should have alert if reference time-tag's time is invalid.\r\n        if (endTime.Value < startTime.Value)\r\n        {\r\n            yield return new IssueTemplateInvalidReferenceTimeTagTime(this).Create(note);\r\n\r\n            yield break;\r\n        }\r\n\r\n        // note's duration should be in the range.\r\n        switch (note.Duration)\r\n        {\r\n            case < MIN_DURATION:\r\n                yield return new IssueTemplateDurationTooShort(this).Create(note);\r\n\r\n                break;\r\n\r\n            case > MAX_DURATION:\r\n                yield return new IssueTemplateDurationTooLong(this).Create(note);\r\n\r\n                break;\r\n        }\r\n\r\n        // todo: check for offset time's range.\r\n    }\r\n\r\n    public class IssueTemplateInvalidReferenceTimeTagTime : IssueTemplate\r\n    {\r\n        public IssueTemplateInvalidReferenceTimeTagTime(ICheck check)\r\n            : base(check, IssueType.Problem, \"Note must have text.\")\r\n        {\r\n        }\r\n\r\n        public Issue Create(Note note)\r\n            => new NoteIssue(note, this);\r\n    }\r\n\r\n    public class IssueTemplateDurationTooShort : IssueTemplate\r\n    {\r\n        public IssueTemplateDurationTooShort(ICheck check)\r\n            : base(check, IssueType.Problem, \"Note's duration too short.\")\r\n        {\r\n        }\r\n\r\n        public Issue Create(Note note)\r\n            => new NoteIssue(note, this);\r\n    }\r\n\r\n    public class IssueTemplateDurationTooLong : IssueTemplate\r\n    {\r\n        public IssueTemplateDurationTooLong(ICheck check)\r\n            : base(check, IssueType.Problem, \"Note's duration too long.\")\r\n        {\r\n        }\r\n\r\n        public Issue Create(Note note)\r\n            => new NoteIssue(note, this);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Checks/CheckStageInfo.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Game.Rulesets.Edit;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Checks;\r\n\r\npublic abstract class CheckStageInfo<TStageInfo> : ICheck\r\n    where TStageInfo : StageInfo\r\n{\r\n    public CheckMetadata Metadata => new(CheckCategory.Files, Description);\r\n\r\n    protected abstract string Description { get; }\r\n\r\n    public IEnumerable<IssueTemplate> PossibleTemplates => baseTemplates.Concat(CustomTemplates);\r\n\r\n    private IEnumerable<IssueTemplate> baseTemplates => new IssueTemplate[]\r\n    {\r\n        new IssueTemplateNoElement(this),\r\n        new IssueTemplateMappingHitObjectNotExist(this),\r\n        new IssueTemplateMappingItemNotExist(this),\r\n    };\r\n\r\n    public abstract IEnumerable<IssueTemplate> CustomTemplates { get; }\r\n\r\n    private readonly IList<Func<TStageInfo, IReadOnlyList<KaraokeHitObject>, IEnumerable<Issue>>> stageInfoCategoryActions\r\n        = new List<Func<TStageInfo, IReadOnlyList<KaraokeHitObject>, IEnumerable<Issue>>>();\r\n\r\n    public void RegisterCategory<TStageElement, THitObject>(Func<TStageInfo, StageElementCategory<TStageElement, THitObject>> categoryAction, int minimumRequiredElements)\r\n        where TStageElement : StageElement, new()\r\n        where THitObject : KaraokeHitObject, IHasPrimaryKey\r\n    {\r\n        stageInfoCategoryActions.Add((info, hitObjects) =>\r\n        {\r\n            var category = categoryAction(info);\r\n            return checkElementCategory(category, hitObjects.OfType<THitObject>().ToList(), minimumRequiredElements);\r\n        });\r\n    }\r\n\r\n    private IEnumerable<Issue> checkElementCategory<TStageElement, THitObject>(StageElementCategory<TStageElement, THitObject> category, IReadOnlyList<THitObject> hitObjects,\r\n                                                                               int minimumRequiredElements)\r\n        where TStageElement : StageElement, new()\r\n        where THitObject : KaraokeHitObject, IHasPrimaryKey\r\n    {\r\n        // check mapping.\r\n        var issues = checkMappings(category, hitObjects).ToList();\r\n\r\n        // check element amount.\r\n        if (category.AvailableElements.Count < minimumRequiredElements)\r\n            issues.Add(new IssueTemplateNoElement(this).Create(minimumRequiredElements));\r\n\r\n        // check elements.\r\n        issues.AddRange(CheckElement(category.DefaultElement));\r\n\r\n        foreach (var element in category.AvailableElements)\r\n        {\r\n            issues.AddRange(CheckElement(element));\r\n        }\r\n\r\n        return issues;\r\n    }\r\n\r\n    private IEnumerable<Issue> checkMappings<TStageElement, THitObject>(StageElementCategory<TStageElement, THitObject> category, IReadOnlyList<THitObject> hitObjects)\r\n        where TStageElement : StageElement, new()\r\n        where THitObject : KaraokeHitObject, IHasPrimaryKey\r\n    {\r\n        var elements = category.AvailableElements;\r\n        var mappings = category.Mappings;\r\n\r\n        foreach (var mapping in mappings)\r\n        {\r\n            if (hitObjects.All(x => x.ID != mapping.Key))\r\n                yield return new IssueTemplateMappingHitObjectNotExist(this).Create();\r\n\r\n            if (elements.All(x => x.ID != mapping.Value))\r\n                yield return new IssueTemplateMappingItemNotExist(this).Create();\r\n        }\r\n    }\r\n\r\n    public IEnumerable<Issue> Run(BeatmapVerifierContext context)\r\n    {\r\n        var property = getStageInfo(context);\r\n        if (property == null)\r\n            return Array.Empty<Issue>();\r\n\r\n        var hitObjects = context.CurrentDifficulty.Playable.HitObjects.OfType<KaraokeHitObject>().ToList();\r\n        var issues = CheckStageInfoWithHitObjects(property, hitObjects).ToList();\r\n\r\n        foreach (var stageInfoCategoryAction in stageInfoCategoryActions)\r\n        {\r\n            issues.AddRange(stageInfoCategoryAction(property, hitObjects));\r\n        }\r\n\r\n        return issues;\r\n\r\n        // todo: get stage info from context.\r\n        static TStageInfo? getStageInfo(BeatmapVerifierContext context)\r\n            => throw new NotImplementedException();\r\n    }\r\n\r\n    public abstract IEnumerable<Issue> CheckStageInfoWithHitObjects(TStageInfo stageInfo, IReadOnlyList<KaraokeHitObject> hitObjects);\r\n\r\n    protected abstract IEnumerable<Issue> CheckElement<TStageElement>(TStageElement element) where TStageElement : StageElement;\r\n\r\n    public class IssueTemplateNoElement : IssueTemplate\r\n    {\r\n        public IssueTemplateNoElement(ICheck check)\r\n            : base(check, IssueType.Warning, \"Should have at least {0} elements in the stage.\")\r\n        {\r\n        }\r\n\r\n        public Issue Create(int minimumRequiredElements) => new(this, minimumRequiredElements);\r\n    }\r\n\r\n    public class IssueTemplateMappingHitObjectNotExist : IssueTemplate\r\n    {\r\n        public IssueTemplateMappingHitObjectNotExist(ICheck check)\r\n            : base(check, IssueType.Warning, \"Maybe caused by hit-object has been deleted. Don't worry, go to the stage editor and will be easy to fix them.\")\r\n        {\r\n        }\r\n\r\n        public Issue Create() => new(this);\r\n    }\r\n\r\n    public class IssueTemplateMappingItemNotExist : IssueTemplate\r\n    {\r\n        public IssueTemplateMappingItemNotExist(ICheck check)\r\n            : base(check, IssueType.Error, \"It's caused by stage element has been deleted, but still remain the mapping data.\")\r\n        {\r\n        }\r\n\r\n        public Issue Create() => new(this);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Checks/Issues/BeatmapClassicLyricTimingPointIssue.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos.Classic;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Checks.Issues;\r\n\r\npublic class BeatmapClassicLyricTimingPointIssue : Issue\r\n{\r\n    public ClassicLyricTimingPoint StartTimingPoint;\r\n\r\n    public ClassicLyricTimingPoint EndTimingPoint;\r\n\r\n    public BeatmapClassicLyricTimingPointIssue(ClassicLyricTimingPoint startTimingPoint, ClassicLyricTimingPoint endTimingPoint, IssueTemplate template, params object[] args)\r\n        : base(template, args)\r\n    {\r\n        StartTimingPoint = startTimingPoint;\r\n        EndTimingPoint = endTimingPoint;\r\n\r\n        Time = Math.Min(StartTimingPoint.Time, EndTimingPoint.Time);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Checks/Issues/BeatmapPageIssue.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Checks.Issues;\r\n\r\npublic class BeatmapPageIssue : Issue\r\n{\r\n    public Page StartPage;\r\n\r\n    public Page EndPage;\r\n\r\n    public BeatmapPageIssue(Page startPage, Page endPage, IssueTemplate template, params object[] args)\r\n        : base(template, args)\r\n    {\r\n        StartPage = startPage;\r\n        EndPage = endPage;\r\n\r\n        Time = Math.Min(StartPage.Time, EndPage.Time);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Checks/Issues/LyricIssue.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Checks.Issues;\r\n\r\npublic class LyricIssue : Issue\r\n{\r\n    public Lyric Lyric;\r\n\r\n    public LyricIssue(Lyric lyric, IssueTemplate template, params object[] args)\r\n        : base(lyric, template, args)\r\n    {\r\n        Lyric = lyric;\r\n\r\n        Time = Lyric.TimeValid ? Lyric.StartTime : null;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Checks/Issues/LyricRubyTagIssue.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Checks.Issues;\r\n\r\npublic class LyricRubyTagIssue : LyricIssue\r\n{\r\n    public readonly RubyTag RubyTag;\r\n\r\n    public LyricRubyTagIssue(Lyric lyric, IssueTemplate template, RubyTag rubyTag, params object[] args)\r\n        : base(lyric, template, args)\r\n    {\r\n        RubyTag = rubyTag;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Checks/Issues/LyricTimeTagIssue.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Checks.Issues;\r\n\r\npublic class LyricTimeTagIssue : LyricIssue\r\n{\r\n    public readonly TimeTag TimeTag;\r\n\r\n    public LyricTimeTagIssue(Lyric lyric, IssueTemplate template, TimeTag timeTag, params object[] args)\r\n        : base(lyric, template, args)\r\n    {\r\n        TimeTag = timeTag;\r\n\r\n        if (lyric.TimeValid)\r\n        {\r\n            Time = TimeTag.Time ?? Lyric.StartTime;\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Checks/Issues/NoteIssue.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Checks.Issues;\r\n\r\npublic class NoteIssue : Issue\r\n{\r\n    public Note Note;\r\n\r\n    public NoteIssue(Note note, IssueTemplate template, params object[] args)\r\n        : base(note, template, args)\r\n    {\r\n        Note = note;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Components/ContextMenu/LyricLockContextMenu.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Types;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Components.ContextMenu;\r\n\r\npublic class LyricLockContextMenu : OsuMenuItem\r\n{\r\n    public LyricLockContextMenu(ILockChangeHandler lockChangeHandler, Lyric lyric, string name)\r\n        : this(lockChangeHandler, new List<Lyric> { lyric }, name)\r\n    {\r\n    }\r\n\r\n    public LyricLockContextMenu(ILockChangeHandler lockChangeHandler, List<Lyric> lyrics, string name)\r\n        : base(name)\r\n    {\r\n        Items = Enum.GetValues<LockState>().Select(l => new OsuMenuItem(l.ToString(), anyLyricInLockState(l) ? MenuItemType.Highlighted : MenuItemType.Standard, () =>\r\n        {\r\n            // todo: how to make lyric as selected?\r\n            lockChangeHandler.Lock(l);\r\n        })).ToList();\r\n\r\n        bool anyLyricInLockState(LockState lockState) => lyrics.Any(lyric => lyric.Lock == lockState);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Components/ContextMenu/SingerContextMenu.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Utils;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Components.ContextMenu;\r\n\r\npublic class SingerContextMenu : OsuMenuItem\r\n{\r\n    public SingerContextMenu(EditorBeatmap beatmap, ILyricSingerChangeHandler lyricSingerChangeHandler, string name, Action? postProcess = null)\r\n        : base(name)\r\n    {\r\n        var lyrics = beatmap.SelectedHitObjects.OfType<Lyric>().ToArray();\r\n\r\n        // todo: should be able to support the sub-singer.\r\n        var karaokeBeatmap = EditorBeatmapUtils.GetPlayableBeatmap(beatmap);\r\n        var singers = karaokeBeatmap.SingerInfo.GetAllSingers();\r\n\r\n        Items = singers.Select(singer => new OsuMenuItem(singer.Name, anySingerInLyric(singer) ? MenuItemType.Highlighted : MenuItemType.Standard, () =>\r\n        {\r\n            // if only one lyric\r\n            if (allSingerInLyric(singer))\r\n            {\r\n                lyricSingerChangeHandler.Remove(singer);\r\n            }\r\n            else\r\n            {\r\n                lyricSingerChangeHandler.Add(singer);\r\n            }\r\n\r\n            postProcess?.Invoke();\r\n        })).ToList();\r\n\r\n        bool anySingerInLyric(Singer singer) => lyrics.Any(lyric => LyricUtils.ContainsSinger(lyric, singer));\r\n\r\n        bool allSingerInLyric(Singer singer) => lyrics.All(lyric => LyricUtils.ContainsSinger(lyric, singer));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Components/Cursor/TimeTagTooltip.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.Cursor;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Components.Cursor;\r\n\r\npublic partial class TimeTagTooltip : BackgroundToolTip<TimeTag>\r\n{\r\n    private const int time_display_height = 25;\r\n\r\n    private Box background = null!;\r\n    private readonly OsuSpriteText trackTimer;\r\n    private readonly OsuSpriteText index;\r\n    private readonly OsuSpriteText indexState;\r\n\r\n    protected override float ContentPadding => 5;\r\n\r\n    public TimeTagTooltip()\r\n    {\r\n        Child = new GridContainer\r\n        {\r\n            AutoSizeAxes = Axes.Both,\r\n            RowDimensions = new[]\r\n            {\r\n                new Dimension(GridSizeMode.Absolute, time_display_height),\r\n                new Dimension(GridSizeMode.Absolute, BORDER),\r\n                new Dimension(GridSizeMode.AutoSize),\r\n            },\r\n            ColumnDimensions = new[]\r\n            {\r\n                new Dimension(GridSizeMode.AutoSize),\r\n            },\r\n            Content = new[]\r\n            {\r\n                new Drawable[]\r\n                {\r\n                    trackTimer = new OsuSpriteText\r\n                    {\r\n                        Font = OsuFont.GetFont(size: 21, fixedWidth: true),\r\n                    },\r\n                },\r\n                null,\r\n                new Drawable[]\r\n                {\r\n                    new FillFlowContainer\r\n                    {\r\n                        AutoSizeAxes = Axes.Both,\r\n                        Spacing = new Vector2(10),\r\n                        Children = new[]\r\n                        {\r\n                            index = new OsuSpriteText\r\n                            {\r\n                                Font = OsuFont.GetFont(size: 12),\r\n                            },\r\n                            indexState = new OsuSpriteText\r\n                            {\r\n                                Font = OsuFont.GetFont(size: 12),\r\n                            },\r\n                        },\r\n                    },\r\n                },\r\n            },\r\n        };\r\n    }\r\n\r\n    protected override Drawable SetBackground()\r\n    {\r\n        return background = new Box\r\n        {\r\n            RelativeSizeAxes = Axes.X,\r\n            Height = time_display_height + BORDER,\r\n        };\r\n    }\r\n\r\n    private TimeTag? lastTimeTag;\r\n\r\n    public override void SetContent(TimeTag timeTag)\r\n    {\r\n        if (timeTag == lastTimeTag)\r\n            return;\r\n\r\n        lastTimeTag = timeTag;\r\n\r\n        trackTimer.Text = TimeTagUtils.FormattedString(timeTag);\r\n        index.Text = $\"Position: {timeTag.Index.Index}\";\r\n        indexState.Text = TextIndexUtils.GetValueByState(timeTag.Index, \"start\", \"end\");\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OsuColour colours)\r\n    {\r\n        background.Colour = colours.Gray2;\r\n        indexState.Colour = colours.Red;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Components/Menus/KaraokeEditorMenu.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Framework.Screens;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Components.Menus;\r\n\r\npublic class KaraokeEditorMenu : MenuItem\r\n{\r\n    public KaraokeEditorMenu(IScreen screen, string text)\r\n        : base(text, () => openKaraokeEditor(screen))\r\n    {\r\n    }\r\n\r\n    private static void openKaraokeEditor(IScreen screen)\r\n    {\r\n        screen.Push(new KaraokeBeatmapEditor());\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Components/Menus/KaraokeSkinEditorMenu.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Framework.Screens;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Skin;\r\nusing osu.Game.Skinning;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Components.Menus;\r\n\r\npublic class KaraokeSkinEditorMenu : MenuItem\r\n{\r\n    public KaraokeSkinEditorMenu(IScreen screen, ISkin skin, string text)\r\n        : base(text, () => openKaraokeSkin(screen, skin))\r\n    {\r\n    }\r\n\r\n    private static void openKaraokeSkin(IScreen screen, ISkin skin)\r\n    {\r\n        screen.Push(new KaraokeSkinEditor(skin));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Components/Sprites/DrawableTextIndex.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.Shapes;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Components.Sprites;\r\n\r\npublic partial class DrawableTextIndex : RightTriangle\r\n{\r\n    private TextIndex.IndexState state;\r\n\r\n    public TextIndex.IndexState State\r\n    {\r\n        get => state;\r\n        set\r\n        {\r\n            state = value;\r\n\r\n            RightAngleDirection = TextIndexUtils.GetValueByState(state, TriangleRightAngleDirection.BottomLeft, TriangleRightAngleDirection.BottomRight);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Components/Sprites/DrawableTimeTag.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Cursor;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Components.Cursor;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Components.Sprites;\r\n\r\npublic sealed partial class DrawableTimeTag : CompositeDrawable, IHasCustomTooltip<TimeTag>\r\n{\r\n    private readonly IBindable<double?> bindableTime = new Bindable<double?>();\r\n\r\n    private readonly DrawableTextIndex drawableTextIndex;\r\n\r\n    public Func<TimeTag, OsuColour, Color4>? TimeTagColourFunc;\r\n\r\n    public DrawableTimeTag()\r\n    {\r\n        InternalChild = drawableTextIndex = new DrawableTextIndex\r\n        {\r\n            RelativeSizeAxes = Axes.Both,\r\n        };\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OsuColour colours)\r\n    {\r\n        bindableTime.BindValueChanged(x =>\r\n        {\r\n            if (timeTag == null)\r\n                return;\r\n\r\n            drawableTextIndex.Colour = TimeTagColourFunc?.Invoke(timeTag, colours) ?? GetDefaultTimeTagColour(colours, timeTag);\r\n        }, true);\r\n    }\r\n\r\n    private TimeTag? timeTag;\r\n\r\n    public TimeTag? TimeTag\r\n    {\r\n        get => timeTag;\r\n        set\r\n        {\r\n            if (timeTag == value)\r\n                return;\r\n\r\n            bindableTime.UnbindBindings();\r\n            timeTag = value;\r\n            Alpha = timeTag == null ? 0 : 1;\r\n\r\n            if (timeTag == null)\r\n                return;\r\n\r\n            bindableTime.BindTo(timeTag.TimeBindable);\r\n            drawableTextIndex.State = timeTag.Index.State;\r\n        }\r\n    }\r\n\r\n    public TimeTag TooltipContent => timeTag ?? new TimeTag(new TextIndex());\r\n\r\n    public ITooltip<TimeTag> GetCustomTooltip() => new TimeTagTooltip();\r\n\r\n    public static Color4 GetDefaultTimeTagColour(OsuColour colours, TimeTag timeTag)\r\n    {\r\n        bool hasTime = timeTag.Time.HasValue;\r\n        if (!hasTime)\r\n            return colours.Gray7;\r\n\r\n        return TextIndexUtils.GetValueByState(timeTag.Index, colours.Yellow, colours.YellowDarker);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Debugging/DebugBeatmapManager.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Diagnostics;\r\nusing System.IO;\r\nusing System.Linq;\r\nusing System.Reflection;\r\nusing System.Text;\r\nusing System.Threading;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Extensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Platform;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Database;\r\nusing osu.Game.Extensions;\r\nusing osu.Game.Overlays.Notifications;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Formats;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Utils;\r\nusing osu.Game.Screens.Edit;\r\nusing osu.Game.Utils;\r\nusing SharpCompress.Archives.Zip;\r\nusing SharpCompress.Common;\r\nusing SharpCompress.Writers.Zip;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Debugging;\r\n\r\n/// <summary>\r\n/// Save or export the beatmap for debug.\r\n/// Beatmap will be json format and might not be the final version.\r\n/// </summary>\r\npublic partial class DebugBeatmapManager : Component\r\n{\r\n    [Resolved]\r\n    private Storage storage { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private EditorBeatmap beatmap { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private BeatmapManager beatmapManager { get; set; } = null!;\r\n\r\n    /// <summary>\r\n    /// Force save the beatmap with json format.\r\n    /// Modified from <see cref=\"BeatmapManager.Save\"/>\r\n    /// </summary>\r\n    public void OverrideTheBeatmapWithJsonFormat()\r\n    {\r\n        var karaokeBeatmap = EditorBeatmapUtils.GetPlayableBeatmap(beatmap);\r\n        save(beatmap.BeatmapInfo, karaokeBeatmap);\r\n    }\r\n\r\n    /// <summary>\r\n    /// Save the beatmap with json format to new difficulty.\r\n    /// Modified from <see cref=\"BeatmapManager.CopyExistingDifficulty\"/>\r\n    /// </summary>\r\n    public void SaveToNewDifficulty()\r\n    {\r\n        var referenceWorkingBeatmap = beatmap;\r\n        var targetBeatmapSet = beatmap.BeatmapInfo.BeatmapSet;\r\n\r\n        if (targetBeatmapSet == null)\r\n        {\r\n            return;\r\n        }\r\n\r\n        // start modifiey\r\n        var newBeatmap = EditorBeatmapUtils.GetPlayableBeatmap(beatmap);\r\n        BeatmapInfo newBeatmapInfo;\r\n\r\n        newBeatmap.BeatmapInfo = newBeatmapInfo = referenceWorkingBeatmap.BeatmapInfo.Clone();\r\n        // assign a new ID to the clone.\r\n        newBeatmapInfo.ID = Guid.NewGuid();\r\n        // add \"(copy)\" suffix to difficulty name, and additionally ensure that it doesn't conflict with any other potentially pre-existing copies.\r\n        newBeatmapInfo.DifficultyName = NamingUtils.GetNextBestName(\r\n            targetBeatmapSet.Beatmaps.Select(b => b.DifficultyName),\r\n            $\"{newBeatmapInfo.DifficultyName} (copy)\");\r\n        // clear the hash, as that's what is used to match .osu files with their corresponding realm beatmaps.\r\n        newBeatmapInfo.Hash = string.Empty;\r\n        // clear online properties.\r\n        newBeatmapInfo.ResetOnlineInfo();\r\n\r\n        addDifficultyToSet(targetBeatmapSet, newBeatmap);\r\n        return;\r\n\r\n        void addDifficultyToSet(BeatmapSetInfo targetBeatmapSet, IBeatmap newBeatmap)\r\n        {\r\n            // populate circular beatmap set info <-> beatmap info references manually.\r\n            // several places like `Save()` or `GetWorkingBeatmap()`\r\n            // rely on them being freely traversable in both directions for correct operation.\r\n            targetBeatmapSet.Beatmaps.Add(newBeatmap.BeatmapInfo);\r\n            newBeatmap.BeatmapInfo.BeatmapSet = targetBeatmapSet;\r\n\r\n            save(newBeatmap.BeatmapInfo, newBeatmap);\r\n        }\r\n    }\r\n\r\n    /// <summary>\r\n    /// Copied from <see cref=\"BeatmapManager.Save\"/>\r\n    /// </summary>\r\n    /// <param name=\"beatmapInfo\"></param>\r\n    /// <param name=\"beatmapContent\"></param>\r\n    /// <exception cref=\"InvalidOperationException\"></exception>\r\n    private void save(BeatmapInfo beatmapInfo, IBeatmap beatmapContent)\r\n    {\r\n        // get realm from beatmapManager using reflection\r\n        if (beatmapManager.GetType().GetProperty(\"Realm\", BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(beatmapManager) is not RealmAccess realm)\r\n        {\r\n            throw new InvalidOperationException();\r\n        }\r\n\r\n        var setInfo = beatmapInfo.BeatmapSet;\r\n        Debug.Assert(setInfo != null);\r\n\r\n        // Difficulty settings must be copied first due to the clone in `Beatmap<>.BeatmapInfo_Set`.\r\n        // This should hopefully be temporary, assuming said clone is eventually removed.\r\n\r\n        // Warning: The directionality here is important. Changes have to be copied *from* beatmapContent (which comes from editor and is being saved)\r\n        // *to* the beatmapInfo (which is a database model and needs to receive values without the taiko slider velocity multiplier for correct operation).\r\n        // CopyTo() will undo such adjustments, while CopyFrom() will not.\r\n        beatmapContent.Difficulty.CopyTo(beatmapInfo.Difficulty);\r\n\r\n        // All changes to metadata are made in the provided beatmapInfo, so this should be copied to the `IBeatmap` before encoding.\r\n        beatmapContent.BeatmapInfo = beatmapInfo;\r\n\r\n        // Since now this is a locally-modified beatmap, we also set all relevant flags to indicate this.\r\n        // Importantly, the `ResetOnlineInfo()` call must happen before encoding, as online ID is encoded into the `.osu` file,\r\n        // which influences the beatmap checksums.\r\n        beatmapInfo.LastLocalUpdate = DateTimeOffset.Now;\r\n        beatmapInfo.Status = BeatmapOnlineStatus.LocallyModified;\r\n        beatmapInfo.ResetOnlineInfo();\r\n\r\n        realm.Write(r =>\r\n        {\r\n            using var stream = new MemoryStream();\r\n\r\n            using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))\r\n            {\r\n                sw.WriteLine(generateJsonBeatmap(beatmapContent));\r\n            }\r\n\r\n            stream.Seek(0, SeekOrigin.Begin);\r\n\r\n            // AddFile generally handles updating/replacing files, but this is a case where the filename may have also changed so let's delete for simplicity.\r\n            var existingFileInfo = beatmapInfo.Path != null ? setInfo.GetFile(beatmapInfo.Path) : null;\r\n            string targetFilename = createBeatmapFilenameFromMetadata(beatmapInfo);\r\n\r\n            // ensure that two difficulties from the set don't point at the same beatmap file.\r\n            if (setInfo.Beatmaps.Any(b => b.ID != beatmapInfo.ID && string.Equals(b.Path, targetFilename, StringComparison.OrdinalIgnoreCase)))\r\n                throw new InvalidOperationException($\"{setInfo.GetDisplayString()} already has a difficulty with the name of '{beatmapInfo.DifficultyName}'.\");\r\n\r\n            if (existingFileInfo != null)\r\n                beatmapManager.DeleteFile(setInfo, existingFileInfo);\r\n\r\n            string oldMd5Hash = beatmapInfo.MD5Hash;\r\n\r\n            beatmapInfo.MD5Hash = stream.ComputeMD5Hash();\r\n            beatmapInfo.Hash = stream.ComputeSHA2Hash();\r\n\r\n            beatmapManager.AddFile(setInfo, stream, createBeatmapFilenameFromMetadata(beatmapInfo));\r\n\r\n            // beatmapManager.updateHashAndMarkDirty(setInfo);\r\n            var method = typeof(BeatmapManager).GetMethod(\"updateHashAndMarkDirty\", BindingFlags.Instance | BindingFlags.NonPublic);\r\n            method?.Invoke(beatmapManager, new object?[] { setInfo });\r\n\r\n            var liveBeatmapSet = r.Find<BeatmapSetInfo>(setInfo.ID)!;\r\n\r\n            setInfo.CopyChangesToRealm(liveBeatmapSet);\r\n\r\n            liveBeatmapSet.Beatmaps.Single(b => b.ID == beatmapInfo.ID)\r\n                          .UpdateLocalScores(r);\r\n        });\r\n\r\n        Debug.Assert(beatmapInfo.BeatmapSet != null);\r\n\r\n        static string createBeatmapFilenameFromMetadata(BeatmapInfo beatmapInfo)\r\n        {\r\n            var metadata = beatmapInfo.Metadata;\r\n            return $\"{metadata.Artist} - {metadata.Title} ({metadata.Author.Username}) [{beatmapInfo.DifficultyName}].osu\".GetValidFilename();\r\n        }\r\n    }\r\n\r\n    /// <summary>\r\n    /// Export the json beatmap only.\r\n    /// </summary>\r\n    public void ExportToJson()\r\n    {\r\n        // note : this is for develop testing purpose.\r\n        // will be removed eventually\r\n        string beatmapName = string.IsNullOrEmpty(beatmap.Name) ? \"[NoName]\" : beatmap.Name;\r\n        var exportStorage = storage.GetStorageForDirectory(\"json\");\r\n        string filename = $\"{beatmapName}.json\";\r\n\r\n        using (var outputStream = exportStorage.GetStream(filename, FileAccess.Write, FileMode.Create))\r\n        using (var sw = new StreamWriter(outputStream))\r\n        {\r\n            sw.WriteLine(generateJsonBeatmap(beatmap));\r\n        }\r\n\r\n        exportStorage.PresentFileExternally(filename);\r\n    }\r\n\r\n    /// <summary>\r\n    /// Export the whole beatmap with:\r\n    /// 1. json format beatmap.\r\n    /// 2. other resource like audio, background.\r\n    /// </summary>\r\n    public void ExportToJsonBeatmap()\r\n    {\r\n        // note : this is for develop testing purpose.\r\n        // will be removed eventually\r\n        string beatmapName = string.IsNullOrEmpty(beatmap.Name) ? \"[NoName]\" : beatmap.Name;\r\n        var exportStorage = storage.GetStorageForDirectory(\"exports\");\r\n        string filename = $\"{beatmapName}.osz\";\r\n\r\n        using (var outputStream = exportStorage.GetStream(filename, FileAccess.Write, FileMode.Create))\r\n        {\r\n            string beatmapText = generateJsonBeatmap(beatmap);\r\n            new KaraokeLegacyBeatmapExporter(storage, filename, beatmapText).ExportToStream(beatmap.BeatmapInfo.BeatmapSet!, outputStream, null);\r\n        }\r\n\r\n        exportStorage.PresentFileExternally(filename);\r\n    }\r\n\r\n    private static string generateJsonBeatmap(IBeatmap beatmap)\r\n    {\r\n        var encoder = new KaraokeJsonBeatmapEncoder();\r\n\r\n        var encodeBeatmap = new Beatmap\r\n        {\r\n            Difficulty = beatmap.Difficulty.Clone(),\r\n            BeatmapInfo = beatmap.BeatmapInfo.Clone(),\r\n            ControlPointInfo = beatmap.ControlPointInfo.DeepClone(),\r\n            Breaks = beatmap.Breaks,\r\n            HitObjects = beatmap.HitObjects.ToList(),\r\n        };\r\n        encodeBeatmap.BeatmapInfo.BeatmapSet = new BeatmapSetInfo();\r\n        encodeBeatmap.BeatmapInfo.Metadata = new BeatmapMetadata\r\n        {\r\n            Title = \"json beatmap\",\r\n            AudioFile = beatmap.Metadata.AudioFile,\r\n            BackgroundFile = beatmap.Metadata.BackgroundFile,\r\n        };\r\n\r\n        return encoder.Encode(encodeBeatmap);\r\n    }\r\n\r\n    private class KaraokeLegacyBeatmapExporter : LegacyBeatmapExporter\r\n    {\r\n        private readonly string filename;\r\n        private readonly string content;\r\n\r\n        public KaraokeLegacyBeatmapExporter(Storage storage, string filename, string content)\r\n            : base(storage)\r\n        {\r\n            this.filename = filename;\r\n            this.content = content;\r\n        }\r\n\r\n        public override void ExportToStream(BeatmapSetInfo model, Stream outputStream, ProgressNotification? notification, CancellationToken cancellationToken = new())\r\n        {\r\n            // base.ExportModelTo(model, outputStream);\r\n            using var zipArchive = ZipArchive.CreateArchive();\r\n\r\n            foreach (INamedFileUsage file in model.Files)\r\n            {\r\n                // do not export other osu beatmap.\r\n                if (file.Filename.EndsWith(\".osu\", StringComparison.Ordinal))\r\n                    continue;\r\n\r\n                zipArchive.AddEntry(file.Filename, UserFileStorage.GetStream(file.File.GetStoragePath()), true);\r\n            }\r\n\r\n            // add the json file.\r\n            using var jsonBeatmapStream = getJsonBeatmapStream();\r\n            zipArchive.AddEntry(filename, jsonBeatmapStream, true);\r\n            zipArchive.SaveTo(outputStream,  new ZipWriterOptions(CompressionType.Deflate));\r\n        }\r\n\r\n        private Stream getJsonBeatmapStream()\r\n        {\r\n            var memoryStream = new MemoryStream();\r\n            var sw = new StreamWriter(memoryStream);\r\n\r\n            sw.WriteLine(content);\r\n            sw.Flush();\r\n\r\n            memoryStream.Position = 0;\r\n            return memoryStream;\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/DrawableKaraokeEditorRuleset.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.UI;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\nusing osu.Game.Rulesets.Mods;\r\nusing osu.Game.Rulesets.UI;\r\nusing osu.Game.Rulesets.UI.Scrolling;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit;\r\n\r\npublic partial class DrawableKaraokeEditorRuleset : DrawableKaraokeRuleset\r\n{\r\n    public new IScrollingInfo ScrollingInfo => base.ScrollingInfo;\r\n\r\n    protected override bool DisplayNotePlayfield => true;\r\n\r\n    public DrawableKaraokeEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod>? mods)\r\n        : base(ruleset, beatmap, mods)\r\n    {\r\n    }\r\n\r\n    protected override Playfield CreatePlayfield() => new KaraokeEditorPlayfield();\r\n\r\n    public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer()\r\n    {\r\n        bool isCalledByComposer = StackTraceUtils.IsStackTraceContains(\"DrawableEditorRulesetWrapper\");\r\n        if (isCalledByComposer)\r\n            return new PlayfieldAdjustmentContainer();\r\n\r\n        return base.CreatePlayfieldAdjustmentContainer();\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/EditorNotePlayfield.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.UI.Components;\r\nusing osu.Game.Rulesets.Karaoke.UI.Scrolling;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit;\r\n\r\npublic partial class EditorNotePlayfield : ScrollingNotePlayfield\r\n{\r\n    private readonly SingerVoiceVisualization singerVoiceVisualization;\r\n\r\n    public EditorNotePlayfield(int columns)\r\n        : base(columns)\r\n    {\r\n        BackgroundLayer.AddRange(new Drawable[]\r\n        {\r\n            new Box\r\n            {\r\n                Depth = 1,\r\n                Name = \"Background\",\r\n                RelativeSizeAxes = Axes.Both,\r\n                Colour = Color4.Black,\r\n                Alpha = 0.5f,\r\n            },\r\n        });\r\n\r\n        HitObjectArea.Add(singerVoiceVisualization = new SingerVoiceVisualization\r\n        {\r\n            Name = \"Scoring Visualization\",\r\n            RelativeSizeAxes = Axes.Both,\r\n            Alpha = 0.6f,\r\n        });\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        // todo : load data from scoring manager.\r\n    }\r\n\r\n    public partial class SingerVoiceVisualization : VoiceVisualization<KeyValuePair<double, float?>>\r\n    {\r\n        protected override double GetTime(KeyValuePair<double, float?> frame) => frame.Key;\r\n\r\n        protected override float GetPosition(KeyValuePair<double, float?> frame) => frame.Value ?? 0;\r\n\r\n        private bool createNew = true;\r\n\r\n        private double minAvailableTime;\r\n\r\n        public void Add(KeyValuePair<double, float?> point)\r\n        {\r\n            // Start time should be largest and cannot be removed.\r\n            double startTime = point.Key;\r\n            if (startTime <= minAvailableTime)\r\n                throw new ArgumentOutOfRangeException($\"{nameof(startTime)} out of range.\");\r\n\r\n            minAvailableTime = startTime;\r\n\r\n            if (!point.Value.HasValue)\r\n            {\r\n                // Next replay frame will create new path\r\n                createNew = true;\r\n                return;\r\n            }\r\n\r\n            if (createNew)\r\n            {\r\n                createNew = false;\r\n\r\n                CreateNew(point);\r\n            }\r\n            else\r\n            {\r\n                Append(point);\r\n            }\r\n        }\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(OsuColour colours)\r\n        {\r\n            Colour = colours.GrayF;\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Export/ExportLyricManager.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.IO;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Platform;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Integration.Formats;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Export;\r\n\r\npublic partial class ExportLyricManager : Component\r\n{\r\n    [Resolved]\r\n    private Storage storage { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private EditorBeatmap beatmap { get; set; } = null!;\r\n\r\n    public void ExportToKar()\r\n    {\r\n        var exportStorage = storage.GetStorageForDirectory(\"kar\");\r\n        string filename = $\"{beatmap.Name}.kar\";\r\n\r\n        using (var outputStream = exportStorage.GetStream(filename, FileAccess.Write, FileMode.Create))\r\n        using (var sw = new StreamWriter(outputStream))\r\n        {\r\n            var encoder = new KarEncoder();\r\n            sw.WriteLine(encoder.Encode(new Beatmap\r\n            {\r\n                HitObjects = beatmap.HitObjects.ToList(),\r\n            }));\r\n        }\r\n\r\n        exportStorage.PresentFileExternally(filename);\r\n    }\r\n\r\n    public void ExportToText()\r\n    {\r\n        var exportStorage = storage.GetStorageForDirectory(\"text\");\r\n        string filename = $\"{beatmap.Name}.txt\";\r\n\r\n        using (var outputStream = exportStorage.GetStream(filename, FileAccess.Write, FileMode.Create))\r\n        using (var sw = new StreamWriter(outputStream))\r\n        {\r\n            var encoder = new LyricTextEncoder();\r\n            sw.WriteLine(encoder.Encode(new Beatmap\r\n            {\r\n                HitObjects = beatmap.HitObjects.ToList(),\r\n            }));\r\n        }\r\n\r\n        exportStorage.PresentFileExternally(filename);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/Beatmaps/BeatmapPropertyDetector.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator.Beatmaps;\r\n\r\n/// <summary>\r\n/// Base interface of the detector.\r\n/// </summary>\r\n/// <typeparam name=\"TProperty\"></typeparam>\r\n/// <typeparam name=\"TConfig\"></typeparam>\r\npublic abstract class BeatmapPropertyDetector<TProperty, TConfig> : PropertyDetector<KaraokeBeatmap, TProperty, TConfig>\r\n    where TConfig : GeneratorConfig, new()\r\n{\r\n    protected BeatmapPropertyDetector(TConfig config)\r\n        : base(config)\r\n    {\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/Beatmaps/BeatmapPropertyGenerator.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator.Beatmaps;\r\n\r\n/// <summary>\r\n/// Base interface of the auto-generator.\r\n/// </summary>\r\n/// <typeparam name=\"TProperty\"></typeparam>\r\n/// <typeparam name=\"TConfig\"></typeparam>\r\npublic abstract class BeatmapPropertyGenerator<TProperty, TConfig> : PropertyGenerator<KaraokeBeatmap, TProperty, TConfig>\r\n    where TConfig : GeneratorConfig, new()\r\n{\r\n    protected BeatmapPropertyGenerator(TConfig config)\r\n        : base(config)\r\n    {\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/Beatmaps/Pages/PageGenerator.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Rulesets.Edit;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\nusing osu.Game.Tests.Beatmaps;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator.Beatmaps.Pages;\r\n\r\npublic class PageGenerator : BeatmapPropertyGenerator<Page[], PageGeneratorConfig>\r\n{\r\n    public PageGenerator(PageGeneratorConfig config)\r\n        : base(config)\r\n    {\r\n    }\r\n\r\n    protected override LocalisableString? GetInvalidMessageFromItem(KaraokeBeatmap item)\r\n    {\r\n        var lyrics = item.HitObjects.OfType<Lyric>().ToArray();\r\n        if (lyrics.Length < 1)\r\n            return \"There's not lyric in the beatmap.\";\r\n\r\n        var timeTagChecker = new CheckLyricTimeTag();\r\n        var invalidIssues = timeTagChecker.Run(getContext(item));\r\n        if (invalidIssues.Any())\r\n            return \"Should not have any time-tag related issues\";\r\n\r\n        return null;\r\n    }\r\n\r\n    private static BeatmapVerifierContext getContext(IBeatmap beatmap)\r\n        => new(beatmap, new TestWorkingBeatmap(beatmap));\r\n\r\n    protected override Page[] GenerateFromItem(KaraokeBeatmap item)\r\n    {\r\n        if (Config.MinTime.Value < CheckBeatmapPageInfo.MIN_INTERVAL || Config.MaxTime.Value > CheckBeatmapPageInfo.MAX_INTERVAL)\r\n            throw new InvalidOperationException(\"Interval time should be validate.\");\r\n\r\n        var existPages = Config.ClearExistPages.Value ? Array.Empty<Page>() : item.PageInfo.SortedPages.ToArray();\r\n        var lyricTimingInfos = item.HitObjects.OfType<Lyric>()\r\n                                   .Where(x => x.TimeValid)\r\n                                   .Select(x => new LyricTimingInfo\r\n                                   {\r\n                                       StartTime = x.StartTime,\r\n                                       EndTime = x.EndTime,\r\n                                   })\r\n                                   .OrderBy(x => x).ToList();\r\n\r\n        if (lyricTimingInfos.Count == 0)\r\n            return existPages;\r\n\r\n        return calculatePageByLyrics(lyricTimingInfos, existPages).ToArray();\r\n    }\r\n\r\n    private IEnumerable<Page> calculatePageByLyrics(IReadOnlyList<LyricTimingInfo> lyricTimingInfos, IReadOnlyList<Page> existPages)\r\n    {\r\n        double currentTime;\r\n\r\n        // create first page with it's start time.\r\n        yield return createReturnPage(existPages.FirstOrDefault()?.Time ?? lyricTimingInfos.FirstOrDefault().StartTime);\r\n\r\n        for (int i = 0; i < lyricTimingInfos.Count; i++)\r\n        {\r\n            bool lsLast = i == lyricTimingInfos.Count - 1;\r\n\r\n            var currentLyricTimingInfo = lyricTimingInfos[i];\r\n            LyricTimingInfo? nextLyricTimingInfo = lsLast ? null : lyricTimingInfos[i + 1];\r\n\r\n            bool getAverageTimeWithNextLyric = nextLyricTimingInfo != null && nextLyricTimingInfo.Value.StartTime > currentLyricTimingInfo.EndTime;\r\n            double expectedEndTime = getAverageTimeWithNextLyric\r\n                ? (currentLyricTimingInfo.EndTime + nextLyricTimingInfo!.Value.StartTime) / 2\r\n                : currentLyricTimingInfo.EndTime;\r\n\r\n            while (currentTime < expectedEndTime)\r\n            {\r\n                if (expectedEndTime - currentTime < Config.MinTime.Value && getAverageTimeWithNextLyric)\r\n                {\r\n                    break;\r\n                }\r\n\r\n                if (expectedEndTime - currentTime > Config.MaxTime.Value)\r\n                {\r\n                    yield return createReturnPage(currentTime + Config.MaxTime.Value);\r\n                }\r\n                else\r\n                {\r\n                    yield return createReturnPage(expectedEndTime);\r\n                }\r\n            }\r\n        }\r\n\r\n        Page createReturnPage(double time)\r\n        {\r\n            currentTime = time;\r\n            return new Page { Time = time };\r\n        }\r\n    }\r\n\r\n    private readonly struct LyricTimingInfo : IComparable<LyricTimingInfo>\r\n    {\r\n        public double StartTime { get; init; }\r\n\r\n        public double EndTime { get; init; }\r\n\r\n        public int CompareTo(LyricTimingInfo other)\r\n        {\r\n            return ComparableUtils.CompareByProperty(this, other,\r\n                t => t.StartTime,\r\n                t => t.EndTime);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/Beatmaps/Pages/PageGeneratorConfig.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator.Beatmaps.Pages;\r\n\r\npublic class PageGeneratorConfig : GeneratorConfig\r\n{\r\n    [ConfigSource(\"Min time\", \"Min interval between pages.\")]\r\n    public Bindable<double> MinTime { get; } = new BindableDouble(CheckBeatmapPageInfo.MIN_INTERVAL)\r\n    {\r\n        MinValue = CheckBeatmapPageInfo.MIN_INTERVAL,\r\n        MaxValue = CheckBeatmapPageInfo.MAX_INTERVAL,\r\n    };\r\n\r\n    [ConfigSource(\"Max time\", \"Max interval between pages.\")]\r\n    public Bindable<double> MaxTime { get; } = new BindableDouble(CheckBeatmapPageInfo.MAX_INTERVAL)\r\n    {\r\n        MinValue = CheckBeatmapPageInfo.MIN_INTERVAL,\r\n        MaxValue = CheckBeatmapPageInfo.MAX_INTERVAL,\r\n    };\r\n\r\n    [ConfigSource(\"Clear the exist page.\", \"Clear the exist page after generated.\")]\r\n    public Bindable<bool> ClearExistPages { get; } = new BindableBool();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/ConfigCategoryAttribute.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Localisation;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator;\r\n\r\npublic class ConfigCategoryAttribute : Attribute, IEquatable<ConfigCategoryAttribute>\r\n{\r\n    public LocalisableString Category { get; }\r\n\r\n    public ConfigCategoryAttribute(string category)\r\n    {\r\n        Category = category;\r\n    }\r\n\r\n    public bool Equals(ConfigCategoryAttribute? other)\r\n    {\r\n        return Category == other?.Category;\r\n    }\r\n\r\n    public override bool Equals(object? obj)\r\n    {\r\n        return obj switch\r\n        {\r\n            ConfigCategoryAttribute category => Equals(category),\r\n            _ => false,\r\n        };\r\n    }\r\n\r\n    public override int GetHashCode()\r\n    {\r\n        return HashCode.Combine(base.GetHashCode(), Category);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/ConfigSourceAttribute.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Game.Configuration;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator;\r\n\r\npublic class ConfigSourceAttribute : SettingSourceAttribute\r\n{\r\n    public ConfigSourceAttribute(Type declaringType, string label, string? description = null)\r\n        : base(declaringType, label, description)\r\n    {\r\n    }\r\n\r\n    public ConfigSourceAttribute(string? label, string? description = null)\r\n        : base(label, description)\r\n    {\r\n    }\r\n\r\n    public ConfigSourceAttribute(Type declaringType, string label, string description, int orderPosition)\r\n        : base(declaringType, label, description, orderPosition)\r\n    {\r\n    }\r\n\r\n    public ConfigSourceAttribute(string label, string description, int orderPosition)\r\n        : base(label, description, orderPosition)\r\n    {\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/DetectorNotSupportedException.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator;\r\n\r\npublic class DetectorNotSupportedException : NotSupportedException\r\n{\r\n    public DetectorNotSupportedException()\r\n        : base(\"Cannot generate the property due to have some invalid fields, please make sure that run the CanDetect() first.\")\r\n    {\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/GeneratorConfig.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator;\r\n\r\npublic abstract class GeneratorConfig;\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/GeneratorConfigExtension.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing System.Reflection;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator;\r\n\r\npublic static class GeneratorConfigExtension\r\n{\r\n    public static IDictionary<ConfigCategoryAttribute, (ConfigSourceAttribute, PropertyInfo)[]> GetOrderedConfigsSourceDictionary(this GeneratorConfig config, ConfigCategoryAttribute defaultCategory)\r\n    {\r\n        return GetOrderedConfigsSourceProperties(config)\r\n               .GroupBy(attr => attr.Item2)\r\n               .ToDictionary(\r\n                   group => group.Key ?? defaultCategory,\r\n                   group => group.Select(x => (x.Item1, x.Item3)).ToArray()\r\n               );\r\n    }\r\n\r\n    public static ICollection<(ConfigSourceAttribute, ConfigCategoryAttribute?, PropertyInfo)> GetOrderedConfigsSourceProperties(this GeneratorConfig config)\r\n        => GetConfigSourceProperties(config)\r\n           .OrderBy(attr => attr.Item1)\r\n           .ToArray();\r\n\r\n    public static IEnumerable<(ConfigSourceAttribute, ConfigCategoryAttribute?, PropertyInfo)> GetConfigSourceProperties(this GeneratorConfig config)\r\n    {\r\n        var type = config.GetType();\r\n\r\n        foreach (var property in type.GetProperties(BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.Instance))\r\n        {\r\n            var configSourceAttribute = property.GetCustomAttribute<ConfigSourceAttribute>(true);\r\n            var configCategoryAttribute = property.GetCustomAttribute<ConfigCategoryAttribute>(true);\r\n\r\n            if (configSourceAttribute == null)\r\n                continue;\r\n\r\n            yield return (configSourceAttribute, configCategoryAttribute, property);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/GeneratorNotSupportedException.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator;\r\n\r\npublic class GeneratorNotSupportedException : NotSupportedException\r\n{\r\n    public GeneratorNotSupportedException()\r\n        : base(\"Cannot generate the property due to have some invalid fields, please make sure that run the CanGenerate() first.\")\r\n    {\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/GeneratorSelector.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Diagnostics.CodeAnalysis;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator;\r\n\r\n/// <summary>\r\n/// Able to select generator by property.\r\n/// </summary>\r\n/// <typeparam name=\"TItem\">The item that want to generate the property.</typeparam>\r\n/// <typeparam name=\"TProperty\">The property in the item that will be generated.</typeparam>\r\n/// <typeparam name=\"TBaseConfig\">The config.</typeparam>\r\npublic abstract class GeneratorSelector<TItem, TProperty, TBaseConfig> : PropertyGenerator<TItem, TProperty>\r\n    where TBaseConfig : GeneratorConfig\r\n{\r\n    private Dictionary<Func<TItem, bool>, Lazy<PropertyGenerator<TItem, TProperty>>> generators { get; } = new();\r\n\r\n    private readonly KaraokeRulesetEditGeneratorConfigManager generatorConfigManager;\r\n\r\n    protected GeneratorSelector(KaraokeRulesetEditGeneratorConfigManager generatorConfigManager)\r\n    {\r\n        this.generatorConfigManager = generatorConfigManager;\r\n    }\r\n\r\n    protected void RegisterGenerator<TGenerator, TConfig>(Func<TItem, bool> selector)\r\n        where TGenerator : PropertyGenerator<TItem, TProperty>\r\n        where TConfig : TBaseConfig, new()\r\n    {\r\n        generators.Add(selector, new Lazy<PropertyGenerator<TItem, TProperty>>(() =>\r\n        {\r\n            var config = generatorConfigManager.Get<TConfig>();\r\n            return ActivatorUtils.CreateInstance<TGenerator>(config);\r\n        }));\r\n    }\r\n\r\n    protected bool TryGetGenerator(TItem item, [MaybeNullWhen(false)] out PropertyGenerator<TItem, TProperty> generator)\r\n    {\r\n        foreach (var (func, propertyGenerator) in generators)\r\n        {\r\n            if (!func(item))\r\n                continue;\r\n\r\n            generator = propertyGenerator.Value;\r\n            return true;\r\n        }\r\n\r\n        generator = null;\r\n        return false;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/Language/LanguageDetector.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Globalization;\r\nusing System.Linq;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.Language;\r\n\r\npublic class LanguageDetector : LyricPropertyDetector<CultureInfo?, LanguageDetectorConfig>\r\n{\r\n    private readonly LanguageDetection.LanguageDetector detector = new();\r\n\r\n    public LanguageDetector(LanguageDetectorConfig config)\r\n        : base(config)\r\n    {\r\n        var targetLanguages = config.AcceptLanguages.Value ?? Array.Empty<CultureInfo>();\r\n\r\n        if (targetLanguages.Any())\r\n        {\r\n            detector.AddLanguages(targetLanguages.Select(x => x.Name).ToArray());\r\n        }\r\n        else\r\n        {\r\n            detector.AddAllLanguages();\r\n        }\r\n    }\r\n\r\n    protected override LocalisableString? GetInvalidMessageFromItem(Lyric item)\r\n    {\r\n        if (string.IsNullOrWhiteSpace(item.Text))\r\n            return \"Lyric should not be empty.\";\r\n\r\n        return null;\r\n    }\r\n\r\n    protected override CultureInfo? DetectFromItem(Lyric item)\r\n    {\r\n        var result = detector.DetectAll(item.Text);\r\n        string? languageCode = result.FirstOrDefault()?.Language;\r\n\r\n        return languageCode == null ? null : new CultureInfo(languageCode);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/Language/LanguageDetectorConfig.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Globalization;\r\nusing osu.Framework.Bindables;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.Language;\r\n\r\npublic class LanguageDetectorConfig : GeneratorConfig\r\n{\r\n    [ConfigSource(\"Accept languages\", \"All accepted languages.\")]\r\n    public Bindable<CultureInfo[]> AcceptLanguages { get; } = new(Array.Empty<CultureInfo>());\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/LyricGeneratorSelector.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Globalization;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics;\r\n\r\npublic abstract class LyricGeneratorSelector<TProperty, TBaseConfig> : GeneratorSelector<Lyric, TProperty, TBaseConfig>\r\n    where TBaseConfig : GeneratorConfig\r\n{\r\n    protected LyricGeneratorSelector(KaraokeRulesetEditGeneratorConfigManager generatorConfigManager)\r\n        : base(generatorConfigManager)\r\n    {\r\n    }\r\n\r\n    protected void RegisterGenerator<TGenerator, TConfig>(CultureInfo cultureInfo)\r\n        where TGenerator : PropertyGenerator<Lyric, TProperty>\r\n        where TConfig : TBaseConfig, new()\r\n    {\r\n        RegisterGenerator<TGenerator, TConfig>(x => EqualityComparer<CultureInfo>.Default.Equals(x.Language, cultureInfo));\r\n    }\r\n\r\n    protected override LocalisableString? GetInvalidMessageFromItem(Lyric item)\r\n    {\r\n        if (item.Language == null)\r\n            return \"Oops, language is missing.\";\r\n\r\n        if (string.IsNullOrWhiteSpace(item.Text))\r\n            return \"Should have the text in the lyric\";\r\n\r\n        if (!TryGetGenerator(item, out var generator))\r\n            return \"Sorry, the language of lyric is not supported yet.\";\r\n\r\n        return generator.GetInvalidMessage(item);\r\n    }\r\n\r\n    protected override TProperty GenerateFromItem(Lyric item)\r\n    {\r\n        if (item.Language == null)\r\n            throw new GeneratorNotSupportedException();\r\n\r\n        if (!TryGetGenerator(item, out var generator))\r\n            throw new GeneratorNotSupportedException();\r\n\r\n        return generator.Generate(item);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/LyricPropertyDetector.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics;\r\n\r\n/// <summary>\r\n/// Base interface of the detector.\r\n/// </summary>\r\n/// <typeparam name=\"TProperty\"></typeparam>\r\n/// <typeparam name=\"TConfig\"></typeparam>\r\npublic abstract class LyricPropertyDetector<TProperty, TConfig> : PropertyDetector<Lyric, TProperty, TConfig>\r\n    where TConfig : GeneratorConfig, new()\r\n{\r\n    protected LyricPropertyDetector(TConfig config)\r\n        : base(config)\r\n    {\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/LyricPropertyGenerator.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics;\r\n\r\n/// <summary>\r\n/// Base interface of the auto-generator.\r\n/// </summary>\r\n/// <typeparam name=\"TProperty\"></typeparam>\r\n/// <typeparam name=\"TConfig\"></typeparam>\r\npublic abstract class LyricPropertyGenerator<TProperty, TConfig> : PropertyGenerator<Lyric, TProperty, TConfig>\r\n    where TConfig : GeneratorConfig, new()\r\n{\r\n    protected LyricPropertyGenerator(TConfig config)\r\n        : base(config)\r\n    {\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/Notes/NoteGenerator.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Extensions.IEnumerableExtensions;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Extensions;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.Notes;\r\n\r\npublic class NoteGenerator : LyricPropertyGenerator<Note[], NoteGeneratorConfig>\r\n{\r\n    public NoteGenerator(NoteGeneratorConfig config)\r\n        : base(config)\r\n    {\r\n    }\r\n\r\n    protected override LocalisableString? GetInvalidMessageFromItem(Lyric item)\r\n    {\r\n        var timeTags = item.TimeTags;\r\n\r\n        if (item.TimeTags.Count < 2)\r\n            return \"Sorry, lyric must have at least two time-tags.\";\r\n\r\n        if (timeTags.Any(x => x.Time == null))\r\n            return \"All time-tag should have the time.\";\r\n\r\n        return null;\r\n    }\r\n\r\n    protected override Note[] GenerateFromItem(Lyric item)\r\n    {\r\n        var timeTags = TimeTagsUtils.ToTimeBasedDictionary(item.TimeTags);\r\n        var notes = new List<Note>();\r\n\r\n        foreach (var timeTag in timeTags)\r\n        {\r\n            // should not continue if\r\n            if (timeTags.LastOrDefault().Key == timeTag.Key)\r\n                break;\r\n\r\n            (double _, var textIndex) = timeTag;\r\n            (double _, var nextTextIndex) = timeTags.GetNext(timeTag);\r\n\r\n            int gapIndex = TextIndexUtils.ToGapIndex(textIndex);\r\n            int nextGapIndex = TextIndexUtils.ToGapIndex(nextTextIndex);\r\n\r\n            // prevent reverse time-tag to generate the note.\r\n            if (gapIndex >= nextGapIndex)\r\n                continue;\r\n\r\n            int timeTagIndex = timeTags.IndexOf(timeTag);\r\n            string text = item.Text[gapIndex..nextGapIndex];\r\n            string? ruby = item.RubyTags?.Where(x => x.StartIndex == gapIndex && x.EndIndex == nextGapIndex - 1).FirstOrDefault()?.Text;\r\n\r\n            if (!string.IsNullOrEmpty(text))\r\n            {\r\n                notes.Add(new Note\r\n                {\r\n                    Text = text,\r\n                    RubyText = ruby,\r\n                    ReferenceLyricId = item.ID,\r\n                    // technically this property should be assigned by beatmap processor, but should be OK to assign here for testing purpose.\r\n                    ReferenceLyric = item,\r\n                    ReferenceTimeTagIndex = timeTagIndex,\r\n                });\r\n            }\r\n        }\r\n\r\n        return notes.ToArray();\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/Notes/NoteGeneratorConfig.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.Notes;\r\n\r\npublic class NoteGeneratorConfig : GeneratorConfig;\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/ReferenceLyric/ReferenceLyricDetector.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.ReferenceLyric;\r\n\r\npublic class ReferenceLyricDetector : LyricPropertyDetector<Lyric?, ReferenceLyricDetectorConfig>\r\n{\r\n    private readonly Lyric[] lyrics;\r\n\r\n    public ReferenceLyricDetector(IEnumerable<Lyric> lyrics, ReferenceLyricDetectorConfig config)\r\n        : base(config)\r\n    {\r\n        this.lyrics = lyrics.ToArray();\r\n    }\r\n\r\n    protected override LocalisableString? GetInvalidMessageFromItem(Lyric item)\r\n    {\r\n        var referencedLyric = getReferenceLyric(item);\r\n        if (referencedLyric == null)\r\n            return \"There's no matched lyric.\";\r\n\r\n        return null;\r\n    }\r\n\r\n    protected override Lyric? DetectFromItem(Lyric item)\r\n    {\r\n        var referencedLyric = getReferenceLyric(item);\r\n        if (referencedLyric == null)\r\n            return null;\r\n\r\n        // prevent first lyric(referenced lyric) reference by other lyric.\r\n        if (referencedLyric.Order > item.Order)\r\n            return null;\r\n\r\n        return referencedLyric;\r\n    }\r\n\r\n    private Lyric? getReferenceLyric(Lyric lyric)\r\n    {\r\n        if (!lyrics.Contains(lyric))\r\n            throw new InvalidOperationException();\r\n\r\n        return lyrics.Except(new[] { lyric }).OrderBy(x => x.Order).FirstOrDefault(x => canBeReferenced(lyric, x));\r\n    }\r\n\r\n    private bool canBeReferenced(Lyric lyric, Lyric referencedLyric)\r\n    {\r\n        string lyricText = lyric.Text;\r\n        string referencedLyricText = referencedLyric.Text;\r\n\r\n        if (lyricText == referencedLyricText)\r\n            return true;\r\n\r\n        if (!Config.IgnorePrefixAndPostfixSymbol.Value)\r\n            return false;\r\n\r\n        // check if contains intersect part between two lyrics.\r\n        if (!lyricText.Contains(referencedLyricText) && !referencedLyricText.Contains(lyricText))\r\n            return false;\r\n\r\n        // check if except part are all symbols.\r\n        var except1 = lyricText.Except(referencedLyricText);\r\n        var except2 = referencedLyricText.Except(lyricText);\r\n        return allCharsEmptyOrSymbol(except1) && allCharsEmptyOrSymbol(except2);\r\n\r\n        static bool allCharsEmptyOrSymbol(IEnumerable<char> chars)\r\n            => chars.All(x => CharUtils.IsSpacing(x) || CharUtils.IsAsciiSymbol(x));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/ReferenceLyric/ReferenceLyricDetectorConfig.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Bindables;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.ReferenceLyric;\r\n\r\npublic class ReferenceLyricDetectorConfig : GeneratorConfig\r\n{\r\n    [ConfigSource(\"Ruby as Katakana\", \"Ruby as Katakana.\")]\r\n    public Bindable<bool> IgnorePrefixAndPostfixSymbol { get; } = new BindableBool(true);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/Romanisation/Ja/JaRomanisationGenerator.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.IO;\r\nusing System.Linq;\r\nusing Lucene.Net.Analysis;\r\nusing Lucene.Net.Analysis.Ja;\r\nusing Lucene.Net.Analysis.TokenAttributes;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Extensions;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.Romanisation.Ja;\r\n\r\npublic class JaRomanisationGenerator : RomanisationGenerator<JaRomanisationGeneratorConfig>\r\n{\r\n    private readonly Analyzer analyzer;\r\n\r\n    public JaRomanisationGenerator(JaRomanisationGeneratorConfig config)\r\n        : base(config)\r\n    {\r\n        analyzer = Analyzer.NewAnonymous((fieldName, reader) =>\r\n        {\r\n            Tokenizer tokenizer = new JapaneseTokenizer(reader, null, true, JapaneseTokenizerMode.SEARCH);\r\n            return new TokenStreamComponents(tokenizer, new JapaneseReadingFormFilter(tokenizer, false));\r\n        });\r\n    }\r\n\r\n    protected override IReadOnlyDictionary<TimeTag, RomanisationGenerateResult> GenerateFromItem(Lyric item)\r\n    {\r\n        // Tokenize the text\r\n        string text = item.Text;\r\n        var tokenStream = analyzer.GetTokenStream(\"dummy\", new StringReader(text));\r\n\r\n        // get the processing tags.\r\n        var parameters = generateParameters(text, tokenStream, Config).ToArray();\r\n\r\n        // then, trying to mapping them with the time-tags.\r\n        return Convert(item.TimeTags, parameters);\r\n    }\r\n\r\n    private static IEnumerable<RomanisationGeneratorParameter> generateParameters(string text, TokenStream tokenStream, JaRomanisationGeneratorConfig config)\r\n    {\r\n        // Reset the stream and convert all result\r\n        tokenStream.Reset();\r\n\r\n        while (true)\r\n        {\r\n            // Read next token\r\n            tokenStream.ClearAttributes();\r\n            tokenStream.IncrementToken();\r\n\r\n            // Get result and offset\r\n            var charTermAttribute = tokenStream.GetAttribute<ICharTermAttribute>();\r\n            var offsetAttribute = tokenStream.GetAttribute<IOffsetAttribute>();\r\n\r\n            // Get parsed result, result is Katakana.\r\n            string katakana = charTermAttribute.ToString();\r\n            if (string.IsNullOrEmpty(katakana))\r\n                break;\r\n\r\n            string parentText = text[offsetAttribute.StartOffset..offsetAttribute.EndOffset];\r\n            bool fromKanji = JpStringUtils.ToKatakana(katakana) != JpStringUtils.ToKatakana(parentText);\r\n\r\n            // Convert to romanised syllable.\r\n            string romanisedSyllable = JpStringUtils.ToRomaji(katakana);\r\n            if (config.Uppercase.Value)\r\n                romanisedSyllable = romanisedSyllable.ToUpper();\r\n\r\n            // Make tag\r\n            yield return new RomanisationGeneratorParameter\r\n            {\r\n                FromKanji = fromKanji,\r\n                StartIndex = offsetAttribute.StartOffset,\r\n                EndIndex = offsetAttribute.EndOffset - 1,\r\n                RomanisedSyllable = romanisedSyllable,\r\n            };\r\n        }\r\n\r\n        // Dispose\r\n        tokenStream.End();\r\n        tokenStream.Dispose();\r\n    }\r\n\r\n    internal static IReadOnlyDictionary<TimeTag, RomanisationGenerateResult> Convert(IList<TimeTag> timeTags, IList<RomanisationGeneratorParameter> parameters)\r\n    {\r\n        var group = createGroup(timeTags, parameters);\r\n        return group.ToDictionary(k => k.Key, x =>\r\n        {\r\n            bool isFirst = timeTags.IndexOf(x.Key) == 0; // todo: use better to mark the first syllable.\r\n            string romanisedSyllable = string.Join(\" \", x.Value.Select(r => r.RomanisedSyllable));\r\n\r\n            return new RomanisationGenerateResult\r\n            {\r\n                FirstSyllable = isFirst,\r\n                RomanisedSyllable = romanisedSyllable,\r\n            };\r\n        });\r\n\r\n        static IReadOnlyDictionary<TimeTag, List<RomanisationGeneratorParameter>> createGroup(IList<TimeTag> timeTags, IList<RomanisationGeneratorParameter> parameters)\r\n        {\r\n            var dictionary = timeTags.ToDictionary(x => x, v => new List<RomanisationGeneratorParameter>());\r\n\r\n            int processedIndex = 0;\r\n\r\n            foreach (var (timeTag, list) in dictionary)\r\n            {\r\n                while (processedIndex < parameters.Count && isTimeTagInRange(timeTags, timeTag, parameters[processedIndex]))\r\n                {\r\n                    list.Add(parameters[processedIndex]);\r\n                    processedIndex++;\r\n                }\r\n            }\r\n\r\n            if (processedIndex < parameters.Count - 1)\r\n                throw new InvalidOperationException(\"Still have romanisations that haven't process\");\r\n\r\n            return dictionary;\r\n        }\r\n\r\n        static bool isTimeTagInRange(IEnumerable<TimeTag> timeTags, TimeTag currentTimeTag, RomanisationGeneratorParameter parameter)\r\n        {\r\n            if (currentTimeTag.Index.State == TextIndex.IndexState.End)\r\n                return false;\r\n\r\n            int romanisationIndex = parameter.StartIndex;\r\n\r\n            var nextTimeTag = timeTags.GetNextMatch(currentTimeTag, x => x.Index > currentTimeTag.Index && x.Index.State == TextIndex.IndexState.Start);\r\n            if (nextTimeTag == null)\r\n                return romanisationIndex >= currentTimeTag.Index.Index;\r\n\r\n            return romanisationIndex >= currentTimeTag.Index.Index && romanisationIndex < nextTimeTag.Index.Index;\r\n        }\r\n    }\r\n\r\n    internal class RomanisationGeneratorParameter\r\n    {\r\n        public bool FromKanji { get; set; }\r\n\r\n        public int StartIndex { get; set; }\r\n\r\n        public int EndIndex { get; set; }\r\n\r\n        public string RomanisedSyllable { get; set; } = string.Empty;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/Romanisation/Ja/JaRomanisationGeneratorConfig.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Bindables;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.Romanisation.Ja;\r\n\r\npublic class JaRomanisationGeneratorConfig : RomanisationGeneratorConfig\r\n{\r\n    [ConfigSource(\"Uppercase\", \"Export romanisation with uppercase.\")]\r\n    public Bindable<bool> Uppercase { get; } = new BindableBool();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/Romanisation/RomanisationGenerateResult.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.Romanisation;\r\n\r\npublic struct RomanisationGenerateResult\r\n{\r\n    public bool FirstSyllable { get; set; }\r\n\r\n    public string? RomanisedSyllable { get; set; }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/Romanisation/RomanisationGenerator.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.Romanisation;\r\n\r\npublic abstract class RomanisationGenerator<TConfig> : LyricPropertyGenerator<IReadOnlyDictionary<TimeTag, RomanisationGenerateResult>, TConfig>\r\n    where TConfig : RomanisationGeneratorConfig, new()\r\n{\r\n    protected RomanisationGenerator(TConfig config)\r\n        : base(config)\r\n    {\r\n    }\r\n\r\n    protected override LocalisableString? GetInvalidMessageFromItem(Lyric item)\r\n    {\r\n        if (string.IsNullOrWhiteSpace(item.Text))\r\n            return \"Lyric should not be empty.\";\r\n\r\n        if (item.TimeTags.FirstOrDefault()?.Index != new TextIndex())\r\n            return \"Should have at least one index and that index should at the start of the lyric.\";\r\n\r\n        return null;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/Romanisation/RomanisationGeneratorConfig.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.Romanisation;\r\n\r\npublic abstract class RomanisationGeneratorConfig : GeneratorConfig;\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/Romanisation/RomanisationGeneratorSelector.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Globalization;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.Romanisation.Ja;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.Romanisation;\r\n\r\npublic class RomanisationGeneratorSelector : LyricGeneratorSelector<IReadOnlyDictionary<TimeTag, RomanisationGenerateResult>, RomanisationGeneratorConfig>\r\n{\r\n    public RomanisationGeneratorSelector(KaraokeRulesetEditGeneratorConfigManager generatorConfigManager)\r\n        : base(generatorConfigManager)\r\n    {\r\n        RegisterGenerator<JaRomanisationGenerator, JaRomanisationGeneratorConfig>(new CultureInfo(17));\r\n        RegisterGenerator<JaRomanisationGenerator, JaRomanisationGeneratorConfig>(new CultureInfo(1041));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/RubyTags/Ja/JaRubyTagGenerator.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.IO;\r\nusing System.Linq;\r\nusing Lucene.Net.Analysis;\r\nusing Lucene.Net.Analysis.Ja;\r\nusing Lucene.Net.Analysis.TokenAttributes;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.RubyTags.Ja;\r\n\r\npublic class JaRubyTagGenerator : RubyTagGenerator<JaRubyTagGeneratorConfig>\r\n{\r\n    private readonly Analyzer analyzer;\r\n\r\n    public JaRubyTagGenerator(JaRubyTagGeneratorConfig config)\r\n        : base(config)\r\n    {\r\n        analyzer = Analyzer.NewAnonymous((fieldName, reader) =>\r\n        {\r\n            Tokenizer tokenizer = new JapaneseTokenizer(reader, null, true, JapaneseTokenizerMode.SEARCH);\r\n            return new TokenStreamComponents(tokenizer, new JapaneseReadingFormFilter(tokenizer, false));\r\n        });\r\n    }\r\n\r\n    protected override RubyTag[] GenerateFromItem(Lyric item)\r\n    {\r\n        // Tokenize the text\r\n        string text = item.Text;\r\n        var tokenStream = analyzer.GetTokenStream(\"dummy\", new StringReader(text));\r\n\r\n        return getProcessingRubyTags(text, tokenStream, Config).ToArray();\r\n    }\r\n\r\n    private static IEnumerable<RubyTag> getProcessingRubyTags(string text, TokenStream tokenStream, JaRubyTagGeneratorConfig config)\r\n    {\r\n        // Reset the stream and convert all result\r\n        tokenStream.Reset();\r\n\r\n        while (true)\r\n        {\r\n            // Read next token\r\n            tokenStream.ClearAttributes();\r\n            tokenStream.IncrementToken();\r\n\r\n            // Get result and offset\r\n            var charTermAttribute = tokenStream.GetAttribute<ICharTermAttribute>();\r\n            var offsetAttribute = tokenStream.GetAttribute<IOffsetAttribute>();\r\n\r\n            // Get parsed result, result is Katakana.\r\n            string katakana = charTermAttribute.ToString();\r\n            if (string.IsNullOrEmpty(katakana))\r\n                break;\r\n\r\n            // Convert to Hiragana as default.\r\n            string hiragana = JpStringUtils.ToHiragana(katakana);\r\n\r\n            if (!config.EnableDuplicatedRuby.Value)\r\n            {\r\n                // Not add duplicated ruby if same as parent.\r\n                string parentText = text[offsetAttribute.StartOffset..offsetAttribute.EndOffset];\r\n                if (parentText == katakana || parentText == hiragana)\r\n                    continue;\r\n            }\r\n\r\n            // Make tag\r\n            yield return new RubyTag\r\n            {\r\n                Text = config.RubyAsKatakana.Value ? katakana : hiragana,\r\n                StartIndex = offsetAttribute.StartOffset,\r\n                EndIndex = offsetAttribute.EndOffset - 1,\r\n            };\r\n        }\r\n\r\n        // Dispose\r\n        tokenStream.End();\r\n        tokenStream.Dispose();\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/RubyTags/Ja/JaRubyTagGeneratorConfig.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Bindables;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.RubyTags.Ja;\r\n\r\npublic class JaRubyTagGeneratorConfig : RubyTagGeneratorConfig\r\n{\r\n    /// <summary>\r\n    /// Generate ruby as Katakana.\r\n    /// </summary>\r\n    [ConfigSource(\"Ruby as Katakana\", \"Ruby as Katakana.\")]\r\n    public Bindable<bool> RubyAsKatakana { get; } = new BindableBool();\r\n\r\n    /// <summary>\r\n    /// Generate ruby even it's same as lyric.\r\n    /// </summary>\r\n    [ConfigSource(\"Enable duplicated ruby.\", \"Enable output duplicated ruby even it's match with lyric.\")]\r\n    public Bindable<bool> EnableDuplicatedRuby { get; } = new BindableBool();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/RubyTags/RubyTagGenerator.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.RubyTags;\r\n\r\npublic abstract class RubyTagGenerator<TConfig> : LyricPropertyGenerator<RubyTag[], TConfig>\r\n    where TConfig : RubyTagGeneratorConfig, new()\r\n{\r\n    protected RubyTagGenerator(TConfig config)\r\n        : base(config)\r\n    {\r\n    }\r\n\r\n    protected override LocalisableString? GetInvalidMessageFromItem(Lyric item)\r\n    {\r\n        if (string.IsNullOrWhiteSpace(item.Text))\r\n            return \"Lyric should not be empty.\";\r\n\r\n        return null;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/RubyTags/RubyTagGeneratorConfig.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.RubyTags;\r\n\r\npublic abstract class RubyTagGeneratorConfig : GeneratorConfig;\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/RubyTags/RubyTagGeneratorSelector.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Globalization;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.RubyTags.Ja;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.RubyTags;\r\n\r\npublic class RubyTagGeneratorSelector : LyricGeneratorSelector<RubyTag[], RubyTagGeneratorConfig>\r\n{\r\n    public RubyTagGeneratorSelector(KaraokeRulesetEditGeneratorConfigManager generatorConfigManager)\r\n        : base(generatorConfigManager)\r\n    {\r\n        RegisterGenerator<JaRubyTagGenerator, JaRubyTagGeneratorConfig>(new CultureInfo(17));\r\n        RegisterGenerator<JaRubyTagGenerator, JaRubyTagGeneratorConfig>(new CultureInfo(1041));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/TimeTags/Ja/JaTimeTagGenerator.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.TimeTags.Ja;\r\n\r\npublic class JaTimeTagGenerator : TimeTagGenerator<JaTimeTagGeneratorConfig>\r\n{\r\n    public JaTimeTagGenerator(JaTimeTagGeneratorConfig config)\r\n        : base(config)\r\n    {\r\n    }\r\n\r\n    /// <summary>\r\n    /// Thanks for RhythmKaTTE's author writing this logic into C#.<br/>\r\n    /// http://juna-idler.blogspot.com/2016/05/rhythmkatte-version-01.html\r\n    /// </summary>\r\n    protected override void TimeTagLogic(Lyric lyric, List<TimeTag> timeTags)\r\n    {\r\n        timeTags.AddRange(generateTimeTagByText(lyric.Text));\r\n\r\n        foreach (var ruby in lyric.RubyTags)\r\n        {\r\n            // remove exist time tag\r\n            timeTags.RemoveAll(x => x.Index > new TextIndex(ruby.StartIndex) && x.Index < new TextIndex(ruby.EndIndex, TextIndex.IndexState.End));\r\n\r\n            // add new time tags created from ruby\r\n            var rubyTags = generateTimeTagByText(ruby.Text);\r\n            var shiftingTimeTags = rubyTags.Select((x, _) => new TimeTag(new TextIndex(ruby.StartIndex, x.Index.State), x.Time));\r\n            timeTags.AddRange(shiftingTimeTags);\r\n        }\r\n    }\r\n\r\n    private IEnumerable<TimeTag> generateTimeTagByText(string text)\r\n    {\r\n        if (string.IsNullOrEmpty(text))\r\n            yield break;\r\n\r\n        for (int i = 1; i < text.Length; i++)\r\n        {\r\n            char c = text[i];\r\n            char pc = text[i - 1];\r\n\r\n            if (CharUtils.IsSpacing(c) && Config.CheckWhiteSpace.Value)\r\n            {\r\n                // ignore continuous white space.\r\n                if (CharUtils.IsSpacing(pc))\r\n                    continue;\r\n\r\n                var timeTag = Config.CheckWhiteSpaceKeyUp.Value\r\n                    ? new TimeTag(new TextIndex(i - 1, TextIndex.IndexState.End))\r\n                    : new TimeTag(new TextIndex(i));\r\n\r\n                if (CharUtils.IsEnglish(pc))\r\n                {\r\n                    if (Config.CheckWhiteSpaceAlphabet.Value)\r\n                        yield return timeTag;\r\n                }\r\n                else if (char.IsDigit(pc))\r\n                {\r\n                    if (Config.CheckWhiteSpaceDigit.Value)\r\n                        yield return timeTag;\r\n                }\r\n                else if (CharUtils.IsAsciiSymbol(pc))\r\n                {\r\n                    if (Config.CheckWhiteSpaceAsciiSymbol.Value)\r\n                        yield return timeTag;\r\n                }\r\n                else\r\n                {\r\n                    yield return timeTag;\r\n                }\r\n            }\r\n            else if (CharUtils.IsEnglish(c) || char.IsNumber(c) || CharUtils.IsAsciiSymbol(c))\r\n            {\r\n                if (CharUtils.IsSpacing(pc) || (!CharUtils.IsEnglish(pc) && !char.IsNumber(pc) && !CharUtils.IsAsciiSymbol(pc)))\r\n                {\r\n                    yield return new TimeTag(new TextIndex(i));\r\n                }\r\n            }\r\n            else if (CharUtils.IsSpacing(pc))\r\n            {\r\n                yield return new TimeTag(new TextIndex(i));\r\n            }\r\n            else\r\n            {\r\n                switch (c)\r\n                {\r\n                    case 'ゃ':\r\n                    case 'ゅ':\r\n                    case 'ょ':\r\n                    case 'ャ':\r\n                    case 'ュ':\r\n                    case 'ョ':\r\n                    case 'ぁ':\r\n                    case 'ぃ':\r\n                    case 'ぅ':\r\n                    case 'ぇ':\r\n                    case 'ぉ':\r\n                    case 'ァ':\r\n                    case 'ィ':\r\n                    case 'ゥ':\r\n                    case 'ェ':\r\n                    case 'ォ':\r\n                    case 'ー':\r\n                    case '～':\r\n                        break;\r\n\r\n                    case 'ん':\r\n                        if (Config.Checkん.Value)\r\n                            yield return new TimeTag(new TextIndex(i));\r\n\r\n                        break;\r\n\r\n                    case 'っ':\r\n                        if (Config.Checkっ.Value)\r\n                            yield return new TimeTag(new TextIndex(i));\r\n\r\n                        break;\r\n\r\n                    default:\r\n                        yield return new TimeTag(new TextIndex(i));\r\n\r\n                        break;\r\n                }\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/TimeTags/Ja/JaTimeTagGeneratorConfig.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.TimeTags.Ja;\r\n\r\npublic class JaTimeTagGeneratorConfig : TimeTagGeneratorConfig\r\n{\r\n    /// <summary>\r\n    /// Add the <see cref=\"TimeTag\"/> if character is \"ん\"\r\n    /// </summary>\r\n    [ConfigCategory(CATEGORY_CHECK_CHARACTER)]\r\n    [ConfigSource(\"Check ん\", \"Check ん or not.\")]\r\n    public Bindable<bool> Checkん { get; } = new BindableBool(true);\r\n\r\n    /// <summary>\r\n    /// Add the <see cref=\"TimeTag\"/> if character is \"っ\"\r\n    /// </summary>\r\n    [ConfigCategory(CATEGORY_CHECK_CHARACTER)]\r\n    [ConfigSource(\"Check っ\", \"Check っ or not.\")]\r\n    public Bindable<bool> Checkっ { get; } = new BindableBool();\r\n\r\n    /// <summary>\r\n    /// Add the <see cref=\"TimeTag\"/> if spacing is next of the alphabet.<br/>\r\n    /// This feature will work only if enable the <see cref=\"TimeTagGeneratorConfig.CheckWhiteSpace\"/>\r\n    /// </summary>\r\n    [ConfigCategory(CATEGORY_CHECK_WHITE_SPACE)]\r\n    [ConfigSource(\"Check white space alphabet\", \"Check white space alphabet.\")]\r\n    public Bindable<bool> CheckWhiteSpaceAlphabet { get; } = new BindableBool();\r\n\r\n    /// <summary>\r\n    /// Add the <see cref=\"TimeTag\"/> if spacing is next of the digit.<br/>\r\n    /// This feature will work only if enable the <see cref=\"TimeTagGeneratorConfig.CheckWhiteSpace\"/>\r\n    /// </summary>\r\n    [ConfigCategory(CATEGORY_CHECK_WHITE_SPACE)]\r\n    [ConfigSource(\"Check white space digit\", \"Check white space digit.\")]\r\n    public Bindable<bool> CheckWhiteSpaceDigit { get; } = new BindableBool();\r\n\r\n    /// <summary>\r\n    /// Add the <see cref=\"TimeTag\"/> if spacing is next of the symbol.<br/>\r\n    /// This feature will work only if enable the <see cref=\"TimeTagGeneratorConfig.CheckWhiteSpace\"/>\r\n    /// </summary>\r\n    [ConfigCategory(CATEGORY_CHECK_WHITE_SPACE)]\r\n    [ConfigSource(\"Check white space ascii symbol\", \"Check white space ascii symbol.\")]\r\n    public Bindable<bool> CheckWhiteSpaceAsciiSymbol { get; } = new BindableBool();\r\n\r\n    public JaTimeTagGeneratorConfig()\r\n    {\r\n        CheckLineEndKeyUp.Default = true;\r\n        CheckLineEndKeyUp.SetDefault();\r\n\r\n        CheckWhiteSpace.Default = true;\r\n        CheckWhiteSpace.SetDefault();\r\n\r\n        CheckWhiteSpaceKeyUp.Default = true;\r\n        CheckWhiteSpaceKeyUp.SetDefault();\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/TimeTags/TimeTagGenerator.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.TimeTags;\r\n\r\npublic abstract class TimeTagGenerator<TConfig> : LyricPropertyGenerator<TimeTag[], TConfig>\r\n    where TConfig : TimeTagGeneratorConfig, new()\r\n{\r\n    protected TimeTagGenerator(TConfig config)\r\n        : base(config)\r\n    {\r\n    }\r\n\r\n    protected override LocalisableString? GetInvalidMessageFromItem(Lyric item)\r\n    {\r\n        if (string.IsNullOrEmpty(item.Text))\r\n            return \"Lyric should not be empty.\";\r\n\r\n        return null;\r\n    }\r\n\r\n    protected sealed override TimeTag[] GenerateFromItem(Lyric item)\r\n    {\r\n        var timeTags = new List<TimeTag>();\r\n        string text = item.Text;\r\n\r\n        if (string.IsNullOrEmpty(text))\r\n            return timeTags.ToArray();\r\n\r\n        if (string.IsNullOrWhiteSpace(text))\r\n        {\r\n            if (Config.CheckBlankLine.Value)\r\n                timeTags.Add(new TimeTag(new TextIndex(0)));\r\n\r\n            return timeTags.ToArray();\r\n        }\r\n\r\n        // create tag at start of lyric\r\n        timeTags.Add(new TimeTag(new TextIndex(0)));\r\n\r\n        if (Config.CheckLineEndKeyUp.Value)\r\n            timeTags.Add(new TimeTag(new TextIndex(text.Length - 1, TextIndex.IndexState.End)));\r\n\r\n        TimeTagLogic(item, timeTags);\r\n\r\n        return timeTags.OrderBy(x => x.Index).ToArray();\r\n    }\r\n\r\n    protected abstract void TimeTagLogic(Lyric lyric, List<TimeTag> timeTags);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/TimeTags/TimeTagGeneratorConfig.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.TimeTags;\r\n\r\npublic abstract class TimeTagGeneratorConfig : GeneratorConfig\r\n{\r\n    protected const string CATEGORY_CHECK_CHARACTER = \"Character checking\";\r\n    protected const string CATEGORY_CHECK_LINE_END = \"Line end checking\";\r\n    protected const string CATEGORY_CHECK_WHITE_SPACE = \"White space checking\";\r\n\r\n    /// <summary>\r\n    /// Will create a <see cref=\"TimeTag\"/> at the first of the lyric if only contains spacing in the <see cref=\"Lyric\"/>.\r\n    /// </summary>\r\n    [ConfigCategory(CATEGORY_CHECK_CHARACTER)]\r\n    [ConfigSource(\"Check blank line\", \"Check blank line or not.\")]\r\n    public Bindable<bool> CheckBlankLine { get; } = new BindableBool();\r\n\r\n    /// <summary>\r\n    /// Add end <see cref=\"TimeTag\"/> at the end of the <see cref=\"Lyric\"/>.\r\n    /// </summary>\r\n    [ConfigCategory(CATEGORY_CHECK_LINE_END)]\r\n    [ConfigSource(\"Use key-up time tag in line end\", \"Use key-up time tag in line end\")]\r\n    public Bindable<bool> CheckLineEndKeyUp { get; } = new BindableBool();\r\n\r\n    /// <summary>\r\n    /// Will add the <see cref=\"TimeTag\"/> if meet the spacing.\r\n    /// </summary>\r\n    [ConfigCategory(CATEGORY_CHECK_WHITE_SPACE)]\r\n    [ConfigSource(\"Check white space\", \"Check white space\")]\r\n    public Bindable<bool> CheckWhiteSpace { get; } = new BindableBool();\r\n\r\n    /// <summary>\r\n    /// Add the end <see cref=\"TimeTag\"/> instead.<br/>\r\n    /// This feature will work only if enable the <see cref=\"CheckWhiteSpace\"/>.\r\n    /// </summary>\r\n    [ConfigCategory(CATEGORY_CHECK_WHITE_SPACE)]\r\n    [ConfigSource(\"Use key-up\", \"Use key-up\")]\r\n    public Bindable<bool> CheckWhiteSpaceKeyUp { get; } = new BindableBool();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/TimeTags/TimeTagGeneratorSelector.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Globalization;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.TimeTags.Ja;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.TimeTags.Zh;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.TimeTags;\r\n\r\npublic class TimeTagGeneratorSelector : LyricGeneratorSelector<TimeTag[], TimeTagGeneratorConfig>\r\n{\r\n    public TimeTagGeneratorSelector(KaraokeRulesetEditGeneratorConfigManager generatorConfigManager)\r\n        : base(generatorConfigManager)\r\n    {\r\n        RegisterGenerator<JaTimeTagGenerator, JaTimeTagGeneratorConfig>(new CultureInfo(17));\r\n        RegisterGenerator<JaTimeTagGenerator, JaTimeTagGeneratorConfig>(new CultureInfo(1041));\r\n        RegisterGenerator<ZhTimeTagGenerator, ZhTimeTagGeneratorConfig>(new CultureInfo(1028));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/TimeTags/Zh/ZhTimeTagGenerator.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.TimeTags.Zh;\r\n\r\npublic class ZhTimeTagGenerator : TimeTagGenerator<ZhTimeTagGeneratorConfig>\r\n{\r\n    public ZhTimeTagGenerator(ZhTimeTagGeneratorConfig config)\r\n        : base(config)\r\n    {\r\n    }\r\n\r\n    protected override void TimeTagLogic(Lyric lyric, List<TimeTag> timeTags)\r\n    {\r\n        string text = lyric.Text;\r\n\r\n        for (int i = 1; i < text.Length; i++)\r\n        {\r\n            if (CharUtils.IsChinese(text[i]))\r\n            {\r\n                timeTags.Add(new TimeTag(new TextIndex(i)));\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/Lyrics/TimeTags/Zh/ZhTimeTagGeneratorConfig.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.TimeTags.Zh;\r\n\r\npublic class ZhTimeTagGeneratorConfig : TimeTagGeneratorConfig;\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/PropertyDetector.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Localisation;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator;\r\n\r\npublic abstract class PropertyDetector<TItem, TProperty, TConfig> : PropertyDetector<TItem, TProperty>\r\n    where TConfig : GeneratorConfig, new()\r\n{\r\n    protected readonly TConfig Config;\r\n\r\n    protected PropertyDetector(TConfig config)\r\n    {\r\n        Config = config;\r\n    }\r\n}\r\n\r\npublic abstract class PropertyDetector<TItem, TProperty>\r\n{\r\n    /// <summary>\r\n    /// Determined if detect <typeparamref name=\"TProperty\"/> from <typeparamref name=\"TItem\"/> is supported.\r\n    /// </summary>\r\n    /// <param name=\"item\"></param>\r\n    /// <returns></returns>\r\n    public bool CanDetect(TItem item) => GetInvalidMessage(item) == null;\r\n\r\n    /// <summary>\r\n    /// Will get the invalid message if <typeparamref name=\"TProperty\"/> from the <typeparamref name=\"TItem\"/> is not able to be detected.\r\n    /// </summary>\r\n    /// <param name=\"item\"></param>\r\n    /// <returns></returns>\r\n    public LocalisableString? GetInvalidMessage(TItem item)\r\n        => GetInvalidMessageFromItem(item);\r\n\r\n    /// <summary>\r\n    /// Detect the <typeparamref name=\"TProperty\"/> from the <typeparamref name=\"TItem\"/>.\r\n    /// </summary>\r\n    /// <param name=\"item\"></param>\r\n    /// <returns></returns>\r\n    public TProperty Detect(TItem item)\r\n    {\r\n        if (!CanDetect(item))\r\n            throw new DetectorNotSupportedException();\r\n\r\n        return DetectFromItem(item);\r\n    }\r\n\r\n    protected abstract LocalisableString? GetInvalidMessageFromItem(TItem item);\r\n\r\n    protected abstract TProperty DetectFromItem(TItem item);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/PropertyGenerator.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Localisation;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator;\r\n\r\npublic abstract class PropertyGenerator<TItem, TProperty, TConfig> : PropertyGenerator<TItem, TProperty>\r\n    where TConfig : GeneratorConfig, new()\r\n{\r\n    protected readonly TConfig Config;\r\n\r\n    protected PropertyGenerator(TConfig config)\r\n    {\r\n        Config = config;\r\n    }\r\n}\r\n\r\npublic abstract class PropertyGenerator<TItem, TProperty>\r\n{\r\n    /// <summary>\r\n    /// Determined if generate <typeparamref name=\"TProperty\"/> from <typeparamref name=\"TItem\"/> is supported.\r\n    /// </summary>\r\n    /// <param name=\"item\"></param>\r\n    /// <returns></returns>\r\n    public bool CanGenerate(TItem item) => GetInvalidMessage(item) == null;\r\n\r\n    /// <summary>\r\n    /// Will get the invalid message if <typeparamref name=\"TProperty\"/> from the <typeparamref name=\"TItem\"/> is not able to be generated.\r\n    /// </summary>\r\n    /// <param name=\"item\"></param>\r\n    /// <returns></returns>\r\n    public LocalisableString? GetInvalidMessage(TItem item)\r\n        => GetInvalidMessageFromItem(item);\r\n\r\n    /// <summary>\r\n    /// Generate the <typeparamref name=\"TProperty\"/> from the <typeparamref name=\"TItem\"/>.\r\n    /// </summary>\r\n    /// <param name=\"item\"></param>\r\n    /// <returns></returns>\r\n    public TProperty Generate(TItem item)\r\n    {\r\n        if (!CanGenerate(item))\r\n            throw new GeneratorNotSupportedException();\r\n\r\n        return GenerateFromItem(item);\r\n    }\r\n\r\n    protected abstract LocalisableString? GetInvalidMessageFromItem(TItem item);\r\n\r\n    protected abstract TProperty GenerateFromItem(TItem item);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/Stages/Classic/ClassicLyricLayoutCategoryGenerator.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos.Classic;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator.Stages.Classic;\r\n\r\npublic class ClassicLyricLayoutCategoryGenerator : StageInfoPropertyGenerator<ClassicLyricLayoutCategory, ClassicLyricLayoutCategoryGeneratorConfig>\r\n{\r\n    public ClassicLyricLayoutCategoryGenerator(ClassicLyricLayoutCategoryGeneratorConfig config)\r\n        : base(config)\r\n    {\r\n    }\r\n\r\n    protected override LocalisableString? GetInvalidMessageFromItem(KaraokeBeatmap item)\r\n    {\r\n        var lyrics = item.HitObjects.OfType<Lyric>().ToArray();\r\n        if (!lyrics.Any())\r\n            return \"Should have lyric in the beatmap.\";\r\n\r\n        return null;\r\n    }\r\n\r\n    protected override ClassicLyricLayoutCategory GenerateFromItem(KaraokeBeatmap item)\r\n    {\r\n        int rowAmount = Config.LyricRowAmount.Value;\r\n        bool applyMappingToTheLyric = Config.ApplyMappingToTheLyric.Value;\r\n\r\n        var lyrics = item.HitObjects.OfType<Lyric>().ToArray();\r\n        var layoutCategory = new ClassicLyricLayoutCategory();\r\n\r\n        // create the element first.\r\n        var layouts = mappingLayoutsToLyric(layoutCategory, rowAmount).ToArray();\r\n\r\n        if (!applyMappingToTheLyric)\r\n            return layoutCategory;\r\n\r\n        // then, mapping to the lyric.\r\n        for (int i = 0; i < lyrics.Length; i++)\r\n        {\r\n            var lyric = lyrics.ElementAt(i);\r\n            var layout = layouts.ElementAt(i % rowAmount);\r\n\r\n            layoutCategory.AddToMapping(layout, lyric);\r\n        }\r\n\r\n        return layoutCategory;\r\n    }\r\n\r\n    private IEnumerable<ClassicLyricLayout> mappingLayoutsToLyric(ClassicLyricLayoutCategory category, int amount)\r\n    {\r\n        switch (amount)\r\n        {\r\n            case 4:\r\n                yield return addElementWithLine(category, 3, ClassicLyricLayoutAlignment.Left);\r\n                yield return addElementWithLine(category, 2, ClassicLyricLayoutAlignment.Right);\r\n                yield return addElementWithLine(category, 1, ClassicLyricLayoutAlignment.Left);\r\n                yield return addElementWithLine(category, 0, ClassicLyricLayoutAlignment.Right);\r\n\r\n                yield break;\r\n\r\n            case 3:\r\n                yield return addElementWithLine(category, 2, ClassicLyricLayoutAlignment.Left);\r\n                yield return addElementWithLine(category, 1, ClassicLyricLayoutAlignment.Center);\r\n                yield return addElementWithLine(category, 0, ClassicLyricLayoutAlignment.Right);\r\n\r\n                yield break;\r\n\r\n            case 2:\r\n                yield return addElementWithLine(category, 1, ClassicLyricLayoutAlignment.Left);\r\n                yield return addElementWithLine(category, 0, ClassicLyricLayoutAlignment.Right);\r\n\r\n                yield break;\r\n\r\n            default:\r\n                throw new InvalidOperationException();\r\n        }\r\n    }\r\n\r\n    private ClassicLyricLayout addElementWithLine(ClassicLyricLayoutCategory category, int line, ClassicLyricLayoutAlignment alignment)\r\n    {\r\n        float horizontalMargin = Config.HorizontalMargin.Value;\r\n\r\n        return category.AddElement(x =>\r\n        {\r\n            x.Name = $\"{generateName(alignment)} {x.ID}\";\r\n            x.Alignment = alignment;\r\n            x.HorizontalMargin = horizontalMargin;\r\n            x.Line = line;\r\n        });\r\n\r\n        static string generateName(ClassicLyricLayoutAlignment alignment) =>\r\n            alignment switch\r\n            {\r\n                ClassicLyricLayoutAlignment.Left => \"Left\",\r\n                ClassicLyricLayoutAlignment.Center => \"Center\",\r\n                ClassicLyricLayoutAlignment.Right => \"Right\",\r\n                _ => throw new ArgumentOutOfRangeException(nameof(alignment), alignment, null),\r\n            };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/Stages/Classic/ClassicLyricLayoutCategoryGeneratorConfig.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos.Classic;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator.Stages.Classic;\r\n\r\npublic class ClassicLyricLayoutCategoryGeneratorConfig : GeneratorConfig\r\n{\r\n    /// <summary>\r\n    /// How may lyric can be in the stage at the same time.\r\n    /// </summary>\r\n    [ConfigSource(\"Lyric amount\", \"How may lyric can be in the stage at the same time.\")]\r\n    public BindableInt LyricRowAmount { get; } = new(2)\r\n    {\r\n        MinValue = 2,\r\n        MaxValue = 4,\r\n    };\r\n\r\n    /// <summary>\r\n    /// Should auto-create the mapping to the lyric or mapping by user.\r\n    /// </summary>\r\n    [ConfigSource(\"Apply mapping to the lyric\", \"Auto-apply the mapping or mapping by user.\")]\r\n    public BindableBool ApplyMappingToTheLyric { get; } = new(true);\r\n\r\n    /// <summary>\r\n    /// Adjust the <see cref=\"ClassicLyricLayout.HorizontalMargin\"/> in the <see cref=\"ClassicLyricLayout\"/>\r\n    /// </summary>\r\n    [ConfigSource(\"Horizontal margin\", \"The margin between lyric and the border of the playfield.\")]\r\n    public BindableFloat HorizontalMargin { get; } = new()\r\n    {\r\n        MinValue = 32,\r\n        MaxValue = 100,\r\n    };\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/Stages/Classic/ClassicLyricTimingInfoGenerator.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos.Classic;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator.Stages.Classic;\r\n\r\npublic class ClassicLyricTimingInfoGenerator : StageInfoPropertyGenerator<ClassicLyricTimingInfo, ClassicLyricTimingInfoGeneratorConfig>\r\n{\r\n    public ClassicLyricTimingInfoGenerator(ClassicLyricTimingInfoGeneratorConfig config)\r\n        : base(config)\r\n    {\r\n    }\r\n\r\n    protected override LocalisableString? GetInvalidMessageFromItem(KaraokeBeatmap item)\r\n    {\r\n        var lyrics = item.HitObjects.OfType<Lyric>().ToArray();\r\n        if (!lyrics.Any())\r\n            return \"Should have lyric in the beatmap.\";\r\n\r\n        return null;\r\n    }\r\n\r\n    protected override ClassicLyricTimingInfo GenerateFromItem(KaraokeBeatmap item)\r\n    {\r\n        int lyricAmount = Config.LyricRowAmount.Value;\r\n\r\n        var lyrics = item.HitObjects.OfType<Lyric>().Where(x => x.TimeValid).ToArray();\r\n        var timingInfo = new ClassicLyricTimingInfo();\r\n\r\n        // lazy to generate the info if the lyric amount is not enough.\r\n        if (lyrics.Length < lyricAmount)\r\n            return timingInfo;\r\n\r\n        // add start timing info.\r\n        var firstTimingPoint = timingInfo.AddTimingPoint();\r\n\r\n        for (int i = 0; i < lyricAmount; i++)\r\n        {\r\n            var showLyric = lyrics.ElementAt(i);\r\n            timingInfo.AddToMapping(firstTimingPoint, showLyric);\r\n        }\r\n\r\n        // should hide the current and show the next n lyric if touch the lyric end time.\r\n        for (int i = 0; i < lyrics.Length - lyricAmount; i++)\r\n        {\r\n            var disappearLyric = lyrics.ElementAt(i);\r\n            var showLyric = lyrics.ElementAt(i + lyricAmount);\r\n\r\n            var timingPoint = timingInfo.AddTimingPoint(x => x.Time = disappearLyric.EndTime);\r\n            timingInfo.AddToMapping(timingPoint, disappearLyric);\r\n            timingInfo.AddToMapping(timingPoint, showLyric);\r\n        }\r\n\r\n        // add end timing info.\r\n        var lastTimingPoint = timingInfo.AddTimingPoint(x => x.Time = lyrics.Last().EndTime);\r\n\r\n        for (int i = lyrics.Length - lyricAmount; i < lyrics.Length; i++)\r\n        {\r\n            var disappearLyric = lyrics.ElementAt(i);\r\n            timingInfo.AddToMapping(lastTimingPoint, disappearLyric);\r\n        }\r\n\r\n        return timingInfo;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/Stages/Classic/ClassicLyricTimingInfoGeneratorConfig.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Bindables;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator.Stages.Classic;\r\n\r\npublic class ClassicLyricTimingInfoGeneratorConfig : GeneratorConfig\r\n{\r\n    /// <summary>\r\n    /// How may lyric can be in the stage at the same time.\r\n    /// </summary>\r\n    [ConfigSource(\"Lyric amount\", \"How may lyric can be in the stage at the same time.\")]\r\n    public BindableInt LyricRowAmount { get; } = new(2)\r\n    {\r\n        MinValue = 2,\r\n        MaxValue = 4,\r\n    };\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/Stages/Classic/ClassicStageInfoGenerator.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos.Classic;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator.Stages.Classic;\r\n\r\npublic class ClassicStageInfoGenerator : StageInfoGenerator<ClassicStageInfoGeneratorConfig>\r\n{\r\n    public ClassicStageInfoGenerator(ClassicStageInfoGeneratorConfig config)\r\n        : base(config)\r\n    {\r\n    }\r\n\r\n    protected override LocalisableString? GetInvalidMessageFromItem(KaraokeBeatmap item)\r\n    {\r\n        var lyrics = item.HitObjects.OfType<Lyric>().ToArray();\r\n        if (!lyrics.Any())\r\n            return \"Should have lyric in the beatmap.\";\r\n\r\n        return null;\r\n    }\r\n\r\n    protected override StageInfo GenerateFromItem(KaraokeBeatmap item)\r\n    {\r\n        int lyricRowAmount = Config.LyricRowAmount.Value;\r\n\r\n        // it's OK not to get the config in the config manager.\r\n        var layoutCategoryGenerator = new ClassicLyricLayoutCategoryGenerator(new ClassicLyricLayoutCategoryGeneratorConfig\r\n        {\r\n            LyricRowAmount =\r\n            {\r\n                Value = lyricRowAmount,\r\n            },\r\n        });\r\n\r\n        // it's OK not to get the config in the config manager.\r\n        var timingInfoGenerator = new ClassicLyricTimingInfoGenerator(new ClassicLyricTimingInfoGeneratorConfig\r\n        {\r\n            LyricRowAmount =\r\n            {\r\n                Value = lyricRowAmount,\r\n            },\r\n        });\r\n\r\n        return new ClassicStageInfo\r\n        {\r\n            LyricLayoutCategory = layoutCategoryGenerator.Generate(item),\r\n            LyricTimingInfo = timingInfoGenerator.Generate(item),\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/Stages/Classic/ClassicStageInfoGeneratorConfig.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Bindables;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator.Stages.Classic;\r\n\r\npublic class ClassicStageInfoGeneratorConfig : StageInfoGeneratorConfig\r\n{\r\n    /// <summary>\r\n    /// How may lyric can be in the stage at the same time.\r\n    /// </summary>\r\n    [ConfigSource(\"Lyric amount\", \"How may lyric can be in the stage at the same time.\")]\r\n    public BindableInt LyricRowAmount { get; } = new(2)\r\n    {\r\n        MinValue = 2,\r\n        MaxValue = 4,\r\n    };\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/Stages/Preview/PreviewStageInfoGenerator.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos.Preview;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator.Stages.Preview;\r\n\r\npublic class PreviewStageInfoGenerator : StageInfoGenerator<PreviewStageInfoGeneratorConfig>\r\n{\r\n    public PreviewStageInfoGenerator(PreviewStageInfoGeneratorConfig config)\r\n        : base(config)\r\n    {\r\n    }\r\n\r\n    protected override LocalisableString? GetInvalidMessageFromItem(KaraokeBeatmap item)\r\n    {\r\n        return null;\r\n    }\r\n\r\n    protected override StageInfo GenerateFromItem(KaraokeBeatmap item)\r\n    {\r\n        return new PreviewStageInfo();\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/Stages/Preview/PreviewStageInfoGeneratorConfig.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator.Stages.Preview;\r\n\r\npublic class PreviewStageInfoGeneratorConfig : StageInfoGeneratorConfig;\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/Stages/StageInfoGenerator.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator.Stages;\r\n\r\npublic abstract class StageInfoGenerator<TStageInfoConfig> : PropertyGenerator<KaraokeBeatmap, StageInfo, TStageInfoConfig>\r\n    where TStageInfoConfig : StageInfoGeneratorConfig, new()\r\n{\r\n    protected StageInfoGenerator(TStageInfoConfig config)\r\n        : base(config)\r\n    {\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/Stages/StageInfoGeneratorConfig.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator.Stages;\r\n\r\npublic class StageInfoGeneratorConfig : GeneratorConfig;\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/Stages/StageInfoGeneratorSelector.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Stages.Classic;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Stages.Preview;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos.Classic;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos.Preview;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator.Stages;\r\n\r\npublic class StageInfoGeneratorSelector<TStageInfo> : GeneratorSelector<KaraokeBeatmap, StageInfo, StageInfoGeneratorConfig>\r\n    where TStageInfo : StageInfo\r\n{\r\n    public StageInfoGeneratorSelector(KaraokeRulesetEditGeneratorConfigManager generatorConfigManager)\r\n        : base(generatorConfigManager)\r\n    {\r\n        registerGenerator<ClassicStageInfoGenerator, ClassicStageInfoGeneratorConfig>(typeof(ClassicStageInfo));\r\n        registerGenerator<PreviewStageInfoGenerator, PreviewStageInfoGeneratorConfig>(typeof(PreviewStageInfo));\r\n    }\r\n\r\n    private void registerGenerator<TGenerator, TConfig>(Type type)\r\n        where TGenerator : StageInfoGenerator<TConfig>\r\n        where TConfig : StageInfoGeneratorConfig, new()\r\n    {\r\n        RegisterGenerator<TGenerator, TConfig>(_ => type == typeof(TStageInfo));\r\n    }\r\n\r\n    protected override LocalisableString? GetInvalidMessageFromItem(KaraokeBeatmap item)\r\n    {\r\n        if (!TryGetGenerator(item, out var generator))\r\n            return \"Sorry, the stage does not support auto-generate.\";\r\n\r\n        return generator.GetInvalidMessage(item);\r\n    }\r\n\r\n    protected override StageInfo GenerateFromItem(KaraokeBeatmap item)\r\n    {\r\n        if (!TryGetGenerator(item, out var generator))\r\n            throw new GeneratorNotSupportedException();\r\n\r\n        return generator.Generate(item);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Generator/Stages/StageInfoPropertyGenerator.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Generator.Stages;\r\n\r\n/// <summary>\r\n/// Base interface of the auto-generator.\r\n/// </summary>\r\n/// <typeparam name=\"TProperty\"></typeparam>\r\n/// <typeparam name=\"TConfig\"></typeparam>\r\npublic abstract class StageInfoPropertyGenerator<TProperty, TConfig> : PropertyGenerator<KaraokeBeatmap, TProperty, TConfig>\r\n    where TConfig : GeneratorConfig, new()\r\n{\r\n    protected StageInfoPropertyGenerator(TConfig config)\r\n        : base(config)\r\n    {\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/KaraokeBeatmapVerifier.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Game.Rulesets.Edit;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit;\r\n\r\npublic class KaraokeBeatmapVerifier : IBeatmapVerifier\r\n{\r\n    private readonly List<ICheck> checks = new()\r\n    {\r\n        new CheckBeatmapAvailableTranslations(),\r\n        new CheckClassicStageInfo(),\r\n        new CheckBeatmapNoteInfo(),\r\n        new CheckBeatmapPageInfo(),\r\n        new CheckLyricLanguage(),\r\n        new CheckLyricReferenceLyric(),\r\n        new CheckLyricRubyTag(),\r\n        new CheckLyricSinger(),\r\n        new CheckLyricText(),\r\n        new CheckLyricTimeTag(),\r\n        new CheckLyricTranslations(),\r\n        new CheckNoteReferenceLyric(),\r\n        new CheckNoteText(),\r\n        new CheckNoteTime(),\r\n    };\r\n\r\n    public IEnumerable<Issue> Run(BeatmapVerifierContext context) => checks.SelectMany(check => check.Run(context));\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/KaraokeBlueprintContainer.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Game.Rulesets.Edit;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Blueprints.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Blueprints.Notes;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Objects;\r\nusing osu.Game.Screens.Edit.Compose.Components;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit;\r\n\r\npublic partial class KaraokeBlueprintContainer : ComposeBlueprintContainer\r\n{\r\n    public KaraokeBlueprintContainer(HitObjectComposer composer)\r\n        : base(composer)\r\n    {\r\n    }\r\n\r\n    public override HitObjectSelectionBlueprint CreateHitObjectBlueprintFor(HitObject hitObject) =>\r\n        hitObject switch\r\n        {\r\n            Note note => new NoteSelectionBlueprint(note),\r\n            Lyric lyric => new LyricSelectionBlueprint(lyric),\r\n            _ => throw new ArgumentOutOfRangeException(nameof(hitObject)),\r\n        };\r\n\r\n    protected override SelectionHandler<HitObject> CreateSelectionHandler() => new KaraokeSelectionHandler();\r\n\r\n    protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint<HitObject> blueprint, Vector2[] originalSnapPositions)> blueprints)\r\n        => false;\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/KaraokeEditorPlayfield.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.UI;\r\nusing osu.Game.Rulesets.Karaoke.UI.Scrolling;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit;\r\n\r\npublic partial class KaraokeEditorPlayfield : KaraokePlayfield\r\n{\r\n    protected override ScrollingNotePlayfield CreateNotePlayfield(int columns)\r\n        => new EditorNotePlayfield(columns);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/KaraokeHitObjectComposer.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Framework.Testing;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Rulesets.Edit;\r\nusing osu.Game.Rulesets.Edit.Tools;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Notes;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Components.Menus;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Debugging;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Skinning.Fonts;\r\nusing osu.Game.Rulesets.Karaoke.UI;\r\nusing osu.Game.Rulesets.Karaoke.UI.Position;\r\nusing osu.Game.Rulesets.Mods;\r\nusing osu.Game.Rulesets.UI;\r\nusing osu.Game.Screens.Edit;\r\nusing osu.Game.Screens.Edit.Components.Menus;\r\nusing osu.Game.Screens.Edit.Compose.Components;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit;\r\n\r\npublic partial class KaraokeHitObjectComposer : HitObjectComposer<KaraokeHitObject>\r\n{\r\n    private DrawableKaraokeEditorRuleset drawableRuleset = null!;\r\n\r\n    [Cached]\r\n    private readonly KaraokeRulesetEditConfigManager editConfigManager;\r\n\r\n    [Cached]\r\n    private readonly KaraokeRulesetEditGeneratorConfigManager generatorConfigManager;\r\n\r\n    [Cached]\r\n    private readonly FontManager fontManager;\r\n\r\n    [Cached(typeof(IKaraokeBeatmapResourcesProvider))]\r\n    private KaraokeBeatmapResourcesProvider karaokeBeatmapResourcesProvider;\r\n\r\n    [Cached(typeof(ILyricRubyTagsChangeHandler))]\r\n    private readonly LyricRubyTagsChangeHandler lyricRubyTagsChangeHandler;\r\n\r\n    [Cached(typeof(INotePositionInfo))]\r\n    private readonly NotePositionInfo notePositionInfo;\r\n\r\n    [Cached(typeof(INotesChangeHandler))]\r\n    private readonly NotesChangeHandler notesChangeHandler;\r\n\r\n    [Cached(typeof(INotePropertyChangeHandler))]\r\n    private readonly NotePropertyChangeHandler notePropertyChangeHandler;\r\n\r\n    [Cached(typeof(ILyricSingerChangeHandler))]\r\n    private readonly LyricSingerChangeHandler lyricSingerChangeHandler;\r\n\r\n    [Cached(typeof(IBeatmapSingersChangeHandler))]\r\n    private readonly BeatmapSingersChangeHandler beatmapSingersChangeHandler;\r\n\r\n    [Cached]\r\n    private readonly DebugBeatmapManager debugBeatmapManager;\r\n\r\n    [Resolved]\r\n    private Editor editor { get; set; } = null!;\r\n\r\n    public KaraokeHitObjectComposer(Ruleset ruleset)\r\n        : base(ruleset)\r\n    {\r\n        editConfigManager = new KaraokeRulesetEditConfigManager();\r\n        generatorConfigManager = new KaraokeRulesetEditGeneratorConfigManager();\r\n\r\n        // Duplicated registration because selection handler need to use it.\r\n        AddInternal(fontManager = new FontManager());\r\n        AddInternal(karaokeBeatmapResourcesProvider = new KaraokeBeatmapResourcesProvider());\r\n\r\n        AddInternal(lyricRubyTagsChangeHandler = new LyricRubyTagsChangeHandler());\r\n        AddInternal(notePositionInfo = new NotePositionInfo());\r\n        AddInternal(notesChangeHandler = new NotesChangeHandler());\r\n        AddInternal(notePropertyChangeHandler = new NotePropertyChangeHandler());\r\n        AddInternal(lyricSingerChangeHandler = new LyricSingerChangeHandler());\r\n        AddInternal(beatmapSingersChangeHandler = new BeatmapSingersChangeHandler());\r\n\r\n        AddInternal(debugBeatmapManager = new DebugBeatmapManager());\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        CreateMenuBar();\r\n    }\r\n\r\n    private DependencyContainer dependencies = null!;\r\n\r\n    protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)\r\n        => dependencies = new DependencyContainer(base.CreateChildDependencies(parent));\r\n\r\n    public new KaraokePlayfield Playfield => drawableRuleset.Playfield;\r\n\r\n    protected override Playfield? PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition)\r\n    {\r\n        // Only note and lyric playfield can interact with mouse input.\r\n        if (Playfield.NotePlayfield.ReceivePositionalInputAt(screenSpacePosition))\r\n            return Playfield.NotePlayfield;\r\n        if (Playfield.LyricPlayfield.ReceivePositionalInputAt(screenSpacePosition))\r\n            return Playfield.LyricPlayfield;\r\n\r\n        return null;\r\n    }\r\n\r\n    protected override DrawableRuleset<KaraokeHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods)\r\n    {\r\n        drawableRuleset = new DrawableKaraokeEditorRuleset(ruleset, beatmap, mods);\r\n\r\n        // This is the earliest we can cache the scrolling info to ourselves, before masks are added to the hierarchy and inject it\r\n        dependencies.CacheAs(drawableRuleset.ScrollingInfo);\r\n\r\n        return drawableRuleset;\r\n    }\r\n\r\n    protected override ComposeBlueprintContainer CreateBlueprintContainer()\r\n        => new KaraokeBlueprintContainer(this);\r\n\r\n    protected void CreateMenuBar()\r\n    {\r\n        var editorMenuBar = editor.ChildrenOfType<EditorMenuBar>().FirstOrDefault();\r\n        if (editorMenuBar == null)\r\n            return;\r\n\r\n        Schedule(() =>\r\n        {\r\n            editorMenuBar.Items = new List<MenuItem>(editorMenuBar.Items)\r\n            {\r\n                new(\"Config\")\r\n                {\r\n                    Items = Array.Empty<MenuItem>(),\r\n                },\r\n                new(\"Tools\")\r\n                {\r\n                    Items = new MenuItem[]\r\n                    {\r\n                        // todo: remove this menu until we have a better way to edit skin.\r\n                        new KaraokeSkinEditorMenu(editor, null!, \"Skin editor\"),\r\n                        new KaraokeEditorMenu(editor, \"Karaoke editor\"),\r\n                    },\r\n                },\r\n                new(\"Debug\")\r\n                {\r\n                    Items = new MenuItem[]\r\n                    {\r\n                        new EditorMenuItem(\"Override beatmap as json format\", MenuItemType.Destructive, () => debugBeatmapManager.OverrideTheBeatmapWithJsonFormat()),\r\n                        new EditorMenuItem(\"Save beatmap to new difficulty as json format\", MenuItemType.Destructive, () => debugBeatmapManager.SaveToNewDifficulty()),\r\n                        new OsuMenuItemSpacer(),\r\n                        new EditorMenuItem(\"Export to json\", MenuItemType.Destructive, () => debugBeatmapManager.ExportToJson()),\r\n                        new EditorMenuItem(\"Export to json beatmap\", MenuItemType.Destructive, () => debugBeatmapManager.ExportToJsonBeatmap()),\r\n                    },\r\n                },\r\n            };\r\n        });\r\n    }\r\n\r\n    protected override IReadOnlyList<CompositionTool> CompositionTools => Array.Empty<CompositionTool>();\r\n\r\n    protected override IEnumerable<Drawable> CreateTernaryButtons() => Array.Empty<Drawable>();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/KaraokeSelectionHandler.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Rulesets.Edit;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Blueprints.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Blueprints.Notes;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Notes;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Components.ContextMenu;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.UI.Components;\r\nusing osu.Game.Rulesets.Karaoke.UI.Position;\r\nusing osu.Game.Rulesets.Karaoke.UI.Scrolling;\r\nusing osu.Game.Rulesets.Objects;\r\nusing osu.Game.Screens.Edit;\r\nusing osu.Game.Screens.Edit.Compose.Components;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit;\r\n\r\npublic partial class KaraokeSelectionHandler : EditorSelectionHandler\r\n{\r\n    [Resolved]\r\n    private EditorBeatmap beatmap { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private INotePositionInfo notePositionInfo { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private HitObjectComposer composer { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private INotesChangeHandler notesChangeHandler { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private INotePropertyChangeHandler notePropertyChangeHandler { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private ILyricSingerChangeHandler lyricSingerChangeHandler { get; set; } = null!;\r\n\r\n    protected ScrollingNotePlayfield NotePlayfield => ((KaraokeHitObjectComposer)composer).Playfield.NotePlayfield;\r\n\r\n    protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint<HitObject>> selection)\r\n    {\r\n        if (selection.All(x => x is LyricSelectionBlueprint))\r\n        {\r\n            return new[]\r\n            {\r\n                createSingerMenuItem(),\r\n            };\r\n        }\r\n\r\n        if (EditorBeatmap.SelectedHitObjects.All(x => x is Note)\r\n            && EditorBeatmap.SelectedHitObjects.Count > 1)\r\n        {\r\n            var menu = new List<MenuItem>();\r\n            var selectedObject = EditorBeatmap.SelectedHitObjects.Cast<Note>().OrderBy(x => x.StartTime).ToArray();\r\n\r\n            // Set multi note display property\r\n            menu.Add(createMultiNoteDisplayPropertyMenuItem(selectedObject));\r\n\r\n            // Combine multi note if they has same start and end index.\r\n            var firstObject = selectedObject.FirstOrDefault();\r\n            if (firstObject != null && selectedObject.All(x => x.ReferenceTimeTagIndex == firstObject.ReferenceTimeTagIndex))\r\n                menu.Add(createCombineNoteMenuItem());\r\n\r\n            return menu;\r\n        }\r\n\r\n        return new List<MenuItem>();\r\n    }\r\n\r\n    private MenuItem createMultiNoteDisplayPropertyMenuItem(IReadOnlyCollection<Note> selectedObject)\r\n    {\r\n        bool display = selectedObject.Count(x => x.Display) >= selectedObject.Count(x => !x.Display);\r\n        string displayText = display ? \"Hide\" : \"Show\";\r\n        return new OsuMenuItem($\"{displayText} {selectedObject.Count} notes.\", display ? MenuItemType.Destructive : MenuItemType.Standard,\r\n            () =>\r\n            {\r\n                notePropertyChangeHandler.ChangeDisplayState(!display);\r\n            });\r\n    }\r\n\r\n    private MenuItem createCombineNoteMenuItem()\r\n    {\r\n        return new OsuMenuItem(\"Combine\", MenuItemType.Standard, () =>\r\n        {\r\n            notesChangeHandler.Combine();\r\n        });\r\n    }\r\n\r\n    private MenuItem createSingerMenuItem()\r\n    {\r\n        return new SingerContextMenu(beatmap, lyricSingerChangeHandler, \"Singer\");\r\n    }\r\n\r\n    public override bool HandleMovement(MoveSelectionEvent<HitObject> moveEvent)\r\n    {\r\n        // Only note can be moved.\r\n        if (moveEvent.Blueprint is not NoteSelectionBlueprint noteSelectionBlueprint)\r\n            return false;\r\n\r\n        var lastTone = noteSelectionBlueprint.HitObject.Tone;\r\n        performColumnMovement(lastTone, moveEvent);\r\n\r\n        return true;\r\n    }\r\n\r\n    private void performColumnMovement(Tone lastTone, MoveSelectionEvent<HitObject> moveEvent)\r\n    {\r\n        if (moveEvent.Blueprint is not NoteSelectionBlueprint)\r\n            return;\r\n\r\n        var calculator = notePositionInfo.Calculator;\r\n\r\n        // get center position\r\n        var screenSpacePosition = moveEvent.Blueprint.ScreenSpaceSelectionPoint + moveEvent.ScreenSpaceDelta;\r\n        var position = NotePlayfield.ToLocalSpace(screenSpacePosition);\r\n        var centerPosition = new Vector2(position.X, position.Y - NotePlayfield.Height / 2);\r\n\r\n        // get delta position\r\n        float lastCenterPosition = calculator.YPositionAt(lastTone);\r\n        float delta = centerPosition.Y - lastCenterPosition;\r\n\r\n        // get offset tone.\r\n        const float trigger_height = ScrollingNotePlayfield.COLUMN_SPACING + DefaultColumnBackground.COLUMN_HEIGHT;\r\n        var offset = delta switch\r\n        {\r\n            > trigger_height => -new Tone { Half = true },\r\n            < 0 => new Tone { Half = true },\r\n            _ => default,\r\n        };\r\n\r\n        if (offset == default(Tone))\r\n            return;\r\n\r\n        notePropertyChangeHandler.OffsetTone(offset);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Setup/Components/FormLanguageList.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Specialized;\r\nusing System.Globalization;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Extensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Cursor;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Graphics.UserInterfaceV2;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.UserInterfaceV2;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Setup.Components;\r\n\r\npublic partial class FormLanguageList : CompositeDrawable\r\n{\r\n    public BindableList<CultureInfo> Languages { get; } = new();\r\n\r\n    public LocalisableString Caption { get; init; }\r\n\r\n    public LocalisableString HintText { get; init; }\r\n\r\n    private Box background = null!;\r\n    private FormFieldCaption caption = null!;\r\n    private FillFlowContainer flow = null!;\r\n\r\n    [Resolved]\r\n    private OverlayColourProvider colourProvider { get; set; } = null!;\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        RelativeSizeAxes = Axes.X;\r\n        AutoSizeAxes = Axes.Y;\r\n\r\n        Masking = true;\r\n        CornerRadius = 5;\r\n\r\n        AddLanguageButton button;\r\n\r\n        InternalChildren = new Drawable[]\r\n        {\r\n            background = new Box\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n                Colour = colourProvider.Background5,\r\n            },\r\n            new FillFlowContainer\r\n            {\r\n                RelativeSizeAxes = Axes.X,\r\n                AutoSizeAxes = Axes.Y,\r\n                Padding = new MarginPadding(9),\r\n                Spacing = new Vector2(7),\r\n                Direction = FillDirection.Vertical,\r\n                Children = new Drawable[]\r\n                {\r\n                    caption = new FormFieldCaption\r\n                    {\r\n                        Caption = Caption,\r\n                        TooltipText = HintText,\r\n                    },\r\n                    flow = new FillFlowContainer\r\n                    {\r\n                        RelativeSizeAxes = Axes.X,\r\n                        AutoSizeAxes = Axes.Y,\r\n                        Direction = FillDirection.Full,\r\n                        Spacing = new Vector2(5),\r\n                        Child = button = new AddLanguageButton\r\n                        {\r\n                            Action = languageInsertionRequested,\r\n                        },\r\n                    },\r\n                },\r\n            },\r\n        };\r\n\r\n        flow.SetLayoutPosition(button, float.MaxValue);\r\n    }\r\n\r\n    protected override void LoadComplete()\r\n    {\r\n        base.LoadComplete();\r\n\r\n        Languages.BindCollectionChanged((_, args) =>\r\n        {\r\n            if (args.Action != NotifyCollectionChangedAction.Replace)\r\n                updateLanguages();\r\n        }, true);\r\n        updateState();\r\n    }\r\n\r\n    protected override bool OnHover(HoverEvent e)\r\n    {\r\n        updateState();\r\n        return true;\r\n    }\r\n\r\n    protected override void OnHoverLost(HoverLostEvent e)\r\n    {\r\n        base.OnHoverLost(e);\r\n        updateState();\r\n    }\r\n\r\n    private void updateState()\r\n    {\r\n        background.Colour = colourProvider.Background5;\r\n        caption.Colour = colourProvider.Content2;\r\n\r\n        BorderThickness = IsHovered ? 2 : 0;\r\n\r\n        if (IsHovered)\r\n            BorderColour = colourProvider.Light4;\r\n    }\r\n\r\n    private void updateLanguages()\r\n    {\r\n        flow.RemoveAll(d => d is LanguageDisplay, true);\r\n\r\n        foreach (var language in Languages)\r\n        {\r\n            flow.Add(new LanguageDisplay\r\n            {\r\n                Current = { Value = language },\r\n                DeleteRequested = languageDeletionRequested,\r\n            });\r\n        }\r\n    }\r\n\r\n    private void languageInsertionRequested(CultureInfo language)\r\n    {\r\n        if (!Languages.Contains(language))\r\n            Languages.Add(language);\r\n    }\r\n\r\n    private void languageDeletionRequested(CultureInfo language) => Languages.Remove(language);\r\n\r\n    private partial class LanguageDisplay : CompositeDrawable, IHasCurrentValue<CultureInfo>\r\n    {\r\n        /// <summary>\r\n        /// Invoked when the user has requested the corresponding to this <see cref=\"CultureInfo\"/>\r\n        /// </summary>\r\n        public Action<CultureInfo>? DeleteRequested;\r\n\r\n        private readonly BindableWithCurrent<CultureInfo> current = new();\r\n\r\n        public Bindable<CultureInfo> Current\r\n        {\r\n            get => current.Current;\r\n            set => current.Current = value;\r\n        }\r\n\r\n        private readonly Box background;\r\n        private readonly OsuSpriteText languageName;\r\n\r\n        public LanguageDisplay()\r\n        {\r\n            AutoSizeAxes = Axes.X;\r\n            Height = 30;\r\n            Masking = true;\r\n            CornerRadius = 5;\r\n\r\n            InternalChildren = new Drawable[]\r\n            {\r\n                background = new Box\r\n                {\r\n                    RelativeSizeAxes = Axes.Both,\r\n                },\r\n                languageName = new OsuSpriteText\r\n                {\r\n                    Anchor = Anchor.CentreLeft,\r\n                    Origin = Anchor.CentreLeft,\r\n                    Padding = new MarginPadding { Left = 10, Right = 32 },\r\n                },\r\n                new IconButton\r\n                {\r\n                    Icon = FontAwesome.Solid.Times,\r\n                    Anchor = Anchor.CentreRight,\r\n                    Origin = Anchor.CentreRight,\r\n                    Size = new Vector2(16),\r\n                    Margin = new MarginPadding { Right = 10 },\r\n                    Action = () =>\r\n                    {\r\n                        DeleteRequested?.Invoke(Current.Value);\r\n                    },\r\n                },\r\n            };\r\n\r\n            current.BindValueChanged(x =>\r\n            {\r\n                languageName.Text = CultureInfoUtils.GetLanguageDisplayText(x.NewValue);\r\n            });\r\n        }\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(OsuColour colours)\r\n        {\r\n            background.Colour = colours.BlueDarker;\r\n            languageName.Colour = colours.BlueLighter;\r\n        }\r\n    }\r\n\r\n    internal partial class AddLanguageButton : CompositeDrawable, IHasPopover\r\n    {\r\n        public Action<CultureInfo>? Action;\r\n\r\n        private readonly Bindable<CultureInfo?> currentLanguage = new();\r\n\r\n        public AddLanguageButton()\r\n        {\r\n            Size = new Vector2(35);\r\n\r\n            InternalChild = new IconButton\r\n            {\r\n                Action = this.ShowPopover,\r\n                Icon = FontAwesome.Solid.Plus,\r\n            };\r\n\r\n            currentLanguage.BindValueChanged(e =>\r\n            {\r\n                this.HidePopover();\r\n\r\n                var language = e.NewValue;\r\n                if (language == null)\r\n                    return;\r\n\r\n                Action?.Invoke(language);\r\n\r\n                currentLanguage.Value = null;\r\n            });\r\n        }\r\n\r\n        public Popover GetPopover() => new LanguageSelectorPopover(currentLanguage)\r\n        {\r\n            EnableEmptyOption = false,\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Setup/Components/FormSingerList.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Specialized;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Extensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Cursor;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Containers;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Graphics.UserInterfaceV2;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.Cursor;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.Drawables;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Singers.Detail;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Setup.Components;\r\n\r\npublic partial class FormSingerList : CompositeDrawable\r\n{\r\n    public BindableList<Singer> Singers { get; } = new();\r\n\r\n    public LocalisableString Caption { get; init; }\r\n\r\n    public LocalisableString HintText { get; init; }\r\n\r\n    private Box background = null!;\r\n    private FormFieldCaption caption = null!;\r\n    private FillFlowContainer flow = null!;\r\n\r\n    [Resolved]\r\n    private OverlayColourProvider colourProvider { get; set; } = null!;\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        RelativeSizeAxes = Axes.X;\r\n        AutoSizeAxes = Axes.Y;\r\n\r\n        Masking = true;\r\n        CornerRadius = 5;\r\n\r\n        AddSingerButton button;\r\n\r\n        InternalChildren = new Drawable[]\r\n        {\r\n            background = new Box\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n                Colour = colourProvider.Background5,\r\n            },\r\n            new FillFlowContainer\r\n            {\r\n                RelativeSizeAxes = Axes.X,\r\n                AutoSizeAxes = Axes.Y,\r\n                Padding = new MarginPadding(9),\r\n                Spacing = new Vector2(7),\r\n                Direction = FillDirection.Vertical,\r\n                Children = new Drawable[]\r\n                {\r\n                    caption = new FormFieldCaption\r\n                    {\r\n                        Caption = Caption,\r\n                        TooltipText = HintText,\r\n                    },\r\n                    flow = new FillFlowContainer\r\n                    {\r\n                        RelativeSizeAxes = Axes.X,\r\n                        AutoSizeAxes = Axes.Y,\r\n                        Direction = FillDirection.Full,\r\n                        Spacing = new Vector2(10),\r\n                        Child = button = new AddSingerButton\r\n                        {\r\n                            Action = singerInsertionRequested,\r\n                        },\r\n                    },\r\n                },\r\n            },\r\n        };\r\n\r\n        flow.SetLayoutPosition(button, float.MaxValue);\r\n    }\r\n\r\n    protected override void LoadComplete()\r\n    {\r\n        base.LoadComplete();\r\n\r\n        Singers.BindCollectionChanged((_, args) =>\r\n        {\r\n            if (args.Action != NotifyCollectionChangedAction.Replace)\r\n                updateSingers();\r\n        }, true);\r\n        updateState();\r\n    }\r\n\r\n    protected override bool OnHover(HoverEvent e)\r\n    {\r\n        updateState();\r\n        return true;\r\n    }\r\n\r\n    protected override void OnHoverLost(HoverLostEvent e)\r\n    {\r\n        base.OnHoverLost(e);\r\n        updateState();\r\n    }\r\n\r\n    private void updateState()\r\n    {\r\n        background.Colour = colourProvider.Background5;\r\n        caption.Colour = colourProvider.Content2;\r\n\r\n        BorderThickness = IsHovered ? 2 : 0;\r\n\r\n        if (IsHovered)\r\n            BorderColour = colourProvider.Light4;\r\n    }\r\n\r\n    private void updateSingers()\r\n    {\r\n        flow.RemoveAll(d => d is SingerDisplay, true);\r\n\r\n        foreach (var singer in Singers)\r\n        {\r\n            flow.Add(new SingerDisplay\r\n            {\r\n                Current = { Value = singer },\r\n                DeleteRequested = languageDeletionRequested,\r\n            });\r\n        }\r\n    }\r\n\r\n    private void singerInsertionRequested()\r\n    {\r\n        var singer = new Singer\r\n        {\r\n            Name = \"New singer\",\r\n        };\r\n        Singers.Add(singer);\r\n    }\r\n\r\n    private void languageDeletionRequested(Singer singer) => Singers.Remove(singer);\r\n\r\n    /// <summary>\r\n    /// A component which displays a singer along with related description text.\r\n    /// </summary>\r\n    private partial class SingerDisplay : CompositeDrawable, IHasCurrentValue<Singer>, IHasContextMenu, IHasPopover\r\n    {\r\n        /// <summary>\r\n        /// Invoked when the user has requested the singer corresponding to this <see cref=\"SingerDisplay\"/>.<br/>\r\n        /// to be removed from its palette.\r\n        /// </summary>\r\n        public Action<Singer>? DeleteRequested;\r\n\r\n        private readonly BindableWithCurrent<Singer> current = new();\r\n\r\n        private OsuSpriteText singerName = null!;\r\n\r\n        public Bindable<Singer> Current\r\n        {\r\n            get => current.Current;\r\n            set => current.Current = value;\r\n        }\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load()\r\n        {\r\n            AutoSizeAxes = Axes.Y;\r\n            Width = 50;\r\n\r\n            InternalChild = new FillFlowContainer\r\n            {\r\n                RelativeSizeAxes = Axes.X,\r\n                AutoSizeAxes = Axes.Y,\r\n                Direction = FillDirection.Vertical,\r\n                Spacing = new Vector2(0, 10),\r\n                Children = new Drawable[]\r\n                {\r\n                    new SingerCircle\r\n                    {\r\n                        Current = { BindTarget = Current },\r\n                    },\r\n                    singerName = new OsuSpriteText\r\n                    {\r\n                        Anchor = Anchor.TopCentre,\r\n                        Origin = Anchor.TopCentre,\r\n                    },\r\n                },\r\n            };\r\n\r\n            Current.BindValueChanged(singer => singerName.Text = singer.NewValue?.Name ?? \"unknown singer\", true);\r\n        }\r\n\r\n        protected override bool OnClick(ClickEvent e)\r\n        {\r\n            this.ShowPopover();\r\n            return base.OnClick(e);\r\n        }\r\n\r\n        public MenuItem[] ContextMenuItems => new MenuItem[]\r\n        {\r\n            new OsuMenuItem(\"Edit singer info\", MenuItemType.Standard, this.ShowPopover),\r\n            new OsuMenuItem(\"Delete\", MenuItemType.Destructive, () =>\r\n            {\r\n                DeleteRequested?.Invoke(Current.Value);\r\n            }),\r\n        };\r\n\r\n        public Popover GetPopover() => new SingerEditPopover(Current.Value);\r\n\r\n        private partial class SingerCircle : Container, IHasCustomTooltip<Singer>\r\n        {\r\n            public Bindable<Singer> Current { get; } = new();\r\n\r\n            private readonly DrawableSingerAvatar singerAvatar;\r\n\r\n            public SingerCircle()\r\n            {\r\n                RelativeSizeAxes = Axes.X;\r\n                Height = 50;\r\n\r\n                CornerRadius = 25;\r\n                Masking = true;\r\n                BorderThickness = 5;\r\n\r\n                Children = new Drawable[]\r\n                {\r\n                    singerAvatar = new DrawableSingerAvatar\r\n                    {\r\n                        Anchor = Anchor.Centre,\r\n                        Origin = Anchor.Centre,\r\n                        RelativeSizeAxes = Axes.Both,\r\n                    },\r\n                };\r\n            }\r\n\r\n            protected override void LoadComplete()\r\n            {\r\n                base.LoadComplete();\r\n\r\n                Current.BindValueChanged(_ => updateSinger(), true);\r\n            }\r\n\r\n            private void updateSinger()\r\n            {\r\n                BorderColour = SingerUtils.GetContentColour(Current.Value);\r\n                singerAvatar.Singer = Current.Value;\r\n            }\r\n\r\n            public ITooltip<Singer> GetCustomTooltip() => new SingerToolTip();\r\n\r\n            public Singer TooltipContent => Current.Value;\r\n        }\r\n    }\r\n\r\n    internal partial class AddSingerButton : CompositeDrawable\r\n    {\r\n        public Action Action\r\n        {\r\n            set => circularButton.Action = value;\r\n        }\r\n\r\n        private readonly OsuClickableContainer circularButton;\r\n\r\n        public AddSingerButton()\r\n        {\r\n            AutoSizeAxes = Axes.Y;\r\n            Width = 50;\r\n\r\n            InternalChild = new FillFlowContainer\r\n            {\r\n                RelativeSizeAxes = Axes.X,\r\n                AutoSizeAxes = Axes.Y,\r\n                Direction = FillDirection.Vertical,\r\n                Spacing = new Vector2(0, 10),\r\n                Children = new Drawable[]\r\n                {\r\n                    circularButton = new OsuClickableContainer\r\n                    {\r\n                        RelativeSizeAxes = Axes.X,\r\n                        Height = 50,\r\n                        CornerRadius = 25,\r\n                        Masking = true,\r\n                        BorderThickness = 5,\r\n                        Children = new Drawable[]\r\n                        {\r\n                            new Box\r\n                            {\r\n                                RelativeSizeAxes = Axes.Both,\r\n                                Colour = Colour4.Transparent,\r\n                                AlwaysPresent = true,\r\n                            },\r\n                            new SpriteIcon\r\n                            {\r\n                                Anchor = Anchor.Centre,\r\n                                Origin = Anchor.Centre,\r\n                                Size = new Vector2(20),\r\n                                Icon = FontAwesome.Solid.Plus,\r\n                            },\r\n                        },\r\n                    },\r\n                    new OsuSpriteText\r\n                    {\r\n                        Anchor = Anchor.TopCentre,\r\n                        Origin = Anchor.TopCentre,\r\n                        Text = \"New\",\r\n                    },\r\n                },\r\n            };\r\n        }\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(OsuColour colours)\r\n        {\r\n            circularButton.BorderColour = colours.BlueDarker;\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Setup/KaraokeNoteSection.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Graphics.UserInterfaceV2;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Screens.Edit.Setup;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Setup;\r\n\r\npublic partial class KaraokeNoteSection : SetupSection\r\n{\r\n    public override LocalisableString Title => \"Note\";\r\n\r\n    private FormCheckBox scorable = null!;\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        Children = new Drawable[]\r\n        {\r\n            scorable = new FormCheckBox\r\n            {\r\n                Caption = \"Scorable\",\r\n                HintText = \"Will not show score playfield if the option is unchecked.\",\r\n                Current = { Value = true },\r\n            },\r\n        };\r\n\r\n        scorable.Current.BindValueChanged(_ => updateValues());\r\n    }\r\n\r\n    private void updateValues()\r\n    {\r\n        if (Beatmap.PlayableBeatmap is not KaraokeBeatmap karaokeBeatmap)\r\n            throw new InvalidOperationException();\r\n\r\n        karaokeBeatmap.Scorable = scorable.Current.Value;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Setup/KaraokeSingerSection.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Setup.Components;\r\nusing osu.Game.Screens.Edit.Setup;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Setup;\r\n\r\npublic partial class KaraokeSingerSection : SetupSection\r\n{\r\n    public override LocalisableString Title => \"Singers\";\r\n\r\n    [Cached(typeof(IKaraokeBeatmapResourcesProvider))]\r\n    private KaraokeBeatmapResourcesProvider karaokeBeatmapResourcesProvider = new();\r\n\r\n    private readonly BeatmapSingersChangeHandler changeHandler = new();\r\n\r\n    private FormSingerList singerList = null!;\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        AddInternal(karaokeBeatmapResourcesProvider);\r\n        AddInternal(changeHandler);\r\n\r\n        Children = new Drawable[]\r\n        {\r\n            singerList = new FormSingerList\r\n            {\r\n                Caption = \"Singer list\",\r\n                HintText = \"All the singers in beatmap.\",\r\n            },\r\n        };\r\n\r\n        if (Beatmap.BeatmapSkin != null)\r\n            singerList.Singers.BindTo(changeHandler.Singers);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Setup/KaraokeTranslationSection.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Specialized;\r\nusing System.Globalization;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Setup.Components;\r\nusing osu.Game.Screens.Edit.Setup;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Setup;\r\n\r\npublic partial class KaraokeTranslationSection : SetupSection\r\n{\r\n    public override LocalisableString Title => \"Translation\";\r\n\r\n    [Cached(typeof(IBeatmapTranslationsChangeHandler))]\r\n    private readonly BeatmapTranslationsChangeHandler changeHandler = new();\r\n\r\n    private FormLanguageList singerList = null!;\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        AddInternal(changeHandler);\r\n\r\n        Children = new Drawable[]\r\n        {\r\n            singerList = new FormLanguageList\r\n            {\r\n                Caption = \"Translation list\",\r\n                HintText = \"All the lyric translation in beatmap.\",\r\n            },\r\n        };\r\n\r\n        singerList.Languages.AddRange(changeHandler.Languages);\r\n        singerList.Languages.BindCollectionChanged((_, args) =>\r\n        {\r\n            switch (args.Action)\r\n            {\r\n                case NotifyCollectionChangedAction.Add:\r\n                    foreach (var language in args.NewItems?.Cast<CultureInfo>() ?? Array.Empty<CultureInfo>())\r\n                    {\r\n                        changeHandler.Add(language);\r\n                    }\r\n\r\n                    break;\r\n\r\n                case NotifyCollectionChangedAction.Remove:\r\n                    foreach (var language in args.OldItems?.Cast<CultureInfo>() ?? Array.Empty<CultureInfo>())\r\n                    {\r\n                        changeHandler.Remove(language);\r\n                    }\r\n\r\n                    break;\r\n            }\r\n        });\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Utils/EditorBeatmapUtils.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Utils;\r\n\r\npublic static class EditorBeatmapUtils\r\n{\r\n    public static IEnumerable<Lyric> GetAllReferenceLyrics(EditorBeatmap editorBeatmap, Lyric referencedLyric)\r\n        => editorBeatmap.HitObjects.OfType<Lyric>().Where(x => x.ReferenceLyric == referencedLyric);\r\n\r\n    public static IEnumerable<Note> GetNotesByLyric(EditorBeatmap editorBeatmap, Lyric lyric)\r\n        => editorBeatmap.HitObjects.OfType<Note>().Where(x => x.ReferenceLyric == lyric);\r\n\r\n    public static KaraokeBeatmap GetPlayableBeatmap(EditorBeatmap editorBeatmap)\r\n    {\r\n        if (editorBeatmap.PlayableBeatmap is not KaraokeBeatmap karaokeBeatmap)\r\n            throw new InvalidCastException();\r\n\r\n        return karaokeBeatmap;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Utils/HitObjectWritableUtils.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Properties;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Types;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Utils;\r\n\r\npublic static class HitObjectWritableUtils\r\n{\r\n    #region Remove lyrics.\r\n\r\n    public static bool IsRemoveLyricLocked(Lyric lyric)\r\n        => GetRemoveLyricLockedBy(lyric) != null;\r\n\r\n    public static LockLyricPropertyBy? GetRemoveLyricLockedBy(Lyric lyric)\r\n    {\r\n        bool lockedByState = isRemoveLyricLockedByState(lyric.Lock);\r\n        if (lockedByState)\r\n            return LockLyricPropertyBy.LockState;\r\n\r\n        return null;\r\n    }\r\n\r\n    private static bool isRemoveLyricLockedByState(LockState lockState)\r\n        => lockState != LockState.None;\r\n\r\n    #endregion\r\n\r\n    #region Lyric property\r\n\r\n    public static bool IsWriteLyricPropertyLocked(Lyric lyric, params string[] propertyNames)\r\n        => GetLyricPropertyLockedBy(lyric, propertyNames) != null;\r\n\r\n    public static bool IsWriteLyricPropertyLocked(Lyric lyric, string propertyName)\r\n        => GetLyricPropertyLockedBy(lyric, propertyName) != null;\r\n\r\n    public static LockLyricPropertyBy? GetLyricPropertyLockedBy(Lyric lyric, params string[] propertyNames)\r\n    {\r\n        var reasons = propertyNames.Select(x => GetLyricPropertyLockedBy(lyric, x))\r\n                                   .Where(x => x != null)\r\n                                   .OfType<LockLyricPropertyBy>()\r\n                                   .ToArray();\r\n\r\n        if (reasons.Contains(LockLyricPropertyBy.ReferenceLyricConfig))\r\n            return LockLyricPropertyBy.ReferenceLyricConfig;\r\n\r\n        if (reasons.Contains(LockLyricPropertyBy.LockState))\r\n            return LockLyricPropertyBy.LockState;\r\n\r\n        return null;\r\n    }\r\n\r\n    public static LockLyricPropertyBy? GetLyricPropertyLockedBy(Lyric lyric, string propertyName)\r\n    {\r\n        bool lockedByConfig = isWriteLyricPropertyLockedByConfig(lyric.ReferenceLyricConfig, propertyName);\r\n        if (lockedByConfig)\r\n            return LockLyricPropertyBy.ReferenceLyricConfig;\r\n\r\n        bool lockedByState = isWriteLyricPropertyLockedByState(lyric.Lock, propertyName);\r\n        if (lockedByState)\r\n            return LockLyricPropertyBy.LockState;\r\n\r\n        return null;\r\n    }\r\n\r\n    private static bool isWriteLyricPropertyLockedByState(LockState lockState, string propertyName)\r\n    {\r\n        // partial lock will only lock some property change like text because they are easy to be modified.\r\n        // fully lock will basically lock all lyric properties.\r\n        return propertyName switch\r\n        {\r\n            nameof(Lyric.ID) => false, // although the id is not changeable, but it's not locked by config.\r\n            nameof(Lyric.Text) => lockState > LockState.None,\r\n            nameof(Lyric.TimeTags) => lockState > LockState.None,\r\n            nameof(Lyric.RubyTags) => lockState > LockState.None,\r\n            nameof(Lyric.StartTime) => lockState > LockState.Partial,\r\n            nameof(Lyric.Duration) => lockState > LockState.Partial,\r\n            nameof(Lyric.TimeValid) => lockState > LockState.Partial,\r\n            nameof(Lyric.SingerIds) => lockState > LockState.Partial,\r\n            nameof(Lyric.Translations) => lockState > LockState.Partial,\r\n            nameof(Lyric.Language) => lockState > LockState.Partial,\r\n            nameof(Lyric.Order) => false, // order can always be changed.\r\n            nameof(Lyric.Lock) => false, // order can always be changed.\r\n            nameof(Lyric.ReferenceLyric) or nameof(Lyric.ReferenceLyricId) => lockState > LockState.Partial,\r\n            nameof(Lyric.ReferenceLyricConfig) => lockState > LockState.Partial,\r\n            // base class\r\n            nameof(Lyric.Samples) => false,\r\n            _ => throw new NotSupportedException(),\r\n        };\r\n    }\r\n\r\n    private static bool isWriteLyricPropertyLockedByConfig(IReferenceLyricPropertyConfig? config, string propertyName)\r\n    {\r\n        return config switch\r\n        {\r\n            ReferenceLyricConfig => false,\r\n            SyncLyricConfig syncLyricConfig => propertyName switch\r\n            {\r\n                nameof(Lyric.ID) => false, // although the id is not changeable, but it's not locked by config.\r\n                nameof(Lyric.Text) => true,\r\n                nameof(Lyric.TimeTags) => syncLyricConfig.SyncTimeTagProperty,\r\n                nameof(Lyric.RubyTags) => true,\r\n                nameof(Lyric.StartTime) => false,\r\n                nameof(Lyric.Duration) => false,\r\n                nameof(Lyric.TimeValid) => false,\r\n                nameof(Lyric.SingerIds) => syncLyricConfig.SyncSingerProperty,\r\n                nameof(Lyric.Translations) => true,\r\n                nameof(Lyric.Language) => true,\r\n                nameof(Lyric.Order) => true,\r\n                nameof(Lyric.Lock) => true,\r\n                nameof(Lyric.ReferenceLyric) or nameof(Lyric.ReferenceLyricId) => false,\r\n                nameof(Lyric.ReferenceLyricConfig) => false,\r\n                // base class\r\n                nameof(Lyric.Samples) => false,\r\n                _ => throw new NotSupportedException(),\r\n            },\r\n            null => false,\r\n            _ => throw new NotSupportedException(),\r\n        };\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Create or remove notes.\r\n\r\n    public static bool IsCreateOrRemoveNoteLocked(Lyric lyric)\r\n        => GetCreateOrRemoveNoteLockedBy(lyric) != null;\r\n\r\n    public static LockLyricPropertyBy? GetCreateOrRemoveNoteLockedBy(Lyric lyric)\r\n    {\r\n        bool lockedByConfig = isCreateOrRemoveNoteLocked(lyric.ReferenceLyricConfig);\r\n        if (lockedByConfig)\r\n            return LockLyricPropertyBy.ReferenceLyricConfig;\r\n\r\n        return null;\r\n    }\r\n\r\n    private static bool isCreateOrRemoveNoteLocked(IReferenceLyricPropertyConfig? config)\r\n    {\r\n        // todo: implementation.\r\n        return config switch\r\n        {\r\n            ReferenceLyricConfig => false,\r\n            SyncLyricConfig => true,\r\n            null => false,\r\n            _ => throw new NotSupportedException(),\r\n        };\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Note property\r\n\r\n    public static bool IsWriteNotePropertyLocked(Note note, params string[] propertyNames)\r\n        => GetNotePropertyLockedBy(note, propertyNames) != null;\r\n\r\n    public static bool IsWriteNotePropertyLocked(Note note, string propertyName)\r\n        => GetNotePropertyLockedBy(note, propertyName) != null;\r\n\r\n    public static LockNotePropertyBy? GetNotePropertyLockedBy(Note note, params string[] propertyNames)\r\n    {\r\n        var reasons = propertyNames.Select(x => GetNotePropertyLockedBy(note, x))\r\n                                   .Where(x => x != null)\r\n                                   .OfType<LockNotePropertyBy>()\r\n                                   .ToArray();\r\n\r\n        if (reasons.Contains(LockNotePropertyBy.ReferenceLyricConfig))\r\n            return LockNotePropertyBy.ReferenceLyricConfig;\r\n\r\n        return null;\r\n    }\r\n\r\n    public static LockNotePropertyBy? GetNotePropertyLockedBy(Note note, string propertyName)\r\n    {\r\n        var lyric = note.ReferenceLyric;\r\n\r\n        bool lockByReferenceLyricConfig = lyric != null && isWriteNotePropertyLockedByReferenceLyric(lyric, propertyName);\r\n        if (lockByReferenceLyricConfig)\r\n            return LockNotePropertyBy.ReferenceLyricConfig;\r\n\r\n        return null;\r\n    }\r\n\r\n    private static bool isWriteNotePropertyLockedByReferenceLyric(Lyric lyric, string propertyName)\r\n    {\r\n        // todo: implement.\r\n        return false;\r\n    }\r\n\r\n    #endregion\r\n}\r\n\r\npublic enum LockLyricPropertyBy\r\n{\r\n    ReferenceLyricConfig,\r\n\r\n    LockState,\r\n}\r\n\r\npublic enum LockNotePropertyBy\r\n{\r\n    ReferenceLyricConfig,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Utils/LockStateUtils.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Types;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Utils;\r\n\r\npublic static class LockStateUtils\r\n{\r\n    public static TLock[] FindUnlockObjects<TLock>(IEnumerable<TLock> objects) where TLock : IHasLock\r\n        => objects.Where(x => x.Lock == LockState.None).ToArray();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Utils/ValueChangedEventUtils.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Utils;\r\n\r\npublic static class ValueChangedEventUtils\r\n{\r\n    public static bool LyricChanged(ValueChangedEvent<ICaretPosition?> e)\r\n    {\r\n        var oldLyric = e.OldValue?.Lyric;\r\n        var newLyric = e.NewValue?.Lyric;\r\n\r\n        return oldLyric != newLyric;\r\n    }\r\n\r\n    public static bool LyricChanged(ValueChangedEvent<RangeCaretPosition?> e)\r\n    {\r\n        var oldRangeCaret = e.OldValue;\r\n        var newRangeCaret = e.NewValue;\r\n\r\n        return oldRangeCaret?.Start.Lyric != newRangeCaret?.Start.Lyric\r\n               || oldRangeCaret?.End.Lyric != newRangeCaret?.End.Lyric;\r\n    }\r\n\r\n    public static bool EditModeChanged(ValueChangedEvent<EditorModeWithEditStep> e)\r\n    {\r\n        if (e.OldValue.Default ^ e.NewValue.Default)\r\n            return true;\r\n\r\n        return e.OldValue.Mode != e.NewValue.Mode;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Edit/Utils/ZoomableScrollContainerUtils.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Edit.Utils;\r\n\r\npublic static class ZoomableScrollContainerUtils\r\n{\r\n    public static float GetZoomLevelForVisibleMilliseconds(EditorClock editorClock, double milliseconds)\r\n        => Math.Max(1, (float)(editorClock.TrackLength / milliseconds));\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Extensions/EnumerableExtensions.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Extensions;\r\n\r\npublic static class EnumerableExtensions\r\n{\r\n    /// <summary>\r\n    /// Retrieves the item after a pivot from an <see cref=\"IEnumerable{T}\"/>.\r\n    /// </summary>\r\n    /// <typeparam name=\"T\">The type of the items stored in the collection.</typeparam>\r\n    /// <param name=\"collection\">The collection to iterate on.</param>\r\n    /// <param name=\"pivot\">The pivot value.</param>\r\n    /// <param name=\"action\">Match action</param>\r\n    /// <returns>The item in <paramref name=\"collection\"/> appearing after <paramref name=\"pivot\"/>, or null if no such item exists.</returns>\r\n    public static T? GetNextMatch<T>(this IEnumerable<T> collection, T pivot, Func<T, bool> action) where T : notnull\r\n    {\r\n        return collection.SkipWhile(i => !EqualityComparer<T>.Default.Equals(i, pivot)).Skip(1).SkipWhile(x => !action(x)).FirstOrDefault();\r\n    }\r\n\r\n    /// <summary>\r\n    /// Retrieves the item before a pivot from an <see cref=\"IEnumerable{T}\"/>.\r\n    /// </summary>\r\n    /// <typeparam name=\"T\">The type of the items stored in the collection.</typeparam>\r\n    /// <param name=\"collection\">The collection to iterate on.</param>\r\n    /// <param name=\"pivot\">The pivot value.</param>\r\n    /// <param name=\"action\">Match action</param>\r\n    /// <returns>The item in <paramref name=\"collection\"/> appearing before <paramref name=\"pivot\"/>, or null if no such item exists.</returns>\r\n    public static T? GetPreviousMatch<T>(this IEnumerable<T> collection, T pivot, Func<T, bool> action) where T : notnull\r\n    {\r\n        return collection.Reverse().SkipWhile(i => !EqualityComparer<T>.Default.Equals(i, pivot)).Skip(1).SkipWhile(x => !action(x)).FirstOrDefault();\r\n    }\r\n\r\n    /// <summary>\r\n    /// Convert [][] to [,]\r\n    /// </summary>\r\n    /// <typeparam name=\"T\"></typeparam>\r\n    /// <param name=\"source\"></param>\r\n    /// <returns></returns>\r\n    public static T[,] To2DArray<T>(this IEnumerable<IEnumerable<T>> source)\r\n    {\r\n        var data = source\r\n                   .Select(x => x.ToArray())\r\n                   .ToArray();\r\n\r\n        var res = new T[data.Length, data.Max(x => x.Length)];\r\n\r\n        for (int i = 0; i < data.Length; ++i)\r\n        {\r\n            for (int j = 0; j < data[i].Length; ++j)\r\n            {\r\n                res[i, j] = data[i][j];\r\n            }\r\n        }\r\n\r\n        return res;\r\n    }\r\n\r\n    public static int IndexOf<T>(this IEnumerable<T> array, T value)\r\n    {\r\n        return array.ToList().IndexOf(value);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Extensions/OsuGameExtensions.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Testing;\r\nusing osu.Game.Overlays;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Extensions;\r\n\r\n/// <summary>\r\n/// Collect dirty logic to get target drawable from <see cref=\"OsuGame\"/>\r\n/// </summary>\r\npublic static class OsuGameExtensions\r\n{\r\n    public static KaraokeRuleset GetRuleset(this DependencyContainer dependencies)\r\n    {\r\n        var rulesets = dependencies.Get<RulesetStore>().AvailableRulesets.Select(info => info.CreateInstance());\r\n        return rulesets.OfType<KaraokeRuleset>().First();\r\n    }\r\n\r\n    private static Container? getBasePlacementContainer(this OsuGame game)\r\n        => game.Children.OfType<Container>().FirstOrDefault(c => c.ChildrenOfType<WaveOverlayContainer>().Any());\r\n\r\n    public static Container? GetChangelogPlacementContainer(this OsuGame game)\r\n    {\r\n        // will place the container where components of an WaveOverlayContainer type exist\r\n        return game.getBasePlacementContainer()?.Children.OfType<Container>().FirstOrDefault(c => c.Children.OfType<WaveOverlayContainer>().Any());\r\n    }\r\n\r\n    public static SettingsOverlay? GetSettingsOverlay(this OsuGame game)\r\n        => game.getBasePlacementContainer()?.ChildrenOfType<SettingsOverlay>().FirstOrDefault();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Extensions/RegexExtensions.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Text.RegularExpressions;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Extensions;\r\n\r\npublic static class RegexExtensions\r\n{\r\n    public static TType GetGroupValue<TType>(this Match match, string key, bool useDefaultValueIfEmpty = true)\r\n    {\r\n        string value = match.Groups[key].Value;\r\n\r\n        // if got empty value, should change to null.\r\n        return TypeUtils.ChangeType<TType>(string.IsNullOrEmpty(value) ? null : value)!;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Extensions/ScrollContainerExtensions.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Extensions;\r\n\r\npublic static class ScrollContainerExtensions\r\n{\r\n    /// <summary>\r\n    /// Extend the <see cref=\"ScrollContainer{T}\"/> to scroll into view with spacing.\r\n    /// </summary>\r\n    /// <param name=\"container\"></param>\r\n    /// <param name=\"d\"></param>\r\n    /// <param name=\"p\"></param>\r\n    /// <param name=\"animated\"></param>\r\n    /// <typeparam name=\"T\"></typeparam>\r\n    public static void ScrollIntoViewWithSpacing<T>(this ScrollContainer<T> container, Drawable d, MarginPadding p, bool animated = true)\r\n        where T : Drawable\r\n    {\r\n        double childPos0 = Math.Clamp(container.GetChildPosInContent(d, -new Vector2(p.Left, p.Top)), 0, container.AvailableContent);\r\n        double childPos1 = Math.Clamp(container.GetChildPosInContent(d, d.DrawSize + new Vector2(p.Right, p.Bottom)), 0, container.AvailableContent);\r\n\r\n        int scrollDim = container.ScrollDirection == Direction.Horizontal ? 0 : 1;\r\n        double minPos = Math.Min(childPos0, childPos1);\r\n        double maxPos = Math.Max(childPos0, childPos1);\r\n\r\n        if (minPos < container.Current || (minPos > container.Current && d.DrawSize[scrollDim] > container.DisplayableContent))\r\n            container.ScrollTo(minPos, animated);\r\n        else if (maxPos > container.Current + container.DisplayableContent)\r\n            container.ScrollTo(maxPos - container.DisplayableContent, animated);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Extensions/TrickyCompositeDrawableExtension.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Reflection;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Extensions;\r\n\r\n/// <summary>\r\n/// It's a tricky extension to get all non-public methods.\r\n/// Should be removed eventually.\r\n/// </summary>\r\npublic static class TrickyCompositeDrawableExtension\r\n{\r\n    public static IReadOnlyList<Drawable>? GetInternalChildren(this CompositeDrawable compositeDrawable)\r\n    {\r\n        // see this shit to access internal property.\r\n        // https://stackoverflow.com/a/7575615/4105113\r\n        var prop = compositeDrawable.GetType().GetProperty(\"InternalChildren\", BindingFlags.Instance |\r\n                                                                               BindingFlags.NonPublic |\r\n                                                                               BindingFlags.Public);\r\n        if (prop == null)\r\n            return null;\r\n\r\n        return (IReadOnlyList<Drawable>)prop.GetValue(compositeDrawable)!;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Extensions/TypeExtensions.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Extensions;\r\n\r\npublic static class TypeExtensions\r\n{\r\n    /// <summary>\r\n    /// Returns <paramref name=\"type\"/>'s <see cref=\"Type.AssemblyQualifiedName\"/>\r\n    /// with the assembly version, culture and public key token values removed.\r\n    /// </summary>\r\n    /// <remarks>\r\n    /// This method is usually used in extensibility scenarios (i.e. for custom rulesets or skins)\r\n    /// when a version-agnostic identifier associated with a C# class - potentially originating from\r\n    /// an external assembly - is needed.\r\n    /// Leaving only the type and assembly names in such a scenario allows to preserve compatibility\r\n    /// across assembly versions.\r\n    /// </remarks>\r\n    public static string GetInvariantInstantiationInfo(this Type type)\r\n    {\r\n        string? assemblyQualifiedName = type.AssemblyQualifiedName;\r\n        if (assemblyQualifiedName == null)\r\n            throw new ArgumentException($\"{type}'s assembly-qualified name is null. Ensure that it is a concrete type and not a generic type parameter.\", nameof(type));\r\n\r\n        return string.Join(',', assemblyQualifiedName.Split(',').Take(2));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Flags/FlagState.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Flags;\r\n\r\npublic class FlagState<TFlag> where TFlag : struct, Enum\r\n{\r\n    #region Public interface\r\n\r\n    private int value;\r\n\r\n    public bool Invalidate(TFlag flags)\r\n    {\r\n        if (!CanInvalidate(flags))\r\n            return false;\r\n\r\n        value &= ~Convert.ToInt32(flags);\r\n\r\n        return true;\r\n    }\r\n\r\n    public void InvalidateAll()\r\n    {\r\n        foreach (TFlag flag in Enum.GetValues(typeof(TFlag)))\r\n        {\r\n            Invalidate(flag);\r\n        }\r\n    }\r\n\r\n    public bool Validate(TFlag flags)\r\n    {\r\n        if (!CanValidate(flags))\r\n            return false;\r\n\r\n        value |= Convert.ToInt32(flags);\r\n\r\n        return true;\r\n    }\r\n\r\n    public void ValidateAll()\r\n    {\r\n        foreach (TFlag flag in Enum.GetValues(typeof(TFlag)))\r\n        {\r\n            Validate(flag);\r\n        }\r\n    }\r\n\r\n    public bool IsValid(TFlag flags)\r\n    {\r\n        return (value & Convert.ToInt32(flags)) == Convert.ToInt32(flags);\r\n    }\r\n\r\n    public TFlag[] GetAllValidFlags()\r\n        => Enum.GetValues<TFlag>().Where(IsValid).ToArray();\r\n\r\n    public TFlag[] GetAllInvalidFlags()\r\n        => Enum.GetValues<TFlag>().Where(x => !IsValid(x)).ToArray();\r\n\r\n    #endregion\r\n\r\n    protected virtual bool CanInvalidate(TFlag flags) => true;\r\n\r\n    protected virtual bool CanValidate(TFlag flags) => true;\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Graphics/Containers/OrderRearrangeableListContainer.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Specialized;\r\nusing System.Diagnostics;\r\nusing System.Linq;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Graphics.Containers;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Graphics.Containers;\r\n\r\npublic abstract partial class OrderRearrangeableListContainer<TModel> : OsuRearrangeableListContainer<TModel>\r\n{\r\n    public event Action<TModel, int>? OnOrderChanged;\r\n\r\n    protected abstract Vector2 Spacing { get; }\r\n\r\n    protected OrderRearrangeableListContainer()\r\n    {\r\n        // this collection change event cannot directly register in parent bindable.\r\n        // So register in here.\r\n        Items.CollectionChanged += collectionChanged;\r\n    }\r\n\r\n    private void collectionChanged(object? sender, NotifyCollectionChangedEventArgs e)\r\n    {\r\n        switch (e.Action)\r\n        {\r\n            // should get the event if user change the position.\r\n            case NotifyCollectionChangedAction.Move:\r\n                Debug.Assert(e.NewItems != null);\r\n\r\n                var item = e.NewItems.OfType<TModel>().First();\r\n                int newIndex = e.NewStartingIndex;\r\n                OnOrderChanged?.Invoke(item, newIndex);\r\n                break;\r\n        }\r\n    }\r\n\r\n    protected sealed override FillFlowContainer<RearrangeableListItem<TModel>> CreateListFillFlowContainer()\r\n        => base.CreateListFillFlowContainer().With(x => x.Spacing = Spacing);\r\n\r\n    private bool displayBottomDrawable;\r\n    private Drawable? bottomDrawable;\r\n\r\n    public bool DisplayBottomDrawable\r\n    {\r\n        get => displayBottomDrawable;\r\n        set\r\n        {\r\n            if (displayBottomDrawable == value)\r\n                return;\r\n\r\n            displayBottomDrawable = value;\r\n\r\n            if (displayBottomDrawable)\r\n            {\r\n                bottomDrawable = CreateBottomDrawable();\r\n                if (bottomDrawable == null)\r\n                    return;\r\n\r\n                bottomDrawable.Anchor |= Anchor.y2;\r\n                bottomDrawable.Origin |= Anchor.y2;\r\n\r\n                // because scroll container only follow list container size, so change the margin to let content bigger.\r\n                ListContainer.Margin = new MarginPadding { Bottom = bottomDrawable.Height + Spacing.Y };\r\n                ScrollContainer.Add(bottomDrawable);\r\n            }\r\n            else\r\n            {\r\n                if (bottomDrawable == null)\r\n                    return;\r\n\r\n                ListContainer.Margin = new MarginPadding();\r\n                ScrollContainer.Remove(bottomDrawable, true);\r\n            }\r\n        }\r\n    }\r\n\r\n    protected virtual Drawable? CreateBottomDrawable() => null;\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Graphics/Containers/RearrangeableTextFlowListContainer.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Containers;\r\nusing osuTK;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Graphics.Containers;\r\n\r\n/// <summary>\r\n/// Implement most feature for searchable text container.\r\n/// </summary>\r\n/// <typeparam name=\"TModel\"></typeparam>\r\npublic partial class RearrangeableTextFlowListContainer<TModel> : OsuRearrangeableListContainer<TModel>\r\n{\r\n    public readonly Bindable<TModel> SelectedSet = new();\r\n\r\n    public Action<TModel>? RequestSelection;\r\n\r\n    private SearchContainer<RearrangeableListItem<TModel>> searchContainer = null!;\r\n\r\n    protected sealed override FillFlowContainer<RearrangeableListItem<TModel>> CreateListFillFlowContainer() => searchContainer = new SearchContainer<RearrangeableListItem<TModel>>\r\n    {\r\n        Spacing = new Vector2(0, 3),\r\n        LayoutDuration = 200,\r\n        LayoutEasing = Easing.OutQuint,\r\n    };\r\n\r\n    public void Filter(string text)\r\n    {\r\n        searchContainer.SearchTerm = text;\r\n    }\r\n\r\n    protected sealed override OsuRearrangeableListItem<TModel> CreateOsuDrawable(TModel item)\r\n        => CreateDrawable(item).With(d =>\r\n        {\r\n            d.SelectedSet.BindTarget = SelectedSet;\r\n            d.RequestSelection = set => RequestSelection?.Invoke(set);\r\n        });\r\n\r\n    protected new virtual DrawableTextListItem CreateDrawable(TModel item)\r\n        => new(item);\r\n\r\n    public partial class DrawableTextListItem : OsuRearrangeableListItem<TModel>, IFilterable\r\n    {\r\n        public readonly Bindable<TModel> SelectedSet = new();\r\n\r\n        public Action<TModel>? RequestSelection;\r\n\r\n        private TextFlowContainer text = null!;\r\n\r\n        private Color4 selectedColour;\r\n\r\n        public DrawableTextListItem(TModel item)\r\n            : base(item)\r\n        {\r\n            Padding = new MarginPadding { Left = 5 };\r\n            ShowDragHandle.Value = false;\r\n        }\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(OsuColour colours)\r\n        {\r\n            selectedColour = colours.Yellow;\r\n            HandleColour = colours.Gray5;\r\n        }\r\n\r\n        protected override void LoadComplete()\r\n        {\r\n            base.LoadComplete();\r\n\r\n            SelectedSet.BindValueChanged(set =>\r\n            {\r\n                bool oldValueMatched = EqualityComparer<TModel>.Default.Equals(set.OldValue, Model);\r\n                bool newValueMatched = EqualityComparer<TModel>.Default.Equals(set.NewValue, Model);\r\n                if (!oldValueMatched && !newValueMatched)\r\n                    return;\r\n\r\n                text.FadeColour(newValueMatched ? selectedColour : Color4.White, FADE_DURATION);\r\n            }, true);\r\n        }\r\n\r\n        protected sealed override Drawable CreateContent() => text = new OsuTextFlowContainer\r\n        {\r\n            RelativeSizeAxes = Axes.X,\r\n            AutoSizeAxes = Axes.Y,\r\n        }.With(x =>\r\n        {\r\n            Schedule(() =>\r\n            {\r\n                // should create the text after BDL loaded.\r\n                CreateDisplayContent(x, Model);\r\n            });\r\n        });\r\n\r\n        protected override bool OnClick(ClickEvent e)\r\n        {\r\n            RequestSelection?.Invoke(Model);\r\n            return true;\r\n        }\r\n\r\n        public virtual IEnumerable<LocalisableString> FilterTerms => new[]\r\n        {\r\n            new LocalisableString(Model?.ToString() ?? string.Empty),\r\n        };\r\n\r\n        protected virtual void CreateDisplayContent(OsuTextFlowContainer textFlowContainer, TModel model)\r\n            => textFlowContainer.AddText(model?.ToString() ?? string.Empty);\r\n\r\n        private bool matchingFilter = true;\r\n\r\n        public bool MatchingFilter\r\n        {\r\n            get => matchingFilter;\r\n            set\r\n            {\r\n                if (matchingFilter == value)\r\n                    return;\r\n\r\n                matchingFilter = value;\r\n                updateFilter();\r\n            }\r\n        }\r\n\r\n        private void updateFilter() => this.FadeTo(MatchingFilter ? 1 : 0, 200);\r\n\r\n        public bool FilteringActive { get; set; }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Graphics/Cursor/BackgroundToolTip.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Cursor;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Game.Graphics;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Graphics.Cursor;\r\n\r\npublic abstract partial class BackgroundToolTip<T> : VisibilityContainer, ITooltip<T>\r\n{\r\n    protected const int BORDER = 5;\r\n\r\n    private readonly Box background;\r\n    private readonly Container content;\r\n\r\n    protected override Container<Drawable> Content => content;\r\n\r\n    protected virtual float ContentPadding => 10;\r\n\r\n    protected BackgroundToolTip()\r\n    {\r\n        AutoSizeAxes = Axes.Both;\r\n        Masking = true;\r\n        CornerRadius = BORDER;\r\n\r\n        InternalChildren = new[]\r\n        {\r\n            background = new Box\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n            },\r\n            SetBackground(),\r\n            content = new Container\r\n            {\r\n                AutoSizeAxes = Axes.Both,\r\n                AutoSizeDuration = 200,\r\n                AutoSizeEasing = Easing.OutQuint,\r\n                Padding = new MarginPadding(ContentPadding),\r\n            },\r\n        };\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OsuColour colours)\r\n    {\r\n        background.Colour = colours.Gray3;\r\n    }\r\n\r\n    public abstract void SetContent(T content);\r\n\r\n    protected virtual Drawable SetBackground() => Empty();\r\n\r\n    public void Move(Vector2 pos) => Position = pos;\r\n\r\n    protected override void PopIn() => this.FadeIn(200, Easing.OutQuint);\r\n\r\n    protected override void PopOut() => this.FadeOut(200, Easing.OutQuint);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Graphics/Cursor/LyricToolTip.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Graphics.Cursor;\r\n\r\npublic partial class LyricTooltip : BackgroundToolTip<Lyric>\r\n{\r\n    private Lyric? lastLyric;\r\n\r\n    public override void SetContent(Lyric lyric)\r\n    {\r\n        if (lyric == lastLyric)\r\n            return;\r\n\r\n        lastLyric = lyric;\r\n\r\n        Child = new DrawableLyricSpriteText(lyric)\r\n        {\r\n            Margin = new MarginPadding(10),\r\n            Font = new FontUsage(size: 32),\r\n            TopTextFont = new FontUsage(size: 12),\r\n            BottomTextFont = new FontUsage(size: 12),\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Graphics/Cursor/SingerToolTip.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Extensions.Color4Extensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas.Types;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.Drawables;\r\nusing osuTK;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Graphics.Cursor;\r\n\r\npublic partial class SingerToolTip : BackgroundToolTip<ISinger>\r\n{\r\n    private const int avatar_size = 60;\r\n    private const int main_text_size = 24;\r\n    private const int sub_text_size = 12;\r\n\r\n    private readonly IBindable<string> bindableName = new Bindable<string>();\r\n    private readonly IBindable<string> bindableRomanisation = new Bindable<string>();\r\n    private readonly IBindable<string> bindableEnglishName = new Bindable<string>();\r\n    private readonly IBindable<string> bindableDescription = new Bindable<string>();\r\n\r\n    [Cached(typeof(IKaraokeBeatmapResourcesProvider))]\r\n    private KaraokeBeatmapResourcesProvider karaokeBeatmapResourcesProvider;\r\n\r\n    private readonly DrawableSingerAvatar avatar;\r\n    private readonly OsuSpriteText name;\r\n    private readonly OsuSpriteText englishName;\r\n    private readonly OsuSpriteText romanisation;\r\n    private readonly OsuSpriteText description;\r\n\r\n    public SingerToolTip()\r\n    {\r\n        // we need to inject this provide in the tooltip because in will need in the drawable singer avatar.\r\n        // and it's not able to get in the BDL due to tooltip container is in the osu.game level.\r\n        AddInternal(karaokeBeatmapResourcesProvider = new KaraokeBeatmapResourcesProvider());\r\n\r\n        Child = new FillFlowContainer\r\n        {\r\n            AutoSizeAxes = Axes.Y,\r\n            Width = 300,\r\n            Direction = FillDirection.Vertical,\r\n            Spacing = new Vector2(15),\r\n            Children = new Drawable[]\r\n            {\r\n                new GridContainer\r\n                {\r\n                    Name = \"Basic info\",\r\n                    RelativeSizeAxes = Axes.X,\r\n                    Height = avatar_size,\r\n                    ColumnDimensions = new[]\r\n                    {\r\n                        new Dimension(GridSizeMode.Absolute, avatar_size),\r\n                        new Dimension(),\r\n                    },\r\n                    Content = new[]\r\n                    {\r\n                        new Drawable[]\r\n                        {\r\n                            avatar = new DrawableSingerAvatar\r\n                            {\r\n                                Name = \"Avatar\",\r\n                                Size = new Vector2(avatar_size),\r\n                            },\r\n                            new Container\r\n                            {\r\n                                RelativeSizeAxes = Axes.Both,\r\n                                Padding = new MarginPadding { Left = 5 },\r\n                                Children = new Drawable[]\r\n                                {\r\n                                    new FillFlowContainer\r\n                                    {\r\n                                        Name = \"Singer name\",\r\n                                        RelativeSizeAxes = Axes.X,\r\n                                        AutoSizeAxes = Axes.Y,\r\n                                        Direction = FillDirection.Vertical,\r\n                                        Spacing = new Vector2(1),\r\n                                        Children = new[]\r\n                                        {\r\n                                            name = new TruncatingSpriteText\r\n                                            {\r\n                                                Name = \"Singer name\",\r\n                                                Font = OsuFont.GetFont(weight: FontWeight.Bold, size: main_text_size),\r\n                                                RelativeSizeAxes = Axes.X,\r\n                                                ShowTooltip = false,\r\n                                            },\r\n                                            romanisation = new TruncatingSpriteText\r\n                                            {\r\n                                                Name = \"Romanisation\",\r\n                                                Font = OsuFont.GetFont(weight: FontWeight.Bold, size: sub_text_size),\r\n                                                RelativeSizeAxes = Axes.X,\r\n                                                ShowTooltip = false,\r\n                                            },\r\n                                        },\r\n                                    },\r\n                                    englishName = new TruncatingSpriteText\r\n                                    {\r\n                                        Name = \"English name\",\r\n                                        Anchor = Anchor.BottomLeft,\r\n                                        Origin = Anchor.BottomLeft,\r\n                                        Font = OsuFont.GetFont(weight: FontWeight.Bold, size: sub_text_size),\r\n                                        RelativeSizeAxes = Axes.X,\r\n                                        ShowTooltip = false,\r\n                                    },\r\n                                },\r\n                            },\r\n                        },\r\n                    },\r\n                },\r\n                description = new OsuSpriteText\r\n                {\r\n                    RelativeSizeAxes = Axes.X,\r\n                    AllowMultiline = true,\r\n                    Colour = Color4.White.Opacity(0.75f),\r\n                    Font = OsuFont.Default.With(size: 14),\r\n                    Name = \"Description\",\r\n                },\r\n            },\r\n        };\r\n\r\n        bindableName.BindValueChanged(e => name.Text = e.NewValue, true);\r\n        bindableRomanisation.BindValueChanged(e => romanisation.Text = string.IsNullOrEmpty(e.NewValue) ? string.Empty : $\"({e.NewValue})\", true);\r\n        bindableEnglishName.BindValueChanged(e => englishName.Text = e.NewValue, true);\r\n        bindableDescription.BindValueChanged(e => description.Text = string.IsNullOrEmpty(e.NewValue) ? \"<No description>\" : e.NewValue, true);\r\n    }\r\n\r\n    private ISinger? lastSinger;\r\n\r\n    public override void SetContent(ISinger singer)\r\n    {\r\n        if (singer == lastSinger)\r\n            return;\r\n\r\n        avatar.Singer = singer;\r\n\r\n        lastSinger = singer;\r\n\r\n        // todo: other type of singer(e.g: sub-singer) might display different info.\r\n        if (singer is not Singer s)\r\n            return;\r\n\r\n        bindableName.UnbindBindings();\r\n        bindableRomanisation.UnbindBindings();\r\n        bindableEnglishName.UnbindBindings();\r\n        bindableDescription.UnbindBindings();\r\n\r\n        bindableName.BindTo(s.NameBindable);\r\n        bindableRomanisation.BindTo(s.RomanisationBindable);\r\n        bindableEnglishName.BindTo(s.EnglishNameBindable);\r\n        bindableDescription.BindTo(s.DescriptionBindable);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Graphics/Drawables/DrawableCircleSingerAvatar.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas.Types;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Graphics.Drawables;\r\n\r\npublic partial class DrawableCircleSingerAvatar : DrawableSingerAvatar\r\n{\r\n    private readonly IBindable<float> bindableHue = new Bindable<float>();\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OsuColour colour)\r\n    {\r\n        Masking = true;\r\n        CornerRadius = Math.Min(DrawSize.X, DrawSize.Y) / 2f;\r\n        BorderThickness = 5;\r\n\r\n        bindableHue.BindValueChanged(_ =>\r\n        {\r\n            BorderColour = Singer != null ? SingerUtils.GetContentColour(Singer) : colour.Gray0;\r\n        }, true);\r\n    }\r\n\r\n    public override ISinger? Singer\r\n    {\r\n        get => base.Singer;\r\n        set\r\n        {\r\n            base.Singer = value;\r\n\r\n            bindableHue.UnbindBindings();\r\n\r\n            if (value is Singer singer)\r\n                bindableHue.BindTo(singer.HueBindable);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Graphics/Drawables/DrawableSingerAvatar.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Graphics.Textures;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas.Types;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Graphics.Drawables;\r\n\r\npublic partial class DrawableSingerAvatar : CompositeDrawable\r\n{\r\n    private readonly IBindable<string> bindableAvatarFile = new Bindable<string>();\r\n\r\n    private readonly Sprite avatar;\r\n\r\n    public DrawableSingerAvatar()\r\n    {\r\n        InternalChild = avatar = new Sprite\r\n        {\r\n            RelativeSizeAxes = Axes.Both,\r\n            FillMode = FillMode.Fit,\r\n            Anchor = Anchor.Centre,\r\n            Origin = Anchor.Centre,\r\n        };\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(LargeTextureStore textures, IKaraokeBeatmapResourcesProvider karaokeBeatmapResourcesProvider)\r\n    {\r\n        bindableAvatarFile.BindValueChanged(_ =>\r\n        {\r\n            if (singer == null)\r\n                avatar.Texture = getDefaultAvatar();\r\n            else\r\n                avatar.Texture = karaokeBeatmapResourcesProvider.GetSingerAvatar(singer) ?? getDefaultAvatar();\r\n\r\n            avatar.FadeInFromZero(500);\r\n        }, true);\r\n\r\n        Texture getDefaultAvatar()\r\n            => textures.Get(\"Online/avatar-guest\");\r\n    }\r\n\r\n    private ISinger? singer;\r\n\r\n    public virtual ISinger? Singer\r\n    {\r\n        get => singer;\r\n        set\r\n        {\r\n            singer = value;\r\n\r\n            if (singer is not Singer s)\r\n                return;\r\n\r\n            bindableAvatarFile.UnbindBindings();\r\n            bindableAvatarFile.BindTo(s.AvatarFileBindable);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Graphics/Drawables/SingerDisplay.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Cursor;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Game.Graphics.Containers;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas.Types;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.Cursor;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Graphics.Drawables;\r\n\r\npublic partial class SingerDisplay : CompositeDrawable, IHasCurrentValue<IReadOnlyList<Singer>>\r\n{\r\n    private const int fade_duration = 1000;\r\n\r\n    public bool DisplayUnrankedText = true;\r\n\r\n    public ExpansionMode ExpansionMode = ExpansionMode.ExpandOnHover;\r\n\r\n    private readonly Bindable<IReadOnlyList<Singer>> current = new();\r\n\r\n    public Bindable<IReadOnlyList<Singer>> Current\r\n    {\r\n        get => current;\r\n        set\r\n        {\r\n            ArgumentNullException.ThrowIfNull(value);\r\n\r\n            current.UnbindBindings();\r\n            current.BindTo(value);\r\n        }\r\n    }\r\n\r\n    private readonly FillFlowContainer<DrawableSinger> iconsContainer;\r\n\r\n    public SingerDisplay()\r\n    {\r\n        AutoSizeAxes = Axes.Both;\r\n\r\n        InternalChild = new FillFlowContainer\r\n        {\r\n            Anchor = Anchor.TopCentre,\r\n            Origin = Anchor.TopCentre,\r\n            AutoSizeAxes = Axes.Both,\r\n            Direction = FillDirection.Vertical,\r\n            Children = new Drawable[]\r\n            {\r\n                iconsContainer = new ReverseChildIDFillFlowContainer<DrawableSinger>\r\n                {\r\n                    Anchor = Anchor.TopCentre,\r\n                    Origin = Anchor.TopCentre,\r\n                    AutoSizeAxes = Axes.Both,\r\n                    Direction = FillDirection.Horizontal,\r\n                },\r\n            },\r\n        };\r\n\r\n        Current.ValueChanged += singers =>\r\n        {\r\n            iconsContainer.Clear();\r\n\r\n            foreach (var singer in singers.NewValue)\r\n            {\r\n                iconsContainer.Add(new DrawableSinger\r\n                {\r\n                    Singer = singer,\r\n                    Name = \"Avatar\",\r\n                    Size = new Vector2(32),\r\n                });\r\n            }\r\n\r\n            if (IsLoaded)\r\n                appearTransform();\r\n        };\r\n    }\r\n\r\n    protected override void Dispose(bool isDisposing)\r\n    {\r\n        base.Dispose(isDisposing);\r\n        Current.UnbindAll();\r\n    }\r\n\r\n    protected override void LoadComplete()\r\n    {\r\n        base.LoadComplete();\r\n\r\n        appearTransform();\r\n        iconsContainer.FadeInFromZero(fade_duration, Easing.OutQuint);\r\n    }\r\n\r\n    private void appearTransform()\r\n    {\r\n        expand();\r\n\r\n        using (iconsContainer.BeginDelayedSequence(1200))\r\n            contract();\r\n    }\r\n\r\n    private void expand()\r\n    {\r\n        if (ExpansionMode != ExpansionMode.AlwaysContracted)\r\n            iconsContainer.TransformSpacingTo(new Vector2(5, 0), 500, Easing.OutQuint);\r\n    }\r\n\r\n    private void contract()\r\n    {\r\n        if (ExpansionMode != ExpansionMode.AlwaysExpanded)\r\n            iconsContainer.TransformSpacingTo(new Vector2(-25, 0), 500, Easing.OutQuint);\r\n    }\r\n\r\n    protected override bool OnHover(HoverEvent e)\r\n    {\r\n        expand();\r\n        return base.OnHover(e);\r\n    }\r\n\r\n    protected override void OnHoverLost(HoverLostEvent e)\r\n    {\r\n        contract();\r\n        base.OnHoverLost(e);\r\n    }\r\n\r\n    private partial class DrawableSinger : DrawableCircleSingerAvatar, IHasCustomTooltip<ISinger>\r\n    {\r\n        public ITooltip<ISinger> GetCustomTooltip() => new SingerToolTip();\r\n\r\n        public ISinger? TooltipContent => Singer;\r\n    }\r\n}\r\n\r\npublic enum ExpansionMode\r\n{\r\n    /// <summary>\r\n    /// The <see cref=\"SingerDisplay\"/> will expand only when hovered.\r\n    /// </summary>\r\n    ExpandOnHover,\r\n\r\n    /// <summary>\r\n    /// The <see cref=\"SingerDisplay\"/> will always be expanded.\r\n    /// </summary>\r\n    AlwaysExpanded,\r\n\r\n    /// <summary>\r\n    /// The <see cref=\"SingerDisplay\"/> will always be contracted.\r\n    /// </summary>\r\n    AlwaysContracted,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Graphics/KaraokeIcon.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Graphics;\r\n\r\npublic static class KaraokeIcon\r\n{\r\n    public static IconUsage Get(int icon) => new((char)icon, \"osuFont\");\r\n\r\n    // ruleset icons in circles\r\n    public static IconUsage RulesetKaraoke => FontAwesome.Solid.PlayCircle;\r\n\r\n    // mod icons\r\n    public static IconUsage ModDisableNote => FontAwesome.Solid.Eraser;\r\n    public static IconUsage ModHiddenNote => OsuIcon.ModHidden;\r\n    public static IconUsage ModHiddenRuby => FontAwesome.Solid.Gem;\r\n    public static IconUsage ModPractice => FontAwesome.Solid.Music;\r\n    public static IconUsage ModAutoPlayBySinger => FontAwesome.Solid.Music;\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Graphics/Shapes/CornerBackground.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Graphics.Shapes;\r\n\r\npublic partial class CornerBackground : CompositeDrawable\r\n{\r\n    public CornerBackground()\r\n    {\r\n        Masking = true;\r\n        CornerRadius = 5;\r\n        AddInternal(new Box { RelativeSizeAxes = Axes.Both });\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Graphics/Shapes/RightTriangle.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Primitives;\r\nusing osu.Framework.Graphics.Rendering;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Graphics.Shapes;\r\n\r\npublic partial class RightTriangle : Sprite\r\n{\r\n    /// <summary>\r\n    /// Creates a new right triangle with a white pixel as texture.\r\n    /// </summary>\r\n    public RightTriangle()\r\n    {\r\n        // Setting the texture would normally set a size of (1, 1), but since the texture is set from BDL it needs to be set here instead.\r\n        // RelativeSizeAxes may not behave as expected if this is not done.\r\n        Size = Vector2.One;\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(IRenderer renderer)\r\n    {\r\n        Texture ??= renderer.WhitePixel;\r\n    }\r\n\r\n    public override RectangleF BoundingBox => toTriangle(ToParentSpace(LayoutRectangle), RightAngleDirection).AABBFloat;\r\n\r\n    private TriangleRightAngleDirection rightAngleDirection = TriangleRightAngleDirection.BottomLeft;\r\n\r\n    public TriangleRightAngleDirection RightAngleDirection\r\n    {\r\n        get => rightAngleDirection;\r\n        set\r\n        {\r\n            rightAngleDirection = value;\r\n            Invalidate();\r\n        }\r\n    }\r\n\r\n    private static Triangle toTriangle(Quad q, TriangleRightAngleDirection rightAngleDirection) =>\r\n        rightAngleDirection switch\r\n        {\r\n            TriangleRightAngleDirection.TopLeft => new Triangle(q.TopLeft, q.TopRight, q.BottomLeft),\r\n            TriangleRightAngleDirection.TopRight => new Triangle(q.TopLeft, q.TopRight, q.BottomRight),\r\n            TriangleRightAngleDirection.BottomLeft => new Triangle(q.TopLeft, q.BottomLeft, q.BottomRight),\r\n            TriangleRightAngleDirection.BottomRight => new Triangle(q.TopRight, q.BottomLeft, q.BottomRight),\r\n            _ => throw new ArgumentOutOfRangeException(nameof(rightAngleDirection), rightAngleDirection, null),\r\n        };\r\n\r\n    public override bool Contains(Vector2 screenSpacePos) => toTriangle(ScreenSpaceDrawQuad, RightAngleDirection).Contains(screenSpacePos);\r\n\r\n    protected override DrawNode CreateDrawNode() => new TriangleDrawNode(this);\r\n\r\n    private class TriangleDrawNode : SpriteDrawNode\r\n    {\r\n        protected new RightTriangle Source => (RightTriangle)base.Source;\r\n\r\n        private TriangleRightAngleDirection rightAngleDirection;\r\n\r\n        public TriangleDrawNode(RightTriangle source)\r\n            : base(source)\r\n        {\r\n        }\r\n\r\n        public override void ApplyState()\r\n        {\r\n            base.ApplyState();\r\n\r\n            rightAngleDirection = Source.RightAngleDirection;\r\n        }\r\n\r\n        protected override void Blit(IRenderer renderer)\r\n        {\r\n            if (DrawRectangle.Width == 0 || DrawRectangle.Height == 0)\r\n                return;\r\n\r\n            renderer.DrawTriangle(Texture, toTriangle(ScreenSpaceDrawQuad, rightAngleDirection), DrawColourInfo.Colour, null, null,\r\n                new Vector2(InflationAmount.X / DrawRectangle.Width, InflationAmount.Y / DrawRectangle.Height), TextureCoords);\r\n        }\r\n\r\n        protected override void BlitOpaqueInterior(IRenderer renderer)\r\n        {\r\n            if (DrawRectangle.Width == 0 || DrawRectangle.Height == 0)\r\n                return;\r\n\r\n            var triangle = toTriangle(ConservativeScreenSpaceDrawQuad, rightAngleDirection);\r\n\r\n            if (renderer.IsMaskingActive)\r\n                renderer.DrawClipped(ref triangle, Texture, DrawColourInfo.Colour);\r\n            else\r\n                renderer.DrawTriangle(Texture, triangle, DrawColourInfo.Colour);\r\n        }\r\n    }\r\n}\r\n\r\npublic enum TriangleRightAngleDirection\r\n{\r\n    TopLeft,\r\n\r\n    TopRight,\r\n\r\n    BottomLeft,\r\n\r\n    BottomRight,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Graphics/Sprites/DisplayLyricProcessor.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.Sprites.Processor;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Graphics.Sprites;\r\n\r\npublic class DisplayLyricProcessor : IDisposable\r\n{\r\n    public Action<IReadOnlyList<PositionText>>? TopTextChanged;\r\n    public Action<string>? CenterTextChanged;\r\n    public Action<IReadOnlyList<PositionText>>? BottomTextChanged;\r\n    public Action<IReadOnlyDictionary<double, TextIndex>>? TimeTagsChanged;\r\n\r\n    private BaseDisplayProcessor? processor;\r\n\r\n    private readonly Lyric lyric;\r\n\r\n    public DisplayLyricProcessor(Lyric lyric)\r\n    {\r\n        this.lyric = lyric;\r\n        reloadProcessor();\r\n    }\r\n\r\n    private LyricDisplayType displayType = LyricDisplayType.Lyric;\r\n\r\n    public LyricDisplayType DisplayType\r\n    {\r\n        get => displayType;\r\n        set\r\n        {\r\n            if (displayType == value)\r\n                return;\r\n\r\n            displayType = value;\r\n            reloadProcessor();\r\n        }\r\n    }\r\n\r\n    private LyricDisplayProperty displayProperty = LyricDisplayProperty.Both;\r\n\r\n    public LyricDisplayProperty DisplayProperty\r\n    {\r\n        get => displayProperty;\r\n        set\r\n        {\r\n            if (displayProperty == value)\r\n                return;\r\n\r\n            displayProperty = value;\r\n            reloadProcessor();\r\n        }\r\n    }\r\n\r\n    private void reloadProcessor()\r\n    {\r\n        // re-create the processor.\r\n        processor?.Dispose();\r\n        processor = GetLyricDisplayProcessor(lyric, DisplayType, DisplayProperty);\r\n        processor = DisplayType switch\r\n        {\r\n            LyricDisplayType.Lyric => new LyricFirstDisplayProcessor(lyric, DisplayProperty),\r\n            LyricDisplayType.RomanisedSyllable => new RomanisedSyllableFirstDisplayProcessor(lyric, DisplayProperty),\r\n            _ => throw new ArgumentOutOfRangeException(),\r\n        };\r\n\r\n        // pass the change event.\r\n        processor.TopTextChanged = x => TopTextChanged?.Invoke(x);\r\n        processor.CenterTextChanged = x => CenterTextChanged?.Invoke(x);\r\n        processor.BottomTextChanged = x => BottomTextChanged?.Invoke(x);\r\n        processor.TimeTagsChanged = x => TimeTagsChanged?.Invoke(x);\r\n\r\n        // trigger update all after update the processor.\r\n        UpdateAll();\r\n    }\r\n\r\n    /// <summary>\r\n    /// Should call this method after Action is bind outside.\r\n    /// </summary>\r\n    public void UpdateAll()\r\n    {\r\n        if (processor == null)\r\n            throw new InvalidOperationException(\"Processor should not be null.\");\r\n\r\n        processor.UpdateAll();\r\n\r\n        // should trigger top text update even not display.\r\n        if (!displayProperty.HasFlag(LyricDisplayProperty.TopText))\r\n        {\r\n            TopTextChanged?.Invoke(Array.Empty<PositionText>());\r\n        }\r\n\r\n        // should trigger bottom text update even not display.\r\n        if (!displayProperty.HasFlag(LyricDisplayProperty.BottomText))\r\n        {\r\n            BottomTextChanged?.Invoke(Array.Empty<PositionText>());\r\n        }\r\n    }\r\n\r\n    public void Dispose()\r\n    {\r\n        processor?.Dispose();\r\n    }\r\n\r\n    public static BaseDisplayProcessor GetLyricDisplayProcessor(Lyric lyric, LyricDisplayType displayType, LyricDisplayProperty displayProperty) =>\r\n        displayType switch\r\n        {\r\n            LyricDisplayType.Lyric => new LyricFirstDisplayProcessor(lyric, displayProperty),\r\n            LyricDisplayType.RomanisedSyllable => new RomanisedSyllableFirstDisplayProcessor(lyric, displayProperty),\r\n            _ => throw new ArgumentOutOfRangeException(),\r\n        };\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Graphics/Sprites/DrawableKaraokeSpriteText.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics.Shaders;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Skinning.Tools;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Graphics.Sprites;\r\n\r\npublic partial class DrawableKaraokeSpriteText : DrawableKaraokeSpriteText<LyricSpriteText>\r\n{\r\n    public DrawableKaraokeSpriteText(Lyric lyric)\r\n        : base(lyric)\r\n    {\r\n    }\r\n}\r\n\r\npublic abstract partial class DrawableKaraokeSpriteText<TSpriteText> : KaraokeSpriteText<TSpriteText> where TSpriteText : LyricSpriteText, new()\r\n{\r\n    private readonly DisplayLyricProcessor processor;\r\n\r\n    [Resolved]\r\n    private ShaderManager? shaderManager { get; set; }\r\n\r\n    protected DrawableKaraokeSpriteText(Lyric lyric)\r\n    {\r\n        processor = new DisplayLyricProcessor(lyric)\r\n        {\r\n            TopTextChanged = topTexts =>\r\n            {\r\n                TopTexts = topTexts;\r\n                OnPropertyChanged();\r\n            },\r\n            CenterTextChanged = text =>\r\n            {\r\n                Text = text;\r\n                OnPropertyChanged();\r\n            },\r\n            BottomTextChanged = bottomTexts =>\r\n            {\r\n                BottomTexts = bottomTexts;\r\n                OnPropertyChanged();\r\n            },\r\n            TimeTagsChanged = timeTags =>\r\n            {\r\n                TimeTags = timeTags;\r\n                OnPropertyChanged();\r\n            },\r\n        };\r\n        processor.UpdateAll();\r\n    }\r\n\r\n    public LyricDisplayType DisplayType\r\n    {\r\n        get => processor.DisplayType;\r\n        set => processor.DisplayType = value;\r\n    }\r\n\r\n    public LyricDisplayProperty DisplayProperty\r\n    {\r\n        get => processor.DisplayProperty;\r\n        set => processor.DisplayProperty = value;\r\n    }\r\n\r\n    // not a good practice but child class need to know the property changed.\r\n    protected virtual void OnPropertyChanged()\r\n    {\r\n    }\r\n\r\n    protected override void Dispose(bool isDisposing)\r\n    {\r\n        base.Dispose(isDisposing);\r\n        processor.Dispose();\r\n    }\r\n\r\n    public void UpdateStyle(LyricStyle style)\r\n    {\r\n        // for prevent issue Collection was modified; enumeration operation may not execute.\r\n        Schedule(() =>\r\n        {\r\n            LeftLyricTextShaders = SkinConverterTool.ConvertLeftSideShader(shaderManager, style);\r\n            RightLyricTextShaders = SkinConverterTool.ConvertRightSideShader(shaderManager, style);\r\n        });\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Graphics/Sprites/DrawableLyricSpriteText.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Graphics.Sprites;\r\n\r\npublic partial class DrawableLyricSpriteText : LyricSpriteText\r\n{\r\n    private readonly DisplayLyricProcessor processor;\r\n\r\n    public DrawableLyricSpriteText(Lyric lyric)\r\n    {\r\n        processor = new DisplayLyricProcessor(lyric)\r\n        {\r\n            TopTextChanged = topTexts =>\r\n            {\r\n                TopTexts = topTexts;\r\n            },\r\n            CenterTextChanged = text =>\r\n            {\r\n                Text = text;\r\n            },\r\n            BottomTextChanged = bottomTexts =>\r\n            {\r\n                BottomTexts = bottomTexts;\r\n            },\r\n        };\r\n        processor.UpdateAll();\r\n    }\r\n\r\n    public LyricDisplayType DisplayType\r\n    {\r\n        get => processor.DisplayType;\r\n        set => processor.DisplayType = value;\r\n    }\r\n\r\n    public LyricDisplayProperty DisplayProperty\r\n    {\r\n        get => processor.DisplayProperty;\r\n        set => processor.DisplayProperty = value;\r\n    }\r\n\r\n    protected override void Dispose(bool isDisposing)\r\n    {\r\n        base.Dispose(isDisposing);\r\n        processor.Dispose();\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Graphics/Sprites/LyricDisplayProperty.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Graphics.Sprites;\r\n\r\npublic enum LyricDisplayProperty\r\n{\r\n    /// <summary>\r\n    /// Display main text only.\r\n    /// </summary>\r\n    None = 1 << 0,\r\n\r\n    /// <summary>\r\n    /// Display <see cref=\"RubyTag.Text\"/> as top text.\r\n    /// </summary>\r\n    TopText = 1 << 1,\r\n\r\n    /// <summary>\r\n    /// Display bottom text.\r\n    /// </summary>\r\n    /// <example>\r\n    /// Display the <see cref=\"TimeTag.RomanisedSyllable\"/> as bottom text if <see cref=\"LyricDisplayType.Lyric\"/>.<br/>\r\n    /// Display the <see cref=\"Lyric.Text\"/> as bottom text if <see cref=\"LyricDisplayType.RomanisedSyllable\"/>.<br/>\r\n    /// </example>\r\n    BottomText = 1 << 2,\r\n\r\n    /// <summary>\r\n    /// Display both top and bottom text.\r\n    /// </summary>\r\n    Both = TopText | BottomText,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Graphics/Sprites/LyricDisplayType.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Graphics.Sprites;\r\n\r\npublic enum LyricDisplayType\r\n{\r\n    /// <summary>\r\n    /// Display the lyric as center of the text.\r\n    /// </summary>\r\n    /// <example>\r\n    /// Top: <see cref=\"RubyTag.Text\"/><br/>\r\n    /// Center: <see cref=\"Lyric.Text\"/><br/>\r\n    /// Bottom: <see cref=\"TimeTag.RomanisedSyllable\"/><br/>\r\n    /// </example>\r\n    Lyric,\r\n\r\n    /// <summary>\r\n    /// Display the romanised lyric as center of the text.\r\n    /// </summary>\r\n    /// <example>\r\n    /// Top: <see cref=\"RubyTag.Text\"/><br/>\r\n    /// Center: <see cref=\"TimeTag.RomanisedSyllable\"/><br/>\r\n    /// Bottom: <see cref=\"Lyric.Text\"/><br/>\r\n    /// </example>\r\n    RomanisedSyllable,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Graphics/Sprites/LyricStyle.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Extensions.Color4Extensions;\r\nusing osu.Framework.Graphics.Shaders;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Graphics.Sprites;\r\n\r\npublic class LyricStyle\r\n{\r\n    public static LyricStyle CreateDefault() => new()\r\n    {\r\n        LeftLyricTextShaders = new ICustomizedShader[]\r\n        {\r\n            new StepShader\r\n            {\r\n                Name = \"Step shader\",\r\n                StepShaders = new ICustomizedShader[]\r\n                {\r\n                    new OutlineShader\r\n                    {\r\n                        Radius = 3,\r\n                        OutlineColour = Color4Extensions.FromHex(\"#CCA532\"),\r\n                    },\r\n                    new ShadowShader\r\n                    {\r\n                        ShadowColour = Color4Extensions.FromHex(\"#6B5B2D\"),\r\n                        ShadowOffset = new Vector2(3),\r\n                    },\r\n                },\r\n            },\r\n        },\r\n        RightLyricTextShaders = new ICustomizedShader[]\r\n        {\r\n            new StepShader\r\n            {\r\n                Name = \"Step shader\",\r\n                StepShaders = new ICustomizedShader[]\r\n                {\r\n                    new OutlineShader\r\n                    {\r\n                        Radius = 3,\r\n                        OutlineColour = Color4Extensions.FromHex(\"#5932CC\"),\r\n                    },\r\n                    new ShadowShader\r\n                    {\r\n                        ShadowColour = Color4Extensions.FromHex(\"#3D2D6B\"),\r\n                        ShadowOffset = new Vector2(3),\r\n                    },\r\n                },\r\n            },\r\n        },\r\n    };\r\n\r\n    public IReadOnlyList<ICustomizedShader> LeftLyricTextShaders = Array.Empty<ICustomizedShader>();\r\n\r\n    public IReadOnlyList<ICustomizedShader> RightLyricTextShaders = Array.Empty<ICustomizedShader>();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Graphics/Sprites/Processor/BaseDisplayProcessor.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Graphics.Sprites.Processor;\r\n\r\npublic abstract class BaseDisplayProcessor : IDisposable\r\n{\r\n    public Action<IReadOnlyList<PositionText>>? TopTextChanged;\r\n    public Action<string>? CenterTextChanged;\r\n    public Action<IReadOnlyList<PositionText>>? BottomTextChanged;\r\n    public Action<IReadOnlyDictionary<double, TextIndex>>? TimeTagsChanged;\r\n\r\n    private readonly Lyric lyric;\r\n    private readonly LyricDisplayProperty displayProperty;\r\n    private readonly AvailableProperty availableProperty;\r\n\r\n    protected BaseDisplayProcessor(Lyric lyric, LyricDisplayProperty displayProperty)\r\n    {\r\n        this.lyric = lyric;\r\n        this.displayProperty = displayProperty;\r\n\r\n        availableProperty = new AvailableProperty(lyric);\r\n\r\n        ProcessBindableChange(availableProperty);\r\n    }\r\n\r\n    protected abstract void ProcessBindableChange(AvailableProperty property);\r\n\r\n    public void UpdateAll()\r\n    {\r\n        UpdateTopText();\r\n        UpdateCenterText();\r\n        UpdateBottomText();\r\n        UpdateTimeTags();\r\n    }\r\n\r\n    protected void UpdateTopText()\r\n    {\r\n        if (!displayProperty.HasFlag(LyricDisplayProperty.TopText))\r\n            return;\r\n\r\n        TopTextChanged?.Invoke(CalculateTopTexts(lyric).ToArray());\r\n    }\r\n\r\n    protected void UpdateCenterText()\r\n    {\r\n        CenterTextChanged?.Invoke(CalculateCenterText(lyric));\r\n    }\r\n\r\n    protected void UpdateBottomText()\r\n    {\r\n        if (!displayProperty.HasFlag(LyricDisplayProperty.BottomText))\r\n            return;\r\n\r\n        BottomTextChanged?.Invoke(CalculateBottomTexts(lyric).ToArray());\r\n    }\r\n\r\n    protected void UpdateTimeTags()\r\n    {\r\n        TimeTagsChanged?.Invoke(CalculateTimeTags(lyric));\r\n    }\r\n\r\n    protected abstract IEnumerable<PositionText> CalculateTopTexts(Lyric lyric);\r\n\r\n    protected abstract string CalculateCenterText(Lyric lyric);\r\n\r\n    protected abstract IEnumerable<PositionText> CalculateBottomTexts(Lyric lyric);\r\n\r\n    protected abstract IReadOnlyDictionary<double, TextIndex> CalculateTimeTags(Lyric lyric);\r\n\r\n    public void Dispose()\r\n    {\r\n        availableProperty.Dispose();\r\n    }\r\n\r\n    protected class AvailableProperty : IDisposable\r\n    {\r\n        public readonly IBindableList<RubyTag> RubyTagsBindable = new BindableList<RubyTag>();\r\n        public readonly IBindable<int> RubyTagsVersion = new Bindable<int>();\r\n        public readonly IBindable<string> TextBindable = new Bindable<string>();\r\n        public readonly IBindableList<TimeTag> TimeTagsBindable = new BindableList<TimeTag>();\r\n        public readonly IBindable<int> TimeTagsRomanisationVersion = new Bindable<int>();\r\n        public readonly IBindable<int> TimeTagsTimingVersion = new Bindable<int>();\r\n\r\n        public AvailableProperty(Lyric lyric)\r\n        {\r\n            RubyTagsBindable.BindTo(lyric.RubyTagsBindable);\r\n            RubyTagsVersion.BindTo(lyric.RubyTagsVersion);\r\n            TextBindable.BindTo(lyric.TextBindable);\r\n            TimeTagsBindable.BindTo(lyric.TimeTagsBindable);\r\n            TimeTagsRomanisationVersion.BindTo(lyric.TimeTagsRomanisationVersion);\r\n            TimeTagsTimingVersion.BindTo(lyric.TimeTagsTimingVersion);\r\n        }\r\n\r\n        public void Dispose()\r\n        {\r\n            RubyTagsBindable.UnbindAll();\r\n            RubyTagsVersion.UnbindAll();\r\n            TextBindable.UnbindAll();\r\n            TimeTagsBindable.UnbindAll();\r\n            TimeTagsRomanisationVersion.UnbindAll();\r\n            TimeTagsTimingVersion.UnbindAll();\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Graphics/Sprites/Processor/LyricFirstDisplayProcessor.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Extensions.IEnumerableExtensions;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Graphics.Sprites.Processor;\r\n\r\npublic class LyricFirstDisplayProcessor : BaseDisplayProcessor\r\n{\r\n    public LyricFirstDisplayProcessor(Lyric lyric, LyricDisplayProperty displayProperty)\r\n        : base(lyric, displayProperty)\r\n    {\r\n    }\r\n\r\n    protected override void ProcessBindableChange(AvailableProperty property)\r\n    {\r\n        property.RubyTagsBindable.BindCollectionChanged((_, _) =>\r\n        {\r\n            // Ruby only display at top.\r\n            UpdateTopText();\r\n        });\r\n        property.RubyTagsVersion.BindValueChanged(_ =>\r\n        {\r\n            // Ruby only display at top.\r\n            UpdateTopText();\r\n        });\r\n        property.TextBindable.BindValueChanged(_ =>\r\n        {\r\n            UpdateAll();\r\n        });\r\n        property.TimeTagsBindable.BindCollectionChanged((_, _) =>\r\n        {\r\n            // If create/remove the time-tag, romanised syllable might be affected.\r\n            UpdateBottomText();\r\n            UpdateTimeTags();\r\n        });\r\n        property.TimeTagsRomanisationVersion.BindValueChanged(_ =>\r\n        {\r\n            UpdateBottomText();\r\n        });\r\n        property.TimeTagsTimingVersion.BindValueChanged(_ =>\r\n        {\r\n            UpdateTimeTags();\r\n        });\r\n    }\r\n\r\n    protected override IEnumerable<PositionText> CalculateTopTexts(Lyric lyric)\r\n    {\r\n        return lyric.RubyTags.Select(toPositionText);\r\n\r\n        static PositionText toPositionText(RubyTag rubyTag)\r\n            => new(rubyTag.Text, rubyTag.StartIndex, rubyTag.EndIndex);\r\n    }\r\n\r\n    protected override string CalculateCenterText(Lyric lyric)\r\n        => lyric.Text;\r\n\r\n    protected override IEnumerable<PositionText> CalculateBottomTexts(Lyric lyric)\r\n    {\r\n        var startTimeTag = lyric.TimeTags.FirstOrDefault();\r\n        if (startTimeTag == null)\r\n            yield break;\r\n\r\n        string collectedRomanisedSyllable = string.Empty;\r\n\r\n        // split the text by first romanisation syllable.\r\n        foreach (var timeTag in lyric.TimeTags)\r\n        {\r\n            // collecting the romanised syllable.\r\n            collectedRomanisedSyllable += timeTag.RomanisedSyllable;\r\n\r\n            if (lyric.TimeTags.Last() == timeTag)\r\n            {\r\n                // should return the collected romanised syllable if is the last one.\r\n                yield return toPositionText(startTimeTag.Index, timeTag.Index, collectedRomanisedSyllable);\r\n            }\r\n            else if (lyric.TimeTags.GetNext(timeTag).FirstSyllable)\r\n            {\r\n                // should return the collected romanised syllable before timeTag with first syllable.\r\n                yield return toPositionText(startTimeTag.Index, timeTag.Index, collectedRomanisedSyllable);\r\n\r\n                startTimeTag = lyric.TimeTags.GetNext(timeTag);\r\n                collectedRomanisedSyllable = string.Empty;\r\n            }\r\n        }\r\n\r\n        yield break;\r\n\r\n        static PositionText toPositionText(TextIndex startIndex, TextIndex endIndex, string text)\r\n        {\r\n            int startCharIndex = TextIndexUtils.ToCharIndex(startIndex);\r\n            int endCharIndex = TextIndexUtils.ToCharIndex(endIndex);\r\n\r\n            return new PositionText(text, startCharIndex, endCharIndex);\r\n        }\r\n    }\r\n\r\n    protected override IReadOnlyDictionary<double, TextIndex> CalculateTimeTags(Lyric lyric)\r\n    {\r\n        return TimeTagsUtils.ToTimeBasedDictionary(lyric.TimeTags);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Graphics/Sprites/Processor/RomanisedSyllableFirstDisplayProcessor.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Graphics.Sprites.Processor;\r\n\r\npublic class RomanisedSyllableFirstDisplayProcessor : BaseDisplayProcessor\r\n{\r\n    public RomanisedSyllableFirstDisplayProcessor(Lyric lyric, LyricDisplayProperty displayProperty)\r\n        : base(lyric, displayProperty)\r\n    {\r\n        // Note: some of the properties are not implemented yet because we are not sure people actually use it.\r\n    }\r\n\r\n    protected override void ProcessBindableChange(AvailableProperty property)\r\n    {\r\n        property.RubyTagsBindable.BindCollectionChanged((_, _) =>\r\n        {\r\n            // Ruby only display at top.\r\n            UpdateTopText();\r\n        });\r\n        property.RubyTagsVersion.BindValueChanged(_ =>\r\n        {\r\n            // Ruby only display at top.\r\n            UpdateTopText();\r\n        });\r\n        property.TextBindable.BindValueChanged(_ =>\r\n        {\r\n            // Text only display at bottom.\r\n            UpdateBottomText();\r\n        });\r\n        property.TimeTagsBindable.BindCollectionChanged((_, _) =>\r\n        {\r\n            // Ruby change might affect the center text, which will affect all property.\r\n            UpdateAll();\r\n        });\r\n        property.TimeTagsRomanisationVersion.BindValueChanged(_ =>\r\n        {\r\n            // Ruby change might affect the center text, which will affect all property.\r\n            UpdateAll();\r\n        });\r\n        property.TimeTagsTimingVersion.BindValueChanged(_ =>\r\n        {\r\n            UpdateTimeTags();\r\n        });\r\n    }\r\n\r\n    protected override IEnumerable<PositionText> CalculateTopTexts(Lyric lyric)\r\n    {\r\n        // todo: implementation needed.\r\n        yield break;\r\n    }\r\n\r\n    protected override string CalculateCenterText(Lyric lyric) =>\r\n        string.Join(\"\", lyric.TimeTags.Select((x, i) =>\r\n        {\r\n            bool hasEmptySpace = i != 0 && x.FirstSyllable;\r\n            return hasEmptySpace ? \" \" + x.RomanisedSyllable : x.RomanisedSyllable;\r\n        }));\r\n\r\n    protected override IEnumerable<PositionText> CalculateBottomTexts(Lyric lyric)\r\n    {\r\n        // todo: implementation needed.\r\n        yield break;\r\n    }\r\n\r\n    protected override IReadOnlyDictionary<double, TextIndex> CalculateTimeTags(Lyric lyric)\r\n    {\r\n        // todo: implementation needed.\r\n        return new Dictionary<double, TextIndex>();\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Graphics/UserInterface/BindableBoolMenuItem.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Graphics.UserInterface;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Graphics.UserInterface;\r\n\r\npublic class BindableBoolMenuItem : ToggleMenuItem\r\n{\r\n    public BindableBoolMenuItem(string text, Bindable<bool> bindable)\r\n        : base(text)\r\n    {\r\n        State.BindTo(bindable);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Graphics/UserInterface/BindableEnumMenuItem.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Extensions;\r\nusing osu.Framework.Extensions.IEnumerableExtensions;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Game.Graphics.UserInterface;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Graphics.UserInterface;\r\n\r\npublic class BindableEnumMenuItem<T> : MenuItem where T : struct, Enum\r\n{\r\n    private readonly Bindable<T> bindableEnum = new();\r\n\r\n    public BindableEnumMenuItem(string text, Bindable<T> bindable)\r\n        : base(text)\r\n    {\r\n        Items = createMenuItems();\r\n\r\n        bindableEnum.BindTo(bindable);\r\n        bindableEnum.BindValueChanged(e =>\r\n        {\r\n            var newSelection = e.NewValue;\r\n            Items.OfType<ToggleMenuItem>().ForEach(x =>\r\n            {\r\n                bool match = x.Text.Value == GetName(newSelection);\r\n                x.State.Value = match;\r\n            });\r\n        }, true);\r\n    }\r\n\r\n    private ToggleMenuItem[] createMenuItems()\r\n    {\r\n        return ValidEnums.Select(e =>\r\n        {\r\n            var item = new ToggleMenuItem(GetName(e), MenuItemType.Standard, _ => UpdateSelection(e));\r\n            return item;\r\n        }).ToArray();\r\n    }\r\n\r\n    protected virtual IEnumerable<T> ValidEnums => Enum.GetValues<T>();\r\n\r\n    protected string GetName(T selection)\r\n        => selection.GetDescription();\r\n\r\n    protected virtual void UpdateSelection(T selection)\r\n    {\r\n        bindableEnum.Value = selection;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Graphics/UserInterfaceV2/FontSelector.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Rendering;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Framework.IO.Stores;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Containers;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Rulesets.Karaoke.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.Containers;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.Shapes;\r\nusing osu.Game.Rulesets.Karaoke.IO.Stores;\r\nusing osu.Game.Rulesets.Karaoke.Skinning.Fonts;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Graphics.UserInterfaceV2;\r\n\r\npublic partial class FontSelector : CompositeDrawable, IHasCurrentValue<FontUsage>\r\n{\r\n    private readonly SpriteText previewText;\r\n    private readonly FontFamilyPropertyList familyProperty;\r\n    private readonly FontPropertyList<string?> weightProperty;\r\n    private readonly FontPropertyList<float> fontSizeProperty;\r\n    private readonly OsuCheckbox fixedWidthCheckbox;\r\n\r\n    private readonly BindableWithCurrent<FontUsage> current = new();\r\n    private readonly BindableList<FontInfo> fonts = new();\r\n\r\n    [Resolved]\r\n    private FontStore fontStore { get; set; } = null!;\r\n\r\n    private KaraokeLocalFontStore localFontStore = null!;\r\n\r\n    public Bindable<FontUsage> Current\r\n    {\r\n        get => current.Current;\r\n        set\r\n        {\r\n            current.Current = value;\r\n\r\n            // should calculate available size until has bindable text.\r\n            fontSizeProperty.Items.Clear();\r\n\r\n            if (value is BindableFontUsage bindableFontUsage)\r\n            {\r\n                fontSizeProperty.Items.AddRange(FontUtils.DefaultFontSize(bindableFontUsage.MinFontSize, bindableFontUsage.MaxFontSize));\r\n            }\r\n            else\r\n            {\r\n                fontSizeProperty.Items.AddRange(FontUtils.DefaultFontSize());\r\n            }\r\n        }\r\n    }\r\n\r\n    public FontSelector()\r\n    {\r\n        InternalChild = new GridContainer\r\n        {\r\n            RelativeSizeAxes = Axes.Both,\r\n            RowDimensions = new[]\r\n            {\r\n                new Dimension(GridSizeMode.Relative, 0.4f),\r\n                new Dimension(),\r\n            },\r\n            Content = new[]\r\n            {\r\n                new Drawable[]\r\n                {\r\n                    previewText = new OsuSpriteText\r\n                    {\r\n                        Anchor = Anchor.Centre,\r\n                        Origin = Anchor.Centre,\r\n                        Text = \"カラオケ, karaoke\",\r\n                    },\r\n                },\r\n                new Drawable[]\r\n                {\r\n                    new Container\r\n                    {\r\n                        Padding = new MarginPadding(10),\r\n                        RelativeSizeAxes = Axes.Both,\r\n                        Child = new GridContainer\r\n                        {\r\n                            RelativeSizeAxes = Axes.Both,\r\n                            ColumnDimensions = new[]\r\n                            {\r\n                                new Dimension(GridSizeMode.Relative, 0.5f),\r\n                                new Dimension(GridSizeMode.Relative, 0.3f),\r\n                                new Dimension(GridSizeMode.Relative, 0.2f),\r\n                            },\r\n                            Content = new[]\r\n                            {\r\n                                new Drawable[]\r\n                                {\r\n                                    familyProperty = new FontFamilyPropertyList\r\n                                    {\r\n                                        Name = \"Font family selection area\",\r\n                                        RelativeSizeAxes = Axes.Both,\r\n                                    },\r\n                                    weightProperty = new FontPropertyList<string?>\r\n                                    {\r\n                                        Name = \"Font widget selection area\",\r\n                                        RelativeSizeAxes = Axes.Both,\r\n                                    },\r\n                                    new GridContainer\r\n                                    {\r\n                                        RelativeSizeAxes = Axes.Both,\r\n                                        RowDimensions = new[]\r\n                                        {\r\n                                            new Dimension(),\r\n                                            new Dimension(GridSizeMode.Absolute, 48),\r\n                                        },\r\n                                        Content = new[]\r\n                                        {\r\n                                            new Drawable[]\r\n                                            {\r\n                                                fontSizeProperty = new FontPropertyList<float>\r\n                                                {\r\n                                                    Name = \"Font size selection area\",\r\n                                                    RelativeSizeAxes = Axes.Both,\r\n                                                },\r\n                                            },\r\n                                            new Drawable[]\r\n                                            {\r\n                                                fixedWidthCheckbox = new OsuCheckbox\r\n                                                {\r\n                                                    Name = \"Font fixed width selection area\",\r\n                                                    RelativeSizeAxes = Axes.X,\r\n                                                    Padding = new MarginPadding(10),\r\n                                                    LabelText = \"FixedWidth\",\r\n                                                },\r\n                                            },\r\n                                        },\r\n                                    },\r\n                                },\r\n                            },\r\n                        },\r\n                    },\r\n                },\r\n            },\r\n        };\r\n\r\n        fonts.BindCollectionChanged((_, b) =>\r\n        {\r\n            // re-calculate if source changed.\r\n            Schedule(() =>\r\n            {\r\n                string[]? oldFamilies = b.OldItems?.OfType<FontInfo>().Select(x => x.Family).Distinct().ToArray();\r\n                string[]? newFamilies = b.NewItems?.OfType<FontInfo>().Select(x => x.Family).Distinct().ToArray();\r\n\r\n                if (oldFamilies != null)\r\n                {\r\n                    familyProperty.Items.RemoveAll(x => oldFamilies.Contains(x));\r\n                }\r\n\r\n                if (newFamilies != null)\r\n                {\r\n                    familyProperty.Items.AddRange(newFamilies);\r\n                }\r\n\r\n                // should reset family selection if user select the font that will be removed or added.\r\n                string? currentFamily = familyProperty.Current.Value;\r\n                bool resetFamily = oldFamilies?.Contains(currentFamily) ?? false;\r\n\r\n                if (resetFamily)\r\n                {\r\n                    familyProperty.Current.Value = familyProperty.Items.FirstOrDefault();\r\n                }\r\n            });\r\n        });\r\n\r\n        familyProperty.Current.BindValueChanged(x =>\r\n        {\r\n            performChange();\r\n\r\n            // re-calculate if family changed.\r\n            string[] weight = fonts.Where(f => f.Family == x.NewValue).Select(f => f.Weight).OfType<string>().Distinct().ToArray();\r\n            weightProperty.Items.Clear();\r\n            weightProperty.Items.AddRange(weight);\r\n\r\n            // set to first or empty if change new family.\r\n            weightProperty.Current.Value = weight.FirstOrDefault();\r\n        });\r\n        weightProperty.Current.BindValueChanged(_ => performChange());\r\n        fontSizeProperty.Current.BindValueChanged(_ => performChange());\r\n        fixedWidthCheckbox.Current.BindValueChanged(_ => performChange());\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(FontManager fontManager, IRenderer renderer)\r\n    {\r\n        fonts.BindTo(fontManager.Fonts);\r\n\r\n        // create local font store and import those files\r\n        localFontStore = new KaraokeLocalFontStore(fontManager, renderer);\r\n        fontStore.AddStore(localFontStore);\r\n\r\n        Current.BindValueChanged(e =>\r\n        {\r\n            var newFont = e.NewValue;\r\n            familyProperty.Current.Value = newFont.Family;\r\n            weightProperty.Current.Value = newFont.Weight;\r\n            fontSizeProperty.Current.Value = newFont.Size;\r\n            fixedWidthCheckbox.Current.Value = newFont.FixedWidth;\r\n        }, true);\r\n    }\r\n\r\n    private void performChange()\r\n    {\r\n        var fontUsage = generateFontUsage();\r\n\r\n        // add font to local font store for preview purpose.\r\n        localFontStore.ClearFont();\r\n        localFontStore.AddFont(fontUsage);\r\n\r\n        previewText.Font = fontUsage;\r\n\r\n        // write-back the value.\r\n        Current.Value = fontUsage;\r\n    }\r\n\r\n    private FontUsage generateFontUsage()\r\n    {\r\n        string? family = familyProperty.Current.Value;\r\n        string? weight = weightProperty.Current.Value;\r\n        float size = fontSizeProperty.Current.Value;\r\n        bool fixedWidth = fixedWidthCheckbox.Current.Value;\r\n        return new FontUsage(family, size, weight, false, fixedWidth);\r\n    }\r\n\r\n    protected override void Dispose(bool isDisposing)\r\n    {\r\n        base.Dispose(isDisposing);\r\n\r\n        fontStore.RemoveStore(localFontStore);\r\n    }\r\n\r\n    internal partial class FontFamilyPropertyList : FontPropertyList<string?>\r\n    {\r\n        protected override RearrangeableTextFlowListContainer<string?> CreateRearrangeableListContainer()\r\n            => new RearrangeableFontFamilyListContainer();\r\n\r\n        private partial class RearrangeableFontFamilyListContainer : RearrangeableTextFlowListContainer<string?>\r\n        {\r\n            protected override DrawableTextListItem CreateDrawable(string? item)\r\n                => new DrawableFontFamilyListItem(item);\r\n\r\n            private partial class DrawableFontFamilyListItem : DrawableTextListItem\r\n            {\r\n                [Resolved]\r\n                private FontManager fontManager { get; set; } = null!;\r\n\r\n                public DrawableFontFamilyListItem(string? item)\r\n                    : base(item)\r\n                {\r\n                }\r\n\r\n                protected override void CreateDisplayContent(OsuTextFlowContainer textFlowContainer, string? model)\r\n                {\r\n                    textFlowContainer.TextAnchor = Anchor.BottomLeft;\r\n                    textFlowContainer.AddText(model ?? string.Empty);\r\n\r\n                    var matchedFormat = fontManager.Fonts\r\n                                                   .Where(x => x.Family == Model).Select(x => x.FontFormat)\r\n                                                   .Distinct()\r\n                                                   .ToArray();\r\n\r\n                    foreach (var format in matchedFormat)\r\n                    {\r\n                        textFlowContainer.AddText(\" \");\r\n                        textFlowContainer.AddArbitraryDrawable(new FontFormatBadge(format));\r\n                    }\r\n                }\r\n            }\r\n        }\r\n\r\n        private partial class FontFormatBadge : CompositeDrawable\r\n        {\r\n            private readonly FontFormat fontFormat;\r\n            private readonly Box box;\r\n            private readonly OsuSpriteText badgeText;\r\n\r\n            public FontFormatBadge(FontFormat fontFormat)\r\n            {\r\n                this.fontFormat = fontFormat;\r\n\r\n                AutoSizeAxes = Axes.Both;\r\n                Masking = true;\r\n                CornerRadius = 3;\r\n                InternalChildren = new Drawable[]\r\n                {\r\n                    box = new Box\r\n                    {\r\n                        RelativeSizeAxes = Axes.Both,\r\n                    },\r\n                    badgeText = new OsuSpriteText\r\n                    {\r\n                        Font = OsuFont.Default.With(size: 10),\r\n                        Margin = new MarginPadding\r\n                        {\r\n                            Vertical = 1,\r\n                            Horizontal = 3,\r\n                        },\r\n                    },\r\n                };\r\n            }\r\n\r\n            [BackgroundDependencyLoader]\r\n            private void load(OsuColour colours)\r\n            {\r\n                box.Colour = fontFormat switch\r\n                {\r\n                    FontFormat.Internal => colours.Gray7,\r\n                    FontFormat.Fnt => colours.Pink,\r\n                    FontFormat.Ttf => colours.Blue,\r\n                    _ => throw new ArgumentOutOfRangeException(nameof(fontFormat)),\r\n                };\r\n\r\n                // todo : might apply translation.\r\n                badgeText.Text = fontFormat.ToString();\r\n            }\r\n        }\r\n    }\r\n\r\n    internal partial class FontPropertyList<T> : CompositeDrawable\r\n    {\r\n        private readonly CornerBackground background;\r\n        private readonly TextPropertySearchTextBox filter;\r\n        private readonly RearrangeableTextFlowListContainer<T> propertyFlowList;\r\n\r\n        private readonly BindableWithCurrent<T> current = new();\r\n\r\n        public Bindable<T> Current\r\n        {\r\n            get => current.Current;\r\n            set => current.Current = value;\r\n        }\r\n\r\n        public BindableList<T> Items => propertyFlowList.Items;\r\n\r\n        public FontPropertyList()\r\n        {\r\n            InternalChild = new Container\r\n            {\r\n                Padding = new MarginPadding(10),\r\n                RelativeSizeAxes = Axes.Both,\r\n                Children = new Drawable[]\r\n                {\r\n                    background = new CornerBackground\r\n                    {\r\n                        RelativeSizeAxes = Axes.Both,\r\n                    },\r\n                    new GridContainer\r\n                    {\r\n                        RelativeSizeAxes = Axes.Both,\r\n                        RowDimensions = new[]\r\n                        {\r\n                            new Dimension(GridSizeMode.Absolute, 40),\r\n                            new Dimension(),\r\n                        },\r\n                        Content = new[]\r\n                        {\r\n                            new Drawable[]\r\n                            {\r\n                                filter = new TextPropertySearchTextBox\r\n                                {\r\n                                    RelativeSizeAxes = Axes.X,\r\n                                },\r\n                            },\r\n                            new Drawable[]\r\n                            {\r\n                                propertyFlowList = CreateRearrangeableListContainer().With(x =>\r\n                                {\r\n                                    x.RelativeSizeAxes = Axes.Both;\r\n                                    x.RequestSelection = item =>\r\n                                    {\r\n                                        Current.Value = item;\r\n                                    };\r\n                                }),\r\n                            },\r\n                        },\r\n                    },\r\n                },\r\n            };\r\n\r\n            filter.Current.BindValueChanged(e => propertyFlowList.Filter(e.NewValue));\r\n            Current.BindValueChanged(e => propertyFlowList.SelectedSet.Value = e.NewValue);\r\n        }\r\n\r\n        protected virtual RearrangeableTextFlowListContainer<T> CreateRearrangeableListContainer()\r\n            => new();\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(OsuColour colours)\r\n        {\r\n            background.Colour = colours.ContextMenuGray;\r\n        }\r\n\r\n        private partial class TextPropertySearchTextBox : SearchTextBox\r\n        {\r\n            protected override Color4 SelectionColour => Color4.Gray;\r\n\r\n            public TextPropertySearchTextBox()\r\n            {\r\n                PlaceholderText = \"Search...\";\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Graphics/UserInterfaceV2/LabelledColourSelector.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Extensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Cursor;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Containers;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Graphics.UserInterfaceV2;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Graphics.UserInterfaceV2;\r\n\r\n// refactor this shit\r\npublic partial class LabelledColourSelector : LabelledComponent<LabelledColourSelector.ColourSelectorDisplay, Colour4>\r\n{\r\n    public LabelledColourSelector()\r\n        : base(true)\r\n    {\r\n    }\r\n\r\n    protected override ColourSelectorDisplay CreateComponent()\r\n        => new();\r\n\r\n    public partial class ColourSelectorDisplay : CompositeDrawable, IHasCurrentValue<Colour4>, IHasPopover\r\n    {\r\n        private readonly BindableWithCurrent<Colour4> current = new();\r\n\r\n        private Box fill = null!;\r\n        private OsuSpriteText colourHexCode = null!;\r\n\r\n        public Bindable<Colour4> Current\r\n        {\r\n            get => current.Current;\r\n            set => current.Current = value;\r\n        }\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(OsuColour colours)\r\n        {\r\n            AutoSizeAxes = Axes.Y;\r\n            RelativeSizeAxes = Axes.X;\r\n\r\n            InternalChild = new FillFlowContainer\r\n            {\r\n                RelativeSizeAxes = Axes.X,\r\n                AutoSizeAxes = Axes.Y,\r\n                Direction = FillDirection.Vertical,\r\n                Spacing = new Vector2(0, 10),\r\n                Children = new Drawable[]\r\n                {\r\n                    new OsuClickableContainer\r\n                    {\r\n                        RelativeSizeAxes = Axes.X,\r\n                        Height = 60,\r\n                        CornerRadius = 10,\r\n                        Masking = true,\r\n                        BorderThickness = 2f,\r\n                        BorderColour = colours.Gray5,\r\n                        Children = new Drawable[]\r\n                        {\r\n                            fill = new Box\r\n                            {\r\n                                RelativeSizeAxes = Axes.Both,\r\n                            },\r\n                            colourHexCode = new OsuSpriteText\r\n                            {\r\n                                Anchor = Anchor.Centre,\r\n                                Origin = Anchor.Centre,\r\n                                Font = OsuFont.Default.With(size: 12),\r\n                            },\r\n                        },\r\n                        Action = this.ShowPopover,\r\n                    },\r\n                },\r\n            };\r\n        }\r\n\r\n        protected override void LoadComplete()\r\n        {\r\n            base.LoadComplete();\r\n\r\n            current.BindValueChanged(_ => updateColour(), true);\r\n        }\r\n\r\n        private void updateColour()\r\n        {\r\n            fill.Colour = current.Value;\r\n            colourHexCode.Text = current.Value.ToHex();\r\n            colourHexCode.Colour = OsuColour.ForegroundTextColourFor(current.Value);\r\n        }\r\n\r\n        public Popover GetPopover() => new OsuPopover(false)\r\n        {\r\n            Child = new OsuColourPicker\r\n            {\r\n                Current = { BindTarget = Current },\r\n            },\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Graphics/UserInterfaceV2/LabelledHueSelector.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Effects;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Game.Graphics.UserInterfaceV2;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Graphics.UserInterfaceV2;\r\n\r\npublic partial class LabelledHueSelector : LabelledComponent<LabelledHueSelector.OsuHueSelector, float>\r\n{\r\n    public LabelledHueSelector()\r\n        : base(true)\r\n    {\r\n    }\r\n\r\n    protected override OsuHueSelector CreateComponent()\r\n        => new();\r\n\r\n    private static EdgeEffectParameters createShadowParameters() => new()\r\n    {\r\n        Type = EdgeEffectType.Shadow,\r\n        Offset = new Vector2(0, 1),\r\n        Radius = 3,\r\n        Colour = Colour4.Black.Opacity(0.3f),\r\n    };\r\n\r\n    /// <summary>\r\n    /// Copied from <see cref=\"OsuHSVColourPicker\"/>\r\n    /// </summary>\r\n    public partial class OsuHueSelector : HSVColourPicker.HueSelector, IHasCurrentValue<float>\r\n    {\r\n        private const float corner_radius = 10;\r\n        private const float control_border_thickness = 3;\r\n\r\n        public Bindable<float> Current\r\n        {\r\n            get => Hue;\r\n            set\r\n            {\r\n                ArgumentNullException.ThrowIfNull(value);\r\n\r\n                Hue.UnbindBindings();\r\n                Hue.BindTo(value);\r\n            }\r\n        }\r\n\r\n        public OsuHueSelector()\r\n        {\r\n            SliderBar.CornerRadius = corner_radius;\r\n            SliderBar.Masking = true;\r\n        }\r\n\r\n        protected override Drawable CreateSliderNub() => new SliderNub(this);\r\n\r\n        private partial class SliderNub : CompositeDrawable\r\n        {\r\n            private readonly Bindable<float> hue;\r\n            private readonly Box fill;\r\n\r\n            public SliderNub(OsuHueSelector osuHueSelector)\r\n            {\r\n                hue = osuHueSelector.Hue.GetBoundCopy();\r\n\r\n                InternalChild = new CircularContainer\r\n                {\r\n                    Height = 35,\r\n                    Width = 10,\r\n                    Origin = Anchor.Centre,\r\n                    Anchor = Anchor.Centre,\r\n                    Masking = true,\r\n                    BorderColour = Colour4.White,\r\n                    BorderThickness = control_border_thickness,\r\n                    EdgeEffect = createShadowParameters(),\r\n                    Child = fill = new Box\r\n                    {\r\n                        RelativeSizeAxes = Axes.Both,\r\n                    },\r\n                };\r\n            }\r\n\r\n            protected override void LoadComplete()\r\n            {\r\n                hue.BindValueChanged(h => fill.Colour = Colour4.FromHSV(h.NewValue, 1, 1), true);\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Graphics/UserInterfaceV2/LabelledImageSelector.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Graphics.UserInterfaceV2;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Graphics.UserInterfaceV2;\r\n\r\n/// <summary>\r\n/// A labelled text box which reveals an inline file chooser when clicked.\r\n/// Will be replaced after has official one.\r\n/// </summary>\r\npublic partial class LabelledImageSelector : LabelledTextBox;\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Graphics/UserInterfaceV2/LabelledRealTimeSliderBar.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Numerics;\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Graphics.UserInterfaceV2;\r\nusing osu.Game.Overlays.Settings;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Graphics.UserInterfaceV2;\r\n\r\npublic partial class LabelledRealTimeSliderBar<TNumber> : LabelledSliderBar<TNumber>\r\n    where TNumber : struct, INumber<TNumber>, IMinMaxValue<TNumber>\r\n{\r\n    protected override SettingsSlider<TNumber> CreateComponent()\r\n        => base.CreateComponent().With(x => x.TransferValueOnCommit = false);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Graphics/UserInterfaceV2/LanguageSelector.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Globalization;\r\nusing System.Linq;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Graphics.Containers;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.Containers;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Graphics.UserInterfaceV2;\r\n\r\npublic partial class LanguageSelector : CompositeDrawable, IHasCurrentValue<CultureInfo?>\r\n{\r\n    private readonly LanguageSelectionSearchTextBox filter;\r\n    private readonly RearrangeableLanguageListContainer languageList;\r\n\r\n    private readonly BindableWithCurrent<CultureInfo?> current = new();\r\n\r\n    public Bindable<CultureInfo?> Current\r\n    {\r\n        get => current.Current;\r\n        set => current.Current = value;\r\n    }\r\n\r\n    public override bool AcceptsFocus => true;\r\n\r\n    public override bool RequestsFocus => true;\r\n\r\n    public LanguageSelector()\r\n    {\r\n        InternalChild = new GridContainer\r\n        {\r\n            RelativeSizeAxes = Axes.Both,\r\n            RowDimensions = new[]\r\n            {\r\n                new Dimension(GridSizeMode.Absolute, 40),\r\n                new Dimension(),\r\n            },\r\n            Content = new[]\r\n            {\r\n                new Drawable[]\r\n                {\r\n                    filter = new LanguageSelectionSearchTextBox\r\n                    {\r\n                        RelativeSizeAxes = Axes.X,\r\n                    },\r\n                },\r\n                new Drawable[]\r\n                {\r\n                    languageList = new RearrangeableLanguageListContainer\r\n                    {\r\n                        RelativeSizeAxes = Axes.Both,\r\n                        RequestSelection = item =>\r\n                        {\r\n                            Current.Value = item.CultureInfo;\r\n                        },\r\n                    },\r\n                },\r\n            },\r\n        };\r\n\r\n        filter.Current.BindValueChanged(e => languageList.Filter(e.NewValue));\r\n        Current.BindValueChanged(e =>\r\n        {\r\n            // we need to wait until language list loaded.\r\n            Schedule(() =>\r\n            {\r\n                languageList.SelectedSet.Value = new LanguageModel(e.NewValue);\r\n            });\r\n        }, true);\r\n\r\n        reloadLanguageList();\r\n    }\r\n\r\n    protected override void OnFocus(FocusEvent e)\r\n    {\r\n        base.OnFocus(e);\r\n\r\n        GetContainingFocusManager().ChangeFocus(filter);\r\n    }\r\n\r\n    private bool enableEmptyOption;\r\n\r\n    public bool EnableEmptyOption\r\n    {\r\n        get => enableEmptyOption;\r\n        set\r\n        {\r\n            enableEmptyOption = value;\r\n\r\n            reloadLanguageList();\r\n        }\r\n    }\r\n\r\n    private void reloadLanguageList()\r\n    {\r\n        var languages = CultureInfoUtils.GetAvailableLanguages().Select(x => new LanguageModel(x));\r\n        languageList.Items.Clear();\r\n\r\n        if (EnableEmptyOption)\r\n        {\r\n            languageList.Items.Insert(0, new LanguageModel(null));\r\n        }\r\n\r\n        languageList.Items.AddRange(languages);\r\n    }\r\n\r\n    private partial class LanguageSelectionSearchTextBox : SearchTextBox\r\n    {\r\n        protected override Color4 SelectionColour => Color4.Gray;\r\n\r\n        public LanguageSelectionSearchTextBox()\r\n        {\r\n            PlaceholderText = \"type in keywords...\";\r\n        }\r\n    }\r\n\r\n    private partial class RearrangeableLanguageListContainer : RearrangeableTextFlowListContainer<LanguageModel>\r\n    {\r\n        protected override DrawableTextListItem CreateDrawable(LanguageModel item)\r\n            => new DrawableLanguageListItem(item);\r\n\r\n        private partial class DrawableLanguageListItem : DrawableTextListItem\r\n        {\r\n            public DrawableLanguageListItem(LanguageModel item)\r\n                : base(item)\r\n            {\r\n            }\r\n\r\n            public override IEnumerable<LocalisableString> FilterTerms\r\n            {\r\n                get\r\n                {\r\n                    var cultureInfo = Model.CultureInfo;\r\n\r\n                    yield return new LocalisableString(CultureInfoUtils.GetLanguageDisplayText(cultureInfo));\r\n\r\n                    if (cultureInfo == null)\r\n                    {\r\n                        yield return new LocalisableString(string.Empty);\r\n                    }\r\n                    else\r\n                    {\r\n                        yield return new LocalisableString(cultureInfo.Name);\r\n                        yield return new LocalisableString(cultureInfo.DisplayName);\r\n                        yield return new LocalisableString(cultureInfo.EnglishName);\r\n                    }\r\n                }\r\n            }\r\n\r\n            protected override void CreateDisplayContent(OsuTextFlowContainer textFlowContainer, LanguageModel model)\r\n            {\r\n                textFlowContainer.AddText(CultureInfoUtils.GetLanguageDisplayText(model.CultureInfo));\r\n            }\r\n        }\r\n    }\r\n\r\n    /// <summary>\r\n    ///  use this struct to warp the <see cref=\"CultureInfo\"/> because <see cref=\"RearrangeableLanguageListContainer\"/> is not able support null value.\r\n    /// </summary>\r\n    private struct LanguageModel\r\n    {\r\n        public LanguageModel(CultureInfo? cultureInfo)\r\n        {\r\n            CultureInfo = cultureInfo;\r\n        }\r\n\r\n        public CultureInfo? CultureInfo { get; }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Graphics/UserInterfaceV2/LanguageSelectorPopover.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Globalization;\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Graphics.UserInterfaceV2;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Graphics.UserInterfaceV2;\r\n\r\npublic partial class LanguageSelectorPopover : OsuPopover\r\n{\r\n    private readonly LanguageSelector languageSelector;\r\n\r\n    public LanguageSelectorPopover(Bindable<CultureInfo?> bindable)\r\n    {\r\n        Child = languageSelector = new LanguageSelector\r\n        {\r\n            Width = 260,\r\n            Height = 400,\r\n            Current = bindable,\r\n        };\r\n    }\r\n\r\n    public bool EnableEmptyOption\r\n    {\r\n        get => languageSelector.EnableEmptyOption;\r\n        set => languageSelector.EnableEmptyOption = value;\r\n    }\r\n\r\n    protected override void LoadComplete()\r\n    {\r\n        base.LoadComplete();\r\n\r\n        GetContainingFocusManager().ChangeFocus(languageSelector);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/IO/Archives/CachedFontArchiveReader.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.IO;\r\nusing System.Linq;\r\nusing osu.Framework.IO.Stores;\r\nusing osu.Game.IO.Archives;\r\nusing SharpCompress.Archives;\r\nusing SharpCompress.Archives.Zip;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.IO.Archives;\r\n\r\n/// <summary>\r\n/// For reading cached font reader.\r\n/// Cached font will be saved as xxx.cached fnt into cached folder.\r\n/// And notice that this class is just copied from <see cref=\"ZipArchiveReader\"/>\r\n/// </summary>\r\npublic class CachedFontArchiveReader : ArchiveReader\r\n{\r\n    private readonly Stream archiveStream;\r\n    private readonly IWritableArchive archive;\r\n\r\n    public CachedFontArchiveReader(Stream archiveStream, string name)\r\n        : base(name)\r\n    {\r\n        this.archiveStream = archiveStream;\r\n        archive = ZipArchive.OpenArchive(archiveStream);\r\n    }\r\n\r\n    public override Stream GetStream(string name)\r\n    {\r\n        // will search .fnt file or image in here.\r\n        string file = Path.HasExtension(name) ? name : $\"{name}.bin\";\r\n        var entry = archive.Entries.SingleOrDefault(e => e.Key == file);\r\n        if (entry == null)\r\n            throw new FileNotFoundException();\r\n\r\n        // allow seeking\r\n        var copy = new MemoryStream();\r\n\r\n        using (var s = entry.OpenEntryStream())\r\n            s.CopyTo(copy);\r\n\r\n        copy.Position = 0;\r\n\r\n        return copy;\r\n    }\r\n\r\n    public override void Dispose()\r\n    {\r\n        archive.Dispose();\r\n        archiveStream.Dispose();\r\n    }\r\n\r\n    public override IEnumerable<string> Filenames => archive.Entries.Select(e => e.Key).ExcludeSystemFileNames();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/IO/Serialization/Converters/ColourConverter.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing Newtonsoft.Json;\r\nusing Newtonsoft.Json.Linq;\r\nusing osu.Framework.Extensions.Color4Extensions;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.IO.Serialization.Converters;\r\n\r\npublic class ColourConverter : JsonConverter<Color4>\r\n{\r\n    public override Color4 ReadJson(JsonReader reader, Type objectType, Color4 existingValue, bool hasExistingValue, JsonSerializer serializer)\r\n    {\r\n        var obj = JToken.Load(reader);\r\n        string? value = obj.Value<string>();\r\n\r\n        if (value == null)\r\n            return new Color4();\r\n\r\n        return Color4Extensions.FromHex(value);\r\n    }\r\n\r\n    public override void WriteJson(JsonWriter writer, Color4 value, JsonSerializer serializer)\r\n    {\r\n        writer.WriteValue(value.ToHex());\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/IO/Serialization/Converters/CultureInfoConverter.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Globalization;\r\nusing Newtonsoft.Json;\r\nusing Newtonsoft.Json.Linq;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.IO.Serialization.Converters;\r\n\r\npublic class CultureInfoConverter : JsonConverter<CultureInfo>\r\n{\r\n    public override CultureInfo? ReadJson(JsonReader reader, Type objectType, CultureInfo? existingValue, bool hasExistingValue, JsonSerializer serializer)\r\n    {\r\n        var obj = JToken.Load(reader);\r\n        int? value = obj.Value<int?>();\r\n\r\n        if (value == null)\r\n            return null;\r\n\r\n        return CultureInfoUtils.CreateLoadCultureInfoById(value.Value);\r\n    }\r\n\r\n    public override void WriteJson(JsonWriter writer, CultureInfo? value, JsonSerializer serializer)\r\n    {\r\n        int? id = value != null ? CultureInfoUtils.GetSaveCultureInfoId(value) : null;\r\n\r\n        writer.WriteValue(id);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/IO/Serialization/Converters/DictionaryConverter.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing Newtonsoft.Json;\r\nusing Newtonsoft.Json.Linq;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.IO.Serialization.Converters;\r\n\r\npublic abstract class DictionaryConverter<TKey, TValue> : JsonConverter<IDictionary<TKey, TValue>> where TKey : notnull\r\n{\r\n    public sealed override IDictionary<TKey, TValue> ReadJson(JsonReader reader, Type objectType, IDictionary<TKey, TValue>? existingValue, bool hasExistingValue, JsonSerializer serializer)\r\n    {\r\n        var obj = JArray.Load(reader);\r\n        return obj.OfType<JObject>().ToDictionary(\r\n            x => deserializeKey((JProperty)x.First!),\r\n            x => deserializeValue((JProperty)x.Last!)\r\n        );\r\n\r\n        TKey deserializeKey(JProperty token)\r\n            => serializer.Deserialize<TKey>(token.Value.CreateReader())!;\r\n\r\n        TValue deserializeValue(JProperty token)\r\n            => serializer.Deserialize<TValue>(token.Value.CreateReader())!;\r\n    }\r\n\r\n    public override void WriteJson(JsonWriter writer, IDictionary<TKey, TValue>? value, JsonSerializer serializer)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(value);\r\n\r\n        writer.WriteStartArray();\r\n\r\n        foreach (var keyValuePair in value)\r\n        {\r\n            var jObject = JObject.FromObject(keyValuePair, serializer);\r\n            jObject.WriteTo(writer);\r\n        }\r\n\r\n        writer.WriteEndArray();\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/IO/Serialization/Converters/ElementIdConverter.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing Newtonsoft.Json;\r\nusing Newtonsoft.Json.Linq;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.IO.Serialization.Converters;\r\n\r\npublic class ElementIdConverter : JsonConverter<ElementId?>\r\n{\r\n    public override ElementId? ReadJson(JsonReader reader, Type objectType, ElementId? existingValue, bool hasExistingValue, JsonSerializer serializer)\r\n    {\r\n        var obj = JToken.Load(reader);\r\n        string? value = obj.Value<string?>();\r\n\r\n        return createElementId(value);\r\n    }\r\n\r\n    private static ElementId? createElementId(string? str) =>\r\n        str switch\r\n        {\r\n            null => null,\r\n            \"\" => ElementId.Empty,\r\n            _ => new ElementId(str),\r\n        };\r\n\r\n    public override void WriteJson(JsonWriter writer, ElementId? value, JsonSerializer serializer)\r\n    {\r\n        string? id = value.ToString();\r\n        writer.WriteValue(id);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/IO/Serialization/Converters/FontUsageConverter.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing Newtonsoft.Json;\r\nusing Newtonsoft.Json.Linq;\r\nusing osu.Framework.Graphics.Sprites;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.IO.Serialization.Converters;\r\n\r\npublic class FontUsageConverter : JsonConverter<FontUsage>\r\n{\r\n    private const float default_text_size = 20;\r\n\r\n    public override FontUsage ReadJson(JsonReader reader, Type objectType, FontUsage existingValue, bool hasExistingValue, JsonSerializer serializer)\r\n    {\r\n        var obj = JToken.Load(reader);\r\n        var properties = obj.Children().OfType<JProperty>().ToArray();\r\n\r\n        if (!properties.Any())\r\n            return new FontUsage(size: default_text_size);\r\n\r\n        var font = new FontUsage(size: default_text_size);\r\n\r\n        return properties.Aggregate(font, (current, property) => property.Name switch\r\n        {\r\n            \"family\" => current.With(property.Value.ToObject<string>()),\r\n            \"weight\" => current.With(weight: property.Value.ToObject<string>()),\r\n            \"size\" => current.With(size: property.Value.ToObject<float>()),\r\n            \"italics\" => current.With(italics: property.Value.ToObject<bool>()),\r\n            \"fixedWidth\" => current.With(fixedWidth: property.Value.ToObject<bool>()),\r\n            _ => current,\r\n        });\r\n    }\r\n\r\n    public override void WriteJson(JsonWriter writer, FontUsage value, JsonSerializer serializer)\r\n    {\r\n        writer.WriteStartObject();\r\n\r\n        if (!string.IsNullOrEmpty(value.Family))\r\n        {\r\n            writer.WritePropertyName(\"family\");\r\n            writer.WriteValue(value.Family);\r\n        }\r\n\r\n        if (!string.IsNullOrEmpty(value.Weight))\r\n        {\r\n            writer.WritePropertyName(\"weight\");\r\n            writer.WriteValue(value.Weight);\r\n        }\r\n\r\n        if (value.Size != default_text_size)\r\n        {\r\n            writer.WritePropertyName(\"size\");\r\n            writer.WriteValue(value.Size);\r\n        }\r\n\r\n        if (value.Italics)\r\n        {\r\n            writer.WritePropertyName(\"italics\");\r\n            writer.WriteValue(true);\r\n        }\r\n\r\n        if (value.FixedWidth)\r\n        {\r\n            writer.WritePropertyName(\"fixedWidth\");\r\n            writer.WriteValue(true);\r\n        }\r\n\r\n        writer.WriteEndObject();\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/IO/Serialization/Converters/GenericTypeConverter.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing System.Reflection;\r\nusing Newtonsoft.Json;\r\nusing Newtonsoft.Json.Linq;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.IO.Serialization.Converters;\r\n\r\npublic abstract class GenericTypeConverter<TType> : GenericTypeConverter<TType, string>\r\n{\r\n    protected override string GetNameByType(MemberInfo type)\r\n        => type.Name;\r\n}\r\n\r\npublic abstract class GenericTypeConverter<TType, TTypeName> : JsonConverter<TType> where TTypeName : notnull\r\n{\r\n    public sealed override TType ReadJson(JsonReader reader, Type objectType, TType? existingValue, bool hasExistingValue, JsonSerializer serializer)\r\n    {\r\n        var jObject = JObject.Load(reader);\r\n        var type = objectType != typeof(TType) ? objectType : getTypeByProperties(jObject);\r\n\r\n        var newReader = jObject.CreateReader();\r\n\r\n        var instance = (TType)Activator.CreateInstance(type)!;\r\n        serializer.Populate(newReader, instance);\r\n        PostProcessValue(instance, jObject, serializer);\r\n        return instance;\r\n\r\n        Type getTypeByProperties(JObject jObj)\r\n        {\r\n            var elementType = GetValueFromProperty<TTypeName>(serializer, jObj, \"$type\");\r\n            return GetTypeByName(elementType);\r\n        }\r\n    }\r\n\r\n    protected static TPropertyType GetValueFromProperty<TPropertyType>(JsonSerializer serializer, JObject jObject, string propertyName)\r\n    {\r\n        var jProperties = jObject.Children().OfType<JProperty>().ToArray();\r\n        var value = jProperties.FirstOrDefault(x => x.Name == propertyName)?.Value;\r\n        if (value == null)\r\n            throw new ArgumentNullException(nameof(value));\r\n\r\n        var elementType = value.ToObject<TPropertyType>(serializer);\r\n        if (elementType == null)\r\n            throw new InvalidCastException(nameof(elementType));\r\n\r\n        return elementType;\r\n    }\r\n\r\n    protected virtual void PostProcessValue(TType existingValue, JObject jObject, JsonSerializer serializer) { }\r\n\r\n    public sealed override void WriteJson(JsonWriter writer, TType? value, JsonSerializer serializer)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(value);\r\n\r\n        var resolver = serializer.ContractResolver;\r\n\r\n        // follow: https://stackoverflow.com/a/59329703\r\n        // not a good way but seems there's no better choice.\r\n        serializer.Converters.Remove(this);\r\n        serializer.ContractResolver = new WritablePropertiesOnlyResolver();\r\n\r\n        var jObject = JObject.FromObject(value, serializer);\r\n\r\n        serializer.Converters.Add(this);\r\n        serializer.ContractResolver = resolver;\r\n\r\n        jObject.AddFirst(new JProperty(\"$type\", GetNameByType(value.GetType())));\r\n        PostProcessJObject(jObject, value, serializer);\r\n        jObject.WriteTo(writer);\r\n    }\r\n\r\n    protected virtual void PostProcessJObject(JObject jObject, TType value, JsonSerializer serializer) { }\r\n\r\n    protected abstract Type GetTypeByName(TTypeName name);\r\n\r\n    protected abstract TTypeName GetNameByType(MemberInfo type);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/IO/Serialization/Converters/KaraokeSkinElementConverter.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Reflection;\r\nusing osu.Game.Rulesets.Karaoke.Skinning.Elements;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.IO.Serialization.Converters;\r\n\r\npublic class KaraokeSkinElementConverter : GenericTypeConverter<IKaraokeSkinElement, ElementType>\r\n{\r\n    protected override Type GetTypeByName(ElementType name)\r\n        => GetObjectType(name);\r\n\r\n    protected override ElementType GetNameByType(MemberInfo type)\r\n        => GetElementType(type);\r\n\r\n    public static ElementType GetElementType(MemberInfo elementType) =>\r\n        elementType switch\r\n        {\r\n            _ when elementType == typeof(LyricFontInfo) => ElementType.LyricFontInfo,\r\n            _ when elementType == typeof(NoteStyle) => ElementType.NoteStyle,\r\n            _ => throw new NotSupportedException(),\r\n        };\r\n\r\n    public static Type GetObjectType(ElementType elementType) =>\r\n        elementType switch\r\n        {\r\n            ElementType.LyricFontInfo => typeof(LyricFontInfo),\r\n            ElementType.NoteStyle => typeof(NoteStyle),\r\n            _ => throw new NotSupportedException(),\r\n        };\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/IO/Serialization/Converters/LyricConverter.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Diagnostics;\r\nusing System.Linq;\r\nusing Newtonsoft.Json;\r\nusing Newtonsoft.Json.Linq;\r\nusing osu.Game.Extensions;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Properties;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.IO.Serialization.Converters;\r\n\r\npublic class LyricConverter : JsonConverter<Lyric>\r\n{\r\n    public override Lyric ReadJson(JsonReader reader, Type objectType, Lyric? existingValue, bool hasExistingValue, JsonSerializer serializer)\r\n    {\r\n        var jObject = JObject.Load(reader);\r\n\r\n        var newReader = jObject.CreateReader();\r\n\r\n        var instance = Activator.CreateInstance<Lyric>();\r\n        serializer.Populate(newReader, instance);\r\n        return instance;\r\n    }\r\n\r\n    public override void WriteJson(JsonWriter writer, Lyric? value, JsonSerializer serializer)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(value);\r\n\r\n        // follow: https://stackoverflow.com/a/59329703\r\n        // not a good way but seems there's no better choice.\r\n        serializer.Converters.Remove(this);\r\n\r\n        var jObject = JObject.FromObject(value, serializer);\r\n\r\n        serializer.Converters.Add(this);\r\n\r\n        // should remove some properties from jObject if has the config.\r\n        if (value.ReferenceLyricConfig != null)\r\n        {\r\n            Debug.Assert(value.ReferenceLyricConfig != null);\r\n\r\n            // note: should convert into snake case.\r\n            string[] removedProperties = removePropertyNamesByConfig(value.ReferenceLyricConfig)\r\n                                         .Select(x => x.ToSnakeCase())\r\n                                         .ToArray();\r\n\r\n            foreach (string removedProperty in removedProperties)\r\n            {\r\n                jObject.Remove(removedProperty);\r\n            }\r\n        }\r\n\r\n        jObject.WriteTo(writer);\r\n    }\r\n\r\n    private IEnumerable<string> removePropertyNamesByConfig(IReferenceLyricPropertyConfig config)\r\n    {\r\n        switch (config)\r\n        {\r\n            case ReferenceLyricConfig:\r\n                yield break;\r\n\r\n            case SyncLyricConfig syncLyricConfig:\r\n                yield return nameof(Lyric.Text);\r\n\r\n                if (syncLyricConfig.SyncTimeTagProperty)\r\n                    yield return nameof(Lyric.TimeTags);\r\n\r\n                yield return nameof(Lyric.RubyTags);\r\n                yield return nameof(Lyric.StartTime);\r\n                yield return nameof(Lyric.Duration);\r\n                yield return nameof(Lyric.EndTime);\r\n\r\n                if (syncLyricConfig.SyncSingerProperty)\r\n                    yield return nameof(Lyric.SingerIds);\r\n\r\n                yield return nameof(Lyric.Translations);\r\n                yield return nameof(Lyric.Language);\r\n                yield return nameof(Lyric.Order);\r\n\r\n                yield break;\r\n\r\n            default:\r\n                throw new ArgumentOutOfRangeException(nameof(config), config, \"unknown config.\");\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/IO/Serialization/Converters/ReferenceLyricPropertyConfigConverter.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Diagnostics;\r\nusing System.Reflection;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Properties;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.IO.Serialization.Converters;\r\n\r\npublic class ReferenceLyricPropertyConfigConverter : GenericTypeConverter<IReferenceLyricPropertyConfig>\r\n{\r\n    protected override Type GetTypeByName(string name)\r\n    {\r\n        var assembly = Assembly.GetExecutingAssembly();\r\n        var type = assembly.GetType($\"osu.Game.Rulesets.Karaoke.Objects.Properties.{name}\");\r\n        Debug.Assert(type != null);\r\n        return type;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/IO/Serialization/Converters/RubyTagConverter.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Text.RegularExpressions;\r\nusing Newtonsoft.Json;\r\nusing Newtonsoft.Json.Linq;\r\nusing osu.Game.Rulesets.Karaoke.Extensions;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.IO.Serialization.Converters;\r\n\r\npublic class RubyTagConverter : JsonConverter<RubyTag>\r\n{\r\n    public override RubyTag ReadJson(JsonReader reader, Type objectType, RubyTag? existingValue, bool hasExistingValue, JsonSerializer serializer)\r\n    {\r\n        var obj = JToken.Load(reader);\r\n        string? value = obj.Value<string>();\r\n\r\n        if (string.IsNullOrEmpty(value))\r\n            return new RubyTag();\r\n\r\n        var regex = new Regex(@\"\\[(?<start>[-0-9]+)(?:,(?<end>[-0-9]+))?\\]:(?<ruby>.*$)\");\r\n        var result = regex.Match(value);\r\n        if (!result.Success)\r\n            return new RubyTag();\r\n\r\n        return new RubyTag\r\n        {\r\n            StartIndex = result.GetGroupValue<int>(\"start\"),\r\n            EndIndex = result.GetGroupValue<int?>(\"end\") ?? result.GetGroupValue<int>(\"start\"),\r\n            Text = result.GetGroupValue<string>(\"ruby\"),\r\n        };\r\n    }\r\n\r\n    public override void WriteJson(JsonWriter writer, RubyTag? value, JsonSerializer serializer)\r\n    {\r\n        if (value == null)\r\n            throw new ArgumentNullException(nameof(value));\r\n\r\n        string str = value.StartIndex == value.EndIndex\r\n            ? $\"[{value.StartIndex}]:{value.Text}\"\r\n            : $\"[{value.StartIndex},{value.EndIndex}]:{value.Text}\";\r\n        writer.WriteValue(str);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/IO/Serialization/Converters/RubyTagsConverter.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.IO.Serialization.Converters;\r\n\r\npublic class RubyTagsConverter : SortableJsonConverter<RubyTag>\r\n{\r\n    protected override IEnumerable<RubyTag> GetSortedValue(IEnumerable<RubyTag> objects)\r\n        => RubyTagsUtils.Sort(objects);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/IO/Serialization/Converters/ShaderConverter.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Diagnostics;\r\nusing Newtonsoft.Json;\r\nusing Newtonsoft.Json.Linq;\r\nusing osu.Framework.Graphics.Shaders;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.IO.Serialization.Converters;\r\n\r\npublic class ShaderConverter : GenericTypeConverter<ICustomizedShader>\r\n{\r\n    protected override void PostProcessJObject(JObject jObject, ICustomizedShader value, JsonSerializer serializer)\r\n    {\r\n        var childShader = getShadersFromParent(value, serializer);\r\n\r\n        if (childShader != null)\r\n        {\r\n            jObject.Remove(\"step_shaders\");\r\n            jObject.Add(\"step_shaders\", childShader);\r\n        }\r\n\r\n        static JArray? getShadersFromParent(ICustomizedShader shader, JsonSerializer serializer)\r\n        {\r\n            if (shader is not StepShader stepShader)\r\n                return null;\r\n\r\n            return JArray.FromObject(stepShader.StepShaders, serializer);\r\n        }\r\n    }\r\n\r\n    protected override Type GetTypeByName(string name)\r\n    {\r\n        // only get name from font\r\n        var assembly = AssemblyUtils.GetAssemblyByName(\"osu.Framework.KaraokeFont\");\r\n        Debug.Assert(assembly != null);\r\n\r\n        var type = assembly.GetType($\"osu.Framework.Graphics.Shaders.{name}\");\r\n        Debug.Assert(type != null);\r\n        return type;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/IO/Serialization/Converters/SortableJsonConverter.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing Newtonsoft.Json;\r\nusing Newtonsoft.Json.Linq;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.IO.Serialization.Converters;\r\n\r\npublic abstract class SortableJsonConverter<TObject> : JsonConverter<IEnumerable<TObject>>\r\n{\r\n    public sealed override IEnumerable<TObject> ReadJson(JsonReader reader, Type objectType, IEnumerable<TObject>? existingValue, bool hasExistingValue, JsonSerializer serializer)\r\n    {\r\n        var obj = JArray.Load(reader);\r\n        var timeTags = obj.Select(x => serializer.Deserialize<TObject>(x.CreateReader())!);\r\n        return GetSortedValue(timeTags);\r\n    }\r\n\r\n    public override void WriteJson(JsonWriter writer, IEnumerable<TObject>? value, JsonSerializer serializer)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(value);\r\n\r\n        // see: https://stackoverflow.com/questions/3330989/order-of-serialized-fields-using-json-net\r\n        var sortedTimeTags = GetSortedValue(value);\r\n\r\n        writer.WriteStartArray();\r\n\r\n        foreach (var timeTag in sortedTimeTags)\r\n        {\r\n            serializer.Serialize(writer, timeTag);\r\n        }\r\n\r\n        writer.WriteEndArray();\r\n    }\r\n\r\n    protected abstract IEnumerable<TObject> GetSortedValue(IEnumerable<TObject> objects);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/IO/Serialization/Converters/StageInfoConverter.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Reflection;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos.Classic;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos.Preview;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.IO.Serialization.Converters;\r\n\r\npublic class StageInfoConverter : GenericTypeConverter<StageInfo>\r\n{\r\n    private const string classic_stage = \"classic\";\r\n    private const string preview_stage = \"preview\";\r\n\r\n    protected override string GetNameByType(MemberInfo type) =>\r\n        type switch\r\n        {\r\n            Type t when t == typeof(ClassicStageInfo) => classic_stage,\r\n            Type t when t == typeof(PreviewStageInfo) => preview_stage,\r\n            _ => throw new InvalidOperationException(),\r\n        };\r\n\r\n    protected override Type GetTypeByName(string name) =>\r\n        name switch\r\n        {\r\n            classic_stage => typeof(ClassicStageInfo),\r\n            preview_stage => typeof(PreviewStageInfo),\r\n            _ => throw new InvalidOperationException(),\r\n        };\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/IO/Serialization/Converters/TimeTagConverter.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Text.RegularExpressions;\r\nusing Newtonsoft.Json;\r\nusing Newtonsoft.Json.Linq;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Extensions;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.IO.Serialization.Converters;\r\n\r\npublic class TimeTagConverter : JsonConverter<TimeTag>\r\n{\r\n    public override TimeTag ReadJson(JsonReader reader, Type objectType, TimeTag? existingValue, bool hasExistingValue, JsonSerializer serializer)\r\n    {\r\n        var obj = JToken.Load(reader);\r\n        string? value = obj.Value<string>();\r\n\r\n        if (string.IsNullOrEmpty(value))\r\n            return new TimeTag(new TextIndex());\r\n\r\n        var regex = new Regex(\"(?<index>[-0-9]+),(?<state>start|end)]:(?<time>[-0-9]+|s*|)\");\r\n        var result = regex.Match(value);\r\n        if (!result.Success)\r\n            return new TimeTag(new TextIndex());\r\n\r\n        int index = result.GetGroupValue<int>(\"index\");\r\n        var state = result.GetGroupValue<string>(\"state\") == \"start\" ? TextIndex.IndexState.Start : TextIndex.IndexState.End;\r\n        int? time = result.GetGroupValue<int?>(\"time\");\r\n\r\n        return new TimeTag(new TextIndex(index, state), time);\r\n    }\r\n\r\n    public override void WriteJson(JsonWriter writer, TimeTag? value, JsonSerializer serializer)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(value);\r\n\r\n        var index = value.Index;\r\n        string state = TextIndexUtils.GetValueByState(index, \"start\", \"end\");\r\n        double? time = value.Time;\r\n\r\n        string str = $\"[{index.Index},{state}]:{time}\";\r\n        writer.WriteValue(str);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/IO/Serialization/Converters/TimeTagsConverter.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.IO.Serialization.Converters;\r\n\r\npublic class TimeTagsConverter : SortableJsonConverter<TimeTag>\r\n{\r\n    protected override IEnumerable<TimeTag> GetSortedValue(IEnumerable<TimeTag> objects)\r\n        => TimeTagsUtils.Sort(objects);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/IO/Serialization/Converters/ToneConverter.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing Newtonsoft.Json;\r\nusing Newtonsoft.Json.Linq;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.IO.Serialization.Converters;\r\n\r\npublic class ToneConverter : JsonConverter<Tone>\r\n{\r\n    public override Tone ReadJson(JsonReader reader, Type objectType, Tone existingValue, bool hasExistingValue, JsonSerializer serializer)\r\n    {\r\n        var obj = JToken.Load(reader);\r\n        double value = obj.Value<double>();\r\n\r\n        bool half = Math.Abs(value) % 1 == 0.5;\r\n        int scale = (int)value - (value < 0 && half ? 1 : 0);\r\n\r\n        return new Tone\r\n        {\r\n            Scale = scale,\r\n            Half = half,\r\n        };\r\n    }\r\n\r\n    public override void WriteJson(JsonWriter writer, Tone value, JsonSerializer serializer)\r\n    {\r\n        double scale = value.Scale + (value.Half ? 0.5 : 0);\r\n        writer.WriteValue(scale);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/IO/Serialization/Converters/TranslationConverter.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Globalization;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.IO.Serialization.Converters;\r\n\r\npublic class TranslationConverter : DictionaryConverter<CultureInfo, string>;\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/IO/Serialization/KaraokeJsonSerializableExtensions.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing Newtonsoft.Json;\r\nusing osu.Game.IO.Serialization;\r\nusing osu.Game.Rulesets.Karaoke.IO.Serialization.Converters;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.IO.Serialization;\r\n\r\npublic static class KaraokeJsonSerializableExtensions\r\n{\r\n    public static JsonSerializerSettings CreateGlobalSettings()\r\n    {\r\n        var globalSetting = JsonSerializableExtensions.CreateGlobalSettings();\r\n\r\n        // hit-object\r\n        globalSetting.Converters.Add(new CultureInfoConverter());\r\n        globalSetting.Converters.Add(new ElementIdConverter());\r\n        globalSetting.Converters.Add(new RubyTagConverter());\r\n        globalSetting.Converters.Add(new RubyTagsConverter());\r\n        globalSetting.Converters.Add(new TimeTagConverter());\r\n        globalSetting.Converters.Add(new TimeTagsConverter());\r\n        globalSetting.Converters.Add(new ToneConverter());\r\n        globalSetting.Converters.Add(new TranslationConverter());\r\n        globalSetting.Converters.Add(new ReferenceLyricPropertyConfigConverter());\r\n        globalSetting.Converters.Add(new LyricConverter());\r\n\r\n        return globalSetting;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/IO/Serialization/SkinJsonSerializableExtensions.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing Newtonsoft.Json;\r\nusing osu.Framework.IO.Serialization;\r\nusing osu.Game.IO.Serialization;\r\nusing osu.Game.Rulesets.Karaoke.IO.Serialization.Converters;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.IO.Serialization;\r\n\r\npublic static class SkinJsonSerializableExtensions\r\n{\r\n    public static JsonSerializerSettings CreateSkinElementGlobalSettings()\r\n    {\r\n        var globalSetting = JsonSerializableExtensions.CreateGlobalSettings();\r\n        globalSetting.ContractResolver = new SnakeCaseKeyContractResolver();\r\n        globalSetting.Converters.Add(new KaraokeSkinElementConverter());\r\n        globalSetting.Converters.Add(new ShaderConverter());\r\n        globalSetting.Converters.Add(new Vector2Converter());\r\n        globalSetting.Converters.Add(new ColourConverter());\r\n        globalSetting.Converters.Add(new FontUsageConverter());\r\n        return globalSetting;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/IO/Serialization/WritablePropertiesOnlyResolver.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing Newtonsoft.Json;\r\nusing Newtonsoft.Json.Serialization;\r\nusing osu.Game.IO.Serialization;\r\nusing osu.Game.Rulesets.Karaoke.Skinning;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.IO.Serialization;\r\n\r\n/// <summary>\r\n/// This contract resolver is for save and load data from <see cref=\"KaraokeSkin\"/>\r\n/// </summary>\r\npublic class WritablePropertiesOnlyResolver : SnakeCaseKeyContractResolver\r\n{\r\n    // we only wants to save properties that only writable.\r\n    protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)\r\n    {\r\n        var props = base.CreateProperties(type, memberSerialization);\r\n        return props.Where(p => p.Writable).ToList();\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/IO/Stores/FntGlyphStore.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics.Textures;\r\nusing osu.Framework.IO.Stores;\r\nusing SharpFNT;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.IO.Stores;\r\n\r\npublic class FntGlyphStore : GlyphStore\r\n{\r\n    public BitmapFont? BitmapFont => Font;\r\n\r\n    public FntGlyphStore(ResourceStore<byte[]> store, string? assetName = null, IResourceStore<TextureUpload>? textureLoader = null)\r\n        : base(store, assetName, textureLoader)\r\n    {\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/IO/Stores/KaraokeLocalFontStore.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Graphics.Rendering;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Graphics.Textures;\r\nusing osu.Framework.IO.Stores;\r\nusing osu.Game.Rulesets.Karaoke.Skinning.Fonts;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.IO.Stores;\r\n\r\npublic class KaraokeLocalFontStore : FontStore\r\n{\r\n    private readonly Dictionary<FontInfo, IResourceStore<TextureUpload>> fontInfos = new();\r\n    private readonly FontManager fontManager;\r\n\r\n    /// <summary>\r\n    /// Construct a font store to be added to a parent font store via <see cref=\"AddFont\"/>.\r\n    /// </summary>\r\n    /// <param name=\"fontManager\">font manager.</param>\r\n    /// <param name=\"renderer\">The renderer to create textures with.</param>\r\n    /// <param name=\"store\">The texture source.</param>\r\n    /// <param name=\"scaleAdjust\">The raw pixel height of the font. Can be used to apply a global scale or metric to font usages.</param>\r\n    public KaraokeLocalFontStore(FontManager fontManager, IRenderer renderer, IResourceStore<TextureUpload>? store = null, float scaleAdjust = 100)\r\n        : base(renderer, store, scaleAdjust)\r\n    {\r\n        this.fontManager = fontManager;\r\n    }\r\n\r\n    public void AddFont(FontUsage fontUsage)\r\n    {\r\n        var fontFormat = fontManager.CheckFontFormat(fontUsage);\r\n        if (fontFormat == null)\r\n            return;\r\n\r\n        var fontInfo = FontUsageUtils.ToFontInfo(fontUsage, fontFormat.Value);\r\n        addFont(fontInfo);\r\n    }\r\n\r\n    private void addFont(FontInfo fontInfo)\r\n    {\r\n        bool hasFont = fontInfos.ContainsKey(fontInfo);\r\n        if (hasFont)\r\n            return;\r\n\r\n        var glyphStore = fontManager.GetGlyphStore(fontInfo);\r\n        if (glyphStore == null)\r\n            return;\r\n\r\n        AddTextureSource(glyphStore);\r\n        fontInfos.Add(fontInfo, glyphStore);\r\n    }\r\n\r\n    private void removeFont(FontInfo fontInfo)\r\n    {\r\n        if (!fontInfos.TryGetValue(fontInfo, out var glyphStore))\r\n            return;\r\n\r\n        RemoveTextureStore(glyphStore);\r\n        fontInfos.Remove(fontInfo);\r\n    }\r\n\r\n    public void ClearFont()\r\n    {\r\n        foreach (var (fontInfo, _) in fontInfos)\r\n        {\r\n            removeFont(fontInfo);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/IO/Stores/TtfGlyphStore.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\n#nullable disable\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Diagnostics;\r\nusing System.IO;\r\nusing System.Linq;\r\nusing System.Threading;\r\nusing System.Threading.Tasks;\r\nusing JetBrains.Annotations;\r\nusing osu.Framework.Extensions;\r\nusing osu.Framework.Graphics.Textures;\r\nusing osu.Framework.IO.Stores;\r\nusing osu.Framework.Logging;\r\nusing osu.Framework.Text;\r\nusing SixLabors.Fonts;\r\nusing SixLabors.Fonts.Unicode;\r\nusing SixLabors.ImageSharp;\r\nusing SixLabors.ImageSharp.Drawing;\r\nusing SixLabors.ImageSharp.Drawing.Processing;\r\nusing SixLabors.ImageSharp.PixelFormats;\r\nusing SixLabors.ImageSharp.Processing;\r\nusing TextBuilder = SixLabors.ImageSharp.Drawing.TextBuilder;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.IO.Stores;\r\n\r\npublic class TtfGlyphStore : IResourceStore<TextureUpload>, IGlyphStore\r\n{\r\n    private const float dpi = 72f;\r\n\r\n    protected readonly string AssetName;\r\n\r\n    public string FontName { get; }\r\n\r\n    public float? Baseline => fontMetrics?.VerticalMetrics.LineHeight;\r\n\r\n    protected readonly ResourceStore<byte[]> Store;\r\n\r\n    [CanBeNull]\r\n    public Font Font => completionSource.Task.GetResultSafely();\r\n\r\n    private FontMetrics fontMetrics => Font?.FontMetrics;\r\n\r\n    private readonly TaskCompletionSource<Font> completionSource = new();\r\n\r\n    /// <summary>\r\n    /// Create a new glyph store.\r\n    /// </summary>\r\n    /// <param name=\"store\">The store to provide font resources.</param>\r\n    /// <param name=\"assetName\">The base name of the font.</param>\r\n    public TtfGlyphStore(ResourceStore<byte[]> store, string assetName = null)\r\n    {\r\n        Store = new ResourceStore<byte[]>(store);\r\n\r\n        Store.AddExtension(\"ttf\");\r\n\r\n        AssetName = assetName;\r\n\r\n        FontName = assetName?.Split('/').Last();\r\n    }\r\n\r\n    private Task fontLoadTask;\r\n\r\n    public Task LoadFontAsync() => fontLoadTask ??= Task.Factory.StartNew(() =>\r\n    {\r\n        try\r\n        {\r\n            Font font;\r\n\r\n            using (var s = Store.GetStream($\"{AssetName}\"))\r\n            {\r\n                var fonts = new FontCollection();\r\n                var fontFamily = fonts.Add(s);\r\n                font = new Font(fontFamily, 1);\r\n            }\r\n\r\n            completionSource.SetResult(font);\r\n        }\r\n        catch (Exception ex)\r\n        {\r\n            Logger.Error(ex, $\"Couldn't load font asset from {AssetName}.\");\r\n            completionSource.SetResult(null);\r\n            throw;\r\n        }\r\n    }, TaskCreationOptions.PreferFairness);\r\n\r\n    public bool HasGlyph(char c)\r\n    {\r\n        return getGlyphMetrics(c) != null;\r\n    }\r\n\r\n    public CharacterGlyph Get(char character)\r\n    {\r\n        if (fontMetrics == null)\r\n            return null;\r\n\r\n        Debug.Assert(Baseline != null);\r\n\r\n        var glyphMetrics = getGlyphMetrics(character);\r\n        if (glyphMetrics == null)\r\n            return null;\r\n\r\n        string text = new(new[] { character });\r\n        var style = new TextOptions(Font);\r\n        var bounds = TextMeasurer.MeasureBounds(text, style);\r\n\r\n        float xOffset = bounds.Left * dpi;\r\n        float yOffset = bounds.Top * dpi;\r\n\r\n        float advanceWidth2 = glyphMetrics.AdvanceWidth * dpi / glyphMetrics.UnitsPerEm;\r\n        return new CharacterGlyph(character, xOffset, yOffset, advanceWidth2, Baseline.Value, this);\r\n    }\r\n\r\n    [CanBeNull]\r\n    private GlyphMetrics getGlyphMetrics(char character)\r\n    {\r\n        if (fontMetrics == null)\r\n            return null;\r\n\r\n        if (!fontMetrics.TryGetGlyphMetrics(new CodePoint(character),\r\n                TextAttributes.None,\r\n                TextDecorations.None,\r\n                LayoutMode.VerticalLeftRight,\r\n                ColorFontSupport.None,\r\n                out var glyphMetrics))\r\n            return null;\r\n\r\n        var targetGlyph = glyphMetrics.FirstOrDefault();\r\n        if (targetGlyph == null || targetGlyph.GlyphType == GlyphType.Fallback)\r\n            return null;\r\n\r\n        return targetGlyph;\r\n    }\r\n\r\n    public int GetKerning(char left, char right)\r\n    {\r\n        // todo: implement.\r\n        return 0;\r\n    }\r\n\r\n    Task<CharacterGlyph> IResourceStore<CharacterGlyph>.GetAsync(string name, CancellationToken cancellationToken) =>\r\n        Task.Run(() => ((IGlyphStore)this).Get(name[0]), cancellationToken);\r\n\r\n    CharacterGlyph IResourceStore<CharacterGlyph>.Get(string name) => Get(name[0]);\r\n\r\n    public TextureUpload Get(string name)\r\n    {\r\n        if (fontMetrics == null) return null;\r\n\r\n        if (name.Length > 1 && !name.StartsWith($\"{FontName}/\", StringComparison.Ordinal))\r\n            return null;\r\n\r\n        return !HasGlyph(name.Last()) ? null : LoadCharacter(name.Last());\r\n    }\r\n\r\n    public virtual async Task<TextureUpload> GetAsync(string name, CancellationToken cancellationToken = default)\r\n    {\r\n        if (name.Length > 1 && !name.StartsWith($\"{FontName}/\", StringComparison.Ordinal))\r\n            return null;\r\n\r\n        await completionSource.Task.ConfigureAwait(false);\r\n\r\n        return LoadCharacter(name.Last());\r\n    }\r\n\r\n    protected int LoadedGlyphCount;\r\n\r\n    protected virtual TextureUpload LoadCharacter(char c)\r\n    {\r\n        if (Font == null)\r\n            return null;\r\n\r\n        LoadedGlyphCount++;\r\n\r\n        // see: https://stackoverflow.com/a/53023454/4105113\r\n        const float texture_scale = dpi;\r\n        var style = new TextOptions(Font);\r\n        string text = new(new[] { c });\r\n        var bounds = TextMeasurer.MeasureBounds(text, style);\r\n        var targetSize = new\r\n        {\r\n            Width = (int)(bounds.Width * texture_scale),\r\n            Height = (int)(bounds.Height * texture_scale),\r\n        };\r\n\r\n        // this is the important line, where we render the glyphs to a vector instead of directly to the image\r\n        // this allows further vector manipulation (scaling, translating) etc without the expensive pixel operations.\r\n        var glyphs = TextBuilder.GenerateGlyphs(text, style);\r\n\r\n        // should calculate this because it will cut the border if width and height scale is not the same.\r\n        float widthScale = targetSize.Width / glyphs.Bounds.Width;\r\n        float heightScale = targetSize.Height / glyphs.Bounds.Height;\r\n        float minScale = Math.Min(widthScale, heightScale);\r\n\r\n        // scale so that it will fit exactly in image shape once rendered\r\n        glyphs = glyphs.Scale(minScale);\r\n\r\n        // move the vectorised glyph so that it touch top and left edges\r\n        // could be tweeked to center horizontally & vertically here\r\n        glyphs = glyphs.Translate(-glyphs.Bounds.Location);\r\n\r\n        // create image with char.\r\n        var img = new Image<Rgba32>(targetSize.Width, targetSize.Height, new Rgba32(255, 255, 255, 0));\r\n        img.Mutate(i => i.Fill(Color.White, glyphs));\r\n        return new TextureUpload(img);\r\n    }\r\n\r\n    public Stream GetStream(string name) => throw new NotSupportedException();\r\n\r\n    public IEnumerable<string> GetAvailableResources() => throw new NotSupportedException();\r\n\r\n    #region IDisposable Support\r\n\r\n    public void Dispose()\r\n    {\r\n        Dispose(true);\r\n        GC.SuppressFinalize(this);\r\n    }\r\n\r\n    protected virtual void Dispose(bool disposing)\r\n    {\r\n    }\r\n\r\n    #endregion\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Integration/Formats/IDecoder.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Integration.Formats;\r\n\r\npublic interface IDecoder<out TTargetFormat> : IDecoder<string, TTargetFormat>;\r\n\r\npublic interface IDecoder<in TSourceFormat, out TTargetFormat>\r\n{\r\n    public TTargetFormat Decode(TSourceFormat source);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Integration/Formats/IEncoder.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Integration.Formats;\r\n\r\npublic interface IEncoder<in TSourceFormat> : IEncoder<TSourceFormat, string>;\r\n\r\npublic interface IEncoder<in TSourceFormat, out TTargetFormat>\r\n{\r\n    public TTargetFormat Encode(TSourceFormat source);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Integration/Formats/KarDecoder.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Integration.Formats;\r\n\r\npublic class KarDecoder : IDecoder<Lyric[]>\r\n{\r\n    public Lyric[] Decode(string source)\r\n    {\r\n        var song = new LrcParser.Parser.Kar.KarParser().Decode(source);\r\n        return LrcParserUtils.ConvertToLyrics(song).ToArray();\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Integration/Formats/KarEncoder.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Beatmaps;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Integration.Formats;\r\n\r\npublic class KarEncoder : IEncoder<Beatmap>\r\n{\r\n    public string Encode(Beatmap source)\r\n    {\r\n        var song = LrcParserUtils.ConvertToSong(source);\r\n        return new LrcParser.Parser.Kar.KarParser().Encode(song);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Integration/Formats/LrcDecoder.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Integration.Formats;\r\n\r\npublic class LrcDecoder : IDecoder<Lyric[]>\r\n{\r\n    public Lyric[] Decode(string source)\r\n    {\r\n        var song = new LrcParser.Parser.Lrc.LrcParser().Decode(source);\r\n        return LrcParserUtils.ConvertToLyrics(song).ToArray();\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Integration/Formats/LrcEncoder.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Beatmaps;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Integration.Formats;\r\n\r\npublic class LrcEncoder : IEncoder<Beatmap>\r\n{\r\n    public string Encode(Beatmap source)\r\n    {\r\n        var song = LrcParserUtils.ConvertToSong(source);\r\n        return new LrcParser.Parser.Lrc.LrcParser().Encode(song);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Integration/Formats/LrcParserUtils.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing LrcParser.Model;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\nusing Lyric = osu.Game.Rulesets.Karaoke.Objects.Lyric;\r\nusing RubyTag = osu.Game.Rulesets.Karaoke.Objects.RubyTag;\r\nusing TextIndex = osu.Framework.Graphics.Sprites.TextIndex;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Integration.Formats;\r\n\r\npublic class LrcParserUtils\r\n{\r\n    public static Song ConvertToSong(Beatmap beatmap)\r\n    {\r\n        // Note : save to lyric will lost some tags with no value.\r\n        return new Song\r\n        {\r\n            Lyrics = beatmap.HitObjects.OfType<Lyric>().Select(encodeLyric).ToList(),\r\n        };\r\n\r\n        static LrcParser.Model.Lyric encodeLyric(Lyric lyric) =>\r\n            new()\r\n            {\r\n                Text = lyric.Text,\r\n                TimeTags = convertTimeTag(lyric.TimeTags),\r\n                RubyTags = convertRubyTag(lyric.RubyTags),\r\n            };\r\n\r\n        static SortedDictionary<LrcParser.Model.TextIndex, int?> convertTimeTag(IList<TimeTag> timeTags)\r\n        {\r\n            // Note : save to lyric will lost some tags with duplicated index.\r\n            var timeTagDictionary = ToDictionary(timeTags).ToDictionary(k => convertTextIndex(k.Key), v => (int?)v.Value);\r\n            return new SortedDictionary<LrcParser.Model.TextIndex, int?>(timeTagDictionary);\r\n        }\r\n\r\n        static LrcParser.Model.TextIndex convertTextIndex(TextIndex textIndex)\r\n        {\r\n            int index = textIndex.Index;\r\n            var state = TextIndexUtils.GetValueByState(textIndex, IndexState.Start, IndexState.End);\r\n\r\n            return new LrcParser.Model.TextIndex(index, state);\r\n        }\r\n\r\n        static List<LrcParser.Model.RubyTag> convertRubyTag(IEnumerable<RubyTag> rubyTags)\r\n            => rubyTags.Select(x => new LrcParser.Model.RubyTag\r\n            {\r\n                Text = x.Text,\r\n                StartCharIndex = x.StartIndex,\r\n                EndCharIndex = x.EndIndex,\r\n            }).ToList();\r\n    }\r\n\r\n    /// <summary>\r\n    /// Convert list of time tag to dictionary.\r\n    /// </summary>\r\n    /// <param name=\"timeTags\">Time tags</param>\r\n    /// <param name=\"applyFix\">Should auto-fix or not</param>\r\n    /// <param name=\"other\">Fix way</param>\r\n    /// <param name=\"self\">Fix way</param>\r\n    /// <returns>Time tags with dictionary format.</returns>\r\n    internal static IReadOnlyDictionary<TextIndex, double> ToDictionary(IList<TimeTag> timeTags, bool applyFix = true, GroupCheck other = GroupCheck.Asc,\r\n                                                                        SelfCheck self = SelfCheck.BasedOnStart)\r\n    {\r\n        // sorted value\r\n        var sortedTimeTags = applyFix ? TimeTagsUtils.FixOverlapping(timeTags, other, self) : TimeTagsUtils.Sort(timeTags);\r\n\r\n        // convert to dictionary, will get start's smallest time and end's largest time.\r\n        return sortedTimeTags.Where(x => x.Time != null).GroupBy(x => x.Index)\r\n                             .Select(x => TextIndexUtils.GetValueByState(x.Key, x.FirstOrDefault, x.LastOrDefault))\r\n                             .ToDictionary(\r\n                                 k => k?.Index ?? throw new ArgumentNullException(nameof(k)),\r\n                                 v => v?.Time ?? throw new ArgumentNullException(nameof(v)));\r\n    }\r\n\r\n    public static IEnumerable<Lyric> ConvertToLyrics(Song song)\r\n    {\r\n        return song.Lyrics.Select((lrcLyric, index) =>\r\n        {\r\n            var lrcTimeTags = lrcLyric.TimeTags.Select(convertTimeTag).ToArray();\r\n            var lrcRubies = lrcLyric.RubyTags.Select(convertRubyTag).ToArray();\r\n            var lrcRubyTimeTags = lrcLyric.RubyTags.Select(convertTimeTagsFromRubyTags).SelectMany(x => x).ToArray();\r\n\r\n            return new Lyric\r\n            {\r\n                Order = index + 1, // should create default order.\r\n                Text = lrcLyric.Text,\r\n                TimeTags = TimeTagsUtils.Sort(lrcTimeTags.Concat(lrcRubyTimeTags)),\r\n                RubyTags = lrcRubies,\r\n            };\r\n        });\r\n\r\n        static TimeTag convertTimeTag(KeyValuePair<LrcParser.Model.TextIndex, int?> timeTag)\r\n            => new(convertTextIndex(timeTag.Key), timeTag.Value);\r\n\r\n        static TextIndex convertTextIndex(LrcParser.Model.TextIndex textIndex)\r\n        {\r\n            int index = textIndex.Index;\r\n            var state = textIndex.State == IndexState.Start ? TextIndex.IndexState.Start : TextIndex.IndexState.End;\r\n\r\n            return new TextIndex(index, state);\r\n        }\r\n\r\n        static RubyTag convertRubyTag(LrcParser.Model.RubyTag rubyTag)\r\n            => new()\r\n            {\r\n                Text = rubyTag.Text,\r\n                StartIndex = rubyTag.StartCharIndex,\r\n                EndIndex = rubyTag.EndCharIndex,\r\n            };\r\n\r\n        static TimeTag[] convertTimeTagsFromRubyTags(LrcParser.Model.RubyTag rubyTag)\r\n        {\r\n            int startIndex = rubyTag.StartCharIndex;\r\n            return rubyTag.TimeTags.Select(x => convertTimeTag(new KeyValuePair<LrcParser.Model.TextIndex, int?>(new LrcParser.Model.TextIndex(startIndex), x.Value))).ToArray();\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Integration/Formats/LyricTextDecoder.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Integration.Formats;\r\n\r\npublic class LyricTextDecoder : IDecoder<Lyric[]>\r\n{\r\n    public Lyric[] Decode(string source)\r\n    {\r\n        return source.Split('\\n').Select((text, index) => new Lyric\r\n        {\r\n            Order = index + 1, // should create default order.\r\n            Text = text,\r\n        }).ToArray();\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Integration/Formats/LyricTextEncoder.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Integration.Formats;\r\n\r\npublic class LyricTextEncoder : IEncoder<IBeatmap>\r\n{\r\n    public string Encode(IBeatmap output)\r\n    {\r\n        var lyrics = output.HitObjects.OfType<Lyric>();\r\n        var lyricTexts = lyrics.Select(x => x.Text).Where(x => !string.IsNullOrWhiteSpace(x));\r\n        return string.Join('\\n', lyricTexts);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Judgements/KaraokeJudgement.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Judgements;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Judgements;\r\n\r\npublic class KaraokeJudgement : Judgement;\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Judgements/KaraokeJudgementResult.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Judgements;\r\nusing osu.Game.Rulesets.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Judgements;\r\n\r\npublic class KaraokeJudgementResult : JudgementResult\r\n{\r\n    public KaraokeJudgementResult(HitObject hitObject, Judgement judgement)\r\n        : base(hitObject, judgement)\r\n    {\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Judgements/KaraokeLyricJudgement.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Scoring;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Judgements;\r\n\r\npublic class KaraokeLyricJudgement : KaraokeJudgement\r\n{\r\n    public override HitResult MaxResult => HitResult.Perfect;\r\n\r\n    protected override double HealthIncreaseFor(HitResult result) => 0;\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Judgements/KaraokeNoteJudgement.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Scoring;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Judgements;\r\n\r\npublic class KaraokeNoteJudgement : KaraokeJudgement\r\n{\r\n    public bool Scorable { get; set; }\r\n\r\n    protected override double HealthIncreaseFor(HitResult result)\r\n    {\r\n        if (!Scorable)\r\n            return 0;\r\n\r\n        return result switch\r\n        {\r\n            HitResult.Miss => -0.125,\r\n            HitResult.Meh => 0.005,\r\n            HitResult.Ok => 0.010,\r\n            HitResult.Good => 0.035,\r\n            HitResult.Great => 0.055,\r\n            HitResult.Perfect => 0.065,\r\n            _ => 0,\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/KaraokeControlInputManager.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.ComponentModel;\r\nusing osu.Framework.Input.Bindings;\r\nusing osu.Game.Input.Bindings;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke;\r\n\r\npublic partial class KaraokeControlInputManager : DatabasedKeyBindingContainer<KaraokeAction>\r\n{\r\n    public KaraokeControlInputManager(RulesetInfo ruleset)\r\n        : base(ruleset, 1, SimultaneousBindingMode.Unique)\r\n    {\r\n    }\r\n}\r\n\r\npublic enum KaraokeAction\r\n{\r\n    [Description(\"First Lyric\")]\r\n    FirstLyric,\r\n\r\n    [Description(\"Previous BaseLyric\")]\r\n    PreviousLyric,\r\n\r\n    [Description(\"Next BaseLyric\")]\r\n    NextLyric,\r\n\r\n    [Description(\"Play and pause\")]\r\n    PlayAndPause,\r\n\r\n    [Description(\"Open/Close adjustment\")]\r\n    OpenPanel,\r\n\r\n    [Description(\"Increase Speed\")]\r\n    IncreaseTempo,\r\n\r\n    [Description(\"Decrease Speed\")]\r\n    DecreaseTempo,\r\n\r\n    [Description(\"Reset Speed\")]\r\n    ResetTempo,\r\n\r\n    [Description(\"Increase pitch\")]\r\n    IncreasePitch,\r\n\r\n    [Description(\"Decrease pitch\")]\r\n    DecreasePitch,\r\n\r\n    [Description(\"Reset pitch\")]\r\n    ResetPitch,\r\n\r\n    [Description(\"Increase vocal pitch\")]\r\n    IncreaseVocalPitch,\r\n\r\n    [Description(\"Decrease vocal pitch\")]\r\n    DecreaseVocalPitch,\r\n\r\n    [Description(\"Reset vocal pitch\")]\r\n    ResetVocalPitch,\r\n\r\n    [Description(\"Increase scoring pitch\")]\r\n    IncreaseScoringPitch,\r\n\r\n    [Description(\"Decrease scoring pitch\")]\r\n    DecreaseScoringPitch,\r\n\r\n    [Description(\"Reset scoring pitch\")]\r\n    ResetScoringPitch,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/KaraokeEditInputManager.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.ComponentModel;\r\nusing System.Linq;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Input.Bindings;\r\nusing osu.Game.Input.Bindings;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke;\r\n\r\npublic partial class KaraokeEditInputManager : DatabasedKeyBindingContainer<KaraokeEditAction>\r\n{\r\n    public KaraokeEditInputManager(RulesetInfo ruleset)\r\n        : base(ruleset, 2, SimultaneousBindingMode.Unique, KeyCombinationMatchingMode.Modifiers)\r\n    {\r\n    }\r\n\r\n    protected override IEnumerable<Drawable> KeyBindingInputQueue\r\n    {\r\n        get\r\n        {\r\n            var queue = base.KeyBindingInputQueue;\r\n            return queue.OrderBy(x => x is IHasIKeyBindingHandlerOrder keyBindingHandlerOrder\r\n                ? keyBindingHandlerOrder.KeyBindingHandlerOrder\r\n                : int.MaxValue);\r\n        }\r\n    }\r\n}\r\n\r\npublic interface IHasIKeyBindingHandlerOrder\r\n{\r\n    int KeyBindingHandlerOrder { get; }\r\n}\r\n\r\npublic enum KaraokeEditAction\r\n{\r\n    // moving\r\n    [Description(\"Up\")]\r\n    MoveToPreviousLyric,\r\n\r\n    [Description(\"Down\")]\r\n    MoveToNextLyric,\r\n\r\n    [Description(\"First Lyric\")]\r\n    MoveToFirstLyric,\r\n\r\n    [Description(\"Last Lyric\")]\r\n    MoveToLastLyric,\r\n\r\n    [Description(\"Left\")]\r\n    MoveToPreviousIndex,\r\n\r\n    [Description(\"Right\")]\r\n    MoveToNextIndex,\r\n\r\n    [Description(\"First index\")]\r\n    MoveToFirstIndex,\r\n\r\n    [Description(\"Last index\")]\r\n    MoveToLastIndex,\r\n\r\n    // Switch edit mode.\r\n    [Description(\"Previous edit mode\")]\r\n    PreviousEditMode,\r\n\r\n    [Description(\"Next edit mode\")]\r\n    NextEditMode,\r\n\r\n    // Edit Ruby tag.\r\n    [Description(\"Reduce ruby-tag start index\")]\r\n    EditRubyTagReduceStartIndex,\r\n\r\n    [Description(\"Increase ruby-tag start index\")]\r\n    EditRubyTagIncreaseStartIndex,\r\n\r\n    [Description(\"Reduce ruby-tag end index\")]\r\n    EditRubyTagReduceEndIndex,\r\n\r\n    [Description(\"Increase ruby-tag end index\")]\r\n    EditRubyTagIncreaseEndIndex,\r\n\r\n    // Edit time-tag.\r\n    [Description(\"Create start time-tag\")]\r\n    CreateStartTimeTag,\r\n\r\n    [Description(\"Create end time-tag\")]\r\n    CreateEndTimeTag,\r\n\r\n    [Description(\"Remove start time-tag\")]\r\n    RemoveStartTimeTag,\r\n\r\n    [Description(\"Remove end time-tag\")]\r\n    RemoveEndTimeTag,\r\n\r\n    [Description(\"Set time\")]\r\n    SetTime,\r\n\r\n    [Description(\"Clear time\")]\r\n    ClearTime,\r\n\r\n    // Action for compose mode.\r\n    [Description(\"Increase font size.\")]\r\n    IncreasePreviewFontSize,\r\n\r\n    [Description(\"Decrease font size.\")]\r\n    DecreasePreviewFontSize,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/KaraokeInputManager.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Input;\r\nusing osu.Framework.Input.Bindings;\r\nusing osu.Framework.Input.Handlers.Microphone;\r\nusing osu.Framework.Input.StateChanges.Events;\r\nusing osu.Framework.Input.States;\r\nusing osu.Framework.Logging;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Input.Handlers;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Mods;\r\nusing osu.Game.Rulesets.Karaoke.UI.Components;\r\nusing osu.Game.Rulesets.Mods;\r\nusing osu.Game.Rulesets.UI;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke;\r\n\r\npublic partial class KaraokeInputManager : RulesetInputManager<KaraokeScoringAction>\r\n{\r\n    public KaraokeInputManager(RulesetInfo ruleset)\r\n        : base(ruleset, 0, SimultaneousBindingMode.All)\r\n    {\r\n        UseParentInput = false;\r\n    }\r\n\r\n    private IBeatmap beatmap = null!;\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(KaraokeRulesetConfigManager config, IBindable<IReadOnlyList<Mod>> mods, IBindable<WorkingBeatmap> beatmap, KaraokeSessionStatics session, EditorBeatmap? editorBeatmap)\r\n    {\r\n        if (editorBeatmap != null)\r\n        {\r\n            session.SetValue(KaraokeRulesetSession.ScoringStatus, ScoringStatusMode.Edit);\r\n            return;\r\n        }\r\n\r\n        this.beatmap = beatmap.Value.Beatmap;\r\n\r\n        bool disableMicrophoneDeviceByMod = mods.Value.OfType<IApplicableToMicrophone>().Any(x => !x.MicrophoneEnabled);\r\n\r\n        if (disableMicrophoneDeviceByMod)\r\n        {\r\n            session.SetValue(KaraokeRulesetSession.ScoringStatus, ScoringStatusMode.AutoPlay);\r\n            return;\r\n        }\r\n\r\n        bool scorable = beatmap.Value.Beatmap.IsScorable();\r\n\r\n        if (!scorable)\r\n        {\r\n            session.SetValue(KaraokeRulesetSession.ScoringStatus, ScoringStatusMode.NotScoring);\r\n            return;\r\n        }\r\n\r\n        try\r\n        {\r\n            string selectedDevice = config.Get<string>(KaraokeRulesetSetting.MicrophoneDevice);\r\n            var microphoneList = new MicrophoneManager().MicrophoneDeviceNames.ToList();\r\n\r\n            // Find index by selection id\r\n            int deviceIndex = microphoneList.IndexOf(selectedDevice);\r\n            AddHandler(new MicrophoneHandler(deviceIndex));\r\n\r\n            session.SetValue(KaraokeRulesetSession.ScoringStatus, ScoringStatusMode.Scoring);\r\n        }\r\n        catch (Exception ex)\r\n        {\r\n            Logger.Error(ex, \"Microphone initialize error.\");\r\n            // todo : set real error by exception\r\n            session.SetValue(KaraokeRulesetSession.ScoringStatus, ScoringStatusMode.WindowsMicrophonePermissionDeclined);\r\n        }\r\n    }\r\n\r\n    protected override InputState CreateInitialState()\r\n        => new KaraokeRulesetInputManagerInputState<KaraokeScoringAction>(base.CreateInitialState());\r\n\r\n    public override void HandleInputStateChange(InputStateChangeEvent inputStateChange)\r\n    {\r\n        switch (inputStateChange)\r\n        {\r\n            case ReplayInputHandler.ReplayStateChangeEvent<KaraokeScoringAction> { Input: ReplayInputHandler.ReplayState<KaraokeScoringAction> replayState } replayStateChanged:\r\n            {\r\n                // Deal with replay event\r\n                // Release event should be trigger first\r\n                if (replayStateChanged.ReleasedActions.Any() && !replayState.PressedActions.Any())\r\n                {\r\n                    foreach (var action in replayStateChanged.ReleasedActions)\r\n                        KeyBindingContainer.TriggerReleased(action);\r\n                }\r\n\r\n                // If any key pressed, the continuous send press event\r\n                if (replayState.PressedActions.Any())\r\n                {\r\n                    foreach (var action in replayState.PressedActions)\r\n                        KeyBindingContainer.TriggerPressed(action);\r\n                }\r\n\r\n                break;\r\n            }\r\n\r\n            case MicrophoneVoiceChangeEvent microphoneSoundChange:\r\n            {\r\n                // Deal with realtime microphone event\r\n                if (microphoneSoundChange.State is not IMicrophoneInputState inputState)\r\n                    throw new NotMicrophoneInputStateException();\r\n\r\n                var lastVoice = microphoneSoundChange.LastVoice;\r\n                var voice = inputState.Microphone.Voice;\r\n\r\n                // Convert beatmap's pitch to scale setting.\r\n                float scale = beatmap.PitchToScale(voice.HasVoice ? voice.Pitch : lastVoice.Pitch);\r\n\r\n                // TODO : adjust scale by\r\n                scale += 5;\r\n\r\n                var action = new KaraokeScoringAction\r\n                {\r\n                    Scale = scale,\r\n                };\r\n\r\n                if (lastVoice.HasVoice && !voice.HasVoice)\r\n                    KeyBindingContainer.TriggerReleased(action);\r\n                else\r\n                    KeyBindingContainer.TriggerPressed(action);\r\n                break;\r\n            }\r\n\r\n            default:\r\n                // Basically should not goes to here\r\n                base.HandleInputStateChange(inputStateChange);\r\n                break;\r\n        }\r\n    }\r\n}\r\n\r\npublic class KaraokeRulesetInputManagerInputState<T> : RulesetInputManagerInputState<T>, IMicrophoneInputState\r\n    where T : struct\r\n{\r\n    public MicrophoneState Microphone { get; }\r\n\r\n    public KaraokeRulesetInputManagerInputState(InputState state)\r\n        : base(state)\r\n    {\r\n        Microphone = new MicrophoneState();\r\n    }\r\n}\r\n\r\npublic struct KaraokeScoringAction\r\n{\r\n    public float Scale { get; set; }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/KaraokeRuleset.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Rendering;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Graphics.Textures;\r\nusing osu.Framework.Input.Bindings;\r\nusing osu.Framework.IO.Stores;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Configuration;\r\nusing osu.Game.Overlays.Settings;\r\nusing osu.Game.Rulesets.Configuration;\r\nusing osu.Game.Rulesets.Difficulty;\r\nusing osu.Game.Rulesets.Edit;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Formats;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Difficulty;\r\nusing osu.Game.Rulesets.Karaoke.Edit;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Setup;\r\nusing osu.Game.Rulesets.Karaoke.Mods;\r\nusing osu.Game.Rulesets.Karaoke.Replays;\r\nusing osu.Game.Rulesets.Karaoke.Resources;\r\nusing osu.Game.Rulesets.Karaoke.Scoring;\r\nusing osu.Game.Rulesets.Karaoke.Skinning.Argon;\r\nusing osu.Game.Rulesets.Karaoke.Skinning.Legacy;\r\nusing osu.Game.Rulesets.Karaoke.Skinning.Triangles;\r\nusing osu.Game.Rulesets.Karaoke.Statistics;\r\nusing osu.Game.Rulesets.Karaoke.UI;\r\nusing osu.Game.Rulesets.Mods;\r\nusing osu.Game.Rulesets.Replays.Types;\r\nusing osu.Game.Rulesets.Scoring;\r\nusing osu.Game.Rulesets.UI;\r\nusing osu.Game.Scoring;\r\nusing osu.Game.Screens.Edit.Setup;\r\nusing osu.Game.Screens.Ranking.Statistics;\r\nusing osu.Game.Skinning;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke;\r\n\r\npublic partial class KaraokeRuleset : Ruleset\r\n{\r\n    public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod>? mods = null) => new DrawableKaraokeRuleset(this, beatmap, mods);\r\n    public override ScoreProcessor CreateScoreProcessor() => new KaraokeScoreProcessor();\r\n    public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new KaraokeBeatmapConverter(beatmap, this);\r\n    public override IBeatmapProcessor CreateBeatmapProcessor(IBeatmap beatmap) => new KaraokeBeatmapProcessor(beatmap);\r\n\r\n    public override PerformanceCalculator CreatePerformanceCalculator() => new KaraokePerformanceCalculator();\r\n\r\n    public const string SHORT_NAME = \"karaoke\";\r\n\r\n    public const int GAMEPLAY_INPUT_VARIANT = 1;\r\n\r\n    public const int EDIT_INPUT_VARIANT = 2;\r\n\r\n    public override IEnumerable<int> AvailableVariants => new[] { GAMEPLAY_INPUT_VARIANT, EDIT_INPUT_VARIANT };\r\n\r\n    public override IEnumerable<KeyBinding> GetDefaultKeyBindings(int variant = 0) =>\r\n        variant switch\r\n        {\r\n            0 =>\r\n                // Vocal\r\n                Array.Empty<KeyBinding>(),\r\n            GAMEPLAY_INPUT_VARIANT => new[]\r\n            {\r\n                // Basic control\r\n                new KeyBinding(InputKey.Number1, KaraokeAction.FirstLyric),\r\n                new KeyBinding(InputKey.Left, KaraokeAction.PreviousLyric),\r\n                new KeyBinding(InputKey.Right, KaraokeAction.NextLyric),\r\n                new KeyBinding(InputKey.Space, KaraokeAction.PlayAndPause),\r\n\r\n                // Panel\r\n                new KeyBinding(InputKey.P, KaraokeAction.OpenPanel),\r\n\r\n                // Advance control\r\n                new KeyBinding(InputKey.Q, KaraokeAction.IncreaseTempo),\r\n                new KeyBinding(InputKey.A, KaraokeAction.DecreaseTempo),\r\n                new KeyBinding(InputKey.Z, KaraokeAction.ResetTempo),\r\n                new KeyBinding(InputKey.W, KaraokeAction.IncreasePitch),\r\n                new KeyBinding(InputKey.S, KaraokeAction.DecreasePitch),\r\n                new KeyBinding(InputKey.X, KaraokeAction.ResetPitch),\r\n                new KeyBinding(InputKey.E, KaraokeAction.IncreaseVocalPitch),\r\n                new KeyBinding(InputKey.D, KaraokeAction.DecreaseVocalPitch),\r\n                new KeyBinding(InputKey.C, KaraokeAction.ResetVocalPitch),\r\n                new KeyBinding(InputKey.R, KaraokeAction.IncreaseScoringPitch),\r\n                new KeyBinding(InputKey.F, KaraokeAction.DecreaseScoringPitch),\r\n                new KeyBinding(InputKey.V, KaraokeAction.ResetScoringPitch),\r\n            },\r\n            EDIT_INPUT_VARIANT => new[]\r\n            {\r\n                // moving\r\n                new KeyBinding(InputKey.Up, KaraokeEditAction.MoveToPreviousLyric),\r\n                new KeyBinding(InputKey.Down, KaraokeEditAction.MoveToNextLyric),\r\n                new KeyBinding(InputKey.PageUp, KaraokeEditAction.MoveToFirstLyric),\r\n                new KeyBinding(InputKey.PageDown, KaraokeEditAction.MoveToLastLyric),\r\n                new KeyBinding(InputKey.Left, KaraokeEditAction.MoveToPreviousIndex),\r\n                new KeyBinding(InputKey.Right, KaraokeEditAction.MoveToNextIndex),\r\n                new KeyBinding(new[] { InputKey.Alt, InputKey.Left }, KaraokeEditAction.MoveToFirstIndex),\r\n                new KeyBinding(new[] { InputKey.Alt, InputKey.Right }, KaraokeEditAction.MoveToLastIndex),\r\n\r\n                new KeyBinding(new[] { InputKey.Alt, InputKey.BracketLeft }, KaraokeEditAction.PreviousEditMode),\r\n                new KeyBinding(new[] { InputKey.Alt, InputKey.BracketRight }, KaraokeEditAction.NextEditMode),\r\n\r\n                // Edit Ruby tag.\r\n                new KeyBinding(new[] { InputKey.Z, InputKey.Left }, KaraokeEditAction.EditRubyTagReduceStartIndex),\r\n                new KeyBinding(new[] { InputKey.Z, InputKey.Right }, KaraokeEditAction.EditRubyTagIncreaseStartIndex),\r\n                new KeyBinding(new[] { InputKey.X, InputKey.Left }, KaraokeEditAction.EditRubyTagReduceEndIndex),\r\n                new KeyBinding(new[] { InputKey.X, InputKey.Right }, KaraokeEditAction.EditRubyTagIncreaseEndIndex),\r\n\r\n                // edit time-tag.\r\n                new KeyBinding(InputKey.Q, KaraokeEditAction.CreateStartTimeTag),\r\n                new KeyBinding(InputKey.W, KaraokeEditAction.CreateEndTimeTag),\r\n                new KeyBinding(InputKey.A, KaraokeEditAction.RemoveStartTimeTag),\r\n                new KeyBinding(InputKey.S, KaraokeEditAction.RemoveEndTimeTag),\r\n                new KeyBinding(InputKey.Enter, KaraokeEditAction.SetTime),\r\n                new KeyBinding(InputKey.BackSpace, KaraokeEditAction.ClearTime),\r\n\r\n                // Action for compose mode.\r\n                new KeyBinding(new[] { InputKey.Control, InputKey.Plus }, KaraokeEditAction.IncreasePreviewFontSize),\r\n                new KeyBinding(new[] { InputKey.Control, InputKey.Minus }, KaraokeEditAction.DecreasePreviewFontSize),\r\n            },\r\n            _ => Array.Empty<KeyBinding>(),\r\n        };\r\n\r\n    public override LocalisableString GetVariantName(int variant)\r\n        => variant switch\r\n        {\r\n            GAMEPLAY_INPUT_VARIANT => \"Gameplay\",\r\n            EDIT_INPUT_VARIANT => \"Composer\",\r\n            _ => throw new ArgumentNullException(nameof(variant)),\r\n        };\r\n\r\n    public override IEnumerable<Mod> GetModsFor(ModType type) =>\r\n        type switch\r\n        {\r\n            ModType.DifficultyReduction => new Mod[]\r\n            {\r\n                new KaraokeModNoFail(),\r\n                new KaraokeModLyricConfiguration(),\r\n                new KaraokeModTranslation(),\r\n            },\r\n            ModType.DifficultyIncrease => new Mod[]\r\n            {\r\n                new KaraokeModHiddenNote(),\r\n                new KaraokeModFlashlight(),\r\n                new MultiMod(new KaraokeModSuddenDeath(), new KaraokeModPerfect(), new KaraokeModWindowsUpdate()),\r\n            },\r\n            ModType.Conversion => new Mod[]\r\n            {\r\n                new MultiMod(new KaraokeModPreviewStage(), new KaraokeModClassicStage()),\r\n            },\r\n\r\n            ModType.Automation => new Mod[]\r\n            {\r\n                new MultiMod(new KaraokeModAutoplay(), new KaraokeModAutoplayBySinger()),\r\n            },\r\n            ModType.Fun => new Mod[]\r\n            {\r\n                new KaraokeModPractice(),\r\n                new KaraokeModDisableNote(),\r\n                new KaraokeModSnow(),\r\n            },\r\n            _ => Array.Empty<Mod>(),\r\n        };\r\n\r\n    public override Drawable CreateIcon() => new KaraokeIcon(this);\r\n\r\n    public override IResourceStore<byte[]> CreateResourceStore()\r\n    {\r\n        var store = new ResourceStore<byte[]>();\r\n\r\n        // add resource in the current dll.\r\n        store.AddStore(base.CreateResourceStore());\r\n\r\n        // add resource dll, it only works in the local because the resource will be packed into main dll in the resource build.\r\n        store.AddStore(new DllResourceStore(KaraokeResources.ResourceAssembly));\r\n\r\n        // add shader resource from font package.\r\n        store.AddStore(new NamespacedResourceStore<byte[]>(new ShaderResourceStore(), \"Resources\"));\r\n\r\n        return store;\r\n    }\r\n\r\n    public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new KaraokeDifficultyCalculator(RulesetInfo, beatmap);\r\n\r\n    public override HitObjectComposer CreateHitObjectComposer() => new KaraokeHitObjectComposer(this);\r\n\r\n    public override IBeatmapVerifier CreateBeatmapVerifier() => new KaraokeBeatmapVerifier();\r\n\r\n    public override string Description => \"karaoke!\";\r\n\r\n    public override string ShortName => \"karaoke!\";\r\n\r\n    public override string PlayingVerb => \"Singing karaoke\";\r\n\r\n    public override ISkin CreateSkinTransformer(ISkin skin, IBeatmap beatmap)\r\n    {\r\n        return skin switch\r\n        {\r\n            TrianglesSkin => new KaraokeTrianglesSkinTransformer(skin, beatmap),\r\n            ArgonSkin => new KaraokeArgonSkinTransformer(skin, beatmap),\r\n            DefaultLegacySkin => new KaraokeClassicSkinTransformer(skin, beatmap),\r\n            LegacySkin => new KaraokeLegacySkinTransformer(skin, beatmap),\r\n            _ => throw new InvalidOperationException(),\r\n        };\r\n    }\r\n\r\n    public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new KaraokeReplayFrame();\r\n\r\n    public override IRulesetConfigManager CreateConfig(SettingsStore? settings) => new KaraokeRulesetConfigManager(settings, RulesetInfo);\r\n\r\n    public override RulesetSettingsSubsection CreateSettings() => new KaraokeSettingsSubsection(this);\r\n\r\n    public override IEnumerable<HitResult> GetValidHitResults()\r\n    {\r\n        return new[]\r\n        {\r\n            HitResult.Great,\r\n            HitResult.Ok,\r\n            HitResult.Meh,\r\n        };\r\n    }\r\n\r\n    public override LocalisableString GetDisplayNameForHitResult(HitResult result)\r\n    {\r\n        return result switch\r\n        {\r\n            HitResult.Great => \"Great\",\r\n            HitResult.Ok => \"OK\",\r\n            HitResult.Meh => \"Meh\",\r\n            _ => base.GetDisplayNameForHitResult(result),\r\n        };\r\n    }\r\n\r\n    public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)\r\n    {\r\n        const int fix_height = 560;\r\n        const int text_size = 14;\r\n        const int spacing = 15;\r\n        const int info_height = 200;\r\n\r\n        // Always display song info\r\n        var statistic = new List<StatisticItem>\r\n        {\r\n            new(\"Metadata\", () => new BeatmapMetadataGraph(playableBeatmap)\r\n            {\r\n                RelativeSizeAxes = Axes.X,\r\n                Height = info_height,\r\n            }),\r\n        };\r\n\r\n        // Set component to remain height\r\n        const int remain_height = fix_height - text_size - spacing - info_height;\r\n\r\n        if (playableBeatmap.IsScorable())\r\n        {\r\n            statistic.Add(new StatisticItem(\"Scoring Result\", () => new ScoringResultGraph(score, playableBeatmap)\r\n            {\r\n                RelativeSizeAxes = Axes.X,\r\n                Height = remain_height - text_size - spacing,\r\n            }));\r\n        }\r\n        else\r\n        {\r\n            statistic.Add(new StatisticItem(\"Result\", () => new NotScorableGraph\r\n            {\r\n                RelativeSizeAxes = Axes.X,\r\n                Height = remain_height - text_size - spacing,\r\n            }));\r\n        }\r\n\r\n        return statistic.ToArray();\r\n    }\r\n\r\n    public override IEnumerable<Drawable> CreateEditorSetupSections() => new Drawable[]\r\n    {\r\n        new MetadataSection(),\r\n        new ResourcesSection(),\r\n        new KaraokeSingerSection(),\r\n        new KaraokeTranslationSection(),\r\n        new KaraokeNoteSection(),\r\n    };\r\n\r\n    public KaraokeRuleset()\r\n    {\r\n        // It's a tricky way to let lazer to read karaoke testing beatmap\r\n        KaraokeLegacyBeatmapDecoder.Register();\r\n        KaraokeJsonBeatmapDecoder.Register();\r\n\r\n        // it's a tricky way for loading customized karaoke beatmap.\r\n        RulesetInfo.OnlineID = 111;\r\n    }\r\n\r\n    private partial class KaraokeIcon : CompositeDrawable\r\n    {\r\n        private readonly KaraokeRuleset ruleset;\r\n\r\n        public KaraokeIcon(KaraokeRuleset ruleset)\r\n        {\r\n            this.ruleset = ruleset;\r\n            Anchor = Anchor.Centre;\r\n            Origin = Anchor.Centre;\r\n\r\n            // Set a fixed size to make Song Select V2 happy\r\n            Size = new Vector2(32);\r\n        }\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(IRenderer renderer)\r\n        {\r\n            InternalChildren = new Drawable[]\r\n            {\r\n                new Sprite\r\n                {\r\n                    Anchor = Anchor.Centre,\r\n                    Origin = Anchor.Centre,\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    FillMode = FillMode.Fit,\r\n                    Scale = new Vector2(0.9f),\r\n                    Texture = new TextureStore(renderer, new TextureLoaderStore(ruleset.CreateResourceStore()), false).Get(\"Textures/logo\"),\r\n                },\r\n                new SpriteIcon\r\n                {\r\n                    Anchor = Anchor.Centre,\r\n                    Origin = Anchor.Centre,\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    Icon = FontAwesome.Regular.Circle,\r\n                },\r\n            };\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/KaraokeSkinComponentLookup.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Skinning;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke;\r\n\r\npublic class KaraokeSkinComponentLookup : SkinComponentLookup<KaraokeSkinComponents>\r\n{\r\n    public KaraokeSkinComponentLookup(KaraokeSkinComponents component)\r\n        : base(component)\r\n    {\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/KaraokeSkinComponents.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke;\r\n\r\npublic enum KaraokeSkinComponents\r\n{\r\n    ColumnBackground,\r\n    StageBackground,\r\n    JudgementLine,\r\n    Note,\r\n    HitExplosion,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Localisation/ChangelogStrings.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Localisation;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Localisation;\r\n\r\npublic static class ChangelogStrings\r\n{\r\n    private const string prefix = @\"osu.Game.Rulesets.Karaoke.Resources.Localisation.ChangelogSection\";\r\n\r\n    /// <summary>\r\n    /// \"View current changelog\"\r\n    /// </summary>\r\n    public static LocalisableString ViewCurrentChangelog => new TranslatableString(getKey(@\"view_current_changelog\"), @\"View current changelog\");\r\n\r\n    private static string getKey(string key) => $@\"{prefix}:{key}\";\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Localisation/CommonStrings.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Localisation;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Localisation;\r\n\r\npublic static class CommonStrings\r\n{\r\n    private const string prefix = @\"osu.Game.Rulesets.Karaoke.Resources.Localisation.Common\";\r\n\r\n    /// <summary>\r\n    /// \"karaoke!\"\r\n    /// </summary>\r\n    public static LocalisableString RulesetName => new TranslatableString(getKey(@\"karaoke\"), @\"karaoke!\");\r\n\r\n    private static string getKey(string key) => $@\"{prefix}:{key}\";\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Localisation/KaraokeSettingsSubsectionStrings.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Localisation;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Localisation;\r\n\r\npublic static class KaraokeSettingsSubsectionStrings\r\n{\r\n    private const string prefix = @\"osu.Game.Rulesets.Karaoke.Resources.Localisation.KaraokeSettingsSubsection\";\r\n\r\n    /// <summary>\r\n    /// \"Scrolling direction\"\r\n    /// </summary>\r\n    public static LocalisableString ScrollingDirection => new TranslatableString(getKey(@\"scrolling_direction\"), @\"Scrolling direction\");\r\n\r\n    /// <summary>\r\n    /// \"Adjust the scroll direction in the scoring area. Will show that in the gameplay if the beatmap is support the scoring.\"\r\n    /// </summary>\r\n    public static LocalisableString ScrollingDirectionTooltip => new TranslatableString(\r\n        getKey(@\"scrolling_direction_tooltip\"),\r\n        @\"Adjust the scroll direction in the scoring area. Will show that in the gameplay if the beatmap is support the scoring.\"\r\n    );\r\n\r\n    /// <summary>\r\n    /// \"Scroll speed\"\r\n    /// </summary>\r\n    public static LocalisableString ScrollSpeed => new TranslatableString(getKey(@\"scroll_speed\"), @\"Scroll speed\");\r\n\r\n    /// <summary>\r\n    /// \"Show cursor while playing\"\r\n    /// </summary>\r\n    public static LocalisableString ShowCursorWhilePlaying => new TranslatableString(getKey(@\"show_cursor_while_playing\"), @\"Show cursor while playing\");\r\n\r\n    /// <summary>\r\n    /// \"Will not showing the cursor while gameplay if not select this option.\"\r\n    /// </summary>\r\n    public static LocalisableString ShowCursorWhilePlayingTooltip =>\r\n        new TranslatableString(getKey(@\"show_cursor_while_playing_tooltip\"), @\"Will not showing the cursor while gameplay if not select this option.\");\r\n\r\n    /// <summary>\r\n    /// \"Prefer language\"\r\n    /// </summary>\r\n    public static LocalisableString PreferLanguage => new TranslatableString(getKey(@\"prefer_language\"), @\"Prefer language\");\r\n\r\n    /// <summary>\r\n    /// \"Select prefer translation language.\"\r\n    /// </summary>\r\n    public static LocalisableString PreferTranslationLanguageTooltip => new TranslatableString(getKey(@\"prefer_language_tooltip\"), @\"Select prefer translation language.\");\r\n\r\n    /// <summary>\r\n    /// \"Microphone device\"\r\n    /// </summary>\r\n    public static LocalisableString MicrophoneDevice => new TranslatableString(getKey(@\"microphone_device\"), @\"Microphone device\");\r\n\r\n    /// <summary>\r\n    /// \"Practice preempt time\"\r\n    /// </summary>\r\n    public static LocalisableString PracticePreemptTime => new TranslatableString(getKey(@\"practice_preempt_time\"), @\"Practice preempt time\");\r\n\r\n    /// <summary>\r\n    /// \"Open ruleset settings\"\r\n    /// </summary>\r\n    public static LocalisableString OpenRulesetSettings => new TranslatableString(getKey(@\"open_ruleset_settings\"), @\"Open ruleset settings\");\r\n\r\n    /// <summary>\r\n    /// \"Open ruleset settings for adjust more configs.\"\r\n    /// </summary>\r\n    public static LocalisableString OpenRulesetSettingsTooltip => new TranslatableString(getKey(@\"open_ruleset_settings_tooltip\"), @\"Open ruleset settings for adjust more configs.\");\r\n\r\n    /// <summary>\r\n    /// \"Change log\"\r\n    /// </summary>\r\n    public static LocalisableString ChangeLog => new TranslatableString(getKey(@\"change_log\"), @\"Change log\");\r\n\r\n    /// <summary>\r\n    /// \"Let&#39;s see what karaoke! changed.\"\r\n    /// </summary>\r\n    public static LocalisableString ChangeLogTooltip => new TranslatableString(getKey(@\"change_log_tooltip\"), @\"Let's see what karaoke! changed.\");\r\n\r\n    private static string getKey(string key) => $@\"{prefix}:{key}\";\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Mods/IApplicableToMicrophone.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Mods;\r\n\r\npublic interface IApplicableToMicrophone\r\n{\r\n    bool MicrophoneEnabled { get; }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Mods/IApplicableToSettingHUDOverlay.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.UI.HUD;\r\nusing osu.Game.Rulesets.Mods;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Mods;\r\n\r\n/// <summary>\r\n/// An interface for mods that apply changes to the <see cref=\"ISettingHUDOverlay\"/>.\r\n/// </summary>\r\npublic interface IApplicableToSettingHUDOverlay : IApplicableMod\r\n{\r\n    /// <summary>\r\n    /// Provide a <see cref=\"ISettingHUDOverlay\"/>. Called once on initialisation of a play instance.\r\n    /// </summary>\r\n    void ApplyToOverlay(ISettingHUDOverlay overlay);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Mods/IApplicableToStage.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos;\r\nusing osu.Game.Rulesets.Mods;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Mods;\r\n\r\npublic interface IApplicableToStage : IApplicableMod\r\n{\r\n    bool CanApply(StageInfo stageInfo);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Mods/IApplicableToStageElement.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Game.Rulesets.Karaoke.Stages;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Mods;\r\n\r\npublic interface IApplicableToStageElement : IApplicableToStage\r\n{\r\n    IEnumerable<IStageElement> PostProcess(IEnumerable<IStageElement> stageElements);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Mods/IApplicableToStageHitObjectCommand.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Commands;\r\nusing osu.Game.Rulesets.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Mods;\r\n\r\npublic interface IApplicableToStageHitObjectCommand : IApplicableToStage\r\n{\r\n    IEnumerable<IStageCommand> PostProcessInitialCommands(HitObject hitObject, IEnumerable<IStageCommand> commands);\r\n\r\n    IEnumerable<IStageCommand> PostProcessStartTimeStateCommands(HitObject hitObject, IEnumerable<IStageCommand> commands);\r\n\r\n    IEnumerable<IStageCommand> PostProcessHitStateCommands(HitObject hitObject, IEnumerable<IStageCommand> commands);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Mods/IApplicableToStageInfo.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Mods;\r\n\r\n/// <summary>\r\n/// An interface for mods that prefer to use the type of <see cref=\"StageInfo\"/>.\r\n/// Also, it can override the parameter of <see cref=\"StageInfo\"/>.\r\n/// </summary>\r\npublic interface IApplicableToStageInfo : IApplicableToStage\r\n{\r\n    StageInfo? CreateDefaultStageInfo(KaraokeBeatmap beatmap);\r\n\r\n    void ApplyToStageInfo(StageInfo stageInfo);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Mods/IApplicableToStagePlayfieldCommand.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Commands;\r\nusing osu.Game.Rulesets.UI;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Mods;\r\n\r\npublic interface IApplicableToStagePlayfieldCommand : IApplicableToStage\r\n{\r\n    IEnumerable<IStageCommand> PostProcessCommands(Playfield playfield, IEnumerable<IStageCommand> commands);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Mods/KaraokeModAutoplay.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Edit;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Replays;\r\nusing osu.Game.Rulesets.Karaoke.UI;\r\nusing osu.Game.Rulesets.Mods;\r\nusing osu.Game.Rulesets.UI;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Mods;\r\n\r\npublic class KaraokeModAutoplay : ModAutoplay, IApplicableToDrawableRuleset<KaraokeHitObject>, IApplicableToMicrophone\r\n{\r\n    public bool MicrophoneEnabled => false;\r\n\r\n    public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)\r\n        => new(new KaraokeAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = \"osu!7pupu\" });\r\n\r\n    public virtual void ApplyToDrawableRuleset(DrawableRuleset<KaraokeHitObject> drawableRuleset)\r\n    {\r\n        // Got no idea why edit ruleset call this shit.\r\n        if (drawableRuleset is DrawableKaraokeEditorRuleset)\r\n            return;\r\n\r\n        if (drawableRuleset.Playfield is not KaraokePlayfield karaokePlayfield)\r\n            return;\r\n\r\n        // todo : add replay visualization into note playfield from here?\r\n        // todo : should have a better way(or called more generic) way to apply replay into replay field.\r\n        var replay = new KaraokeAutoGenerator(drawableRuleset.Beatmap).Generate();\r\n        var notePlayfield = karaokePlayfield.NotePlayfield as NotePlayfield;\r\n        var frames = replay.Frames.OfType<KaraokeReplayFrame>();\r\n\r\n        // for safety purpose should clear reply to make sure not cause crash if apply to ruleset runs more then one times.\r\n        notePlayfield?.ClearReplay();\r\n\r\n        foreach (var frame in frames)\r\n        {\r\n            notePlayfield?.AddReplay(frame);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Mods/KaraokeModAutoplayBySinger.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.IO;\r\nusing System.Linq;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Extensions;\r\nusing osu.Game.Rulesets.Karaoke.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Replays;\r\nusing osu.Game.Rulesets.Karaoke.UI;\r\nusing osu.Game.Rulesets.Mods;\r\nusing osu.Game.Rulesets.UI;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Mods;\r\n\r\npublic class KaraokeModAutoplayBySinger : KaraokeModAutoplay\r\n{\r\n    public override string Name => \"Autoplay by singer\";\r\n    public override string Acronym => \"ABS\";\r\n    public override LocalisableString Description => \"Autoplay mode but replay's record is by singer's voice.\";\r\n\r\n    public override IconUsage? Icon => KaraokeIcon.ModAutoPlayBySinger;\r\n\r\n    private Stream? trackData;\r\n\r\n    public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)\r\n        => new(new KaraokeAutoGeneratorBySinger(beatmap, trackData).Generate(), new ModCreatedUser { Username = \"karaoke!singer\" });\r\n\r\n    public override void ApplyToDrawableRuleset(DrawableRuleset<KaraokeHitObject> drawableRuleset)\r\n    {\r\n        if (drawableRuleset.Playfield is not KaraokePlayfield karaokePlayfield)\r\n            return;\r\n\r\n        var workingBeatmap = karaokePlayfield.WorkingBeatmap;\r\n        string? path = getPathForFile(workingBeatmap.BeatmapInfo);\r\n        trackData = workingBeatmap.GetStream(path);\r\n\r\n        base.ApplyToDrawableRuleset(drawableRuleset);\r\n    }\r\n\r\n    private string? getPathForFile(BeatmapInfo beatmapInfo)\r\n    {\r\n        var beatmapSetInfo = beatmapInfo.BeatmapSet;\r\n        string audioFile = beatmapInfo.Metadata.AudioFile;\r\n\r\n        return beatmapSetInfo?.Files.SingleOrDefault(f => string.Equals(f.Filename, audioFile, StringComparison.OrdinalIgnoreCase))?.File.GetStoragePath();\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Mods/KaraokeModClassicStage.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Stages.Classic;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos.Classic;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Mods;\r\n\r\npublic class KaraokeModClassicStage : ModStage<ClassicStageInfo>\r\n{\r\n    public override string Name => \"Classic stage\";\r\n\r\n    public override string Acronym => \"CS\";\r\n\r\n    public override LocalisableString Description => \"Karaoke mod like other karaoke game or software.\";\r\n\r\n    public override Type[] IncompatibleMods => new Type[]\r\n    {\r\n        typeof(KaraokeModPreviewStage),\r\n    };\r\n\r\n    protected override ClassicStageInfo CreateStageInfo(KaraokeBeatmap beatmap)\r\n    {\r\n        var config = new ClassicStageInfoGeneratorConfig();\r\n        var generator = new ClassicStageInfoGenerator(config);\r\n\r\n        return (ClassicStageInfo)generator.Generate(beatmap);\r\n    }\r\n\r\n    protected override void ApplyToStageInfo(ClassicStageInfo stageInfo)\r\n    {\r\n        // todo: adjust stage by config.\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Mods/KaraokeModDisableNote.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Mods;\r\nusing osu.Game.Rulesets.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Mods;\r\n\r\npublic class KaraokeModDisableNote : Mod, IApplicableToHitObject\r\n{\r\n    public override string Name => \"Disable note\";\r\n\r\n    public override LocalisableString Description => \"Disable note\";\r\n    public override string Acronym => \"DN\";\r\n    public override double ScoreMultiplier => 0;\r\n    public override IconUsage? Icon => KaraokeIcon.ModDisableNote;\r\n    public override ModType Type => ModType.Fun;\r\n\r\n    public void ApplyToHitObject(HitObject hitObject)\r\n    {\r\n        if (hitObject is Note note)\r\n        {\r\n            // Disable all the note\r\n            note.Display = false;\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Mods/KaraokeModFlashlight.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Layout;\r\nusing osu.Game.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.UI;\r\nusing osu.Game.Rulesets.Mods;\r\nusing osu.Game.Rulesets.UI;\r\nusing osu.Game.Rulesets.UI.Scrolling;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Mods;\r\n\r\npublic partial class KaraokeModFlashlight : ModFlashlight<KaraokeHitObject>\r\n{\r\n    public override double ScoreMultiplier => 1;\r\n    public override Type[] IncompatibleMods => new[] { typeof(ModHidden) };\r\n\r\n    [SettingSource(\"Flashlight size\", \"Multiplier applied to the default flashlight size.\")]\r\n    public override BindableFloat SizeMultiplier { get; } = new()\r\n    {\r\n        MinValue = 0.5f,\r\n        MaxValue = 3f,\r\n        Default = 1f,\r\n        Value = 1f,\r\n        Precision = 0.1f,\r\n    };\r\n\r\n    [SettingSource(\"Change size based on combo\", \"Decrease the flashlight size as combo increases.\")]\r\n    public override BindableBool ComboBasedSize { get; } = new()\r\n    {\r\n        Default = false,\r\n        Value = false,\r\n    };\r\n\r\n    public override float DefaultFlashlightSize => 50;\r\n\r\n    public override void ApplyToDrawableRuleset(DrawableRuleset<KaraokeHitObject> drawableRuleset)\r\n    {\r\n        base.ApplyToDrawableRuleset(drawableRuleset);\r\n\r\n        var drawableKaraokeRuleset = drawableRuleset as DrawableKaraokeRuleset;\r\n\r\n        var notePlayfield = drawableKaraokeRuleset?.Playfield.NotePlayfield;\r\n        if (notePlayfield == null)\r\n            return;\r\n\r\n        var flashlight = drawableKaraokeRuleset?.KeyBindingInputManager.Children.OfType<KaraokeFlashlight>().FirstOrDefault();\r\n        if (flashlight == null)\r\n            return;\r\n\r\n        flashlight.RelativeSizeAxes = Axes.X;\r\n\r\n        // notePlayfield.Height - 30*2;\r\n        flashlight.Height = 190;\r\n        flashlight.Y = 80;\r\n    }\r\n\r\n    protected override Flashlight CreateFlashlight() => new KaraokeFlashlight(this);\r\n\r\n    internal partial class KaraokeFlashlight : Flashlight\r\n    {\r\n        private readonly LayoutValue flashlightProperties = new(Invalidation.DrawSize);\r\n\r\n        private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();\r\n        private readonly IBindable<double> timeRange = new Bindable<double>();\r\n\r\n        public KaraokeFlashlight(KaraokeModFlashlight modFlashlight)\r\n            : base(modFlashlight)\r\n        {\r\n            AddLayout(flashlightProperties);\r\n        }\r\n\r\n        protected override void Update()\r\n        {\r\n            base.Update();\r\n\r\n            if (flashlightProperties.IsValid)\r\n                return;\r\n\r\n            FlashlightSize = new Vector2(DrawSize.X * flashLightMultiple, DrawHeight);\r\n            FlashlightPosition = new Vector2(DrawPosition.X, 0) + (scrollingDirection == KaraokeScrollingDirection.Right ? DrawSize : new Vector2());\r\n            flashlightProperties.Validate();\r\n        }\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(IScrollingInfo scrollingInfo)\r\n        {\r\n            direction.BindTo(scrollingInfo.Direction);\r\n            direction.BindValueChanged(OnDirectionChanged, true);\r\n\r\n            timeRange.BindTo(scrollingInfo.TimeRange);\r\n            timeRange.BindValueChanged(OnTimeRangeChanged, true);\r\n        }\r\n\r\n        private KaraokeScrollingDirection scrollingDirection;\r\n\r\n        protected virtual void OnDirectionChanged(ValueChangedEvent<ScrollingDirection> e)\r\n        {\r\n            scrollingDirection = (KaraokeScrollingDirection)e.NewValue;\r\n            flashlightProperties.Invalidate();\r\n        }\r\n\r\n        private float flashLightMultiple;\r\n\r\n        protected virtual void OnTimeRangeChanged(ValueChangedEvent<double> e)\r\n        {\r\n            flashLightMultiple = 3000 / (float)e.NewValue;\r\n            flashlightProperties.Invalidate();\r\n        }\r\n\r\n        protected override void UpdateFlashlightSize(float size)\r\n        {\r\n            this.TransformTo(nameof(FlashlightSize), new Vector2(DrawWidth, size), FLASHLIGHT_FADE_DURATION);\r\n        }\r\n\r\n        protected override string FragmentShader => \"RectangularFlashlight\";\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Mods/KaraokeModHiddenNote.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Drawables;\r\nusing osu.Game.Rulesets.Mods;\r\nusing osu.Game.Rulesets.Objects.Drawables;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Mods;\r\n\r\npublic class KaraokeModHiddenNote : ModHidden\r\n{\r\n    public override LocalisableString Description => \"Notes fade out before you sing them!\";\r\n    public override double ScoreMultiplier => 1.06;\r\n    public override Type[] IncompatibleMods => new[] { typeof(ModFlashlight<KaraokeHitObject>) };\r\n    public override IconUsage? Icon => KaraokeIcon.ModHiddenNote;\r\n\r\n    private const double fade_in_duration_multiplier = -1;\r\n    private const double fade_out_duration_multiplier = 0.3;\r\n\r\n    public override void ApplyToDrawableHitObject(DrawableHitObject dho)\r\n    {\r\n        if (dho is DrawableNote drawableNote)\r\n        {\r\n            var note = drawableNote.HitObject;\r\n            note.TimeFadeIn = note.TimePreempt * fade_in_duration_multiplier;\r\n        }\r\n\r\n        base.ApplyToDrawableHitObject(dho);\r\n    }\r\n\r\n    protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)\r\n    {\r\n        // todo : not really sure what this do so just copy the code in below.\r\n        if (hitObject is not DrawableNote note)\r\n            return;\r\n\r\n        var h = note.HitObject;\r\n\r\n        double fadeOutStartTime = h.StartTime - h.TimePreempt + h.TimeFadeIn;\r\n        double fadeOutDuration = h.TimePreempt * fade_out_duration_multiplier;\r\n\r\n        // new duration from completed fade in to end (before fading out)\r\n        double longFadeDuration = h.EndTime - fadeOutStartTime;\r\n\r\n        // Apply duration\r\n        using (note.BeginAbsoluteSequence(fadeOutStartTime))\r\n            note.FadeOut(fadeOutDuration, Easing.Out);\r\n\r\n        // Show after exceed hit point\r\n        using (note.BeginAbsoluteSequence(fadeOutStartTime + longFadeDuration))\r\n            note.FadeIn(fadeOutDuration, Easing.Out);\r\n    }\r\n\r\n    protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state)\r\n    {\r\n        if (hitObject is not DrawableNote note)\r\n            return;\r\n\r\n        var h = note.HitObject;\r\n\r\n        double fadeOutStartTime = h.StartTime - h.TimePreempt + h.TimeFadeIn;\r\n        double fadeOutDuration = h.TimePreempt * fade_out_duration_multiplier;\r\n\r\n        // new duration from completed fade in to end (before fading out)\r\n        double longFadeDuration = h.EndTime - fadeOutStartTime;\r\n\r\n        // Apply duration\r\n        using (note.BeginAbsoluteSequence(fadeOutStartTime))\r\n            note.FadeOut(fadeOutDuration, Easing.Out);\r\n\r\n        // Show after exceed hit point\r\n        using (note.BeginAbsoluteSequence(fadeOutStartTime + longFadeDuration))\r\n            note.FadeIn(fadeOutDuration, Easing.Out);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Mods/KaraokeModLyricConfiguration.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Drawables;\r\nusing osu.Game.Rulesets.Mods;\r\nusing osu.Game.Rulesets.Objects.Drawables;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Mods;\r\n\r\npublic class KaraokeModLyricConfiguration : Mod, IApplicableToDrawableHitObject\r\n{\r\n    public override string Name => \"Adjust Lyric Display\";\r\n    public override LocalisableString Description => \"Determined display lyric or romanisation, show the ruby, translation or not.\";\r\n    public override double ScoreMultiplier => 1.0f;\r\n    public override string Acronym => \"AL\";\r\n\r\n    [SettingSource(\"Display type\", \"Display the lyric or romanisation as main text.\", 0)]\r\n    public Bindable<LyricDisplayType> DisplayType { get; } = new();\r\n\r\n    [SettingSource(\"Display property\", \"Display the top text or bottom text.\", 1)]\r\n    public Bindable<LyricDisplayProperty> DisplayProperty { get; } = new(LyricDisplayProperty.Both);\r\n\r\n    public void ApplyToDrawableHitObject(DrawableHitObject drawable)\r\n    {\r\n        if (drawable is not DrawableLyric drawableLyric)\r\n            return;\r\n\r\n        drawableLyric.ChangeDisplayType(DisplayType.Value);\r\n        drawableLyric.ChangeDisplayProperty(DisplayProperty.Value);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Mods/KaraokeModNoFail.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Mods;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Mods;\r\n\r\npublic class KaraokeModNoFail : ModNoFail;\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Mods/KaraokeModPerfect.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Mods;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Mods;\r\n\r\npublic class KaraokeModPerfect : ModPerfect;\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Mods/KaraokeModPractice.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Replays;\r\nusing osu.Game.Rulesets.Karaoke.UI;\r\nusing osu.Game.Rulesets.Karaoke.UI.HUD;\r\nusing osu.Game.Rulesets.Karaoke.UI.PlayerSettings;\r\nusing osu.Game.Rulesets.Mods;\r\nusing osu.Game.Rulesets.UI;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Mods;\r\n\r\npublic class KaraokeModPractice : ModAutoplay, IApplicableToDrawableRuleset<KaraokeHitObject>, IApplicableToSettingHUDOverlay\r\n{\r\n    public override string Name => \"Practice\";\r\n    public override string Acronym => \"Practice\";\r\n    public override double ScoreMultiplier => 0.0f;\r\n    public override IconUsage? Icon => KaraokeIcon.ModPractice;\r\n    public override ModType Type => ModType.Fun;\r\n\r\n    [SettingSource(\"Preempt time\", \"Preempt time the target wants to sing.\")]\r\n    public BindableDouble LyricPreemptTime { get; } = new(3000)\r\n    {\r\n        MinValue = 0,\r\n        MaxValue = 5000.0,\r\n        Precision = 100.0\r\n    };\r\n\r\n    public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)\r\n        => new(new KaraokeAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = \"practice master\" });\r\n\r\n    public void ApplyToDrawableRuleset(DrawableRuleset<KaraokeHitObject> drawableRuleset)\r\n    {\r\n        if (drawableRuleset.Playfield is KaraokePlayfield karaokePlayfield)\r\n        {\r\n            karaokePlayfield.DisplayCursor = new BindableBool\r\n            {\r\n                Default = true,\r\n                Value = true,\r\n            };\r\n        }\r\n    }\r\n\r\n    public void ApplyToOverlay(ISettingHUDOverlay overlay)\r\n    {\r\n        // Add practice overlay\r\n        overlay.AddExtraOverlay(new PracticeOverlay());\r\n\r\n        // Add playback group into main overlay\r\n        overlay.AddSettingsGroup(new PlaybackSettings\r\n        {\r\n            Expanded =\r\n            {\r\n                Value = false,\r\n            },\r\n        });\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Mods/KaraokeModPreviewStage.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Stages.Preview;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos.Preview;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Mods;\r\n\r\npublic class KaraokeModPreviewStage : ModStage<PreviewStageInfo>\r\n{\r\n    public override string Name => \"Preview stage\";\r\n\r\n    public override string Acronym => \"PS\";\r\n\r\n    public override LocalisableString Description => \"Focus on preview the lyric text.\";\r\n\r\n    public override Type[] IncompatibleMods => new Type[]\r\n    {\r\n        typeof(KaraokeModClassicStage),\r\n    };\r\n\r\n    protected override PreviewStageInfo CreateStageInfo(KaraokeBeatmap beatmap)\r\n    {\r\n        var config = new PreviewStageInfoGeneratorConfig();\r\n        var generator = new PreviewStageInfoGenerator(config);\r\n\r\n        return (PreviewStageInfo)generator.Generate(beatmap);\r\n    }\r\n\r\n    protected override void ApplyToStageInfo(PreviewStageInfo stageInfo)\r\n    {\r\n        // todo: adjust stage by config.\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Mods/KaraokeModSnow.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Localisation;\r\nusing osu.Framework.Timing;\r\nusing osu.Framework.Utils;\r\nusing osu.Game.Rulesets.Mods;\r\nusing osu.Game.Screens.Play;\r\nusing osuTK;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Mods;\r\n\r\npublic partial class KaraokeModSnow : Mod, IApplicableToHUD\r\n{\r\n    public override string Name => \"Snow\";\r\n\r\n    public override LocalisableString Description => \"Display some snow\";\r\n    public override string Acronym => \"SW\";\r\n    public override double ScoreMultiplier => 1.0f;\r\n    public override IconUsage? Icon => FontAwesome.Regular.Snowflake;\r\n    public override ModType Type => ModType.Fun;\r\n\r\n    public void ApplyToHUD(HUDOverlay overlay)\r\n    {\r\n        overlay.Add(CreateSnowContainer);\r\n    }\r\n\r\n    protected virtual SnowContainer CreateSnowContainer => new()\r\n    {\r\n        SnowGenerateParSecond = 1,\r\n        EnableNewSnow = true,\r\n        SnowExpireTime = 6000,\r\n        Enabled = true,\r\n        Speed = 1,\r\n        WingAffection = 3,\r\n        SnowSize = 0.3f,\r\n        TexturePath = \"Mod/Snow/Snow\",\r\n        Clock = new FramedClock(new StopwatchClock(true)),\r\n        RelativeSizeAxes = Axes.Both,\r\n        Depth = 1,\r\n    };\r\n\r\n    protected partial class SnowContainer : CompositeDrawable\r\n    {\r\n        // Max can have 1000 snow at the scene\r\n        public int SnowGenerateParSecond { get; set; }\r\n\r\n        // If disable ,will stop snow\r\n        public bool EnableNewSnow { get; set; }\r\n\r\n        // Snow expire time\r\n        public int SnowExpireTime { get; set; }\r\n\r\n        // If disable ,will pause and no show will fall down\r\n        public bool Enabled { get; set; }\r\n\r\n        // Snow speed\r\n        public float Speed { get; set; }\r\n\r\n        // Wing speed\r\n        public float WingAffection { get; set; }\r\n\r\n        // Snow size\r\n        public float SnowSize { get; set; }\r\n\r\n        // Texture path\r\n        public string TexturePath { get; set; } = null!;\r\n\r\n        protected override void Update()\r\n        {\r\n            if (!Enabled)\r\n                return;\r\n\r\n            base.Update();\r\n\r\n            double currentTime = Time.Current;\r\n\r\n            bool isCreateShow = !InternalChildren.Any() ||\r\n                                (InternalChildren.LastOrDefault() as SnowSprite)?.CreateTime\r\n                                + 1000 / SnowGenerateParSecond < currentTime;\r\n\r\n            // If can generate new snow\r\n            if (isCreateShow && EnableNewSnow)\r\n            {\r\n                float currentAlpha = (float)RNG.Next(0, 255) / 255;\r\n                int width = (int)DrawWidth;\r\n                var newFlake = new SnowSprite\r\n                {\r\n                    Anchor = Anchor.TopCentre,\r\n                    Origin = Anchor.Centre,\r\n                    Colour = Color4.White,\r\n                    Position = new Vector2(RNG.Next(-width / 2, width / 2), -40),\r\n                    Depth = 1,\r\n                    CreateTime = currentTime,\r\n                    Size = new Vector2(50, 50),\r\n                    Scale = new Vector2(1, 1) * SnowSize,\r\n                    Alpha = currentAlpha,\r\n                    HorizontalSpeed = RNG.Next(-100, 100) + WingAffection * 10,\r\n                };\r\n                AddInternal(newFlake);\r\n            }\r\n\r\n            // Update each snow position\r\n            foreach (var drawable in InternalChildren)\r\n            {\r\n                if (drawable is not SnowSprite snow)\r\n                    continue;\r\n\r\n                snow.X += snow.HorizontalSpeed / 1000f;\r\n                snow.Y += 1 * Speed;\r\n\r\n                // Recycle\r\n                if (snow.CreateTime + SnowExpireTime < currentTime)\r\n                    InternalChildren.ToList().Remove(snow);\r\n            }\r\n        }\r\n\r\n        /// <summary>\r\n        /// Show spirit\r\n        /// </summary>\r\n        private partial class SnowSprite : Circle\r\n        {\r\n            public float HorizontalSpeed { get; init; }\r\n\r\n            public double CreateTime { get; init; }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Mods/KaraokeModSuddenDeath.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Mods;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Mods;\r\n\r\npublic class KaraokeModSuddenDeath : ModSuddenDeath;\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Mods/KaraokeModTranslation.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Globalization;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Drawables;\r\nusing osu.Game.Rulesets.Mods;\r\nusing osu.Game.Rulesets.Objects.Drawables;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Mods;\r\n\r\npublic class KaraokeModTranslation : Mod, IApplicableToDrawableHitObject\r\n{\r\n    public override string Name => $\"Translation to {CultureInfo.Value}\";\r\n\r\n    public override LocalisableString Description => \"Display prefer translation by ruleset configuration.\";\r\n\r\n    public override double ScoreMultiplier => 1.0f;\r\n\r\n    public override string Acronym => \"LT\";\r\n\r\n    [SettingSource(\"Default language\", \"Select default language\", 0, SettingControlType = typeof(LanguageSettingsControl))]\r\n    public BindableCultureInfo CultureInfo { get; } = new(new CultureInfo(\"en-US\"));\r\n\r\n    public void ApplyToDrawableHitObject(DrawableHitObject drawable)\r\n    {\r\n        if (drawable is not DrawableLyric drawableLyric)\r\n            return;\r\n\r\n        drawableLyric.ChangePreferTranslationLanguage(CultureInfo.Value);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Mods/KaraokeModWindowsUpdate.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing osu.Framework.Extensions.IEnumerableExtensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Judgements;\r\nusing osu.Game.Rulesets.Mods;\r\nusing osu.Game.Rulesets.Scoring;\r\nusing osu.Game.Rulesets.UI;\r\nusing osu.Game.Screens.Play;\r\nusing osuTK;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Mods;\r\n\r\npublic partial class KaraokeModWindowsUpdate : ModSuddenDeath, IApplicableToHUD\r\n{\r\n    public override string Name => \"Windows update\";\r\n    public override string Acronym => \"WD\";\r\n    public override IconUsage? Icon => FontAwesome.Brands.Windows;\r\n    public override LocalisableString Description => \"Once you missed, windows will upppppdate your osu!\";\r\n\r\n    private HUDOverlay overlay = null!;\r\n    private WindowsUpdateContainer? windowsUpdateContainer;\r\n\r\n    public void ApplyToHUD(HUDOverlay overlay)\r\n    {\r\n        this.overlay = overlay;\r\n    }\r\n\r\n    protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result)\r\n    {\r\n        bool displayWindowsUpdateScreen = base.FailCondition(healthProcessor, result);\r\n\r\n        if (displayWindowsUpdateScreen && windowsUpdateContainer == null)\r\n        {\r\n            overlay.Add(windowsUpdateContainer = new WindowsUpdateContainer\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n            });\r\n        }\r\n\r\n        return false;\r\n    }\r\n\r\n    private partial class WindowsUpdateContainer : CompositeDrawable\r\n    {\r\n        public WindowsUpdateContainer()\r\n        {\r\n            InternalChildren = new Drawable[]\r\n            {\r\n                new Box\r\n                {\r\n                    Name = \"Background\",\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    Colour = new Color4(0, 120, 215, 255),\r\n                },\r\n                new LoadingIcon\r\n                {\r\n                    Name = \"Loading icon\",\r\n                    Anchor = Anchor.Centre,\r\n                    Origin = Anchor.Centre,\r\n                    Colour = Color4.White,\r\n                    Y = -80,\r\n                },\r\n                new OsuSpriteText\r\n                {\r\n                    Name = \"Update progress text\",\r\n                    Text = \"Working on updates 87 % complete.\",\r\n                    Anchor = Anchor.Centre,\r\n                    Origin = Anchor.Centre,\r\n                    Scale = new Vector2(1.5f),\r\n                },\r\n                new OsuSpriteText\r\n                {\r\n                    Name = \"Take a while text\",\r\n                    Text = \"Don't turn off your PC. This will take a while.\",\r\n                    Anchor = Anchor.Centre,\r\n                    Origin = Anchor.Centre,\r\n                    Scale = new Vector2(1.5f),\r\n                    Y = -25,\r\n                },\r\n                new OsuSpriteText\r\n                {\r\n                    Name = \"Restart text\",\r\n                    Text = \"Your PC will restart several times.\",\r\n                    Anchor = Anchor.BottomCentre,\r\n                    Origin = Anchor.Centre,\r\n                    Scale = new Vector2(1.3f),\r\n                    Y = -30,\r\n                },\r\n            };\r\n        }\r\n\r\n        private partial class LoadingIcon : ModIcon\r\n        {\r\n            public LoadingIcon()\r\n                : base(new KaraokeModWindowsUpdate())\r\n            {\r\n                // Hide the text on the bottom.\r\n                Children.OfType<OsuSpriteText>().ForEach(x => x.Hide());\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Mods/LanguageSettingsControl.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Globalization;\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Overlays.Settings;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.UserInterfaceV2;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Mods;\r\n\r\npublic partial class LanguageSettingsControl : SettingsItem<CultureInfo?>\r\n{\r\n    protected override Drawable CreateControl() => new LanguageSelector\r\n    {\r\n        RelativeSizeAxes = Axes.X,\r\n        Height = 300,\r\n    };\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Mods/ModStage.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos;\r\nusing osu.Game.Rulesets.Mods;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Mods;\r\n\r\npublic abstract class ModStage<TStageInfo> : Mod, IApplicableToStageInfo\r\n    where TStageInfo : StageInfo\r\n{\r\n    public sealed override ModType Type => ModType.Conversion;\r\n\r\n    /// <summary>\r\n    /// Change the stage type should not affect the score.\r\n    /// </summary>\r\n    public override double ScoreMultiplier => 1;\r\n\r\n    public override Type[] IncompatibleMods => new[] { typeof(ModStage<TStageInfo>) }.Except(new[] { GetType() }).ToArray();\r\n\r\n    public bool CanApply(StageInfo stageInfo)\r\n    {\r\n        return stageInfo is TStageInfo;\r\n    }\r\n\r\n    public StageInfo? CreateDefaultStageInfo(KaraokeBeatmap beatmap)\r\n    {\r\n        return CreateStageInfo(beatmap);\r\n    }\r\n\r\n    public void ApplyToStageInfo(StageInfo stageInfo)\r\n    {\r\n        if (stageInfo is not TStageInfo tStageInfo)\r\n            throw new ArgumentException($\"The stage info is not matched with {GetType().Name}\");\r\n\r\n        ApplyToStageInfo(tStageInfo);\r\n    }\r\n\r\n    protected abstract TStageInfo? CreateStageInfo(KaraokeBeatmap beatmap);\r\n\r\n    protected abstract void ApplyToStageInfo(TStageInfo stageInfo);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Objects/BarLine.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Judgements;\r\nusing osu.Game.Rulesets.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Objects;\r\n\r\npublic class BarLine : KaraokeHitObject, IBarLine\r\n{\r\n    public bool Major\r\n    {\r\n        get => MajorBindable.Value;\r\n        set => MajorBindable.Value = value;\r\n    }\r\n\r\n    public readonly Bindable<bool> MajorBindable = new BindableBool();\r\n\r\n    public override Judgement CreateJudgement() => new IgnoreJudgement();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Objects/Drawables/DrawableBarLine.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osuTK;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Objects.Drawables;\r\n\r\n/// <summary>\r\n/// Visualises a <see cref=\"BarLine\"/>. Although this derives DrawableKaraokeHitObject,\r\n/// this does not handle input/sound like a normal hit object.\r\n/// </summary>\r\npublic partial class DrawableBarLine : DrawableKaraokeScrollingHitObject<BarLine>\r\n{\r\n    /// <summary>\r\n    /// Height of major bar line triangles.\r\n    /// </summary>\r\n    private const float triangle_width = 12;\r\n\r\n    /// <summary>\r\n    /// Offset of the major bar line triangles from the sides of the bar line.\r\n    /// </summary>\r\n    private const float triangle_offset = 9;\r\n\r\n    /// <summary>\r\n    /// The visual line tracker.\r\n    /// </summary>\r\n    private Box line = null!;\r\n\r\n    /// <summary>\r\n    /// Container with triangles. Only visible for major lines.\r\n    /// </summary>\r\n    private Container triangleContainer = null!;\r\n\r\n    private readonly Bindable<bool> major = new();\r\n\r\n    public DrawableBarLine()\r\n        : this(null)\r\n    {\r\n    }\r\n\r\n    public DrawableBarLine(BarLine? barLine)\r\n        : base(barLine)\r\n    {\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        RelativeSizeAxes = Axes.Y;\r\n        Width = 2f;\r\n\r\n        AddRangeInternal(new Drawable[]\r\n        {\r\n            line = new Box\r\n            {\r\n                Name = \"Bar line\",\r\n                Anchor = Anchor.CentreLeft,\r\n                Origin = Anchor.CentreLeft,\r\n                RelativeSizeAxes = Axes.Both,\r\n                Colour = new Color4(255, 204, 33, 255),\r\n            },\r\n            triangleContainer = new Container\r\n            {\r\n                Anchor = Anchor.Centre,\r\n                Origin = Anchor.Centre,\r\n                RelativeSizeAxes = Axes.Both,\r\n                Children = new[]\r\n                {\r\n                    new EquilateralTriangle\r\n                    {\r\n                        Name = \"Up triangle\",\r\n                        Anchor = Anchor.TopCentre,\r\n                        Origin = Anchor.Centre,\r\n                        Size = new Vector2(triangle_width),\r\n                        Y = -triangle_offset,\r\n                        Rotation = 180,\r\n                    },\r\n                    new EquilateralTriangle\r\n                    {\r\n                        Name = \"Down triangle\",\r\n                        Anchor = Anchor.BottomCentre,\r\n                        Origin = Anchor.Centre,\r\n                        Size = new Vector2(triangle_width),\r\n                        Y = triangle_offset,\r\n                        Rotation = 0,\r\n                    },\r\n                },\r\n            },\r\n        });\r\n    }\r\n\r\n    protected override void LoadComplete()\r\n    {\r\n        base.LoadComplete();\r\n        major.BindValueChanged(updateMajor, true);\r\n    }\r\n\r\n    private void updateMajor(ValueChangedEvent<bool> major)\r\n    {\r\n        line.Alpha = major.NewValue ? 1f : 0.75f;\r\n        triangleContainer.Alpha = major.NewValue ? 1 : 0;\r\n    }\r\n\r\n    protected override void OnApply()\r\n    {\r\n        base.OnApply();\r\n        major.BindTo(HitObject.MajorBindable);\r\n    }\r\n\r\n    protected override void OnFree()\r\n    {\r\n        base.OnFree();\r\n        major.UnbindFrom(HitObject.MajorBindable);\r\n    }\r\n\r\n    protected override void UpdateInitialTransforms()\r\n    {\r\n    }\r\n\r\n    protected override void UpdateStartTimeStateTransforms()\r\n    {\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Objects/Drawables/DrawableKaraokeHitObject.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Game.Rulesets.Judgements;\r\nusing osu.Game.Rulesets.Karaoke.Judgements;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Drawables;\r\nusing osu.Game.Rulesets.Objects.Drawables;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Objects.Drawables;\r\n\r\npublic partial class DrawableKaraokeHitObject : DrawableHitObject<KaraokeHitObject>\r\n{\r\n    [Resolved]\r\n    private IStageHitObjectRunner? stageRunner { get; set; }\r\n\r\n    protected DrawableKaraokeHitObject(KaraokeHitObject? hitObject)\r\n        : base(hitObject!)\r\n    {\r\n    }\r\n\r\n    protected sealed override double InitialLifetimeOffset\r\n        => stageRunner?.GetStartTimeOffset(HitObject) ?? base.InitialLifetimeOffset;\r\n\r\n    protected override JudgementResult CreateResult(Judgement judgement) => new KaraokeJudgementResult(HitObject, judgement);\r\n\r\n    protected override void UpdateInitialTransforms()\r\n        => stageRunner?.UpdateInitialTransforms(this);\r\n\r\n    protected override void UpdateStartTimeStateTransforms()\r\n        => stageRunner?.UpdateStartTimeStateTransforms(this);\r\n\r\n    protected override void UpdateHitStateTransforms(ArmedState state)\r\n        => stageRunner?.UpdateHitStateTransforms(this, state);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Objects/Drawables/DrawableKaraokeScrollingHitObject.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Rulesets.Objects.Drawables;\r\nusing osu.Game.Rulesets.UI.Scrolling;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Objects.Drawables;\r\n\r\npublic abstract partial class DrawableKaraokeScrollingHitObject : DrawableHitObject<KaraokeHitObject>\r\n{\r\n    protected readonly IBindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>();\r\n\r\n    protected readonly IBindable<double> TimeRange = new Bindable<double>();\r\n\r\n    protected DrawableKaraokeScrollingHitObject(KaraokeHitObject? hitObject)\r\n        : base(hitObject!)\r\n    {\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(IScrollingInfo scrollingInfo)\r\n    {\r\n        Direction.BindTo(scrollingInfo.Direction);\r\n        Direction.BindValueChanged(OnDirectionChanged, true);\r\n\r\n        TimeRange.BindTo(scrollingInfo.TimeRange);\r\n        TimeRange.BindValueChanged(OnTimeRangeChanged, true);\r\n    }\r\n\r\n    private double computedLifetimeStart;\r\n\r\n    public override double LifetimeStart\r\n    {\r\n        get => base.LifetimeStart;\r\n        set\r\n        {\r\n            computedLifetimeStart = value;\r\n\r\n            if (!AlwaysAlive)\r\n                base.LifetimeStart = value;\r\n        }\r\n    }\r\n\r\n    private double computedLifetimeEnd;\r\n\r\n    public override double LifetimeEnd\r\n    {\r\n        get => base.LifetimeEnd;\r\n        set\r\n        {\r\n            computedLifetimeEnd = value;\r\n\r\n            if (!AlwaysAlive)\r\n                base.LifetimeEnd = value;\r\n        }\r\n    }\r\n\r\n    private bool alwaysAlive;\r\n\r\n    /// <summary>\r\n    /// Whether this <see cref=\"DrawableKaraokeHitObject\"/> should always remain alive.\r\n    /// </summary>\r\n    internal bool AlwaysAlive\r\n    {\r\n        get => alwaysAlive;\r\n        set\r\n        {\r\n            if (alwaysAlive == value)\r\n                return;\r\n\r\n            alwaysAlive = value;\r\n\r\n            if (value)\r\n            {\r\n                // Set the base lifetimes directly, to avoid mangling the computed lifetimes\r\n                base.LifetimeStart = double.MinValue;\r\n                base.LifetimeEnd = double.MaxValue;\r\n            }\r\n            else\r\n            {\r\n                LifetimeStart = computedLifetimeStart;\r\n                LifetimeEnd = computedLifetimeEnd;\r\n            }\r\n        }\r\n    }\r\n\r\n    protected virtual void OnDirectionChanged(ValueChangedEvent<ScrollingDirection> e)\r\n    {\r\n        Anchor = Origin = e.NewValue == ScrollingDirection.Left ? Anchor.CentreLeft : Anchor.CentreRight;\r\n    }\r\n\r\n    protected virtual void OnTimeRangeChanged(ValueChangedEvent<double> e)\r\n    {\r\n    }\r\n}\r\n\r\npublic abstract partial class DrawableKaraokeScrollingHitObject<TObject> : DrawableKaraokeScrollingHitObject\r\n    where TObject : KaraokeHitObject\r\n{\r\n    public new TObject HitObject => (TObject)base.HitObject;\r\n\r\n    protected DrawableKaraokeScrollingHitObject(TObject? hitObject)\r\n        : base(hitObject)\r\n    {\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Objects/Drawables/DrawableLyric.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Globalization;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Extensions.IEnumerableExtensions;\r\nusing osu.Framework.Extensions.ObjectExtensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Skinning.Elements;\r\nusing osu.Game.Skinning;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Objects.Drawables;\r\n\r\npublic partial class DrawableLyric : DrawableKaraokeHitObject\r\n{\r\n    private Container<DrawableKaraokeSpriteText> lyricPieces = null!;\r\n    private OsuSpriteText translationText = null!;\r\n\r\n    private readonly BindableBool useTranslationBindable = new();\r\n    private readonly Bindable<CultureInfo> preferLanguageBindable = new();\r\n\r\n    private readonly Bindable<FontUsage> mainFontUsageBindable = new();\r\n    private readonly Bindable<FontUsage> rubyFontUsageBindable = new();\r\n    private readonly Bindable<int> rubyMarginBindable = new();\r\n    private readonly Bindable<FontUsage> romanisationFontUsageBindable = new();\r\n    private readonly Bindable<int> romanisationMarginBindable = new();\r\n    private readonly Bindable<FontUsage> translationFontUsageBindable = new();\r\n\r\n    private readonly BindableDictionary<CultureInfo, string> translationTextBindable = new();\r\n\r\n    public event Action<DrawableLyric>? OnLyricStart;\r\n    public event Action<DrawableLyric>? OnLyricEnd;\r\n\r\n    public new Lyric HitObject => (Lyric)base.HitObject;\r\n\r\n    public DrawableLyric()\r\n        : this(null)\r\n    {\r\n    }\r\n\r\n    public DrawableLyric(Lyric? hitObject)\r\n        : base(hitObject)\r\n    {\r\n    }\r\n\r\n    [BackgroundDependencyLoader(true)]\r\n    private void load(KaraokeRulesetConfigManager? config)\r\n    {\r\n        AutoSizeAxes = Axes.Both;\r\n\r\n        AddInternal(lyricPieces = new Container<DrawableKaraokeSpriteText>\r\n        {\r\n            AutoSizeAxes = Axes.Both,\r\n        });\r\n        AddInternal(translationText = new OsuSpriteText\r\n        {\r\n            Anchor = Anchor.BottomLeft,\r\n            Origin = Anchor.TopLeft,\r\n        });\r\n\r\n        useTranslationBindable.BindValueChanged(_ => applyTranslation(), true);\r\n        preferLanguageBindable.BindValueChanged(_ => applyTranslation(), true);\r\n\r\n        if (config != null)\r\n        {\r\n            config.BindWith(KaraokeRulesetSetting.MainFont, mainFontUsageBindable);\r\n            config.BindWith(KaraokeRulesetSetting.RubyFont, rubyFontUsageBindable);\r\n            config.BindWith(KaraokeRulesetSetting.RubyMargin, rubyMarginBindable);\r\n            config.BindWith(KaraokeRulesetSetting.RomanisationFont, romanisationFontUsageBindable);\r\n            config.BindWith(KaraokeRulesetSetting.RomanisationMargin, romanisationMarginBindable);\r\n            config.BindWith(KaraokeRulesetSetting.TranslationFont, translationFontUsageBindable);\r\n        }\r\n\r\n        mainFontUsageBindable.BindValueChanged(_ => updateLyricFontInfo());\r\n        rubyFontUsageBindable.BindValueChanged(_ => updateLyricFontInfo());\r\n        rubyMarginBindable.BindValueChanged(_ => updateLyricFontInfo());\r\n        romanisationFontUsageBindable.BindValueChanged(_ => updateLyricFontInfo());\r\n        romanisationMarginBindable.BindValueChanged(_ => updateLyricFontInfo());\r\n        translationFontUsageBindable.BindValueChanged(_ => updateLyricFontInfo());\r\n\r\n        // property in hitobject.\r\n        translationTextBindable.BindCollectionChanged((_, _) => { applyTranslation(); });\r\n    }\r\n\r\n    public void ChangeDisplayType(LyricDisplayType lyricDisplayType)\r\n    {\r\n        lyricPieces.ForEach(x => x.DisplayType = lyricDisplayType);\r\n    }\r\n\r\n    public void ChangeDisplayProperty(LyricDisplayProperty lyricDisplayProperty)\r\n    {\r\n        lyricPieces.ForEach(x => x.DisplayProperty = lyricDisplayProperty);\r\n    }\r\n\r\n    public void ChangePreferTranslationLanguage(CultureInfo? language)\r\n    {\r\n        if (language != null && translationTextBindable.TryGetValue(language, out string? translation))\r\n            translationText.Text = translation;\r\n        else\r\n        {\r\n            translationText.Text = string.Empty;\r\n        }\r\n    }\r\n\r\n    protected override void OnApply()\r\n    {\r\n        base.OnApply();\r\n\r\n        lyricPieces.Clear();\r\n        lyricPieces.Add(new DrawableKaraokeSpriteText(HitObject));\r\n        ApplySkin(CurrentSkin, false);\r\n\r\n        translationTextBindable.BindTo(HitObject.TranslationsBindable);\r\n    }\r\n\r\n    protected override void OnFree()\r\n    {\r\n        base.OnFree();\r\n\r\n        translationTextBindable.UnbindFrom(HitObject.TranslationsBindable);\r\n    }\r\n\r\n    protected override void ApplySkin(ISkinSource skin, bool allowFallback)\r\n    {\r\n        base.ApplySkin(skin, allowFallback);\r\n\r\n        updateLyricFontInfo();\r\n    }\r\n\r\n    private void updateLyricFontInfo()\r\n    {\r\n        if (CurrentSkin == null)\r\n            return;\r\n\r\n        if (HitObject.IsNull())\r\n            return;\r\n\r\n        var lyricFontInfo = CurrentSkin.GetConfig<Lyric, LyricFontInfo>(HitObject)?.Value;\r\n        lyricFontInfo?.ApplyTo(this);\r\n    }\r\n\r\n    private void applyTranslation()\r\n    {\r\n        var language = preferLanguageBindable.Value;\r\n        bool useTranslation = useTranslationBindable.Value;\r\n\r\n        if (!useTranslation || language == null)\r\n        {\r\n            translationText.Text = string.Empty;\r\n        }\r\n        else\r\n        {\r\n            if (translationTextBindable.TryGetValue(language, out string? translation))\r\n                translationText.Text = translation;\r\n        }\r\n    }\r\n\r\n    protected override void CheckForResult(bool userTriggered, double timeOffset)\r\n    {\r\n        if (!HitObject.TimeValid)\r\n            return;\r\n\r\n        if (timeOffset + HitObject.Duration >= 0 && HitObject.HitWindows.CanBeHit(timeOffset + HitObject.Duration))\r\n        {\r\n            // note: CheckForResult will not being triggered when roll-back the time.\r\n            // so there's no need to consider the case while roll-back.\r\n            OnLyricStart?.Invoke(this);\r\n            return;\r\n        }\r\n\r\n        if (timeOffset >= 0 && HitObject.HitWindows.CanBeHit(timeOffset))\r\n        {\r\n            OnLyricEnd?.Invoke(this);\r\n        }\r\n\r\n        if (timeOffset < 0)\r\n            return;\r\n\r\n        ApplyMaxResult();\r\n    }\r\n\r\n    protected override void UpdateInitialTransforms()\r\n    {\r\n        base.UpdateInitialTransforms();\r\n\r\n        lyricPieces.ForEach(x => x.RefreshStateTransforms());\r\n    }\r\n\r\n    public void ApplyToLyricPieces(Action<DrawableKaraokeSpriteText> action)\r\n    {\r\n        foreach (var lyricPiece in lyricPieces)\r\n            action?.Invoke(lyricPiece);\r\n    }\r\n\r\n    public void ApplyToTranslationText(Action<OsuSpriteText> action) => action.Invoke(translationText);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Objects/Drawables/DrawableNote.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Diagnostics;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Extensions.ObjectExtensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Input.Bindings;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\nusing osu.Game.Rulesets.Karaoke.Judgements;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Skinning.Default;\r\nusing osu.Game.Rulesets.Karaoke.Skinning.Elements;\r\nusing osu.Game.Rulesets.Karaoke.UI.Position;\r\nusing osu.Game.Rulesets.Scoring;\r\nusing osu.Game.Rulesets.UI.Scrolling;\r\nusing osu.Game.Skinning;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Objects.Drawables;\r\n\r\n/// <summary>\r\n/// Visualises a <see cref=\"Note\"/> hit object.\r\n/// </summary>\r\npublic partial class DrawableNote : DrawableKaraokeScrollingHitObject<Note>, IKeyBindingHandler<KaraokeScoringAction>\r\n{\r\n    private readonly SkinnableDrawable background;\r\n    private readonly OsuSpriteText textPiece;\r\n\r\n    /// <summary>\r\n    /// Time at which the user started holding this hold note. Null if the user is not holding this hold note.\r\n    /// </summary>\r\n    private double? holdStartTime;\r\n\r\n    public IBindable<bool> IsHitting => isHitting;\r\n\r\n    private readonly Bindable<bool> isHitting = new();\r\n\r\n    private readonly IBindable<NotePositionCalculator> positionBindable = new Bindable<NotePositionCalculator>();\r\n\r\n    public readonly IBindable<string> TextBindable = new Bindable<string>();\r\n    public readonly IBindable<string?> RubyTextBindable = new Bindable<string?>();\r\n    public readonly IBindableDictionary<Singer, SingerState[]> SingersBindable = new BindableDictionary<Singer, SingerState[]>();\r\n    public readonly IBindable<bool> DisplayBindable = new Bindable<bool>();\r\n    public readonly IBindable<Tone> ToneBindable = new Bindable<Tone>();\r\n\r\n    public DrawableNote()\r\n        : this(null)\r\n    {\r\n    }\r\n\r\n    public DrawableNote(Note? hitObject)\r\n        : base(hitObject)\r\n    {\r\n        AddRangeInternal(new Drawable[]\r\n        {\r\n            background = new SkinnableDrawable(new KaraokeSkinComponentLookup(KaraokeSkinComponents.Note), _ => new DefaultBodyPiece { RelativeSizeAxes = Axes.Both }),\r\n            textPiece = new OsuSpriteText(),\r\n        });\r\n\r\n        // Comment it because i'm not sure will it be used in the future or not.\r\n        /*\r\n        AccentColour.BindValueChanged(colour =>\r\n        {\r\n            bodyPiece.AccentColour = colour.NewValue;\r\n        }, true);\r\n        */\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(INotePositionInfo notePositionInfo)\r\n    {\r\n        positionBindable.BindTo(notePositionInfo.Position);\r\n\r\n        positionBindable.BindValueChanged(_ => updateNotePositionAndHeight());\r\n        TextBindable.BindValueChanged(_ => { changeText(HitObject); });\r\n        RubyTextBindable.BindValueChanged(_ => { changeText(HitObject); });\r\n        SingersBindable.BindCollectionChanged((_, _) => { ApplySkin(CurrentSkin, false); });\r\n        DisplayBindable.BindValueChanged(e => { (Result.Judgement as KaraokeNoteJudgement)!.Scorable = e.NewValue; });\r\n        ToneBindable.BindValueChanged(_ => updateNotePositionAndHeight());\r\n\r\n        void updateNotePositionAndHeight()\r\n        {\r\n            Y = notePositionInfo.Calculator.YPositionAt(HitObject);\r\n            Height = notePositionInfo.Calculator.ColumnHeight;\r\n        }\r\n    }\r\n\r\n    protected override void OnApply()\r\n    {\r\n        base.OnApply();\r\n\r\n        TextBindable.BindTo(HitObject.TextBindable);\r\n        RubyTextBindable.BindTo(HitObject.RubyTextBindable);\r\n        DisplayBindable.BindTo(HitObject.DisplayBindable);\r\n        ToneBindable.BindTo(HitObject.ToneBindable);\r\n        SingersBindable.BindTo(HitObject.SingersBindable);\r\n    }\r\n\r\n    protected override void OnFree()\r\n    {\r\n        base.OnFree();\r\n\r\n        TextBindable.UnbindFrom(HitObject.TextBindable);\r\n        RubyTextBindable.UnbindFrom(HitObject.RubyTextBindable);\r\n        DisplayBindable.UnbindFrom(HitObject.DisplayBindable);\r\n        ToneBindable.UnbindFrom(HitObject.ToneBindable);\r\n        SingersBindable.UnbindFrom(HitObject.SingersBindable);\r\n    }\r\n\r\n    protected override void ApplySkin(ISkinSource skin, bool allowFallback)\r\n    {\r\n        base.ApplySkin(skin, allowFallback);\r\n\r\n        if (CurrentSkin == null)\r\n            return;\r\n\r\n        if (HitObject.IsNull())\r\n            return;\r\n\r\n        var noteSkin = skin.GetConfig<Note, NoteStyle>(HitObject)?.Value;\r\n        noteSkin?.ApplyTo(this);\r\n    }\r\n\r\n    protected override void OnDirectionChanged(ValueChangedEvent<ScrollingDirection> e)\r\n    {\r\n        base.OnDirectionChanged(e);\r\n\r\n        textPiece.Anchor = textPiece.Origin = e.NewValue == ScrollingDirection.Left ? Anchor.CentreLeft : Anchor.CentreRight;\r\n    }\r\n\r\n    protected override void OnTimeRangeChanged(ValueChangedEvent<double> e)\r\n    {\r\n        base.OnTimeRangeChanged(e);\r\n\r\n        float paddingSize = 5 + 7 * 1000 / (float)e.NewValue;\r\n        textPiece.Padding = new MarginPadding { Left = paddingSize, Right = paddingSize };\r\n    }\r\n\r\n    private void changeText(Note note)\r\n    {\r\n        // todo: should apply the setting.\r\n        textPiece.Text = NoteUtils.DisplayText(note, true);\r\n    }\r\n\r\n    protected void BeginSing()\r\n    {\r\n        holdStartTime = Time.Current;\r\n        isHitting.Value = true;\r\n    }\r\n\r\n    protected void EndSing()\r\n    {\r\n        holdStartTime = null;\r\n        isHitting.Value = false;\r\n\r\n        UpdateResult(true);\r\n    }\r\n\r\n    protected override void CheckForResult(bool userTriggered, double timeOffset)\r\n    {\r\n        Debug.Assert(HitObject.HitWindows != null);\r\n\r\n        if (!userTriggered)\r\n        {\r\n            if (!HitObject.HitWindows.CanBeHit(timeOffset))\r\n                ApplyResult(HitResult.Miss);\r\n            return;\r\n        }\r\n\r\n        var result = HitObject.HitWindows.ResultFor(timeOffset);\r\n        if (result == HitResult.None)\r\n            return;\r\n\r\n        ApplyResult(result);\r\n    }\r\n\r\n    public bool OnPressed(KeyBindingPressEvent<KaraokeScoringAction> e)\r\n    {\r\n        // Make sure the action happened within the body of the hold note\r\n        if ((Time.Current < HitObject.StartTime && holdStartTime == null) || (Time.Current > HitObject.EndTime && holdStartTime == null))\r\n            return false;\r\n\r\n        if (holdStartTime == null)\r\n        {\r\n            // User start singing this note\r\n            BeginSing();\r\n        }\r\n        else if (Time.Current > HitObject.EndTime || Time.Current < HitObject.StartTime)\r\n        {\r\n            // User stop singing this note\r\n            EndSing();\r\n        }\r\n\r\n        return false;\r\n    }\r\n\r\n    public void OnReleased(KeyBindingReleaseEvent<KaraokeScoringAction> e)\r\n    {\r\n        // Make sure that the user started holding the key during the hold note\r\n        if (!holdStartTime.HasValue)\r\n            return;\r\n\r\n        // User stop singing this note\r\n        EndSing();\r\n    }\r\n\r\n    public void ApplyToLyricText(Action<OsuSpriteText> action) => action.Invoke(textPiece);\r\n\r\n    public void ApplyToBackground(Action<SkinnableDrawable> action) => action.Invoke(background);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Objects/KaraokeHitObject.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Objects;\r\n\r\npublic class KaraokeHitObject : HitObject\r\n{\r\n    public double TimePreempt = 600;\r\n    public double TimeFadeIn = 400;\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Objects/LegacyProperties.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Globalization;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Objects;\r\n\r\n// todo: this function is used for legacy karaoke beatmap, will be removed eventually.\r\npublic class LegacyProperties : KaraokeHitObject\r\n{\r\n    public IList<CultureInfo> AvailableTranslationLanguages { get; set; } = new List<CultureInfo>();\r\n\r\n    public IDictionary<int, Singer> Singers { get; set; } = new Dictionary<int, Singer>();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Objects/Lyric.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Globalization;\r\nusing Newtonsoft.Json;\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Beatmaps.ControlPoints;\r\nusing osu.Game.Extensions;\r\nusing osu.Game.Rulesets.Judgements;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.IO.Serialization;\r\nusing osu.Game.Rulesets.Karaoke.Judgements;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Properties;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Types;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Workings;\r\nusing osu.Game.Rulesets.Karaoke.Scoring;\r\nusing osu.Game.Rulesets.Objects.Types;\r\nusing osu.Game.Rulesets.Scoring;\r\nusing osu.Game.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Objects;\r\n\r\npublic partial class Lyric : KaraokeHitObject, IHasPage, IHasDuration, IHasSingers, IHasOrder, IHasLock, IHasPrimaryKey, IDeepCloneable<Lyric>\r\n{\r\n    private void updateStateByDataProperty(LyricWorkingProperty workingProperty)\r\n        => workingPropertyValidator.UpdateStateByDataProperty(workingProperty);\r\n\r\n    /// <summary>\r\n    /// Primary key.\r\n    /// </summary>\r\n    [JsonProperty]\r\n    public ElementId ID { get; private set; } = ElementId.NewElementId();\r\n\r\n    [JsonIgnore]\r\n    public readonly Bindable<string> TextBindable = new(string.Empty);\r\n\r\n    /// <summary>\r\n    /// Text of the lyric\r\n    /// </summary>\r\n    public string Text\r\n    {\r\n        get => TextBindable.Value;\r\n        set => TextBindable.Value = value;\r\n    }\r\n\r\n    [JsonIgnore]\r\n    public readonly BindableList<TimeTag> TimeTagsBindable = new();\r\n\r\n    /// <summary>\r\n    /// Time tags\r\n    /// </summary>\r\n    public IList<TimeTag> TimeTags\r\n    {\r\n        get => TimeTagsBindable;\r\n        set\r\n        {\r\n            TimeTagsBindable.Clear();\r\n            TimeTagsBindable.AddRange(value);\r\n        }\r\n    }\r\n\r\n    [JsonIgnore]\r\n    public readonly BindableList<RubyTag> RubyTagsBindable = new();\r\n\r\n    /// <summary>\r\n    /// List of ruby tags\r\n    /// </summary>\r\n    public IList<RubyTag> RubyTags\r\n    {\r\n        get => RubyTagsBindable;\r\n        set\r\n        {\r\n            RubyTagsBindable.Clear();\r\n            RubyTagsBindable.AddRange(value);\r\n        }\r\n    }\r\n\r\n    [JsonIgnore]\r\n    public readonly BindableList<ElementId> SingerIdsBindable = new();\r\n\r\n    /// <summary>\r\n    /// Singers\r\n    /// </summary>\r\n    public IList<ElementId> SingerIds\r\n    {\r\n        get => SingerIdsBindable;\r\n        set\r\n        {\r\n            SingerIdsBindable.Clear();\r\n            SingerIdsBindable.AddRange(value);\r\n        }\r\n    }\r\n\r\n    [JsonIgnore]\r\n    public readonly BindableDictionary<CultureInfo, string> TranslationsBindable = new();\r\n\r\n    /// <summary>\r\n    /// Translations\r\n    /// </summary>\r\n    public IDictionary<CultureInfo, string> Translations\r\n    {\r\n        get => TranslationsBindable;\r\n        set\r\n        {\r\n            TranslationsBindable.Clear();\r\n            TranslationsBindable.AddRange(value);\r\n        }\r\n    }\r\n\r\n    [JsonIgnore]\r\n    public readonly Bindable<CultureInfo?> LanguageBindable = new();\r\n\r\n    /// <summary>\r\n    /// Language\r\n    /// </summary>\r\n    public CultureInfo? Language\r\n    {\r\n        get => LanguageBindable.Value;\r\n        set => LanguageBindable.Value = value;\r\n    }\r\n\r\n    [JsonIgnore]\r\n    public readonly Bindable<int> OrderBindable = new();\r\n\r\n    /// <summary>\r\n    /// Order\r\n    /// </summary>\r\n    public int Order\r\n    {\r\n        get => OrderBindable.Value;\r\n        set => OrderBindable.Value = value;\r\n    }\r\n\r\n    [JsonIgnore]\r\n    public readonly Bindable<LockState> LockBindable = new();\r\n\r\n    /// <summary>\r\n    /// Lock\r\n    /// </summary>\r\n    public LockState Lock\r\n    {\r\n        get => LockBindable.Value;\r\n        set => LockBindable.Value = value;\r\n    }\r\n\r\n    private ElementId? referenceLyricId;\r\n\r\n    public ElementId? ReferenceLyricId\r\n    {\r\n        get => referenceLyricId;\r\n        set\r\n        {\r\n            referenceLyricId = value;\r\n            updateStateByDataProperty(LyricWorkingProperty.ReferenceLyric);\r\n        }\r\n    }\r\n\r\n    [JsonIgnore]\r\n    public readonly Bindable<IReferenceLyricPropertyConfig?> ReferenceLyricConfigBindable = new();\r\n\r\n    /// <summary>\r\n    /// Config for define the strategy to sync the property from the lyric.\r\n    /// </summary>\r\n    public IReferenceLyricPropertyConfig? ReferenceLyricConfig\r\n    {\r\n        get => ReferenceLyricConfigBindable.Value;\r\n        set => ReferenceLyricConfigBindable.Value = value;\r\n    }\r\n\r\n    public Lyric()\r\n    {\r\n        workingPropertyValidator = new LyricWorkingPropertyValidator(this);\r\n\r\n        initInternalBindingEvent();\r\n        initReferenceLyricEvent();\r\n    }\r\n\r\n    public override Judgement CreateJudgement() => new KaraokeLyricJudgement();\r\n\r\n    protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)\r\n    {\r\n        base.ApplyDefaultsToSelf(controlPointInfo, difficulty);\r\n\r\n        // Add because it will cause error on exit then enter gameplay.\r\n        StartTimeBindable.UnbindAll();\r\n    }\r\n\r\n    protected override HitWindows CreateHitWindows() => new KaraokeLyricHitWindows();\r\n\r\n    public Lyric DeepClone()\r\n    {\r\n        string serializeString = JsonConvert.SerializeObject(this, KaraokeJsonSerializableExtensions.CreateGlobalSettings());\r\n        var lyric = JsonConvert.DeserializeObject<Lyric>(serializeString, KaraokeJsonSerializableExtensions.CreateGlobalSettings())!;\r\n\r\n        lyric.ChangeId(ElementId.NewElementId());\r\n        lyric.ReferenceLyric = ReferenceLyric;\r\n\r\n        return lyric;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Objects/Lyric_Binding.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Collections.Specialized;\r\nusing System.Diagnostics;\r\nusing System.Linq;\r\nusing Newtonsoft.Json;\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Properties;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Workings;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Objects;\r\n\r\n/// <summary>\r\n/// Placing the binding-related logic.\r\n/// </summary>\r\npublic partial class Lyric\r\n{\r\n    [JsonIgnore]\r\n    public IBindable<int> TimeTagsTimingVersion => timeTagsTimingVersion;\r\n\r\n    private readonly Bindable<int> timeTagsTimingVersion = new();\r\n\r\n    [JsonIgnore]\r\n    public IBindable<int> TimeTagsRomanisationVersion => timeTagsRomanisationVersion;\r\n\r\n    private readonly Bindable<int> timeTagsRomanisationVersion = new();\r\n\r\n    [JsonIgnore]\r\n    public IBindable<int> RubyTagsVersion => rubyTagsVersion;\r\n\r\n    private readonly Bindable<int> rubyTagsVersion = new();\r\n\r\n    [JsonIgnore]\r\n    public IBindable<int> ReferenceLyricConfigVersion => referenceLyricConfigVersion;\r\n\r\n    private readonly Bindable<int> referenceLyricConfigVersion = new();\r\n\r\n    [JsonIgnore]\r\n    public IBindable<int> LyricPropertyWritableVersion => lyricPropertyWritableVersion;\r\n\r\n    private readonly Bindable<int> lyricPropertyWritableVersion = new();\r\n\r\n    private void initInternalBindingEvent()\r\n    {\r\n        TimeTagsBindable.CollectionChanged += (_, args) =>\r\n        {\r\n            switch (args.Action)\r\n            {\r\n                case NotifyCollectionChangedAction.Add:\r\n                    Debug.Assert(args.NewItems != null);\r\n\r\n                    foreach (var c in args.NewItems.Cast<TimeTag>())\r\n                    {\r\n                        c.TimingChanged += timingInvalidate;\r\n                        c.SyllableChanged += romanisationInvalidate;\r\n                    }\r\n\r\n                    break;\r\n\r\n                case NotifyCollectionChangedAction.Reset:\r\n                case NotifyCollectionChangedAction.Remove:\r\n                    Debug.Assert(args.OldItems != null);\r\n\r\n                    foreach (var c in args.OldItems.Cast<TimeTag>())\r\n                    {\r\n                        c.TimingChanged -= timingInvalidate;\r\n                        c.SyllableChanged -= romanisationInvalidate;\r\n                    }\r\n\r\n                    break;\r\n            }\r\n\r\n            updateLyricTime();\r\n\r\n            void timingInvalidate() => timeTagsTimingVersion.Value++;\r\n            void romanisationInvalidate() => timeTagsRomanisationVersion.Value++;\r\n        };\r\n\r\n        TimeTagsTimingVersion.ValueChanged += _ =>\r\n        {\r\n            updateLyricTime();\r\n        };\r\n\r\n        RubyTagsBindable.CollectionChanged += (_, args) =>\r\n        {\r\n            switch (args.Action)\r\n            {\r\n                case NotifyCollectionChangedAction.Add:\r\n                    Debug.Assert(args.NewItems != null);\r\n\r\n                    foreach (var c in args.NewItems.Cast<RubyTag>())\r\n                        c.Changed += invalidate;\r\n                    break;\r\n\r\n                case NotifyCollectionChangedAction.Reset:\r\n                case NotifyCollectionChangedAction.Remove:\r\n                    Debug.Assert(args.OldItems != null);\r\n\r\n                    foreach (var c in args.OldItems.Cast<RubyTag>())\r\n                        c.Changed -= invalidate;\r\n                    break;\r\n            }\r\n\r\n            void invalidate() => rubyTagsVersion.Value++;\r\n        };\r\n\r\n        SingerIdsBindable.CollectionChanged += (_, _) =>\r\n        {\r\n            updateStateByDataProperty(LyricWorkingProperty.Singers);\r\n        };\r\n\r\n        LockBindable.ValueChanged += e =>\r\n        {\r\n            lyricPropertyWritableVersion.Value++;\r\n        };\r\n\r\n        ReferenceLyricConfigBindable.ValueChanged += e =>\r\n        {\r\n            if (e.OldValue != null)\r\n            {\r\n                e.OldValue.Changed -= invalidate;\r\n            }\r\n\r\n            if (e.NewValue != null)\r\n            {\r\n                e.NewValue.Changed += invalidate;\r\n            }\r\n\r\n            void invalidate() => referenceLyricConfigVersion.Value++;\r\n        };\r\n\r\n        void updateLyricTime()\r\n        {\r\n            double? startTime = TimeTagsUtils.GetStartTime(TimeTags);\r\n            double? endTime = TimeTagsUtils.GetEndTime(TimeTags);\r\n\r\n            if (startTime != null && endTime != null)\r\n            {\r\n                StartTime = startTime.Value;\r\n                Duration = endTime.Value - StartTime;\r\n                TimeValid = true;\r\n            }\r\n            else\r\n            {\r\n                StartTime = 0;\r\n                Duration = 0;\r\n                TimeValid = false;\r\n            }\r\n        }\r\n    }\r\n\r\n    private void initReferenceLyricEvent()\r\n    {\r\n        ReferenceLyricConfigVersion.ValueChanged += e =>\r\n        {\r\n            ReferenceLyricBindable.TriggerChange();\r\n        };\r\n\r\n        ReferenceLyricConfigBindable.ValueChanged += e =>\r\n        {\r\n            ReferenceLyricBindable.TriggerChange();\r\n        };\r\n\r\n        ReferenceLyricBindable.ValueChanged += e =>\r\n        {\r\n            lyricPropertyWritableVersion.Value++;\r\n\r\n            // text.\r\n            bindValueChange(e, l => l.TextBindable, (lyric, config) =>\r\n            {\r\n                if (config is not SyncLyricConfig)\r\n                    return;\r\n\r\n                Text = lyric.Text;\r\n            });\r\n\r\n            // time-tags.\r\n            bindListValueChange(e, l => l.TimeTagsBindable, (lyric, config) =>\r\n            {\r\n                if (config is not SyncLyricConfig syncLyricConfig || !syncLyricConfig.SyncTimeTagProperty)\r\n                    return;\r\n\r\n                TimeTags = lyric.TimeTags.Select(x =>\r\n                {\r\n                    var newTimeTag = x.DeepClone();\r\n                    newTimeTag.Time += config.OffsetTime;\r\n                    return newTimeTag;\r\n                }).ToArray();\r\n            });\r\n\r\n            bindValueChange(e, l => l.TimeTagsTimingVersion, (_, config) =>\r\n            {\r\n                if (config is not SyncLyricConfig syncLyricConfig || !syncLyricConfig.SyncTimeTagProperty)\r\n                    return;\r\n\r\n                syncProperty(x => x.TimeTags, (from, to) =>\r\n                {\r\n                    to.Time = from.Time;\r\n                });\r\n            }, false);\r\n\r\n            bindValueChange(e, l => l.TimeTagsRomanisationVersion, (_, config) =>\r\n            {\r\n                if (config is not SyncLyricConfig syncLyricConfig || !syncLyricConfig.SyncTimeTagProperty)\r\n                    return;\r\n\r\n                syncProperty(x => x.TimeTags, (from, to) =>\r\n                {\r\n                    to.FirstSyllable = from.FirstSyllable;\r\n                    to.RomanisedSyllable = from.RomanisedSyllable;\r\n                });\r\n            }, false);\r\n\r\n            // todo: start time and end time?\r\n\r\n            // ruby-tags.\r\n            bindListValueChange(e, l => l.RubyTagsBindable, (lyric, config) =>\r\n            {\r\n                if (config is not SyncLyricConfig)\r\n                    return;\r\n\r\n                RubyTags = lyric.RubyTags.Select(x => x.DeepClone()).ToArray();\r\n            });\r\n\r\n            bindValueChange(e, l => l.RubyTagsVersion, (_, config) =>\r\n            {\r\n                if (config is not SyncLyricConfig)\r\n                    return;\r\n\r\n                syncProperty(x => x.RubyTags, (from, to) =>\r\n                {\r\n                    to.StartIndex = from.StartIndex;\r\n                    to.EndIndex = from.EndIndex;\r\n                    to.Text = from.Text;\r\n                });\r\n            }, false);\r\n\r\n            // todo: start-time, end-time and offset.\r\n\r\n            // singers.\r\n            bindListValueChange(e, l => l.SingerIdsBindable, (lyric, config) =>\r\n            {\r\n                if (config is not SyncLyricConfig syncLyricConfig || !syncLyricConfig.SyncSingerProperty)\r\n                    return;\r\n\r\n                SingerIds = lyric.SingerIds;\r\n            });\r\n\r\n            // translations.\r\n            bindDictionaryValueChange(e, l => l.TranslationsBindable, (lyric, config) =>\r\n            {\r\n                if (config is not SyncLyricConfig)\r\n                    return;\r\n\r\n                Translations = lyric.Translations;\r\n            });\r\n\r\n            // language.\r\n            bindValueChange(e, l => l.LanguageBindable, (lyric, config) =>\r\n            {\r\n                if (config is not SyncLyricConfig)\r\n                    return;\r\n\r\n                Language = lyric.Language;\r\n            });\r\n        };\r\n    }\r\n\r\n    private void bindValueChange<T>(ValueChangedEvent<Lyric?> e, Func<Lyric, IBindable<T>> getProperty, Action<Lyric, IReferenceLyricPropertyConfig> syncAction, bool triggerChangeOnBind = true)\r\n    {\r\n        if (e.OldValue != null)\r\n        {\r\n            getProperty(e.OldValue).ValueChanged -= propertyChanged;\r\n        }\r\n\r\n        if (e.NewValue != null)\r\n        {\r\n            getProperty(e.NewValue).ValueChanged += propertyChanged;\r\n\r\n            if (triggerChangeOnBind)\r\n                triggerPropertyChanged();\r\n        }\r\n\r\n        void propertyChanged(ValueChangedEvent<T> _) => triggerPropertyChanged();\r\n\r\n        void triggerPropertyChanged()\r\n        {\r\n            if (ReferenceLyricConfig == null || ReferenceLyric == null)\r\n                return;\r\n\r\n            // trigger change\r\n            syncAction(ReferenceLyric, ReferenceLyricConfig);\r\n        }\r\n    }\r\n\r\n    private void bindListValueChange<T>(ValueChangedEvent<Lyric?> e, Func<Lyric, IBindableList<T>> getProperty, Action<Lyric, IReferenceLyricPropertyConfig> syncAction,\r\n                                        bool triggerChangeOnBind = true)\r\n    {\r\n        if (e.OldValue != null)\r\n        {\r\n            getProperty(e.OldValue).CollectionChanged -= propertyChanged;\r\n        }\r\n\r\n        if (e.NewValue != null)\r\n        {\r\n            getProperty(e.NewValue).CollectionChanged += propertyChanged;\r\n\r\n            if (triggerChangeOnBind)\r\n                triggerPropertyChanged();\r\n        }\r\n\r\n        void propertyChanged(object? sender, NotifyCollectionChangedEventArgs _) => triggerPropertyChanged();\r\n\r\n        void triggerPropertyChanged()\r\n        {\r\n            if (ReferenceLyricConfig == null || ReferenceLyric == null)\r\n                return;\r\n\r\n            // trigger change\r\n            syncAction(ReferenceLyric, ReferenceLyricConfig);\r\n        }\r\n    }\r\n\r\n    private void bindDictionaryValueChange<TKey, TValue>(ValueChangedEvent<Lyric?> e, Func<Lyric, IBindableDictionary<TKey, TValue>> getProperty,\r\n                                                         Action<Lyric, IReferenceLyricPropertyConfig> syncAction, bool triggerChangeOnBind = true)\r\n        where TKey : notnull\r\n    {\r\n        if (e.OldValue != null)\r\n        {\r\n            getProperty(e.OldValue).CollectionChanged -= propertyChanged;\r\n        }\r\n\r\n        if (e.NewValue != null)\r\n        {\r\n            getProperty(e.NewValue).CollectionChanged += propertyChanged;\r\n\r\n            if (triggerChangeOnBind)\r\n                triggerPropertyChanged();\r\n        }\r\n\r\n        void propertyChanged(object? sender, NotifyDictionaryChangedEventArgs<TKey, TValue> _) => triggerPropertyChanged();\r\n\r\n        void triggerPropertyChanged()\r\n        {\r\n            if (ReferenceLyricConfig == null || ReferenceLyric == null)\r\n                return;\r\n\r\n            // trigger change\r\n            syncAction(ReferenceLyric, ReferenceLyricConfig);\r\n        }\r\n    }\r\n\r\n    private void syncProperty<TItem>(Func<Lyric, IList<TItem>> getProperty, Action<TItem, TItem> performPaste)\r\n    {\r\n        Debug.Assert(ReferenceLyric != null);\r\n\r\n        var fromList = getProperty(ReferenceLyric);\r\n        var toList = getProperty(this);\r\n\r\n        Debug.Assert(fromList.Count == toList.Count);\r\n\r\n        for (int i = 0; i < fromList.Count; i++)\r\n        {\r\n            performPaste(fromList[i], toList[i]);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Objects/Lyric_Working.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing Newtonsoft.Json;\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Extensions;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Types;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Workings;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Objects;\r\n\r\n/// <summary>\r\n/// Placing the properties that set by <see cref=\"KaraokeBeatmapProcessor\"/> or being calculated.\r\n/// Those properties will not be saved into the beatmap.\r\n/// </summary>\r\npublic partial class Lyric : IHasWorkingProperty<LyricWorkingProperty>\r\n{\r\n    [JsonIgnore]\r\n    private readonly LyricWorkingPropertyValidator workingPropertyValidator;\r\n\r\n    public bool InvalidateWorkingProperty(LyricWorkingProperty workingProperty)\r\n        => workingPropertyValidator.Invalidate(workingProperty);\r\n\r\n    private void updateStateByWorkingProperty(LyricWorkingProperty workingProperty)\r\n        => workingPropertyValidator.UpdateStateByWorkingProperty(workingProperty);\r\n\r\n    public LyricWorkingProperty[] GetAllInvalidWorkingProperties()\r\n        => workingPropertyValidator.GetAllInvalidFlags();\r\n\r\n    public void ValidateWorkingProperty(KaraokeBeatmap beatmap)\r\n    {\r\n        foreach (var flag in GetAllInvalidWorkingProperties())\r\n        {\r\n            switch (flag)\r\n            {\r\n                case LyricWorkingProperty.Singers:\r\n                    Singers = getSingers(beatmap, SingerIds);\r\n                    break;\r\n\r\n                case LyricWorkingProperty.Page:\r\n                    PageIndex = getPageIndex(beatmap, StartTime);\r\n                    break;\r\n\r\n                case LyricWorkingProperty.ReferenceLyric:\r\n                    ReferenceLyric = findLyricById(beatmap, ReferenceLyricId);\r\n                    break;\r\n\r\n                default:\r\n                    throw new ArgumentOutOfRangeException();\r\n            }\r\n        }\r\n\r\n        static IDictionary<Singer, SingerState[]> getSingers(KaraokeBeatmap beatmap, IEnumerable<ElementId> singerIds)\r\n            => beatmap.SingerInfo.GetSingerByIds(singerIds.ToArray());\r\n\r\n        static int? getPageIndex(KaraokeBeatmap beatmap, double startTime)\r\n            => beatmap.PageInfo.GetPageIndexAt(startTime);\r\n\r\n        static Lyric? findLyricById(IBeatmap beatmap, ElementId? id) =>\r\n            id == null ? null : beatmap.HitObjects.OfType<Lyric>().Single(x => x.ID == id);\r\n    }\r\n\r\n    /// <summary>\r\n    /// Min value of <see cref=\"TimeTags\"/> time.\r\n    /// </summary>\r\n    [JsonIgnore]\r\n    public override double StartTime\r\n    {\r\n        get => base.StartTime;\r\n        set => base.StartTime = value;\r\n    }\r\n\r\n    [JsonIgnore]\r\n    public readonly Bindable<double> DurationBindable = new BindableDouble();\r\n\r\n    /// <summary>\r\n    /// Duration of <see cref=\"TimeTags\"/>\r\n    /// </summary>\r\n    [JsonIgnore]\r\n    public double Duration\r\n    {\r\n        get => DurationBindable.Value;\r\n        set => DurationBindable.Value = value;\r\n    }\r\n\r\n    /// <summary>\r\n    /// Max value of <see cref=\"TimeTags\"/> time.\r\n    /// </summary>\r\n    [JsonIgnore]\r\n    public double EndTime => StartTime + Duration;\r\n\r\n    public bool TimeValid { get; private set; }\r\n\r\n    [JsonIgnore]\r\n    public readonly BindableDictionary<Singer, SingerState[]> SingersBindable = new();\r\n\r\n    /// <summary>\r\n    /// Singers\r\n    /// </summary>\r\n    [JsonIgnore]\r\n    public IDictionary<Singer, SingerState[]> Singers\r\n    {\r\n        get => SingersBindable;\r\n        set\r\n        {\r\n            SingersBindable.Clear();\r\n            SingersBindable.AddRange(value);\r\n            updateStateByWorkingProperty(LyricWorkingProperty.Singers);\r\n        }\r\n    }\r\n\r\n    [JsonIgnore]\r\n    public readonly Bindable<int?> PageIndexBindable = new();\r\n\r\n    /// <summary>\r\n    /// Order\r\n    /// </summary>\r\n    [JsonIgnore]\r\n    public int? PageIndex\r\n    {\r\n        get => PageIndexBindable.Value;\r\n        set\r\n        {\r\n            PageIndexBindable.Value = value;\r\n            updateStateByWorkingProperty(LyricWorkingProperty.Page);\r\n        }\r\n    }\r\n\r\n    [JsonIgnore]\r\n    public readonly Bindable<Lyric?> ReferenceLyricBindable = new();\r\n\r\n    /// <summary>\r\n    /// Reference lyric.\r\n    /// Link the same or similar lyric for reference or sync the properties.\r\n    /// </summary>\r\n    [JsonIgnore]\r\n    public Lyric? ReferenceLyric\r\n    {\r\n        get => ReferenceLyricBindable.Value;\r\n        set\r\n        {\r\n            ReferenceLyricBindable.Value = value;\r\n            updateStateByWorkingProperty(LyricWorkingProperty.ReferenceLyric);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Objects/Note.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing Newtonsoft.Json;\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Judgements;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.IO.Serialization;\r\nusing osu.Game.Rulesets.Karaoke.Judgements;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Types;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Workings;\r\nusing osu.Game.Rulesets.Karaoke.Scoring;\r\nusing osu.Game.Rulesets.Objects.Types;\r\nusing osu.Game.Rulesets.Scoring;\r\nusing osu.Game.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Objects;\r\n\r\npublic partial class Note : KaraokeHitObject, IHasPage, IHasDuration, IHasText, IDeepCloneable<Note>\r\n{\r\n    private void updateStateByDataProperty(NoteWorkingProperty workingProperty)\r\n        => workingPropertyValidator.UpdateStateByDataProperty(workingProperty);\r\n\r\n    [JsonIgnore]\r\n    public readonly Bindable<string> TextBindable = new();\r\n\r\n    /// <summary>\r\n    /// Text display on the note.\r\n    /// </summary>\r\n    /// <example>\r\n    /// 花\r\n    /// </example>\r\n    public string Text\r\n    {\r\n        get => TextBindable.Value;\r\n        set => TextBindable.Value = value;\r\n    }\r\n\r\n    [JsonIgnore]\r\n    public readonly Bindable<string?> RubyTextBindable = new();\r\n\r\n    /// <summary>\r\n    /// Ruby text.\r\n    /// Should placing something like ruby, 拼音 or ふりがな.\r\n    /// Will be display only if <see cref=\"KaraokeRulesetSetting.DisplayNoteRubyText\"/> is true.\r\n    /// </summary>\r\n    /// <example>\r\n    /// はな\r\n    /// </example>\r\n    public string? RubyText\r\n    {\r\n        get => RubyTextBindable.Value;\r\n        set => RubyTextBindable.Value = value;\r\n    }\r\n\r\n    [JsonIgnore]\r\n    public readonly Bindable<bool> DisplayBindable = new();\r\n\r\n    /// <summary>\r\n    /// Display this note\r\n    /// </summary>\r\n    public bool Display\r\n    {\r\n        get => DisplayBindable.Value;\r\n        set => DisplayBindable.Value = value;\r\n    }\r\n\r\n    [JsonIgnore]\r\n    public readonly Bindable<Tone> ToneBindable = new();\r\n\r\n    /// <summary>\r\n    /// Tone of this note\r\n    /// </summary>\r\n    public virtual Tone Tone\r\n    {\r\n        get => ToneBindable.Value;\r\n        set => ToneBindable.Value = value;\r\n    }\r\n\r\n    [JsonIgnore]\r\n    public readonly Bindable<double> StartTimeOffsetBindable = new BindableDouble();\r\n\r\n    /// <summary>\r\n    /// Offset time relative to the start time.\r\n    /// </summary>\r\n    public double StartTimeOffset\r\n    {\r\n        get => StartTimeOffsetBindable.Value;\r\n        set => StartTimeOffsetBindable.Value = value;\r\n    }\r\n\r\n    [JsonIgnore]\r\n    public readonly Bindable<double> EndTimeOffsetBindable = new BindableDouble();\r\n\r\n    /// <summary>\r\n    /// Offset time relative to the end time.\r\n    /// Negative value means the adjusted time is smaller than actual.\r\n    /// </summary>\r\n    public double EndTimeOffset\r\n    {\r\n        get => EndTimeOffsetBindable.Value;\r\n        set => EndTimeOffsetBindable.Value = value;\r\n    }\r\n\r\n    private ElementId? referenceLyricId;\r\n\r\n    public ElementId? ReferenceLyricId\r\n    {\r\n        get => referenceLyricId;\r\n        set\r\n        {\r\n            referenceLyricId = value;\r\n            updateStateByDataProperty(NoteWorkingProperty.ReferenceLyric);\r\n        }\r\n    }\r\n\r\n    [JsonIgnore]\r\n    public readonly Bindable<int> ReferenceTimeTagIndexBindable = new();\r\n\r\n    public int ReferenceTimeTagIndex\r\n    {\r\n        get => ReferenceTimeTagIndexBindable.Value;\r\n        set => ReferenceTimeTagIndexBindable.Value = value;\r\n    }\r\n\r\n    public Note()\r\n    {\r\n        workingPropertyValidator = new NoteWorkingPropertyValidator(this);\r\n\r\n        initInternalBindingEvent();\r\n        initReferenceLyricEvent();\r\n    }\r\n\r\n    public override Judgement CreateJudgement() => new KaraokeNoteJudgement();\r\n\r\n    protected override HitWindows CreateHitWindows() => new KaraokeNoteHitWindows();\r\n\r\n    public Note DeepClone()\r\n    {\r\n        string serializeString = JsonConvert.SerializeObject(this, KaraokeJsonSerializableExtensions.CreateGlobalSettings());\r\n        var note = JsonConvert.DeserializeObject<Note>(serializeString, KaraokeJsonSerializableExtensions.CreateGlobalSettings())!;\r\n\r\n        note.ReferenceLyric = ReferenceLyric;\r\n\r\n        return note;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Objects/Note_Binding.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Objects;\r\n\r\n/// <summary>\r\n/// Placing the binding-related logic.\r\n/// </summary>\r\npublic partial class Note\r\n{\r\n    private void initInternalBindingEvent()\r\n    {\r\n        StartTimeOffsetBindable.ValueChanged += _ => syncStartTimeAndDurationFromTimeTag();\r\n        EndTimeOffsetBindable.ValueChanged += _ => syncStartTimeAndDurationFromTimeTag();\r\n        ReferenceTimeTagIndexBindable.ValueChanged += _ => syncStartTimeAndDurationFromTimeTag();\r\n    }\r\n\r\n    private void initReferenceLyricEvent()\r\n    {\r\n        ReferenceLyricBindable.ValueChanged += e =>\r\n        {\r\n            if (e.OldValue != null)\r\n                e.OldValue.TimeTagsTimingVersion.ValueChanged -= timeTagsTimingVersionChanged;\r\n\r\n            if (e.NewValue != null)\r\n                e.NewValue.TimeTagsTimingVersion.ValueChanged += timeTagsTimingVersionChanged;\r\n\r\n            syncStartTimeAndDurationFromTimeTag();\r\n            syncReferenceLyricSingers();\r\n        };\r\n\r\n        void timeTagsTimingVersionChanged(ValueChangedEvent<int> e) => syncStartTimeAndDurationFromTimeTag();\r\n    }\r\n\r\n    private void syncStartTimeAndDurationFromTimeTag()\r\n    {\r\n        var startTimeTag = StartReferenceTimeTag;\r\n        var endTimeTag = EndReferenceTimeTag;\r\n\r\n        double startTime = startTimeTag?.Time ?? 0;\r\n        double endTime = endTimeTag?.Time ?? 0;\r\n        double duration = endTime - startTime;\r\n\r\n        StartTimeBindable.Value = startTimeTag == null ? 0 : startTime + StartTimeOffset;\r\n        DurationBindable.Value = endTimeTag == null ? 0 : Math.Max(duration - StartTimeOffset + EndTimeOffset, 0);\r\n    }\r\n\r\n    private void syncReferenceLyricSingers()\r\n    {\r\n        Singers = ReferenceLyricBindable.Value?.Singers ?? new Dictionary<Singer, SingerState[]>();\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Objects/Note_Working.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing Newtonsoft.Json;\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Extensions;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Types;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Workings;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Objects;\r\n\r\n/// <summary>\r\n/// Placing the properties that set by <see cref=\"KaraokeBeatmapProcessor\"/> or being calculated.\r\n/// Those properties will not be saved into the beatmap.\r\n/// </summary>\r\npublic partial class Note : IHasWorkingProperty<NoteWorkingProperty>\r\n{\r\n    [JsonIgnore]\r\n    private readonly NoteWorkingPropertyValidator workingPropertyValidator;\r\n\r\n    public bool InvalidateWorkingProperty(NoteWorkingProperty workingProperty)\r\n        => workingPropertyValidator.Invalidate(workingProperty);\r\n\r\n    private void updateStateByWorkingProperty(NoteWorkingProperty workingProperty)\r\n        => workingPropertyValidator.UpdateStateByWorkingProperty(workingProperty);\r\n\r\n    public NoteWorkingProperty[] GetAllInvalidWorkingProperties()\r\n        => workingPropertyValidator.GetAllInvalidFlags();\r\n\r\n    public void ValidateWorkingProperty(KaraokeBeatmap beatmap)\r\n    {\r\n        foreach (var flag in GetAllInvalidWorkingProperties())\r\n        {\r\n            switch (flag)\r\n            {\r\n                case NoteWorkingProperty.Page:\r\n                    PageIndex = getPageIndex(beatmap, StartTime);\r\n                    break;\r\n\r\n                case NoteWorkingProperty.ReferenceLyric:\r\n                    ReferenceLyric = findLyricById(beatmap, ReferenceLyricId);\r\n                    break;\r\n\r\n                default:\r\n                    throw new ArgumentOutOfRangeException();\r\n            }\r\n        }\r\n\r\n        static int? getPageIndex(KaraokeBeatmap beatmap, double startTime)\r\n            => beatmap.PageInfo.GetPageIndexAt(startTime);\r\n\r\n        static Lyric? findLyricById(IBeatmap beatmap, ElementId? id) =>\r\n            id == null ? null : beatmap.HitObjects.OfType<Lyric>().Single(x => x.ID == id);\r\n    }\r\n\r\n    [JsonIgnore]\r\n    public readonly Bindable<int?> PageIndexBindable = new();\r\n\r\n    /// <summary>\r\n    /// Order\r\n    /// </summary>\r\n    [JsonIgnore]\r\n    public int? PageIndex\r\n    {\r\n        get => PageIndexBindable.Value;\r\n        set\r\n        {\r\n            PageIndexBindable.Value = value;\r\n            updateStateByWorkingProperty(NoteWorkingProperty.Page);\r\n        }\r\n    }\r\n\r\n    /// <summary>\r\n    /// Start time.\r\n    /// There's no need to save the time because it's calculated by the <see cref=\"TimeTag\"/>\r\n    /// </summary>\r\n    [JsonIgnore]\r\n    public override double StartTime\r\n    {\r\n        get => base.StartTime;\r\n        set => throw new NotSupportedException($\"The time will auto-sync via {nameof(ReferenceLyric)} and {nameof(ReferenceTimeTagIndex)}.\");\r\n    }\r\n\r\n    [JsonIgnore]\r\n    public readonly Bindable<double> DurationBindable = new BindableDouble();\r\n\r\n    /// <summary>\r\n    /// Duration.\r\n    /// There's no need to save the time because it's calculated by the <see cref=\"TimeTag\"/>\r\n    /// </summary>\r\n    [JsonIgnore]\r\n    public double Duration\r\n    {\r\n        get => DurationBindable.Value;\r\n        set => throw new NotSupportedException($\"The time will auto-sync via {nameof(ReferenceLyric)} and {nameof(ReferenceTimeTagIndex)}.\");\r\n    }\r\n\r\n    /// <summary>\r\n    /// End time.\r\n    /// There's no need to save the time because it's calculated by the <see cref=\"TimeTag\"/>\r\n    /// </summary>\r\n    [JsonIgnore]\r\n    public double EndTime => StartTime + Duration;\r\n\r\n    [JsonIgnore]\r\n    public readonly Bindable<Lyric?> ReferenceLyricBindable = new();\r\n\r\n    [JsonIgnore]\r\n    public readonly BindableDictionary<Singer, SingerState[]> SingersBindable = new();\r\n\r\n    /// <summary>\r\n    /// Singers\r\n    /// </summary>\r\n    [JsonIgnore]\r\n    public IDictionary<Singer, SingerState[]> Singers\r\n    {\r\n        get => SingersBindable;\r\n        set\r\n        {\r\n            SingersBindable.Clear();\r\n            SingersBindable.AddRange(value);\r\n        }\r\n    }\r\n\r\n    /// <summary>\r\n    /// Relative lyric.\r\n    /// Technically parent lyric will not change after assign, but should not restrict in model layer.\r\n    /// </summary>\r\n    [JsonIgnore]\r\n    public Lyric? ReferenceLyric\r\n    {\r\n        get => ReferenceLyricBindable.Value;\r\n        set\r\n        {\r\n            ReferenceLyricBindable.Value = value;\r\n            updateStateByWorkingProperty(NoteWorkingProperty.ReferenceLyric);\r\n        }\r\n    }\r\n\r\n    public TimeTag? StartReferenceTimeTag => ReferenceLyric?.TimeTags.ElementAtOrDefault(ReferenceTimeTagIndex);\r\n\r\n    public TimeTag? EndReferenceTimeTag => ReferenceLyric?.TimeTags.ElementAtOrDefault(ReferenceTimeTagIndex + 1);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Objects/Properties/IReferenceLyricPropertyConfig.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Objects.Properties;\r\n\r\npublic interface IReferenceLyricPropertyConfig\r\n{\r\n    double OffsetTime { get; set; }\r\n\r\n    public event Action? Changed;\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Objects/Properties/ReferenceLyricConfig.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing Newtonsoft.Json;\r\nusing osu.Framework.Bindables;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Objects.Properties;\r\n\r\npublic class ReferenceLyricConfig : IReferenceLyricPropertyConfig\r\n{\r\n    public event Action? Changed;\r\n\r\n    public ReferenceLyricConfig()\r\n    {\r\n        OffsetTimeBindable.ValueChanged += _ => Changed?.Invoke();\r\n    }\r\n\r\n    [JsonIgnore]\r\n    public readonly Bindable<double> OffsetTimeBindable = new BindableDouble();\r\n\r\n    public double OffsetTime\r\n    {\r\n        get => OffsetTimeBindable.Value;\r\n        set => OffsetTimeBindable.Value = value;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Objects/Properties/SyncLyricConfig.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.ComponentModel;\r\nusing System.Text.Json.Serialization;\r\nusing osu.Framework.Bindables;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Objects.Properties;\r\n\r\npublic class SyncLyricConfig : IReferenceLyricPropertyConfig\r\n{\r\n    public event Action? Changed;\r\n\r\n    public SyncLyricConfig()\r\n    {\r\n        OffsetTimeBindable.ValueChanged += _ => Changed?.Invoke();\r\n        SyncSingerPropertyBindable.ValueChanged += _ => Changed?.Invoke();\r\n        SyncTimeTagPropertyBindable.ValueChanged += _ => Changed?.Invoke();\r\n    }\r\n\r\n    [JsonIgnore]\r\n    public readonly Bindable<double> OffsetTimeBindable = new BindableDouble();\r\n\r\n    public double OffsetTime\r\n    {\r\n        get => OffsetTimeBindable.Value;\r\n        set => OffsetTimeBindable.Value = value;\r\n    }\r\n\r\n    [JsonIgnore]\r\n    public readonly Bindable<bool> SyncSingerPropertyBindable = new BindableBool(true);\r\n\r\n    /// <summary>\r\n    /// Sync the singer from referenced lyric.\r\n    /// </summary>\r\n    [DefaultValue(true)]\r\n    public bool SyncSingerProperty\r\n    {\r\n        get => SyncSingerPropertyBindable.Value;\r\n        set => SyncSingerPropertyBindable.Value = value;\r\n    }\r\n\r\n    [JsonIgnore]\r\n    public readonly Bindable<bool> SyncTimeTagPropertyBindable = new BindableBool(true);\r\n\r\n    /// <summary>\r\n    /// Sync the time-tags from referenced lyric.\r\n    /// </summary>\r\n    [DefaultValue(true)]\r\n    public bool SyncTimeTagProperty\r\n    {\r\n        get => SyncTimeTagPropertyBindable.Value;\r\n        set => SyncTimeTagPropertyBindable.Value = value;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Objects/RubyTag.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing Newtonsoft.Json;\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Objects;\r\n\r\npublic class RubyTag : IDeepCloneable<RubyTag>\r\n{\r\n    /// <summary>\r\n    /// Invoked when any property of this <see cref=\"RubyTag\"/> is changed.\r\n    /// </summary>\r\n    public event Action? Changed;\r\n\r\n    public RubyTag()\r\n    {\r\n        TextBindable.ValueChanged += _ => Changed?.Invoke();\r\n        StartIndexBindable.ValueChanged += _ => Changed?.Invoke();\r\n        EndIndexBindable.ValueChanged += _ => Changed?.Invoke();\r\n    }\r\n\r\n    [JsonIgnore]\r\n    public readonly Bindable<string> TextBindable = new(string.Empty);\r\n\r\n    /// <summary>\r\n    /// If kanji Matched, then apply ruby\r\n    /// </summary>\r\n    public string Text\r\n    {\r\n        get => TextBindable.Value;\r\n        set => TextBindable.Value = value;\r\n    }\r\n\r\n    [JsonIgnore]\r\n    public readonly BindableInt StartIndexBindable = new();\r\n\r\n    /// <summary>\r\n    /// Start index\r\n    /// </summary>\r\n    public int StartIndex\r\n    {\r\n        get => StartIndexBindable.Value;\r\n        set => StartIndexBindable.Value = value;\r\n    }\r\n\r\n    [JsonIgnore]\r\n    public readonly BindableInt EndIndexBindable = new();\r\n\r\n    /// <summary>\r\n    /// End index\r\n    /// </summary>\r\n    public int EndIndex\r\n    {\r\n        get => EndIndexBindable.Value;\r\n        set => EndIndexBindable.Value = value;\r\n    }\r\n\r\n    public RubyTag DeepClone()\r\n        => new()\r\n        {\r\n            Text = Text,\r\n            StartIndex = StartIndex,\r\n            EndIndex = EndIndex,\r\n        };\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Objects/TimeTag.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing Newtonsoft.Json;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Objects;\r\n\r\npublic class TimeTag : IDeepCloneable<TimeTag>\r\n{\r\n    /// <summary>\r\n    /// Invoked when <see cref=\"Time\"/> of this <see cref=\"TimeTag\"/> is changed.\r\n    /// </summary>\r\n    public event Action? TimingChanged;\r\n\r\n    /// <summary>\r\n    /// Invoked when <see cref=\"FirstSyllable\"/> or <see cref=\"RomanisedSyllable\"/> of this <see cref=\"TimeTag\"/> is changed.\r\n    /// </summary>\r\n    public event Action? SyllableChanged;\r\n\r\n    public TimeTag(TextIndex index, double? time = null)\r\n    {\r\n        Index = index;\r\n        Time = time;\r\n\r\n        TimeBindable.ValueChanged += _ => TimingChanged?.Invoke();\r\n        FirstSyllableBindable.ValueChanged += _ => SyllableChanged?.Invoke();\r\n        RomanisedSyllableBindable.ValueChanged += _ => SyllableChanged?.Invoke();\r\n    }\r\n\r\n    /// <summary>\r\n    /// Time tag's index.\r\n    /// Notice that this index means index of characters.\r\n    /// </summary>\r\n    public TextIndex Index { get; }\r\n\r\n    [JsonIgnore]\r\n    public readonly Bindable<double?> TimeBindable = new();\r\n\r\n    /// <summary>\r\n    /// Time\r\n    /// </summary>\r\n    public double? Time\r\n    {\r\n        get => TimeBindable.Value;\r\n        set => TimeBindable.Value = value;\r\n    }\r\n\r\n    [JsonIgnore]\r\n    public readonly Bindable<bool> FirstSyllableBindable = new();\r\n\r\n    /// <summary>\r\n    /// Mark if this romanised syllable is the first letter of the romanisation.\r\n    /// </summary>\r\n    /// <example>\r\n    /// There's the Japanese lyric:<br/>\r\n    /// 枯れた世界に輝く<br/>\r\n    /// There's the romanisation:<br/>\r\n    /// kareta sekai ni kagayaku.<br/>\r\n    /// And it will be separated as:<br/>\r\n    /// ka|re|ta se|kai ni ka|ga|ya|ku.<br/>\r\n    /// If this is the first or(4th) time-tag, then this value should be true.<br/>\r\n    /// If this ts the 2th or 3th time-tag, then this value should be false.<br/>\r\n    /// </example>\r\n    public bool FirstSyllable\r\n    {\r\n        get => FirstSyllableBindable.Value;\r\n        set => FirstSyllableBindable.Value = value;\r\n    }\r\n\r\n    [JsonIgnore]\r\n    public readonly Bindable<string?> RomanisedSyllableBindable = new();\r\n\r\n    /// <summary>\r\n    /// Romanised syllable\r\n    /// </summary>\r\n    /// <example>\r\n    /// Ka, ra, o, ke.\r\n    /// </example>\r\n    public string? RomanisedSyllable\r\n    {\r\n        get => RomanisedSyllableBindable.Value;\r\n        set => RomanisedSyllableBindable.Value = value;\r\n    }\r\n\r\n    public TimeTag DeepClone()\r\n    {\r\n        return new TimeTag(Index, Time)\r\n        {\r\n            FirstSyllable = FirstSyllable,\r\n            RomanisedSyllable = RomanisedSyllable,\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Objects/Title.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Game.Rulesets.Objects.Types;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Objects;\r\n\r\npublic class Title : KaraokeHitObject, IHasDuration\r\n{\r\n    public string Name { get; set; } = null!;\r\n\r\n    public int KaraokeLayoutIndex { get; set; }\r\n\r\n    public double Duration { get; set; }\r\n\r\n    public double EndTime => StartTime + Duration;\r\n\r\n    public int LineInterval { get; set; }\r\n\r\n    public bool ShowRuby { get; set; }\r\n\r\n    public IList<TitlePart> TitleParts { get; set; } = new List<TitlePart>();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Objects/TitlePart.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Objects;\r\n\r\npublic class TitlePart\r\n{\r\n    public string Title { get; set; } = null!;\r\n\r\n    public int KaraokeFontIndex { get; set; }\r\n\r\n    public bool Continuous { get; set; }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Objects/Tone.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Objects;\r\n\r\npublic struct Tone : IComparable<Tone>, IComparable<int>, IEquatable<Tone>, IEquatable<int>\r\n{\r\n    public int Scale { get; set; }\r\n\r\n    public bool Half { get; set; }\r\n\r\n    public Tone(int scale, bool half = false)\r\n    {\r\n        Scale = scale;\r\n        Half = half;\r\n    }\r\n\r\n    public int CompareTo(Tone other)\r\n    {\r\n        return ComparableUtils.CompareByProperty(this, other,\r\n            t => t.Scale,\r\n            t => t.Half);\r\n    }\r\n\r\n    public int CompareTo(int other)\r\n    {\r\n        return CompareTo(new Tone(other));\r\n    }\r\n\r\n    public bool Equals(Tone other) => Scale == other.Scale && Half == other.Half;\r\n\r\n    public bool Equals(int other) => Scale == other && Half == false;\r\n\r\n    public override bool Equals(object? obj)\r\n    {\r\n        return obj switch\r\n        {\r\n            Tone tone => Equals(tone),\r\n            int intValue => Equals(intValue),\r\n            _ => false,\r\n        };\r\n    }\r\n\r\n    public override int GetHashCode() => HashCode.Combine(Scale, Half);\r\n\r\n    public static Tone operator +(Tone left, Tone right) => add(left, right);\r\n\r\n    public static Tone operator +(Tone tone1, int scale) => tone1 + new Tone { Scale = scale };\r\n\r\n    private static Tone add(Tone tone1, Tone tone2) => new()\r\n    {\r\n        Scale = tone1.Scale + tone2.Scale + (tone1.Half && tone2.Half ? 1 : 0),\r\n        Half = tone1.Half ^ tone2.Half,\r\n    };\r\n\r\n    public static Tone operator -(Tone tone1, Tone tone2) => subtract(tone1, tone2);\r\n\r\n    public static Tone operator -(Tone tone1, int scale) => tone1 - new Tone { Scale = scale };\r\n\r\n    private static Tone subtract(Tone tone1, Tone tone2) => tone1 + -tone2;\r\n\r\n    public static Tone operator -(Tone tone) => negate(tone);\r\n\r\n    private static Tone negate(Tone tone) => tone with\r\n    {\r\n        Scale = -tone.Scale + (tone.Half ? -1 : 0),\r\n    };\r\n\r\n    public static bool operator ==(Tone tone1, Tone tone2) => tone1.Equals(tone2);\r\n\r\n    public static bool operator !=(Tone tone1, Tone tone2) => !tone1.Equals(tone2);\r\n\r\n    public static bool operator ==(Tone tone1, int tone2) => tone1.Equals(tone2);\r\n\r\n    public static bool operator !=(Tone tone1, int tone2) => !tone1.Equals(tone2);\r\n\r\n    public static bool operator >(Tone tone1, Tone tone2) => tone1.CompareTo(tone2) > 0;\r\n\r\n    public static bool operator >=(Tone tone1, Tone tone2) => tone1.CompareTo(tone2) >= 0;\r\n\r\n    public static bool operator <(Tone tone1, Tone tone2) => tone1.CompareTo(tone2) < 0;\r\n\r\n    public static bool operator <=(Tone tone1, Tone tone2) => tone1.CompareTo(tone2) <= 0;\r\n\r\n    public static bool operator >(Tone tone1, int tone2) => tone1.CompareTo(tone2) > 0;\r\n\r\n    public static bool operator >=(Tone tone1, int tone2) => tone1.CompareTo(tone2) >= 0;\r\n\r\n    public static bool operator <(Tone tone1, int tone2) => tone1.CompareTo(tone2) < 0;\r\n\r\n    public static bool operator <=(Tone tone1, int tone2) => tone1.CompareTo(tone2) <= 0;\r\n\r\n    public override string ToString() => $\"Scale:{Scale}, Half:{Half}\";\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Objects/Types/IHasLock.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.ComponentModel;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Objects.Types;\r\n\r\npublic interface IHasLock\r\n{\r\n    LockState Lock { get; set; }\r\n}\r\n\r\npublic enum LockState\r\n{\r\n    [Description(\"None\")]\r\n    None,\r\n\r\n    [Description(\"Partial\")]\r\n    Partial,\r\n\r\n    [Description(\"Full\")]\r\n    Full,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Objects/Types/IHasOrder.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Objects.Types;\r\n\r\npublic interface IHasOrder\r\n{\r\n    int Order { get; set; }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Objects/Types/IHasPage.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Objects.Types;\r\n\r\npublic interface IHasPage\r\n{\r\n    int? PageIndex { get; set; }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Objects/Types/IHasSingers.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Objects.Types;\r\n\r\npublic interface IHasSingers\r\n{\r\n    IList<ElementId> SingerIds { get; }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Objects/Types/IHasText.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Objects.Types;\r\n\r\npublic interface IHasText\r\n{\r\n    string Text { get; set; }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Objects/Types/IHasWorkingProperty.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Objects.Types;\r\n\r\npublic interface IHasWorkingProperty<TWorkingProperty> : IHasWorkingProperty\r\n    where TWorkingProperty : struct, Enum\r\n{\r\n    bool InvalidateWorkingProperty(TWorkingProperty workingProperty);\r\n\r\n    TWorkingProperty[] GetAllInvalidWorkingProperties();\r\n}\r\n\r\npublic interface IHasWorkingProperty\r\n{\r\n    void ValidateWorkingProperty(KaraokeBeatmap beatmap);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Objects/Utils/LyricUtils.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Extensions;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Objects.Utils;\r\n\r\npublic static class LyricUtils\r\n{\r\n    #region progessing\r\n\r\n    public static void RemoveText(Lyric lyric, int charGap, int count = 1)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(lyric);\r\n\r\n        int textLength = lyric.Text.Length;\r\n        if (textLength == 0)\r\n            return;\r\n\r\n        if (charGap < 0 || charGap > textLength)\r\n            throw new ArgumentOutOfRangeException(nameof(charGap));\r\n\r\n        if (count < 0)\r\n            throw new ArgumentOutOfRangeException(nameof(charGap));\r\n\r\n        if (charGap + count >= textLength)\r\n            count = textLength - charGap;\r\n\r\n        // deal with ruby and romanisation.\r\n        // might remove and shifting.\r\n        lyric.RubyTags = processTags(lyric.RubyTags, charGap, count);\r\n        lyric.TimeTags = processTimeTags(lyric.TimeTags, charGap, count);\r\n\r\n        // deal with text\r\n        string newLyric = lyric.Text[..charGap] + lyric.Text[(charGap + count)..];\r\n        lyric.Text = newLyric;\r\n\r\n        static IList<RubyTag> processTags(IList<RubyTag> tags, int charGap, int count)\r\n        {\r\n            // shifting index.\r\n            foreach (var tag in tags)\r\n            {\r\n                if (tag.StartIndex > charGap + count)\r\n                {\r\n                    tag.StartIndex -= count;\r\n                    tag.EndIndex -= count;\r\n                }\r\n                else if (tag.StartIndex > charGap)\r\n                {\r\n                    tag.StartIndex = charGap;\r\n                    tag.EndIndex -= count;\r\n                }\r\n                else if (tag.EndIndex >= charGap)\r\n                {\r\n                    tag.EndIndex = Math.Max(charGap - 1, tag.EndIndex - count);\r\n                }\r\n            }\r\n\r\n            // if end index less or equal than start index, means this tag has been deleted.\r\n            return tags.Where(x => x.StartIndex <= x.EndIndex).ToArray();\r\n        }\r\n\r\n        static IList<TimeTag> processTimeTags(IEnumerable<TimeTag> timeTags, int charGap, int count)\r\n        {\r\n            int endCharGap = charGap + count;\r\n            return timeTags.Where(x => !(x.Index.Index >= charGap && x.Index.Index < endCharGap))\r\n                           .Select(t => t.Index.Index > charGap ? TimeTagUtils.ShiftingTimeTag(t, -count) : t)\r\n                           .ToArray();\r\n        }\r\n    }\r\n\r\n    public static void AddText(Lyric lyric, int charGap, string text)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(lyric);\r\n\r\n        // make position is at the range.\r\n        string lyricText = lyric.Text;\r\n        int lyricTextLength = lyricText.Length;\r\n        charGap = Math.Clamp(charGap, 0, lyricTextLength);\r\n\r\n        int offset = text.Length;\r\n        if (offset == 0)\r\n            return;\r\n\r\n        // deal with ruby and romanisation with shifting.\r\n        lyric.RubyTags = processTags(lyric.RubyTags, charGap, offset);\r\n        lyric.TimeTags = processTimeTags(lyric.TimeTags, charGap, offset);\r\n\r\n        // deal with text\r\n        string newLyricText = lyricText[..charGap] + text + lyricText[charGap..];\r\n        lyric.Text = newLyricText;\r\n\r\n        static RubyTag[] processTags(IEnumerable<RubyTag> tags, int charGap, int offset) =>\r\n            tags.Select(x =>\r\n                {\r\n                    if (x.StartIndex >= charGap)\r\n                        x.StartIndex += offset;\r\n                    if (x.EndIndex >= charGap)\r\n                        x.EndIndex += offset;\r\n                    return x;\r\n                })\r\n                .ToArray();\r\n\r\n        static TimeTag[] processTimeTags(IEnumerable<TimeTag> timeTags, int charGap, int offset)\r\n            => timeTags.Select(t => t.Index.Index >= charGap ? TimeTagUtils.ShiftingTimeTag(t, offset) : t).ToArray();\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Time tag\r\n\r\n    public static bool HasTimedTimeTags(Lyric lyric)\r\n        => lyric.TimeTags.Any(x => x.Time.HasValue);\r\n\r\n    public static string GetTimeTagIndexDisplayText(Lyric lyric, TextIndex index)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(lyric);\r\n\r\n        string text = lyric.Text;\r\n        if (string.IsNullOrEmpty(text))\r\n            throw new ArgumentNullException(nameof(text));\r\n\r\n        // not showing text if index out of range.\r\n        if (index.Index < 0 || index.Index >= text.Length)\r\n            return \"-\";\r\n\r\n        var timeTags = lyric.TimeTags;\r\n\r\n        return TextIndexUtils.GetValueByState(index, () =>\r\n        {\r\n            var nextTimeTag = timeTags.FirstOrDefault(x => x.Index > index);\r\n            int startIndex = index.Index;\r\n            int endIndex = TextIndexUtils.ToGapIndex(nextTimeTag?.Index ?? new TextIndex(text.Length));\r\n            return $\"{text.Substring(startIndex, endIndex - startIndex)}-\";\r\n        }, () =>\r\n        {\r\n            var previousTimeTag = timeTags.Reverse().FirstOrDefault(x => x.Index < index);\r\n            int startIndex = previousTimeTag?.Index.Index ?? 0;\r\n            int endIndex = index.Index + 1;\r\n            return $\"-{text.Substring(startIndex, endIndex - startIndex)}\";\r\n        });\r\n    }\r\n\r\n    public static string GetTimeTagDisplayText(Lyric lyric, TimeTag timeTag)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(timeTag);\r\n\r\n        return GetTimeTagIndexDisplayText(lyric, timeTag.Index);\r\n    }\r\n\r\n    public static string GetTimeTagDisplayRubyText(Lyric lyric, TimeTag timeTag)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(timeTag);\r\n\r\n        var state = timeTag.Index.State;\r\n\r\n        // should check has ruby in target lyric with target index.\r\n        var matchRuby = lyric.RubyTags.Where(x =>\r\n        {\r\n            int charIndex = TextIndexUtils.ToCharIndex(timeTag.Index);\r\n            return TextIndexUtils.GetValueByState(state,\r\n                () => x.StartIndex <= charIndex && x.EndIndex >= charIndex,\r\n                () => x.StartIndex <= charIndex && x.EndIndex >= charIndex);\r\n        }).FirstOrDefault();\r\n\r\n        if (matchRuby == null || string.IsNullOrEmpty(matchRuby.Text))\r\n            return GetTimeTagDisplayText(lyric, timeTag);\r\n\r\n        // get all the rubies with same index.\r\n        var timeTagsWithSameIndex = lyric.TimeTags.Where(x =>\r\n        {\r\n            var startTextIndex = new TextIndex(matchRuby.StartIndex);\r\n            var endTextIndex = new TextIndex(matchRuby.EndIndex, TextIndex.IndexState.End);\r\n\r\n            return x.Index >= startTextIndex && x.Index <= endTextIndex;\r\n        }).ToList();\r\n\r\n        // get ruby text and should notice exceed case if time-tag is more than ruby text.\r\n        int index = timeTagsWithSameIndex.IndexOf(timeTag);\r\n        string text = matchRuby.Text;\r\n        string subtext = timeTagsWithSameIndex.Count == 1 ? text : text.Substring(Math.Min(text.Length - 1, index), 1);\r\n\r\n        // return substring with format.\r\n        return TextIndexUtils.GetValueByState(state, $\"({subtext})-\", $\"-({subtext})\");\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Ruby tag\r\n\r\n    public static bool AbleToInsertRubyTagAtIndex(Lyric lyric, int index)\r\n        => index >= 0 && index <= lyric.Text.Length;\r\n\r\n    #endregion\r\n\r\n    #region Time display\r\n\r\n    public static string LyricTimeFormattedString(Lyric lyric)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(lyric);\r\n\r\n        string startTime = lyric.StartTime.ToEditorFormattedString();\r\n        string endTime = lyric.EndTime.ToEditorFormattedString();\r\n        return $\"{startTime} - {endTime}\";\r\n    }\r\n\r\n    public static string TimeTagTimeFormattedString(Lyric lyric)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(lyric);\r\n\r\n        var availableTimeTags = lyric.TimeTags.Where(x => x.Time != null).ToArray();\r\n        var minTimeTag = availableTimeTags.MinBy(x => x.Time);\r\n        var maxTimeTag = availableTimeTags.MaxBy(x => x.Time);\r\n\r\n        string startTime = TimeTagUtils.FormattedString(minTimeTag ?? new TimeTag(new TextIndex()));\r\n        string endTime = TimeTagUtils.FormattedString(maxTimeTag ?? new TimeTag(new TextIndex()));\r\n        return $\"{startTime} - {endTime}\";\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Singer\r\n\r\n    public static bool ContainsSinger(Lyric lyric, Singer singer)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(lyric);\r\n        ArgumentNullException.ThrowIfNull(singer);\r\n\r\n        return lyric.SingerIds.Contains(singer.ID);\r\n    }\r\n\r\n    public static bool OnlyContainsSingers(Lyric lyric, List<Singer> singers)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(singers);\r\n\r\n        var singerIds = singers.Select(x => x.ID);\r\n        return lyric.SingerIds.All(x => singerIds.Contains(x));\r\n    }\r\n\r\n    #endregion\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Objects/Utils/LyricsUtils.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Globalization;\r\nusing System.Linq;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Objects.Utils;\r\n\r\npublic static class LyricsUtils\r\n{\r\n    #region processing\r\n\r\n    public static Tuple<Lyric, Lyric> SplitLyric(Lyric lyric, int splitIndex)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(lyric);\r\n\r\n        string lyricText = lyric.Text;\r\n        if (string.IsNullOrEmpty(lyricText))\r\n            throw new ArgumentNullException(nameof(lyricText));\r\n\r\n        if (splitIndex < 0 || splitIndex > lyricText.Length)\r\n            throw new ArgumentOutOfRangeException(nameof(splitIndex));\r\n\r\n        if (splitIndex == 0 || splitIndex == lyricText.Length)\r\n            throw new InvalidOperationException($\"{nameof(splitIndex)} cannot cut at first or last index.\");\r\n\r\n        var firstTimeTag = lyric.TimeTags.Where(x => x.Index.Index < splitIndex).ToList();\r\n        var secondTimeTag = lyric.TimeTags.Where(x => x.Index.Index >= splitIndex).ToList();\r\n\r\n        // add delta time-tag if does not have end time-tag.\r\n        if (firstTimeTag.Count > 0 && secondTimeTag.Count > 0)\r\n        {\r\n            var firstTag = firstTimeTag.LastOrDefault();\r\n            var secondTag = secondTimeTag.FirstOrDefault();\r\n\r\n            if (firstTag != null && secondTag != null)\r\n            {\r\n                // add end tag at end of first lyric if does not have tag in there.\r\n                if (!firstTimeTag.Any(x => x.Index.Index == splitIndex - 1 && x.Index.State == TextIndex.IndexState.End))\r\n                {\r\n                    var endTagIndex = new TextIndex(splitIndex - 1, TextIndex.IndexState.End);\r\n                    var endTag = TimeTagsUtils.GenerateCenterTimeTag(firstTag, secondTag, endTagIndex);\r\n                    firstTimeTag.Add(endTag);\r\n                }\r\n\r\n                // add start tag at start of second lyric if does not have tag in there.\r\n                if (!secondTimeTag.Any(x => x.Index.Index == splitIndex && x.Index.State == TextIndex.IndexState.Start))\r\n                {\r\n                    var endTagIndex = new TextIndex(splitIndex);\r\n                    var startTag = TimeTagsUtils.GenerateCenterTimeTag(firstTag, secondTag, endTagIndex);\r\n                    secondTimeTag.Add(startTag);\r\n                }\r\n            }\r\n        }\r\n\r\n        // todo : should implement time and duration\r\n        var firstLyric = lyric.DeepClone();\r\n        firstLyric.Text = lyric.Text[..splitIndex];\r\n        firstLyric.TimeTags = firstTimeTag.ToArray();\r\n        firstLyric.RubyTags = lyric.RubyTags.Where(x => x.StartIndex < splitIndex && x.EndIndex < splitIndex).ToArray();\r\n\r\n        // todo : should implement time and duration\r\n        string secondLyricText = lyric.Text[splitIndex..];\r\n        var secondLyric = lyric.DeepClone();\r\n        secondLyric.Text = secondLyricText;\r\n        secondLyric.TimeTags = shiftingTimeTag(secondTimeTag.ToArray(), -splitIndex);\r\n        secondLyric.RubyTags = shiftingRubyTag(lyric.RubyTags.Where(x => x.StartIndex >= splitIndex && x.EndIndex >= splitIndex).ToArray(), secondLyricText, -splitIndex);\r\n\r\n        return new Tuple<Lyric, Lyric>(firstLyric, secondLyric);\r\n    }\r\n\r\n    public static Lyric CombineLyric(Lyric firstLyric, Lyric secondLyric)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(firstLyric);\r\n        ArgumentNullException.ThrowIfNull(secondLyric);\r\n\r\n        int offsetIndexForSecondLyric = firstLyric.Text.Length;\r\n        string lyricText = firstLyric.Text + secondLyric.Text;\r\n\r\n        var timeTags = new List<TimeTag>();\r\n        timeTags.AddRange(firstLyric.TimeTags);\r\n        timeTags.AddRange(shiftingTimeTag(secondLyric.TimeTags, offsetIndexForSecondLyric));\r\n\r\n        var rubyTags = new List<RubyTag>();\r\n        rubyTags.AddRange(firstLyric.RubyTags);\r\n        rubyTags.AddRange(shiftingRubyTag(secondLyric.RubyTags, lyricText, offsetIndexForSecondLyric));\r\n\r\n        var singers = new List<ElementId>();\r\n        singers.AddRange(firstLyric.SingerIds);\r\n        singers.AddRange(secondLyric.SingerIds);\r\n\r\n        bool sameLanguage = EqualityComparer<CultureInfo?>.Default.Equals(firstLyric.Language, secondLyric.Language);\r\n        var language = sameLanguage ? firstLyric.Language : null;\r\n\r\n        return new Lyric\r\n        {\r\n            Text = lyricText,\r\n            TimeTags = timeTags.ToArray(),\r\n            RubyTags = rubyTags.ToArray(),\r\n            SingerIds = singers.Distinct().ToArray(),\r\n            Language = language,\r\n        };\r\n    }\r\n\r\n    private static TimeTag[] shiftingTimeTag(IEnumerable<TimeTag> timeTags, int offset)\r\n        => timeTags.Select(t => TimeTagUtils.ShiftingTimeTag(t, offset)).ToArray();\r\n\r\n    private static RubyTag[] shiftingRubyTag(IEnumerable<RubyTag> rubyTags, string lyric, int offset)\r\n        => rubyTags.Select(t =>\r\n        {\r\n            (int startIndex, int endIndex) = RubyTagUtils.GetShiftingIndex(t, lyric, offset);\r\n            return new RubyTag\r\n            {\r\n                Text = t.Text,\r\n                StartIndex = startIndex,\r\n                EndIndex = endIndex,\r\n            };\r\n        }).ToArray();\r\n\r\n    #endregion\r\n\r\n    #region Time tags\r\n\r\n    public static bool HasTimedTimeTags(IEnumerable<Lyric> lyrics)\r\n        => lyrics.Any(LyricUtils.HasTimedTimeTags);\r\n\r\n    #endregion\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Objects/Utils/NoteUtils.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Objects.Utils;\r\n\r\npublic static class NoteUtils\r\n{\r\n    /// <summary>\r\n    /// Get the display text while gameplay or in editor.\r\n    /// </summary>\r\n    /// <param name=\"note\">Note</param>\r\n    /// <param name=\"useRubyTextIfHave\">Should use ruby text first if have.</param>\r\n    /// <returns>Text should be display.</returns>\r\n    public static string DisplayText(Note note, bool useRubyTextIfHave = false)\r\n    {\r\n        if (!useRubyTextIfHave)\r\n            return note.Text;\r\n\r\n        return string.IsNullOrEmpty(note.RubyText) ? note.Text : note.RubyText;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Objects/Utils/NotesUtils.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Objects.Utils;\r\n\r\npublic static class NotesUtils\r\n{\r\n    public static Tuple<Note, Note> SplitNote(Note note, double percentage = 0.5)\r\n    {\r\n        switch (percentage)\r\n        {\r\n            case < 0 or > 1:\r\n                throw new ArgumentOutOfRangeException(nameof(note));\r\n\r\n            case 0 or 1:\r\n                throw new InvalidOperationException($\"{nameof(percentage)} cannot be {0} or {1}.\");\r\n        }\r\n\r\n        double firstNoteDuration = note.Duration * percentage;\r\n        double secondNoteDuration = note.Duration * (1 - percentage);\r\n\r\n        var firstNote = note.DeepClone();\r\n        firstNote.EndTimeOffset = note.EndTimeOffset - secondNoteDuration;\r\n\r\n        var secondNote = note.DeepClone();\r\n        secondNote.StartTimeOffset = note.StartTimeOffset + firstNoteDuration;\r\n\r\n        return new Tuple<Note, Note>(firstNote, secondNote);\r\n    }\r\n\r\n    public static Note CombineNote(Note firstLyric, Note secondLyric)\r\n    {\r\n        if (firstLyric.ReferenceLyric != secondLyric.ReferenceLyric)\r\n            throw new InvalidOperationException($\"{nameof(firstLyric.ReferenceLyric)} and {nameof(secondLyric.ReferenceLyric)} should be same.\");\r\n\r\n        if (firstLyric.ReferenceTimeTagIndex != secondLyric.ReferenceTimeTagIndex)\r\n            throw new InvalidOperationException($\"{nameof(firstLyric.ReferenceTimeTagIndex)} and {nameof(secondLyric.ReferenceTimeTagIndex)} should be same.\");\r\n\r\n        var combinedLyric = firstLyric.DeepClone();\r\n        combinedLyric.StartTimeOffset = Math.Min(firstLyric.StartTimeOffset, secondLyric.StartTimeOffset);\r\n        combinedLyric.EndTimeOffset = Math.Max(firstLyric.EndTimeOffset, secondLyric.EndTimeOffset);\r\n\r\n        return combinedLyric;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Objects/Utils/OrderUtils.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Types;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Objects.Utils;\r\n\r\npublic static class OrderUtils\r\n{\r\n    /// <summary>\r\n    /// Check objects contains duplicated ids.\r\n    /// </summary>\r\n    /// <typeparam name=\"T\">IHasOrder</typeparam>\r\n    /// <param name=\"objects\">objects</param>\r\n    /// <returns>contain duplicated id or not</returns>\r\n    public static bool ContainDuplicatedId<T>(T[] objects) where T : IHasOrder\r\n    {\r\n        ArgumentNullException.ThrowIfNull(objects);\r\n\r\n        return objects.Length != objects.Select(x => x.Order).Distinct().Count();\r\n    }\r\n\r\n    /// <summary>\r\n    /// Get min order number\r\n    /// </summary>\r\n    /// <typeparam name=\"T\">IHasOrder</typeparam>\r\n    /// <param name=\"objects\">objects</param>\r\n    /// <returns>min order number.</returns>\r\n    public static int GetMinOrderNumber<T>(IEnumerable<T> objects) where T : IHasOrder\r\n    {\r\n        return objects.OrderBy(x => x.Order).FirstOrDefault()?.Order ?? 0;\r\n    }\r\n\r\n    /// <summary>\r\n    /// Get max order number\r\n    /// </summary>\r\n    /// <typeparam name=\"T\">IHasOrder</typeparam>\r\n    /// <param name=\"objects\">objects</param>\r\n    /// <returns>max order number.</returns>\r\n    public static int GetMaxOrderNumber<T>(IEnumerable<T> objects) where T : IHasOrder\r\n    {\r\n        return objects.OrderByDescending(x => x.Order).FirstOrDefault()?.Order ?? 0;\r\n    }\r\n\r\n    /// <summary>\r\n    /// Get sorted objects\r\n    /// </summary>\r\n    /// <typeparam name=\"T\">IHasOrder</typeparam>\r\n    /// <param name=\"objects\">objects</param>\r\n    /// <returns>sorted result</returns>\r\n    public static T[] Sorted<T>(IEnumerable<T> objects) where T : IHasOrder\r\n    {\r\n        return objects.OrderBy(x => x.Order).ToArray();\r\n    }\r\n\r\n    /// <summary>\r\n    /// Shifting order.\r\n    /// </summary>\r\n    /// <typeparam name=\"T\"></typeparam>\r\n    /// <param name=\"objects\"></param>\r\n    /// <param name=\"offset\"></param>\r\n    public static void ShiftingOrder<T>(IEnumerable<T> objects, int offset) where T : class, IHasOrder\r\n    {\r\n        foreach (var processObject in objects)\r\n        {\r\n            processObject.Order += offset;\r\n        }\r\n    }\r\n\r\n    /// <summary>\r\n    /// Re-generate order number if has gap between two order number\r\n    /// </summary>\r\n    /// <example>\r\n    /// Valid: 1, 2, 3, 4<br/>\r\n    /// Should be generated: 1, 3, 4, 5, 7<br/>\r\n    /// </example>\r\n    /// <typeparam name=\"T\">IHasOrder</typeparam>\r\n    /// <param name=\"objects\">objects</param>\r\n    /// <param name=\"startFrom\">start order should from</param>\r\n    /// <param name=\"changeOrderAction\">has call-back if order has been changed.</param>\r\n    public static void ResortOrder<T>(T[] objects, int startFrom = 1, Action<T, int, int>? changeOrderAction = null) where T : IHasOrder\r\n    {\r\n        int minOrderNumber = GetMinOrderNumber(objects.ToArray());\r\n        int maxOrderNumber = GetMaxOrderNumber(objects.ToArray());\r\n\r\n        // todo : should deal with the case if new start order is between min and max order number\r\n        bool orderByAsc = startFrom <= minOrderNumber;\r\n        var processObjects = orderByAsc ? objects.OrderBy(x => x.Order) : objects.OrderByDescending(x => x.Order);\r\n\r\n        int targetOrder = orderByAsc ? startFrom : startFrom - minOrderNumber + objects.Length;\r\n\r\n        foreach (var processObject in processObjects)\r\n        {\r\n            if (processObject.Order != targetOrder)\r\n                changeOrder(processObject, targetOrder);\r\n\r\n            targetOrder = orderByAsc ? targetOrder + 1 : targetOrder - 1;\r\n        }\r\n\r\n        void changeOrder(T obj, int newOrder)\r\n        {\r\n            int oldOrder = obj.Order;\r\n            obj.Order = newOrder;\r\n\r\n            // call invoke for outside updating.\r\n            changeOrderAction?.Invoke(obj, oldOrder, newOrder);\r\n        }\r\n    }\r\n\r\n    /// <summary>\r\n    /// Change order.\r\n    /// </summary>\r\n    /// <typeparam name=\"T\">IHasOrder</typeparam>\r\n    /// <param name=\"objects\">objects</param>\r\n    /// <param name=\"oldOrder\">old order</param>\r\n    /// <param name=\"newOrder\">new oder</param>\r\n    /// <param name=\"changeOrderAction\">has call-back if order has been changed.</param>\r\n    public static void ChangeOrder<T>(T[] objects, int oldOrder, int newOrder, Action<T, int, int>? changeOrderAction = null) where T : IHasOrder\r\n    {\r\n        if (oldOrder == newOrder)\r\n            return;\r\n\r\n        // check old order number should be in the exist list\r\n        if (!objects.Select(x => x.Order).Contains(oldOrder))\r\n            throw new ArgumentOutOfRangeException(nameof(oldOrder), $\"new order number {oldOrder} is not in the range of {nameof(objects)}.\");\r\n\r\n        // check new order number should be in the exist list\r\n        if (!objects.Select(x => x.Order).Contains(newOrder))\r\n            throw new ArgumentOutOfRangeException(nameof(newOrder), $\"new order number {newOrder} is not in the range of {nameof(objects)}.\");\r\n\r\n        // get objects that will need to change order\r\n        int minAffectOrder = Math.Min(oldOrder, newOrder);\r\n        int maxAffectOrder = Math.Max(oldOrder, newOrder);\r\n        var affectObjects = objects.Where(x => x.Order >= minAffectOrder && x.Order <= maxAffectOrder);\r\n\r\n        // get shifting order\r\n        int orderOffset = newOrder > oldOrder ? -1 : 1;\r\n\r\n        // get order order object info\r\n        const int old_order_temp_id = -1;\r\n        var oldOrderObject = objects.FirstOrDefault(x => x.Order == oldOrder);\r\n        if (oldOrderObject == null)\r\n            return;\r\n\r\n        // set old order to -1 for order duplicated issue\r\n        changeOrder(oldOrderObject, old_order_temp_id);\r\n\r\n        // switching order\r\n        affectObjects = orderOffset > 0 ? affectObjects.OrderByDescending(x => x.Order) : affectObjects.OrderBy(x => x.Order);\r\n\r\n        foreach (var affectObject in affectObjects)\r\n        {\r\n            if (affectObject.Order == old_order_temp_id)\r\n                continue;\r\n\r\n            int affectObjectNewOrder = affectObject.Order + orderOffset;\r\n            changeOrder(affectObject, affectObjectNewOrder);\r\n        }\r\n\r\n        // set old order to new order\r\n        changeOrder(oldOrderObject, newOrder);\r\n\r\n        // post check should not have duplicated ids.\r\n        ContainDuplicatedId(objects.ToArray());\r\n\r\n        void changeOrder(T obj, int n)\r\n        {\r\n            int o = obj.Order;\r\n            obj.Order = n;\r\n\r\n            // call invoke for outside updating.\r\n            changeOrderAction?.Invoke(obj, o, n);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Objects/Utils/RubyTagUtils.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Graphics.Sprites;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Objects.Utils;\r\n\r\npublic static class RubyTagUtils\r\n{\r\n    public static Tuple<int, int> GetFixedIndex(RubyTag rubyTag, string lyric)\r\n        => GetShiftingIndex(rubyTag, lyric, 0);\r\n\r\n    public static Tuple<int, int> GetShiftingIndex(RubyTag rubyTag, string lyric, int offset)\r\n    {\r\n        if (string.IsNullOrEmpty(lyric))\r\n            throw new InvalidOperationException($\"{nameof(lyric)} cannot be empty.\");\r\n\r\n        const int min_index = 0;\r\n        int maxIndex = lyric.Length - 1;\r\n\r\n        int newStartIndex = Math.Clamp(rubyTag.StartIndex + offset, min_index, maxIndex);\r\n        int newEndIndex = Math.Clamp(rubyTag.EndIndex + offset, min_index, maxIndex);\r\n        return new Tuple<int, int>(Math.Min(newStartIndex, newEndIndex), Math.Max(newStartIndex, newEndIndex));\r\n    }\r\n\r\n    public static bool OutOfRange(RubyTag rubyTag, string lyric)\r\n    {\r\n        return OutOfRange(lyric, rubyTag.StartIndex) || OutOfRange(lyric, rubyTag.EndIndex);\r\n    }\r\n\r\n    public static bool ValidNewStartIndex(RubyTag rubyTag, int newStartIndex)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(rubyTag);\r\n\r\n        return newStartIndex <= rubyTag.EndIndex;\r\n    }\r\n\r\n    public static bool ValidNewEndIndex(RubyTag rubyTag, int newEndIndex)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(rubyTag);\r\n\r\n        return newEndIndex >= rubyTag.StartIndex;\r\n    }\r\n\r\n    public static bool OutOfRange(string lyric, int index)\r\n    {\r\n        if (string.IsNullOrEmpty(lyric))\r\n            return true;\r\n\r\n        const int min_index = 0;\r\n        int maxIndex = lyric.Length - 1;\r\n\r\n        return index < min_index || index > maxIndex;\r\n    }\r\n\r\n    public static bool EmptyText(RubyTag rubyTag)\r\n    {\r\n        return string.IsNullOrWhiteSpace(rubyTag.Text);\r\n    }\r\n\r\n    /// <summary>\r\n    /// Display tag with position format\r\n    /// </summary>\r\n    /// <example>\r\n    /// ka(-2~-1)<br/>\r\n    /// ra(4~6)<br/>\r\n    /// </example>\r\n    /// <param name=\"rubyTag\"></param>\r\n    /// <returns></returns>\r\n    public static string PositionFormattedString(RubyTag rubyTag)\r\n    {\r\n        string text = string.IsNullOrWhiteSpace(rubyTag.Text) ? \"empty\" : rubyTag.Text;\r\n        return $\"{text}({rubyTag.StartIndex} ~ {rubyTag.EndIndex})\";\r\n    }\r\n\r\n    public static string GetTextFromLyric(RubyTag rubyTag, string lyric)\r\n    {\r\n        (int startIndex, int endIndex) = GetFixedIndex(rubyTag, lyric);\r\n        return lyric.Substring(startIndex, endIndex - startIndex + 1);\r\n    }\r\n\r\n    public static PositionText ToPositionText(RubyTag rubyTag)\r\n        => new(rubyTag.Text, rubyTag.StartIndex, rubyTag.EndIndex);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Objects/Utils/RubyTagsUtils.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.ComponentModel;\r\nusing System.Data;\r\nusing System.Linq;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Objects.Utils;\r\n\r\npublic static class RubyTagsUtils\r\n{\r\n    public static RubyTag[] Sort(IEnumerable<RubyTag> rubyTags, Sorting sorting = Sorting.Asc) =>\r\n        sorting switch\r\n        {\r\n            Sorting.Asc => rubyTags.OrderBy(x => x.StartIndex).ThenBy(x => x.EndIndex).ToArray(),\r\n            Sorting.Desc => rubyTags.OrderByDescending(x => x.EndIndex).ThenByDescending(x => x.StartIndex).ToArray(),\r\n            _ => throw new InvalidEnumArgumentException(nameof(sorting)),\r\n        };\r\n\r\n    public static RubyTag[] FindOutOfRange(IEnumerable<RubyTag> rubyTags, string lyric)\r\n    {\r\n        return rubyTags.Where(x => RubyTagUtils.OutOfRange(x, lyric)).ToArray();\r\n    }\r\n\r\n    public static RubyTag[] FindOverlapping(IList<RubyTag> rubyTags, Sorting sorting = Sorting.Asc)\r\n    {\r\n        // check is null or empty\r\n        if (!rubyTags.Any())\r\n            return Array.Empty<RubyTag>();\r\n\r\n        // todo : need to make sure is need to sort in here?\r\n        var sortedRubyTags = Sort(rubyTags, sorting);\r\n\r\n        var invalidList = new List<RubyTag>();\r\n\r\n        // check end is less or equal to start index\r\n        invalidList.AddRange(sortedRubyTags.Where(x => x.EndIndex < x.StartIndex));\r\n\r\n        // find other is smaller or bigger\r\n        foreach (var rubyTag in sortedRubyTags)\r\n        {\r\n            if (invalidList.Contains(rubyTag))\r\n                continue;\r\n\r\n            var checkTags = sortedRubyTags.Except(new[] { rubyTag });\r\n\r\n            switch (sorting)\r\n            {\r\n                case Sorting.Asc:\r\n                    // start index within tne target\r\n                    invalidList.AddRange(checkTags.Where(x => x.StartIndex >= rubyTag.StartIndex && x.StartIndex <= rubyTag.EndIndex));\r\n                    break;\r\n\r\n                case Sorting.Desc:\r\n                    // end index within tne target\r\n                    invalidList.AddRange(checkTags.Where(x => x.EndIndex >= rubyTag.StartIndex && x.EndIndex <= rubyTag.EndIndex));\r\n                    break;\r\n\r\n                default:\r\n                    throw new InvalidEnumArgumentException(nameof(sorting));\r\n            }\r\n        }\r\n\r\n        return Sort(invalidList.Distinct());\r\n    }\r\n\r\n    public static RubyTag[] FindEmptyText(IEnumerable<RubyTag> rubyTags)\r\n    {\r\n        return rubyTags.Where(RubyTagUtils.EmptyText).ToArray();\r\n    }\r\n\r\n    public static RubyTag Combine(RubyTag rubyTagA, RubyTag rubyTagB)\r\n    {\r\n        return Combine(new[] { rubyTagA, rubyTagB });\r\n    }\r\n\r\n    public static RubyTag Combine(RubyTag[] rubyTags)\r\n    {\r\n        if (rubyTags == null || !rubyTags.Any())\r\n            throw new ArgumentNullException(nameof(rubyTags));\r\n\r\n        var sortingValue = Sort(rubyTags);\r\n        var firstValue = sortingValue.FirstOrDefault();\r\n        var lastValue = sortingValue.LastOrDefault();\r\n\r\n        if (firstValue == null)\r\n            throw new NoNullAllowedException(nameof(firstValue));\r\n\r\n        if (lastValue == null)\r\n            throw new NoNullAllowedException(nameof(lastValue));\r\n\r\n        return new RubyTag\r\n        {\r\n            StartIndex = firstValue.StartIndex,\r\n            EndIndex = lastValue.EndIndex,\r\n            Text = string.Join(string.Empty, sortingValue.Select(x => x.Text)),\r\n        };\r\n    }\r\n\r\n    public enum Sorting\r\n    {\r\n        /// <summary>\r\n        /// Mark next time tag is error if conflict.\r\n        /// </summary>\r\n        Asc,\r\n\r\n        /// <summary>\r\n        /// Mark previous tag is error if conflict.\r\n        /// </summary>\r\n        Desc,\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Objects/Utils/TimeTagUtils.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Extensions;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Objects.Utils;\r\n\r\npublic static class TimeTagUtils\r\n{\r\n    /// <summary>\r\n    /// Shifting time-tag.\r\n    /// </summary>\r\n    /// <param name=\"timeTag\"></param>\r\n    /// <param name=\"offset\"></param>\r\n    /// <returns></returns>\r\n    public static TimeTag ShiftingTimeTag(TimeTag timeTag, int offset)\r\n    {\r\n        var index = TextIndexUtils.ShiftingIndex(timeTag.Index, offset);\r\n        double? time = timeTag.Time;\r\n        return new TimeTag(index, time);\r\n    }\r\n\r\n    /// <summary>\r\n    /// Display string with time format\r\n    /// </summary>\r\n    /// <example>\r\n    /// 02:32:155<br/>\r\n    /// --:--:---<br/>\r\n    /// </example>\r\n    /// <param name=\"timeTag\"></param>\r\n    /// <returns></returns>\r\n    public static string FormattedString(TimeTag timeTag)\r\n    {\r\n        return timeTag.Time?.ToEditorFormattedString() ?? \"--:--:---\";\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Objects/Utils/TimeTagsUtils.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.ComponentModel;\r\nusing System.Linq;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Objects.Utils;\r\n\r\npublic static class TimeTagsUtils\r\n{\r\n    /// <summary>\r\n    /// Generate center time-tag with time.\r\n    /// </summary>\r\n    /// <param name=\"startTimeTag\"></param>\r\n    /// <param name=\"endTimeTag\"></param>\r\n    /// <param name=\"index\"></param>\r\n    /// <returns></returns>\r\n    public static TimeTag GenerateCenterTimeTag(TimeTag startTimeTag, TimeTag endTimeTag, TextIndex index)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(startTimeTag);\r\n        ArgumentNullException.ThrowIfNull(endTimeTag);\r\n\r\n        if (startTimeTag.Index > endTimeTag.Index)\r\n            throw new InvalidOperationException($\"{nameof(endTimeTag.Index)} cannot larger than {startTimeTag.Index}\");\r\n\r\n        if (index < startTimeTag.Index || index > endTimeTag.Index)\r\n            throw new InvalidOperationException($\"{nameof(endTimeTag.Index)} cannot larger than {startTimeTag.Index}\");\r\n\r\n        if (startTimeTag.Time == null || endTimeTag.Time == null)\r\n            return new TimeTag(index);\r\n\r\n        int diffFromStartToEnd = getTimeCalculationIndex(endTimeTag.Index) - getTimeCalculationIndex(startTimeTag.Index);\r\n        int diffFromStartToNow = getTimeCalculationIndex(index) - getTimeCalculationIndex(startTimeTag.Index);\r\n        if (diffFromStartToEnd == 0 || diffFromStartToNow == 0)\r\n            return new TimeTag(index, startTimeTag.Time);\r\n\r\n        double? time = startTimeTag.Time +\r\n                       (endTimeTag.Time - startTimeTag.Time)\r\n                       / diffFromStartToEnd\r\n                       * diffFromStartToNow;\r\n\r\n        return new TimeTag(index, time);\r\n\r\n        static int getTimeCalculationIndex(TextIndex calculationIndex)\r\n            => TextIndexUtils.ToGapIndex(calculationIndex);\r\n    }\r\n\r\n    public static TimeTag GenerateCenterTimeTag(TimeTag startTimeTag, TimeTag endTimeTag, int index)\r\n        => GenerateCenterTimeTag(startTimeTag, endTimeTag, new TextIndex(index));\r\n\r\n    /// <summary>\r\n    /// Sort list of time tags by index and time.\r\n    /// </summary>\r\n    /// <param name=\"timeTags\">Time tags</param>\r\n    /// <returns>Sorted time tags</returns>\r\n    public static IList<TimeTag> Sort(IEnumerable<TimeTag> timeTags)\r\n    {\r\n        return timeTags.OrderBy(x => x.Index)\r\n                       .ThenBy(x => x.Time).ToArray();\r\n    }\r\n\r\n    /// <summary>\r\n    /// Find out of range time-tag.\r\n    /// </summary>\r\n    /// <param name=\"timeTags\"></param>\r\n    /// <param name=\"lyric\"></param>\r\n    /// <returns></returns>\r\n    public static TimeTag[] FindOutOfRange(IEnumerable<TimeTag> timeTags, string lyric)\r\n    {\r\n        return timeTags.Where(x => x.Index.Index < 0 || x.Index.Index >= lyric.Length).ToArray();\r\n    }\r\n\r\n    /// <summary>\r\n    /// Find time-tag that has no time.\r\n    /// </summary>\r\n    /// <param name=\"timeTags\"></param>\r\n    /// <returns></returns>\r\n    public static TimeTag[] FindNoneTime(IEnumerable<TimeTag> timeTags)\r\n        => timeTags.Where(x => x.Time == null).ToArray();\r\n\r\n    /// <summary>\r\n    /// Check lyric has start time-tag\r\n    /// </summary>\r\n    /// <param name=\"timeTags\"></param>\r\n    /// <param name=\"lyric\"></param>\r\n    public static bool HasStartTimeTagInLyric(IEnumerable<TimeTag> timeTags, string lyric)\r\n        => !string.IsNullOrEmpty(lyric) && timeTags.Any(x => x.Index.State == TextIndex.IndexState.Start && x.Index.Index == 0);\r\n\r\n    /// <summary>\r\n    /// Check lyric has end time-tag\r\n    /// </summary>\r\n    /// <param name=\"timeTags\"></param>\r\n    /// <param name=\"lyric\"></param>\r\n    public static bool HasEndTimeTagInLyric(IEnumerable<TimeTag> timeTags, string lyric)\r\n        => timeTags.Any(x => x.Index.State == TextIndex.IndexState.End && x.Index.Index == lyric.Length - 1);\r\n\r\n    /// <summary>\r\n    /// Find overlapping time tags.\r\n    /// </summary>\r\n    /// <param name=\"timeTags\">Time tags</param>\r\n    /// <param name=\"other\">Check way</param>\r\n    /// <param name=\"self\">Check way</param>\r\n    /// <returns>List of overlapping time tags</returns>\r\n    public static IList<TimeTag> FindOverlapping(IEnumerable<TimeTag> timeTags, GroupCheck other = GroupCheck.Asc, SelfCheck self = SelfCheck.BasedOnStart)\r\n    {\r\n        var sortedTimeTags = Sort(timeTags);\r\n        var groupedTimeTags = sortedTimeTags.GroupBy(x => x.Index.Index);\r\n\r\n        var overlappingTimeTagList = new List<TimeTag>();\r\n\r\n        foreach (var groupedTimeTag in groupedTimeTags)\r\n        {\r\n            var startTimeGroup = groupedTimeTag.Where(x => x.Index.State == TextIndex.IndexState.Start && x.Time != null);\r\n            var endTimeGroup = groupedTimeTag.Where(x => x.Index.State == TextIndex.IndexState.End && x.Time != null);\r\n\r\n            // add overlapping group into list.\r\n            var groupOverlapping = findGroupOverlapping();\r\n            overlappingTimeTagList.AddRange(groupOverlapping);\r\n\r\n            // add overlapping self into list.\r\n            var selfOverlapping = findSelfOverlapping();\r\n            overlappingTimeTagList.AddRange(selfOverlapping);\r\n\r\n            IEnumerable<TimeTag> findGroupOverlapping()\r\n            {\r\n                switch (other)\r\n                {\r\n                    case GroupCheck.Asc:\r\n                        // mark next is overlapping if smaller then self\r\n                        double? groupMaxTime = groupedTimeTag.Max(x => x.Time);\r\n                        if (groupMaxTime == null)\r\n                            return Array.Empty<TimeTag>();\r\n\r\n                        return sortedTimeTags.Where(x => x.Index.Index > groupedTimeTag.Key && x.Time < groupMaxTime).ToList();\r\n\r\n                    case GroupCheck.Desc:\r\n                        // mark previous is overlapping if larger then self\r\n                        double? groupMinTime = groupedTimeTag.Min(x => x.Time);\r\n                        if (groupMinTime == null)\r\n                            return Array.Empty<TimeTag>();\r\n\r\n                        return sortedTimeTags.Where(x => x.Index.Index < groupedTimeTag.Key && x.Time > groupMinTime).ToList();\r\n\r\n                    default:\r\n                        return Array.Empty<TimeTag>();\r\n                }\r\n            }\r\n\r\n            IEnumerable<TimeTag> findSelfOverlapping()\r\n            {\r\n                switch (self)\r\n                {\r\n                    case SelfCheck.BasedOnStart:\r\n                        double? maxStartTime = startTimeGroup.Max(x => x.Time);\r\n                        if (maxStartTime == null)\r\n                            return Array.Empty<TimeTag>();\r\n\r\n                        return endTimeGroup.Where(x => x.Time != null && x.Time.Value < maxStartTime.Value).ToList();\r\n\r\n                    case SelfCheck.BasedOnEnd:\r\n                        double? minEndTime = endTimeGroup.Min(x => x.Time);\r\n                        if (minEndTime == null)\r\n                            return Array.Empty<TimeTag>();\r\n\r\n                        return startTimeGroup.Where(x => x.Time != null && x.Time.Value > minEndTime.Value).ToList();\r\n\r\n                    default:\r\n                        return Array.Empty<TimeTag>();\r\n                }\r\n            }\r\n        }\r\n\r\n        return Sort(overlappingTimeTagList.Distinct());\r\n    }\r\n\r\n    /// <summary>\r\n    /// Auto fix overlapping time tags.\r\n    /// </summary>\r\n    /// <param name=\"timeTags\">Time tags</param>\r\n    /// <param name=\"other\">Fix way</param>\r\n    /// <param name=\"self\">Fix way</param>\r\n    /// <returns>Fixed time tags.</returns>\r\n    public static IList<TimeTag> FixOverlapping(IList<TimeTag> timeTags, GroupCheck other = GroupCheck.Asc, SelfCheck self = SelfCheck.BasedOnStart)\r\n    {\r\n        if (!timeTags.Any())\r\n            return timeTags;\r\n\r\n        var sortedTimeTags = Sort(timeTags);\r\n        var groupedTimeTags = sortedTimeTags.GroupBy(x => x.Index.Index).ToArray();\r\n\r\n        var overlappingTimeTags = FindOverlapping(timeTags, other, self);\r\n        var validTimeTags = sortedTimeTags.Except(overlappingTimeTags);\r\n\r\n        foreach (var overlappingTimeTag in overlappingTimeTags)\r\n        {\r\n            int listIndex = sortedTimeTags.IndexOf(overlappingTimeTag);\r\n            var timeTag = overlappingTimeTag.Index;\r\n\r\n            // fix self-overlapping\r\n            var groupedTimeTag = groupedTimeTags.FirstOrDefault(x => x.Key == timeTag.Index)?.ToList();\r\n            var startTimeGroup = groupedTimeTag?.Where(x => x.Index.State == TextIndex.IndexState.Start && x.Time != null);\r\n            var endTimeGroup = groupedTimeTag?.Where(x => x.Index.State == TextIndex.IndexState.End && x.Time != null);\r\n\r\n            switch (timeTag.State)\r\n            {\r\n                case TextIndex.IndexState.Start:\r\n                    double? minEndTime = endTimeGroup?.Min(x => x.Time);\r\n\r\n                    if (minEndTime != null && minEndTime < overlappingTimeTag.Time)\r\n                    {\r\n                        sortedTimeTags[listIndex] = new TimeTag(timeTag, minEndTime);\r\n                        continue;\r\n                    }\r\n\r\n                    break;\r\n\r\n                case TextIndex.IndexState.End:\r\n                    double? maxStartTime = startTimeGroup?.Max(x => x.Time);\r\n\r\n                    if (maxStartTime != null && maxStartTime > overlappingTimeTag.Time)\r\n                    {\r\n                        sortedTimeTags[listIndex] = new TimeTag(timeTag, maxStartTime);\r\n                        continue;\r\n                    }\r\n\r\n                    break;\r\n\r\n                default:\r\n                    throw new InvalidEnumArgumentException(nameof(timeTag.State));\r\n            }\r\n\r\n            // fix previous or next value to apply\r\n            switch (other)\r\n            {\r\n                case GroupCheck.Asc:\r\n                    // find previous value to apply.\r\n                    double? previousValidValue = sortedTimeTags.Reverse().FirstOrDefault(x => x.Index.Index < timeTag.Index && x.Time != null)?.Time;\r\n                    sortedTimeTags[listIndex] = new TimeTag(timeTag, previousValidValue);\r\n                    break;\r\n\r\n                case GroupCheck.Desc:\r\n                    // find next value to apply.\r\n                    double? nextValidValue = sortedTimeTags.FirstOrDefault(x => x.Index.Index > timeTag.Index && x.Time != null)?.Time;\r\n                    sortedTimeTags[listIndex] = new TimeTag(timeTag, nextValidValue);\r\n                    break;\r\n\r\n                default:\r\n                    throw new InvalidEnumArgumentException(nameof(other));\r\n            }\r\n        }\r\n\r\n        return sortedTimeTags;\r\n    }\r\n\r\n    /// <summary>\r\n    /// Convert list of time tag to dictionary.\r\n    /// WIll sort by the time.\r\n    /// </summary>\r\n    /// <param name=\"timeTags\">Time tags</param>\r\n    /// <returns>Time tags with dictionary format.</returns>\r\n    public static IReadOnlyDictionary<double, TextIndex> ToTimeBasedDictionary(IList<TimeTag> timeTags)\r\n    {\r\n        // convert to dictionary, will get start's smallest time and end's largest time.\r\n        return timeTags.Where(x => x.Time != null)\r\n                       .OrderBy(x => x.Time)\r\n                       .GroupBy(x => x.Time)\r\n                       .Select(x =>\r\n                           // will always get the first time-tag for now.\r\n                           x.FirstOrDefault())\r\n                       .ToDictionary(k => k?.Time ?? throw new ArgumentNullException(nameof(k)),\r\n                           v => v?.Index ?? throw new ArgumentNullException(nameof(v)));\r\n    }\r\n\r\n    /// <summary>\r\n    /// Get start time.\r\n    /// </summary>\r\n    /// <param name=\"timeTags\">Time tags</param>\r\n    /// <returns>Start time</returns>\r\n    public static double? GetStartTime(IList<TimeTag> timeTags)\r\n    {\r\n        return timeTags.MinBy(x => x.Time)?.Time;\r\n    }\r\n\r\n    /// <summary>\r\n    /// Get End time.\r\n    /// </summary>\r\n    /// <param name=\"timeTags\">Time tags</param>\r\n    /// <returns>End time</returns>\r\n    public static double? GetEndTime(IList<TimeTag> timeTags)\r\n    {\r\n        return timeTags.MaxBy(x => x.Time)?.Time;\r\n    }\r\n}\r\n\r\npublic enum GroupCheck\r\n{\r\n    /// <summary>\r\n    /// Mark next time tag is error if conflict.\r\n    /// </summary>\r\n    Asc,\r\n\r\n    /// <summary>\r\n    /// Mark previous tag is error if conflict.\r\n    /// </summary>\r\n    Desc,\r\n}\r\n\r\npublic enum SelfCheck\r\n{\r\n    /// <summary>\r\n    /// Mark end time tag is error if conflict.\r\n    /// </summary>\r\n    BasedOnStart,\r\n\r\n    /// <summary>\r\n    /// Mark start time tag is error if conflict.\r\n    /// </summary>\r\n    BasedOnEnd,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Objects/Workings/HitObjectWorkingPropertyValidator.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Flags;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Objects.Workings;\r\n\r\n/// <summary>\r\n/// This class is used to check the working property is same as data property in the <typeparamref name=\"THitObject\"/>.\r\n/// Should mark as invalid when data property is changed.\r\n/// Should mark as valid when working property is synced with data property\r\n/// </summary>\r\n/// <typeparam name=\"THitObject\"></typeparam>\r\n/// <typeparam name=\"TFlag\"></typeparam>\r\npublic abstract class HitObjectWorkingPropertyValidator<THitObject, TFlag> : FlagState<TFlag>\r\n    where TFlag : struct, Enum\r\n{\r\n    private readonly THitObject hitObject;\r\n\r\n    protected HitObjectWorkingPropertyValidator(THitObject hitObject)\r\n    {\r\n        this.hitObject = hitObject;\r\n\r\n        ValidateAll();\r\n        invalidateCannotCheckSyncProperties();\r\n    }\r\n\r\n    /// <summary>\r\n    /// We need to invalidate the properties that can't check working property sync.\r\n    /// For able to apply the value in the <see cref=\"KaraokeBeatmapProcessor\"/>\r\n    /// </summary>\r\n    private void invalidateCannotCheckSyncProperties()\r\n    {\r\n        foreach (TFlag flag in Enum.GetValues(typeof(TFlag)))\r\n        {\r\n            if (HasDataProperty(flag))\r\n                continue;\r\n\r\n            Invalidate(flag);\r\n        }\r\n    }\r\n\r\n    /// <summary>\r\n    /// This method is called after change the data property.\r\n    /// We should make sure that the working property is same as data property.\r\n    /// Note that this property should only called inside the <typeparamref name=\"THitObject\"/>\r\n    /// </summary>\r\n    /// <param name=\"flag\"></param>\r\n    public bool UpdateStateByDataProperty(TFlag flag)\r\n    {\r\n        if (!CanInvalidate(flag))\r\n        {\r\n            // will caused if data property become same as working property again.\r\n            Validate(flag);\r\n        }\r\n\r\n        return Invalidate(flag);\r\n    }\r\n\r\n    /// <summary>\r\n    /// This method is called after assign the working property changed in the <typeparamref name=\"THitObject\"/> by <see cref=\"KaraokeBeatmapProcessor\"/>.\r\n    /// We should make sure that the working property is same as data property.\r\n    /// Note that this property should only called inside the <typeparamref name=\"THitObject\"/>\r\n    /// </summary>\r\n    /// <param name=\"flag\"></param>\r\n    public bool UpdateStateByWorkingProperty(TFlag flag)\r\n    {\r\n        if (!CanValidate(flag))\r\n            throw new InvalidWorkingPropertyAssignException();\r\n\r\n        return Validate(flag);\r\n    }\r\n\r\n    protected sealed override bool CanInvalidate(TFlag flags)\r\n        => !HasDataProperty(flags) || !IsWorkingPropertySynced(hitObject, flags);\r\n\r\n    protected sealed override bool CanValidate(TFlag flags)\r\n        => !HasDataProperty(flags) || IsWorkingPropertySynced(hitObject, flags);\r\n\r\n    protected abstract bool HasDataProperty(TFlag flags);\r\n\r\n    protected abstract bool IsWorkingPropertySynced(THitObject hitObject, TFlag flags);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Objects/Workings/InvalidWorkingPropertyAssignException.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Objects.Workings;\r\n\r\npublic class InvalidWorkingPropertyAssignException : Exception;\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Objects/Workings/LyricWorkingProperty.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Objects.Workings;\r\n\r\n/// <summary>\r\n/// Specifies which properties in the <see cref=\"Lyric\"/> are being invalidated.\r\n/// </summary>\r\n[Flags]\r\npublic enum LyricWorkingProperty\r\n{\r\n    /// <summary>\r\n    /// <see cref=\"Lyric.Singers\"/> is being invalidated.\r\n    /// </summary>\r\n    Singers = 1 << 0,\r\n\r\n    /// <summary>\r\n    /// <see cref=\"Lyric.PageIndex\"/> is being invalidated.\r\n    /// </summary>\r\n    Page = 1 << 1,\r\n\r\n    /// <summary>\r\n    /// <see cref=\"Lyric.ReferenceLyric\"/> is being invalidated.\r\n    /// </summary>\r\n    ReferenceLyric = 1 << 2,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Objects/Workings/LyricWorkingPropertyValidator.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Objects.Workings;\r\n\r\npublic class LyricWorkingPropertyValidator : HitObjectWorkingPropertyValidator<Lyric, LyricWorkingProperty>\r\n{\r\n    public LyricWorkingPropertyValidator(Lyric hitObject)\r\n        : base(hitObject)\r\n    {\r\n    }\r\n\r\n    protected override bool HasDataProperty(LyricWorkingProperty flags) =>\r\n        flags switch\r\n        {\r\n            LyricWorkingProperty.Singers => true,\r\n            LyricWorkingProperty.Page => false,\r\n            LyricWorkingProperty.ReferenceLyric => true,\r\n            _ => throw new ArgumentOutOfRangeException(nameof(flags), flags, null),\r\n        };\r\n\r\n    protected override bool IsWorkingPropertySynced(Lyric hitObject, LyricWorkingProperty flags) =>\r\n        flags switch\r\n        {\r\n            LyricWorkingProperty.Singers => isWorkingSingerSynced(hitObject),\r\n            LyricWorkingProperty.Page => throw new InvalidOperationException(),\r\n            LyricWorkingProperty.ReferenceLyric => isReferenceLyricSynced(hitObject),\r\n            _ => throw new ArgumentOutOfRangeException(nameof(flags), flags, null),\r\n        };\r\n\r\n    private bool isWorkingSingerSynced(Lyric lyric)\r\n    {\r\n        var lyricSingerIds = lyric.SingerIds.OrderBy(x => x).Distinct();\r\n        var workingSingerIds = lyric.Singers.ToArray().Select(x =>\r\n        {\r\n            var ids = new List<ElementId> { x.Key.ID };\r\n            ids.AddRange(x.Value.Select(singer => singer.ID));\r\n            return ids;\r\n        }).SelectMany(x => x).OrderBy(x => x).Distinct();\r\n\r\n        return lyricSingerIds.SequenceEqual(workingSingerIds);\r\n    }\r\n\r\n    private bool isReferenceLyricSynced(Lyric lyric)\r\n    {\r\n        return lyric.ReferenceLyric?.ID == lyric.ReferenceLyricId;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Objects/Workings/NoteWorkingProperty.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Objects.Workings;\r\n\r\n/// <summary>\r\n/// Specifies which properties in the <see cref=\"Note\"/> are being invalidated.\r\n/// </summary>\r\n[Flags]\r\npublic enum NoteWorkingProperty\r\n{\r\n    /// <summary>\r\n    /// <see cref=\"Note.PageIndex\"/> is being invalidated.\r\n    /// </summary>\r\n    Page = 1,\r\n\r\n    /// <summary>\r\n    /// <see cref=\"Note.ReferenceLyric\"/> is being invalidated.\r\n    /// </summary>\r\n    ReferenceLyric = 1 << 1,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Objects/Workings/NoteWorkingPropertyValidator.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Objects.Workings;\r\n\r\npublic class NoteWorkingPropertyValidator : HitObjectWorkingPropertyValidator<Note, NoteWorkingProperty>\r\n{\r\n    public NoteWorkingPropertyValidator(Note hitObject)\r\n        : base(hitObject)\r\n    {\r\n    }\r\n\r\n    protected override bool HasDataProperty(NoteWorkingProperty flags) =>\r\n        flags switch\r\n        {\r\n            NoteWorkingProperty.Page => false,\r\n            NoteWorkingProperty.ReferenceLyric => true,\r\n            _ => throw new ArgumentOutOfRangeException(nameof(flags), flags, null),\r\n        };\r\n\r\n    protected override bool IsWorkingPropertySynced(Note hitObject, NoteWorkingProperty flags) =>\r\n        flags switch\r\n        {\r\n            NoteWorkingProperty.Page => true,\r\n            NoteWorkingProperty.ReferenceLyric => hitObject.ReferenceLyric?.ID == hitObject.ReferenceLyricId,\r\n            _ => throw new ArgumentOutOfRangeException(nameof(flags), flags, null),\r\n        };\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Online/API/Requests/ChangelogRequestUtils.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Text.RegularExpressions;\r\nusing System.Threading.Tasks;\r\nusing Octokit;\r\nusing osu.Game.Rulesets.Karaoke.Extensions;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Online.API.Requests;\r\n\r\npublic static class ChangelogRequestUtils\r\n{\r\n    public const string ORGANIZATION_NAME = \"karaoke-dev\";\r\n\r\n    private const string project_name = $\"{ORGANIZATION_NAME}.github.io\";\r\n    private const string branch_name = \"master\";\r\n    private const string changelog_path = \"content/changelog\";\r\n\r\n    public static Task<IReadOnlyList<RepositoryContent>> GetAllChangelogs(IGitHubClient client)\r\n    {\r\n        return client\r\n               .Repository\r\n               .Content\r\n               .GetAllContents(ORGANIZATION_NAME, project_name, changelog_path);\r\n    }\r\n\r\n    public static string GetDocumentUrl(RepositoryContent content)\r\n        => $\"https://raw.githubusercontent.com/{ORGANIZATION_NAME}/{project_name}/{branch_name}/{content.Path}/\";\r\n\r\n    public static string GetRootUrl(RepositoryContent content)\r\n        => content.HtmlUrl;\r\n\r\n    public static string GetVersion(RepositoryContent content)\r\n        => content.Name;\r\n\r\n    public static DateTimeOffset GetPublishDateFromName(RepositoryContent content)\r\n    {\r\n        string? name = content.Name;\r\n        var regex = new Regex(\"(?<year>[-0-9]+).(?<month>[-0-9]{2})(?<day>[-0-9]{2})\");\r\n        var result = regex.Match(name);\r\n        if (!result.Success)\r\n            return DateTimeOffset.MaxValue;\r\n\r\n        int year = result.GetGroupValue<int>(\"year\");\r\n        int month = result.GetGroupValue<int>(\"month\");\r\n        int day = result.GetGroupValue<int>(\"day\");\r\n\r\n        return new DateTimeOffset(new DateTime(year, month, day));\r\n    }\r\n\r\n    public static async Task<string> GetChangelogContent(IGitHubClient client, string version)\r\n    {\r\n        string changeLogPath = $\"{changelog_path}/{version}/index.md\";\r\n        byte[]? content = await client\r\n                                .Repository\r\n                                .Content\r\n                                .GetRawContent(ORGANIZATION_NAME, project_name, changeLogPath)\r\n                                .ConfigureAwait(false);\r\n\r\n        // convert the content to a string\r\n        return System.Text.Encoding.UTF8.GetString(content);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Online/API/Requests/GetChangelogBuildRequest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Threading.Tasks;\r\nusing Octokit;\r\nusing osu.Game.Rulesets.Karaoke.Online.API.Requests.Responses;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Online.API.Requests;\r\n\r\npublic class GetChangelogBuildRequest : GithubAPIRequest<APIChangelogBuild>\r\n{\r\n    private readonly APIChangelogBuild originBuild;\r\n\r\n    public GetChangelogBuildRequest(APIChangelogBuild originBuild)\r\n        : base(ChangelogRequestUtils.ORGANIZATION_NAME)\r\n    {\r\n        this.originBuild = originBuild;\r\n    }\r\n\r\n    protected override async Task<APIChangelogBuild> Perform(IGitHubClient client)\r\n    {\r\n        string contentString = await ChangelogRequestUtils.GetChangelogContent(client, originBuild.Version).ConfigureAwait(false);\r\n        return originBuild.CreateBuildWithContent(contentString);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Online/API/Requests/GetChangelogRequest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing System.Threading.Tasks;\r\nusing Octokit;\r\nusing osu.Framework.Extensions.IEnumerableExtensions;\r\nusing osu.Game.Rulesets.Karaoke.Online.API.Requests.Responses;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Online.API.Requests;\r\n\r\npublic class GetChangelogRequest : GithubAPIRequest<APIChangelogIndex>\r\n{\r\n    public GetChangelogRequest()\r\n        : base(ChangelogRequestUtils.ORGANIZATION_NAME)\r\n    {\r\n    }\r\n\r\n    protected override async Task<APIChangelogIndex> Perform(IGitHubClient client)\r\n    {\r\n        var builds = await getAllBuilds(client).ConfigureAwait(false);\r\n        var previewBuilds = (await Task.WhenAll(builds.Take(5).Select(x => createPreviewBuild(client, x))).ConfigureAwait(false)).ToList();\r\n        int[] years = generateYears(builds);\r\n\r\n        return new APIChangelogIndex\r\n        {\r\n            Years = years,\r\n            Builds = builds,\r\n            PreviewBuilds = previewBuilds,\r\n        };\r\n    }\r\n\r\n    private static async Task<List<APIChangelogBuild>> getAllBuilds(IGitHubClient client)\r\n    {\r\n        var reposAscending = await ChangelogRequestUtils.GetAllChangelogs(client).ConfigureAwait(false);\r\n\r\n        var builds = reposAscending\r\n                     .Reverse()\r\n                     .Where(x => x.Type == ContentType.Dir)\r\n                     .Select(createBuild)\r\n                     .ToList();\r\n\r\n        // adjust the mapping of previous/next versions by hand.\r\n        foreach (var build in builds)\r\n        {\r\n            build.Versions.Previous = builds.GetPrevious(build);\r\n            build.Versions.Next = builds.GetNext(build);\r\n        }\r\n\r\n        return builds;\r\n    }\r\n\r\n    private static APIChangelogBuild createBuild(RepositoryContent content)\r\n    {\r\n        return new APIChangelogBuild\r\n        {\r\n            DocumentUrl = ChangelogRequestUtils.GetDocumentUrl(content),\r\n            RootUrl = ChangelogRequestUtils.GetRootUrl(content),\r\n            Version = ChangelogRequestUtils.GetVersion(content),\r\n            PublishedAt = ChangelogRequestUtils.GetPublishDateFromName(content),\r\n        };\r\n    }\r\n\r\n    private static async Task<APIChangelogBuild> createPreviewBuild(IGitHubClient client, APIChangelogBuild originBuild)\r\n    {\r\n        string contentString = await ChangelogRequestUtils.GetChangelogContent(client, originBuild.Version).ConfigureAwait(false);\r\n        return originBuild.CreateBuildWithContent(contentString);\r\n    }\r\n\r\n    private static int[] generateYears(IEnumerable<APIChangelogBuild> builds)\r\n        => builds.Select(x => x.PublishedAt.Year).Distinct().ToArray();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Online/API/Requests/GithubAPIRequest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Threading.Tasks;\r\nusing Octokit;\r\nusing osu.Game.Online.API;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Online.API.Requests;\r\n\r\npublic abstract class GithubAPIRequest<T> where T : class\r\n{\r\n    protected virtual GitHubClient CreateGitHubClient() => new(new ProductHeaderValue(organizationName));\r\n\r\n    /// <summary>\r\n    /// Invoked on successful completion of an API request.\r\n    /// This will be scheduled to the API's internal scheduler (run on update thread automatically).\r\n    /// </summary>\r\n    public event APISuccessHandler<T>? Success;\r\n\r\n    /// <summary>\r\n    /// Invoked on failure to complete an API request.\r\n    /// This will be scheduled to the API's internal scheduler (run on update thread automatically).\r\n    /// </summary>\r\n    public event APIFailureHandler? Failure;\r\n\r\n    private readonly object completionStateLock = new();\r\n\r\n    /// <summary>\r\n    /// The state of this request, from an outside perspective.\r\n    /// This is used to ensure correct notification events are fired.\r\n    /// </summary>\r\n    public APIRequestCompletionState CompletionState { get; private set; }\r\n\r\n    private readonly string organizationName;\r\n\r\n    protected GithubAPIRequest(string organizationName)\r\n    {\r\n        this.organizationName = organizationName;\r\n    }\r\n\r\n    public async Task Perform()\r\n    {\r\n        var client = CreateGitHubClient();\r\n\r\n        try\r\n        {\r\n            var response = await Perform(client).ConfigureAwait(false);\r\n            triggerSuccess(response);\r\n        }\r\n        catch (Exception e)\r\n        {\r\n            triggerFailure(e);\r\n        }\r\n    }\r\n\r\n    protected abstract Task<T> Perform(IGitHubClient client);\r\n\r\n    private void triggerSuccess(T response)\r\n    {\r\n        lock (completionStateLock)\r\n        {\r\n            if (CompletionState != APIRequestCompletionState.Waiting)\r\n                return;\r\n\r\n            CompletionState = APIRequestCompletionState.Completed;\r\n        }\r\n\r\n        Success?.Invoke(response);\r\n    }\r\n\r\n    private void triggerFailure(Exception e)\r\n    {\r\n        lock (completionStateLock)\r\n        {\r\n            if (CompletionState != APIRequestCompletionState.Waiting)\r\n                return;\r\n\r\n            CompletionState = APIRequestCompletionState.Failed;\r\n        }\r\n\r\n        Failure?.Invoke(e);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Online/API/Requests/Responses/APIChangelogBuild.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Text.RegularExpressions;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Online.API.Requests.Responses;\r\n\r\npublic class APIChangelogBuild\r\n{\r\n    /// <summary>\r\n    /// The URL of the loaded document.\r\n    /// </summary>\r\n    public string DocumentUrl { get; set; } = null!;\r\n\r\n    /// <summary>\r\n    /// The base URL for all root-relative links.\r\n    /// </summary>\r\n    public string RootUrl { get; set; } = null!;\r\n\r\n    /// <summary>\r\n    /// Version number\r\n    /// </summary>\r\n    /// <example>2023.0123</example>\r\n    /// <example>2023.1111</example>\r\n    public string Version { get; set; } = null!;\r\n\r\n    /// <summary>\r\n    /// Display version\r\n    /// </summary>\r\n    public string DisplayVersion => Version;\r\n\r\n    /// <summary>\r\n    /// Might be preview or detail markdown content.\r\n    /// And the content is markdown format.\r\n    /// </summary>\r\n    public string? Content { get; set; }\r\n\r\n    /// <summary>\r\n    /// Version\r\n    /// </summary>\r\n    public VersionNavigation Versions { get; } = new();\r\n\r\n    /// <summary>\r\n    /// Created date.\r\n    /// </summary>\r\n    public DateTimeOffset PublishedAt { get; set; }\r\n\r\n    public class VersionNavigation\r\n    {\r\n        /// <summary>\r\n        /// Next version\r\n        /// </summary>\r\n        public APIChangelogBuild? Next { get; set; }\r\n\r\n        /// <summary>\r\n        /// Previous version\r\n        /// </summary>\r\n        public APIChangelogBuild? Previous { get; set; }\r\n    }\r\n\r\n    public override string ToString() => $\"Karaoke! {DisplayVersion}\";\r\n\r\n    public APIChangelogBuild CreateBuildWithContent(string content)\r\n    {\r\n        return new APIChangelogBuild\r\n        {\r\n            DocumentUrl = DocumentUrl,\r\n            RootUrl = RootUrl,\r\n            Version = Version,\r\n            Content = content,\r\n            Versions =\r\n            {\r\n                Previous = Versions.Previous,\r\n                Next = Versions.Next,\r\n            },\r\n            PublishedAt = PublishedAt,\r\n        };\r\n    }\r\n\r\n    public string? GetFormattedContent()\r\n    {\r\n        if (Content == null)\r\n            return null;\r\n\r\n        // for able to parsing the badge, need to replace the \" [content] \" with \" [content](content) \";\r\n        const string pattern = @\"(?<=\\s)\\[(.*?)\\](?=\\s)\";\r\n        return Regex.Replace(Content, pattern, m => $\"[{m.Groups[1].Value}]({m.Groups[1].Value})\");\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Online/API/Requests/Responses/APIChangelogIndex.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Online.API.Requests.Responses;\r\n\r\npublic class APIChangelogIndex\r\n{\r\n    /// <summary>\r\n    /// All available builds with no content.\r\n    /// </summary>\r\n    public List<APIChangelogBuild> Builds { get; set; } = new();\r\n\r\n    /// <summary>\r\n    /// All preview builds display in the main page.\r\n    /// </summary>\r\n    public List<APIChangelogBuild> PreviewBuilds { get; set; } = new();\r\n\r\n    /// <summary>\r\n    /// All available years that will be shown in the sidebar.\r\n    /// </summary>\r\n    public int[] Years { get; set; } = Array.Empty<int>();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Overlays/Changelog/ChangeLogMarkdownContainer.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing Markdig.Syntax.Inlines;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Colour;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Layout;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Containers.Markdown;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Online.API.Requests.Responses;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Overlays.Changelog;\r\n\r\npublic partial class ChangeLogMarkdownContainer : OsuMarkdownContainer\r\n{\r\n    public ChangeLogMarkdownContainer(APIChangelogBuild build)\r\n    {\r\n        DocumentUrl = build.DocumentUrl;\r\n        RootUrl = build.RootUrl;\r\n        Text = build.GetFormattedContent();\r\n    }\r\n\r\n    public override OsuMarkdownTextFlowContainer CreateTextFlow() => new ChangeLogMarkdownTextFlowContainer();\r\n\r\n    public override SpriteText CreateSpriteText() => base.CreateSpriteText().With(s =>\r\n    {\r\n        s.Font = OsuFont.GetFont(size: 16, weight: FontWeight.Regular);\r\n    });\r\n\r\n    /// <summary>\r\n    /// Re-calculate image size by changelog width.\r\n    /// </summary>\r\n    public partial class ChangeLogMarkdownTextFlowContainer : OsuMarkdownTextFlowContainer\r\n    {\r\n        protected override void AddImage(LinkInline linkInline) => AddDrawable(new ChangeLogMarkdownImage(linkInline));\r\n\r\n        protected override void AddLinkText(string text, LinkInline linkInline)\r\n        {\r\n            if (linkInline.Url == null)\r\n                return;\r\n\r\n            var pullRequestInfo = ChangelogPullRequestInfo.GetPullRequestInfoFromLink(text, linkInline.Url);\r\n\r\n            if (pullRequestInfo != null)\r\n            {\r\n                addPullRequestInfo(pullRequestInfo);\r\n                return;\r\n            }\r\n\r\n            var badgeInfo = ChangelogBadgeInfo.GetBadgeInfoFromLink(text);\r\n\r\n            if (badgeInfo != null)\r\n            {\r\n                addBadgeInfo(badgeInfo);\r\n                return;\r\n            }\r\n\r\n            base.AddLinkText(text, linkInline);\r\n        }\r\n\r\n        private void addPullRequestInfo(ChangelogPullRequestInfo pullRequestInfo)\r\n        {\r\n            var pullRequests = pullRequestInfo.PullRequests;\r\n            var users = pullRequestInfo.Users;\r\n\r\n            if (pullRequests.Any())\r\n            {\r\n                AddText(\"(\");\r\n\r\n                for (int index = 0; index < pullRequests.Length; index++)\r\n                {\r\n                    var pullRequest = pullRequests[index];\r\n                    AddDrawable(new OsuMarkdownLinkText($\"{pullRequestInfo.RepoName}#{pullRequest.Number}\", new LinkInline\r\n                    {\r\n                        Url = pullRequest.Url,\r\n                    }));\r\n\r\n                    if (index != pullRequests.Length - 1)\r\n                        AddText(\", \");\r\n                }\r\n\r\n                AddText(\")\");\r\n            }\r\n\r\n            foreach (var user in users)\r\n            {\r\n                var textScale = new Vector2(0.7f);\r\n                AddText(\"    by \", t =>\r\n                {\r\n                    t.Scale = textScale;\r\n                    t.Padding = new MarginPadding { Top = 6 };\r\n                });\r\n                AddDrawable(new UserLinkText(user.UserName, new LinkInline\r\n                {\r\n                    Url = user.Url,\r\n                })\r\n                {\r\n                    Scale = textScale,\r\n                });\r\n            }\r\n        }\r\n\r\n        private void addBadgeInfo(ChangelogBadgeInfo badgeInfo)\r\n        {\r\n            AddDrawable(new Badge\r\n            {\r\n                BadgeText = badgeInfo.Text,\r\n                BadgeColour = badgeInfo.Color,\r\n            });\r\n        }\r\n\r\n        /// <summary>\r\n        /// Override <see cref=\"OsuMarkdownImage\"/> to limit image display size\r\n        /// </summary>\r\n        /// <returns></returns>\r\n        private partial class ChangeLogMarkdownImage : OsuMarkdownImage\r\n        {\r\n            private readonly LayoutValue widthSizeCache = new(Invalidation.DrawSize);\r\n\r\n            public ChangeLogMarkdownImage(LinkInline linkInline)\r\n                : base(linkInline)\r\n            {\r\n                AutoSizeAxes = Axes.None;\r\n                RelativeSizeAxes = Axes.X;\r\n\r\n                AddLayout(widthSizeCache);\r\n            }\r\n\r\n            private bool imageLoaded;\r\n\r\n            protected override void Update()\r\n            {\r\n                base.Update();\r\n\r\n                // unable to get texture size on OnLoadComplete event, so use this way.\r\n                if (!imageLoaded && InternalChild.Width != 0)\r\n                {\r\n                    computeImageSize();\r\n                    imageLoaded = true;\r\n                }\r\n\r\n                if (widthSizeCache.IsValid)\r\n                    return;\r\n\r\n                computeImageSize();\r\n                widthSizeCache.Validate();\r\n            }\r\n\r\n            private void computeImageSize()\r\n            {\r\n                // if image is larger then parent size, then adjust image scale\r\n                float scale = Math.Min(1, DrawWidth / InternalChild.Width);\r\n\r\n                InternalChild.Scale = new Vector2(scale);\r\n                Height = InternalChild.Height * scale;\r\n            }\r\n        }\r\n\r\n        private partial class UserLinkText : OsuMarkdownLinkText\r\n        {\r\n            public UserLinkText(string text, LinkInline linkInline)\r\n                : base(text, linkInline)\r\n            {\r\n                Padding = new MarginPadding { Top = 6 };\r\n            }\r\n        }\r\n\r\n        private partial class Badge : CompositeDrawable\r\n        {\r\n            private readonly Box background;\r\n            private readonly OsuSpriteText text;\r\n\r\n            public Badge()\r\n            {\r\n                AutoSizeAxes = Axes.Both;\r\n                Masking = true;\r\n                CornerRadius = 5;\r\n\r\n                InternalChildren = new Drawable[]\r\n                {\r\n                    background = new Box\r\n                    {\r\n                        RelativeSizeAxes = Axes.Both,\r\n                        Colour = Colour4.White,\r\n                    },\r\n                    text = new OsuSpriteText\r\n                    {\r\n                        Anchor = Anchor.Centre,\r\n                        Origin = Anchor.Centre,\r\n                        Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold),\r\n                        Margin = new MarginPadding { Horizontal = 5, Vertical = 3 },\r\n                    },\r\n                };\r\n            }\r\n\r\n            public ColourInfo BadgeColour\r\n            {\r\n                get => background.Colour;\r\n                set => background.Colour = value;\r\n            }\r\n\r\n            public string BadgeText\r\n            {\r\n                get => text.Text.ToString();\r\n                set => text.Text = value;\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Overlays/Changelog/ChangelogBadgeInfo.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Extensions.Color4Extensions;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Overlays.Changelog;\r\n\r\npublic class ChangelogBadgeInfo\r\n{\r\n    // follow the definition in the https://github.com/karaoke-dev/karaoke-dev.github.io/blob/master/layouts/partials/script/badge.html\r\n    private static readonly IDictionary<string, string> colour_mappings = new Dictionary<string, string>\r\n    {\r\n        { \"outdated\", \"#808080\" },\r\n        { \"rejected\", \"#FF0000\" },\r\n    };\r\n\r\n    public Color4 Color { get; init; } = Color4.White;\r\n\r\n    public string Text { get; init; } = string.Empty;\r\n\r\n    /// <summary>\r\n    /// Trying to parse the badge from the text.\r\n    /// </summary>\r\n    /// <example>\r\n    /// [outdated]<br/>\r\n    /// [rejected]\r\n    /// </example>\r\n    /// <param name=\"text\">Link text</param>\r\n    /// <returns></returns>\r\n    public static ChangelogBadgeInfo? GetBadgeInfoFromLink(string text)\r\n    {\r\n        if (!colour_mappings.TryGetValue(text, out string? repoUrl))\r\n            return null;\r\n\r\n        return new ChangelogBadgeInfo\r\n        {\r\n            Text = text,\r\n            Color = Color4Extensions.FromHex(repoUrl),\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Overlays/Changelog/ChangelogBuild.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Containers;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Online.API.Requests.Responses;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Overlays.Changelog;\r\n\r\n/// <summary>\r\n/// Display full content in <see cref=\"APIChangelogBuild\"/>\r\n/// </summary>\r\npublic partial class ChangelogBuild : FillFlowContainer\r\n{\r\n    public const float HORIZONTAL_PADDING = 70;\r\n\r\n    public Action<APIChangelogBuild>? SelectBuild;\r\n\r\n    protected readonly APIChangelogBuild Build;\r\n\r\n    public readonly ChangeLogMarkdownContainer ChangelogEntries;\r\n\r\n    public ChangelogBuild(APIChangelogBuild build)\r\n    {\r\n        Build = build;\r\n\r\n        RelativeSizeAxes = Axes.X;\r\n        AutoSizeAxes = Axes.Y;\r\n        Direction = FillDirection.Vertical;\r\n        Padding = new MarginPadding { Horizontal = HORIZONTAL_PADDING };\r\n\r\n        Children = new Drawable[]\r\n        {\r\n            CreateHeader(),\r\n            ChangelogEntries = new ChangeLogMarkdownContainer(build)\r\n            {\r\n                RelativeSizeAxes = Axes.X,\r\n                AutoSizeAxes = Axes.Y,\r\n            },\r\n        };\r\n    }\r\n\r\n    protected virtual FillFlowContainer CreateHeader() => new()\r\n    {\r\n        Anchor = Anchor.TopCentre,\r\n        Origin = Anchor.TopCentre,\r\n        AutoSizeAxes = Axes.Both,\r\n        Direction = FillDirection.Horizontal,\r\n        Margin = new MarginPadding { Top = 20 },\r\n        Children = new Drawable[]\r\n        {\r\n            new OsuHoverContainer\r\n            {\r\n                Anchor = Anchor.Centre,\r\n                Origin = Anchor.Centre,\r\n                AutoSizeAxes = Axes.Both,\r\n                Action = () => SelectBuild?.Invoke(Build),\r\n                Child = new FillFlowContainer<SpriteText>\r\n                {\r\n                    AutoSizeAxes = Axes.Both,\r\n                    Margin = new MarginPadding { Horizontal = 40 },\r\n                    Children = new[]\r\n                    {\r\n                        new OsuSpriteText\r\n                        {\r\n                            Text = \"Karaoke!\",\r\n                            Font = OsuFont.GetFont(weight: FontWeight.Medium, size: 19),\r\n                        },\r\n                        new OsuSpriteText\r\n                        {\r\n                            Text = \" \",\r\n                            Font = OsuFont.GetFont(weight: FontWeight.Medium, size: 19),\r\n                        },\r\n                        new OsuSpriteText\r\n                        {\r\n                            Text = Build.DisplayVersion,\r\n                            Font = OsuFont.GetFont(weight: FontWeight.Light, size: 19),\r\n                            Colour = Color4.Red,\r\n                        },\r\n                    },\r\n                },\r\n            },\r\n        },\r\n    };\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Overlays/Changelog/ChangelogContent.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Rulesets.Karaoke.Online.API.Requests.Responses;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Overlays.Changelog;\r\n\r\n/// <summary>\r\n/// Base change log content\r\n/// </summary>\r\npublic abstract partial class ChangelogContent : FillFlowContainer\r\n{\r\n    public Action<APIChangelogBuild>? BuildSelected;\r\n\r\n    protected void SelectBuild(APIChangelogBuild build) => BuildSelected?.Invoke(build);\r\n\r\n    protected ChangelogContent()\r\n    {\r\n        RelativeSizeAxes = Axes.X;\r\n        AutoSizeAxes = Axes.Y;\r\n        Direction = FillDirection.Vertical;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Overlays/Changelog/ChangelogHeader.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Localisation;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Resources.Localisation.Web;\r\nusing osu.Game.Rulesets.Karaoke.Online.API.Requests.Responses;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Overlays.Changelog;\r\n\r\n/// <summary>\r\n/// Change log header, display <see cref=\"APIChangelogBuild\"/> title\r\n/// </summary>\r\npublic partial class ChangelogHeader : BreadcrumbControlOverlayHeader\r\n{\r\n    public readonly Bindable<APIChangelogBuild?> Build = new();\r\n\r\n    public Action? ListingSelected;\r\n\r\n    public static LocalisableString ListingString => LayoutStrings.HeaderChangelogIndex;\r\n\r\n    public ChangelogHeader()\r\n    {\r\n        TabControl.AddItem(ListingString);\r\n\r\n        Current.ValueChanged += e =>\r\n        {\r\n            if (e.NewValue == ListingString)\r\n                ListingSelected?.Invoke();\r\n        };\r\n\r\n        Build.BindValueChanged(e =>\r\n        {\r\n            if (e.OldValue != null)\r\n                TabControl.RemoveItem(e.OldValue.DisplayVersion);\r\n\r\n            if (e.NewValue != null)\r\n            {\r\n                TabControl.AddItem(e.NewValue.DisplayVersion);\r\n                Current.Value = e.NewValue.DisplayVersion;\r\n            }\r\n            else\r\n            {\r\n                Current.Value = ListingString;\r\n            }\r\n        });\r\n    }\r\n\r\n    protected override OverlayTitle CreateTitle() => new ChangelogHeaderTitle();\r\n\r\n    private partial class ChangelogHeaderTitle : OverlayTitle\r\n    {\r\n        public ChangelogHeaderTitle()\r\n        {\r\n            Title = PageTitleStrings.MainChangelogControllerDefault;\r\n            Description = NamedOverlayComponentStrings.ChangelogDescription;\r\n            Icon = OsuIcon.ChangelogB;\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Overlays/Changelog/ChangelogListing.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Online.API.Requests.Responses;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Overlays.Changelog;\r\n\r\n/// <summary>\r\n/// Display list of <see cref=\"APIChangelogBuild\"/>\r\n/// </summary>\r\npublic partial class ChangelogListing : ChangelogContent\r\n{\r\n    private readonly IReadOnlyList<APIChangelogBuild> entries;\r\n\r\n    public ChangelogListing(List<APIChangelogBuild> entries)\r\n    {\r\n        this.entries = entries;\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OverlayColourProvider colourProvider, Bindable<APIChangelogBuild?> current)\r\n    {\r\n        foreach (var build in entries)\r\n        {\r\n            if (Children.Count != 0)\r\n            {\r\n                Add(new Box\r\n                {\r\n                    RelativeSizeAxes = Axes.X,\r\n                    Height = 2,\r\n                    Colour = colourProvider.Background6,\r\n                    Margin = new MarginPadding { Top = 30 },\r\n                });\r\n            }\r\n\r\n            Add(new ChangelogBuild(build)\r\n            {\r\n                Masking = true,\r\n                AutoSizeAxes = Axes.None,\r\n                Height = 300,\r\n                SelectBuild = SelectBuild,\r\n            });\r\n        }\r\n\r\n        if (entries.Any())\r\n        {\r\n            Add(new ShowMoreButton\r\n            {\r\n                Anchor = Anchor.TopCentre,\r\n                Origin = Anchor.TopCentre,\r\n                Padding = new MarginPadding { Top = 15, Bottom = 15 },\r\n                Action = () =>\r\n                {\r\n                    current.Value = entries.LastOrDefault();\r\n                },\r\n            });\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Overlays/Changelog/ChangelogPullRequestInfo.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing System.Text.RegularExpressions;\r\nusing osu.Game.Rulesets.Karaoke.Extensions;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Overlays.Changelog;\r\n\r\npublic class ChangelogPullRequestInfo\r\n{\r\n    private static readonly IDictionary<string, string> repo_urls = new Dictionary<string, string>\r\n    {\r\n        { \"karaoke\", \"https://github.com/karaoke-dev/karaoke/\" },\r\n        { \"edge\", \"https://github.com/karaoke-dev/karaoke/\" },\r\n        { \"github.io\", \"https://github.com/karaoke-dev/karaoke-dev.github.io/\" },\r\n        { \"launcher\", \"https://github.com/karaoke-dev/launcher/\" },\r\n        { \"sample\", \"https://github.com/karaoke-dev/sample-beatmap/\" },\r\n        { \"microphone-package\", \"https://github.com/karaoke-dev/osu-framework-microphone/\" },\r\n        { \"font-package\", \"https://github.com/karaoke-dev/osu-framework-font/\" },\r\n    };\r\n\r\n    public string RepoName { get; init; } = string.Empty;\r\n\r\n    public PullRequestInfo[] PullRequests { get; init; } = Array.Empty<PullRequestInfo>();\r\n\r\n    public UserInfo[] Users { get; init; } = Array.Empty<UserInfo>();\r\n\r\n    public readonly struct PullRequestInfo\r\n    {\r\n        public PullRequestInfo(string repoName, int number)\r\n        {\r\n            Number = number;\r\n            Url = new Uri(new Uri(repo_urls[repoName]), $\"pull/{number}\").AbsoluteUri;\r\n        }\r\n\r\n        public int Number { get; } = 0;\r\n\r\n        public string Url { get; } = string.Empty;\r\n    }\r\n\r\n    public readonly struct UserInfo\r\n    {\r\n        public UserInfo(string useName)\r\n        {\r\n            UserName = useName;\r\n        }\r\n\r\n        public string UserName { get; } = string.Empty;\r\n\r\n        public string Url => $\"https://github.com/{UserName}\";\r\n    }\r\n\r\n    /// <summary>\r\n    /// Trying to parse the pull-request info from the url.\r\n    /// </summary>\r\n    /// <example>\r\n    /// #2152@andy840119<br/>\r\n    /// #2152<br/>\r\n    /// #2152#2153<br/>\r\n    /// @andy840119<br/>\r\n    /// @andy@andy840119\r\n    /// </example>\r\n    /// <param name=\"repo\">Link text</param>\r\n    /// <param name=\"info\">Link url</param>\r\n    /// <returns></returns>\r\n    public static ChangelogPullRequestInfo? GetPullRequestInfoFromLink(string repo, string info)\r\n    {\r\n        if (!repo_urls.ContainsKey(repo))\r\n            return null;\r\n\r\n        const string pull_request_key = \"pull_request\";\r\n        const string username_key = \"username\";\r\n        const string pull_request_regex = $\"#(?<{pull_request_key}>[0-9]+)|@(?<{username_key}>[0-9A-z]+)\";\r\n\r\n        // note: should have at least one pr number or one username,\r\n        var result = Regex.Matches(info, pull_request_regex, RegexOptions.IgnoreCase);\r\n        if (!result.Any())\r\n            return null;\r\n\r\n        // get pull-request or username\r\n        var prs = result.Select(x => x.GetGroupValue<string>(pull_request_key))\r\n                        .Where(x => !string.IsNullOrEmpty(x))\r\n                        .Distinct();\r\n        var usernames = result.Select(x => x.GetGroupValue<string>(username_key))\r\n                              .Where(x => !string.IsNullOrEmpty(x))\r\n                              .Distinct();\r\n\r\n        return new ChangelogPullRequestInfo\r\n        {\r\n            RepoName = repo,\r\n            PullRequests = prs.Select(pr => new PullRequestInfo(repo, int.Parse(pr))).ToArray(),\r\n            Users = usernames.Select(userName => new UserInfo(userName)).ToArray(),\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Overlays/Changelog/ChangelogSingleBuild.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Threading.Tasks;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Extensions.Color4Extensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Online.API.Requests;\r\nusing osu.Game.Rulesets.Karaoke.Online.API.Requests.Responses;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Overlays.Changelog;\r\n\r\n/// <summary>\r\n/// Display <see cref=\"APIChangelogBuild\"/> detail\r\n/// </summary>\r\npublic partial class ChangelogSingleBuild : ChangelogContent\r\n{\r\n    private readonly APIChangelogBuild originBuild;\r\n\r\n    public ChangelogSingleBuild(APIChangelogBuild build)\r\n    {\r\n        originBuild = build;\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OverlayColourProvider colourProvider)\r\n    {\r\n        Task.Run(async () =>\r\n        {\r\n            var tcs = new TaskCompletionSource<bool>();\r\n\r\n            var req = new GetChangelogBuildRequest(originBuild);\r\n            req.Success += build =>\r\n            {\r\n                Schedule(() =>\r\n                {\r\n                    Children = new Drawable[]\r\n                    {\r\n                        new ChangelogBuildWithNavigation(build) { SelectBuild = SelectBuild },\r\n                        new Box\r\n                        {\r\n                            RelativeSizeAxes = Axes.X,\r\n                            Height = 2,\r\n                            Colour = colourProvider.Background6,\r\n                            Margin = new MarginPadding { Top = 30 },\r\n                        },\r\n                    };\r\n                });\r\n            };\r\n            req.Failure += _ =>\r\n            {\r\n                // todo: maybe display some\r\n            };\r\n\r\n            await req.Perform().ConfigureAwait(false);\r\n\r\n            return tcs.Task;\r\n        }).Unwrap();\r\n    }\r\n\r\n    public partial class ChangelogBuildWithNavigation : ChangelogBuild\r\n    {\r\n        public ChangelogBuildWithNavigation(APIChangelogBuild build)\r\n            : base(build)\r\n        {\r\n        }\r\n\r\n        protected override FillFlowContainer CreateHeader()\r\n        {\r\n            var fill = base.CreateHeader();\r\n\r\n            fill.Insert(-1, new NavigationIconButton(Build.Versions.Next)\r\n            {\r\n                Icon = FontAwesome.Solid.ChevronLeft,\r\n                SelectBuild = b => SelectBuild?.Invoke(b),\r\n            });\r\n            fill.Insert(1, new NavigationIconButton(Build.Versions.Previous)\r\n            {\r\n                Icon = FontAwesome.Solid.ChevronRight,\r\n                SelectBuild = b => SelectBuild?.Invoke(b),\r\n            });\r\n\r\n            return fill;\r\n        }\r\n    }\r\n\r\n    private partial class NavigationIconButton : IconButton\r\n    {\r\n        public Action<APIChangelogBuild>? SelectBuild;\r\n\r\n        public NavigationIconButton(APIChangelogBuild? build)\r\n        {\r\n            Anchor = Anchor.Centre;\r\n            Origin = Anchor.Centre;\r\n\r\n            if (build == null) return;\r\n\r\n            TooltipText = build.DisplayVersion;\r\n\r\n            Action = () =>\r\n            {\r\n                SelectBuild?.Invoke(build);\r\n                Enabled.Value = false;\r\n            };\r\n        }\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(OsuColour colours)\r\n        {\r\n            HoverColour = colours.GreyVioletLight.Opacity(0.6f);\r\n            FlashColour = colours.GreyVioletLighter;\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Overlays/Changelog/Sidebar/ChangelogSection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Diagnostics;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Extensions.IEnumerableExtensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Containers;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Online.API.Requests.Responses;\r\nusing osuTK;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Overlays.Changelog.Sidebar;\r\n\r\npublic partial class ChangelogSection : CompositeDrawable\r\n{\r\n    private const int animation_duration = 250;\r\n    private const float font_size = 16;\r\n\r\n    public readonly BindableBool Expanded = new(true);\r\n\r\n    public ChangelogSection(int year, IReadOnlyList<APIChangelogBuild> posts)\r\n    {\r\n        Debug.Assert(posts.All(p =>\r\n        {\r\n            ArgumentNullException.ThrowIfNull(p);\r\n\r\n            return p.PublishedAt.Year == year;\r\n        }));\r\n\r\n        RelativeSizeAxes = Axes.X;\r\n        AutoSizeAxes = Axes.Y;\r\n        Masking = true;\r\n\r\n        InternalChild = new FillFlowContainer\r\n        {\r\n            RelativeSizeAxes = Axes.X,\r\n            AutoSizeAxes = Axes.Y,\r\n            Direction = FillDirection.Vertical,\r\n            Children = new Drawable[]\r\n            {\r\n                new PostsContainer\r\n                {\r\n                    Expanded = { BindTarget = Expanded },\r\n                    Children = posts.Select(p => new PostButton(p)).ToArray(),\r\n                },\r\n            },\r\n        };\r\n    }\r\n\r\n    private partial class PostButton : OsuHoverContainer\r\n    {\r\n        protected override IEnumerable<Drawable> EffectTargets => new[] { text };\r\n\r\n        private readonly TextFlowContainer text;\r\n        private readonly APIChangelogBuild post;\r\n\r\n        public PostButton(APIChangelogBuild post)\r\n        {\r\n            this.post = post;\r\n\r\n            RelativeSizeAxes = Axes.X;\r\n            AutoSizeAxes = Axes.Y;\r\n            Child = text = new TextFlowContainer(t => t.Font = OsuFont.GetFont(size: font_size))\r\n            {\r\n                RelativeSizeAxes = Axes.X,\r\n                AutoSizeAxes = Axes.Y,\r\n                Text = post.DisplayVersion,\r\n            };\r\n        }\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(OverlayColourProvider overlayColours, Bindable<APIChangelogBuild?> current)\r\n        {\r\n            current.BindValueChanged(e =>\r\n            {\r\n                bool isCurrent = post == e.NewValue;\r\n\r\n                // update hover color.\r\n                Colour = isCurrent ? Color4.White : overlayColours.Light2;\r\n                HoverColour = isCurrent ? Color4.White : overlayColours.Light1;\r\n\r\n                // update font.\r\n                text.Children.OfType<SpriteText>().ForEach(f =>\r\n                {\r\n                    f.Font = OsuFont.GetFont(size: font_size, weight: isCurrent ? FontWeight.SemiBold : FontWeight.Medium);\r\n                });\r\n            }, true);\r\n\r\n            TooltipText = ChangelogStrings.ViewCurrentChangelog;\r\n\r\n            Action = () => current.Value = post;\r\n        }\r\n    }\r\n\r\n    private partial class PostsContainer : Container\r\n    {\r\n        public readonly BindableBool Expanded = new();\r\n\r\n        protected override Container<Drawable> Content { get; }\r\n\r\n        public PostsContainer()\r\n        {\r\n            RelativeSizeAxes = Axes.X;\r\n            AutoSizeAxes = Axes.Y;\r\n            AutoSizeDuration = animation_duration;\r\n            AutoSizeEasing = Easing.Out;\r\n            InternalChild = Content = new FillFlowContainer\r\n            {\r\n                Margin = new MarginPadding { Top = 5 },\r\n                RelativeSizeAxes = Axes.X,\r\n                AutoSizeAxes = Axes.Y,\r\n                Direction = FillDirection.Vertical,\r\n                Spacing = new Vector2(0, 5),\r\n                Alpha = 0,\r\n            };\r\n        }\r\n\r\n        protected override void LoadComplete()\r\n        {\r\n            base.LoadComplete();\r\n            Expanded.BindValueChanged(updateState, true);\r\n        }\r\n\r\n        private void updateState(ValueChangedEvent<bool> expanded)\r\n        {\r\n            ClearTransforms(true);\r\n\r\n            if (expanded.NewValue)\r\n            {\r\n                AutoSizeAxes = Axes.Y;\r\n                Content.FadeIn(animation_duration, Easing.OutQuint);\r\n            }\r\n            else\r\n            {\r\n                AutoSizeAxes = Axes.None;\r\n                this.ResizeHeightTo(0, animation_duration, Easing.OutQuint);\r\n\r\n                Content.FadeOut(animation_duration, Easing.OutQuint);\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Overlays/Changelog/Sidebar/ChangelogSidebar.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Game.Graphics.Containers;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Online.API.Requests.Responses;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Overlays.Changelog.Sidebar;\r\n\r\npublic partial class ChangelogSidebar : CompositeDrawable\r\n{\r\n    [Cached]\r\n    public readonly Bindable<APIChangelogIndex> Metadata = new();\r\n\r\n    [Cached]\r\n    public readonly Bindable<int> Year = new();\r\n\r\n    private FillFlowContainer<ChangelogSection> changelogsFlow = null!;\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OverlayColourProvider colourProvider, Bindable<APIChangelogBuild?> current)\r\n    {\r\n        RelativeSizeAxes = Axes.Y;\r\n        Width = 250;\r\n        InternalChildren = new Drawable[]\r\n        {\r\n            new Box\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n                Colour = colourProvider.Background4,\r\n            },\r\n            new Box\r\n            {\r\n                RelativeSizeAxes = Axes.Y,\r\n                Width = OsuScrollContainer.SCROLL_BAR_WIDTH,\r\n                Anchor = Anchor.TopRight,\r\n                Origin = Anchor.TopRight,\r\n                Colour = colourProvider.Background3,\r\n                Alpha = 0.5f,\r\n            },\r\n            new Container\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n                Padding = new MarginPadding { Right = -3 }, // Compensate for scrollbar margin\r\n                Child = new OsuScrollContainer\r\n                {\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    Child = new Container\r\n                    {\r\n                        RelativeSizeAxes = Axes.X,\r\n                        AutoSizeAxes = Axes.Y,\r\n                        Padding = new MarginPadding { Right = 3 }, // Add 3px back\r\n                        Child = new Container\r\n                        {\r\n                            RelativeSizeAxes = Axes.X,\r\n                            AutoSizeAxes = Axes.Y,\r\n                            Padding = new MarginPadding\r\n                            {\r\n                                Vertical = 20,\r\n                                Left = 50,\r\n                                Right = 30,\r\n                            },\r\n                            Child = new FillFlowContainer\r\n                            {\r\n                                Direction = FillDirection.Vertical,\r\n                                RelativeSizeAxes = Axes.X,\r\n                                AutoSizeAxes = Axes.Y,\r\n                                Spacing = new Vector2(0, 20),\r\n                                Children = new Drawable[]\r\n                                {\r\n                                    new YearsPanel(),\r\n                                    changelogsFlow = new FillFlowContainer<ChangelogSection>\r\n                                    {\r\n                                        AutoSizeAxes = Axes.Y,\r\n                                        RelativeSizeAxes = Axes.X,\r\n                                        Direction = FillDirection.Vertical,\r\n                                        Spacing = new Vector2(0, 10),\r\n                                    },\r\n                                },\r\n                            },\r\n                        },\r\n                    },\r\n                },\r\n            },\r\n        };\r\n\r\n        // should switch year selection if user switch changelog and the new changelog is not current year.\r\n        current.BindValueChanged(e =>\r\n        {\r\n            if (e.NewValue == null)\r\n                return;\r\n\r\n            Year.Value = e.NewValue.PublishedAt.Year;\r\n        });\r\n    }\r\n\r\n    protected override void LoadComplete()\r\n    {\r\n        base.LoadComplete();\r\n\r\n        Metadata.BindValueChanged(e => onMetadataChanged(e.NewValue, Year.Value), true);\r\n        Year.BindValueChanged(e => onMetadataChanged(Metadata.Value, e.NewValue), true);\r\n    }\r\n\r\n    private void onMetadataChanged(APIChangelogIndex? metadata, int targetYear)\r\n    {\r\n        changelogsFlow.Clear();\r\n\r\n        if (metadata == null)\r\n            return;\r\n\r\n        var builds = metadata.Builds;\r\n\r\n        if (builds.Any() != true)\r\n            return;\r\n\r\n        var lookup = builds.ToLookup(post => post.PublishedAt.Year);\r\n        var posts = lookup[targetYear].ToList();\r\n        changelogsFlow.Add(new ChangelogSection(targetYear, posts));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Overlays/Changelog/Sidebar/YearsPanel.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Containers;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Online.API.Requests.Responses;\r\nusing osuTK;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Overlays.Changelog.Sidebar;\r\n\r\npublic partial class YearsPanel : CompositeDrawable\r\n{\r\n    private readonly Bindable<APIChangelogIndex> metadata = new();\r\n\r\n    private FillFlowContainer yearsFlow = null!;\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OverlayColourProvider overlayColours, Bindable<APIChangelogIndex> metadata)\r\n    {\r\n        this.metadata.BindTo(metadata);\r\n\r\n        AutoSizeAxes = Axes.Y;\r\n        RelativeSizeAxes = Axes.X;\r\n        Masking = true;\r\n        CornerRadius = 6;\r\n        InternalChildren = new Drawable[]\r\n        {\r\n            new Box\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n                Colour = overlayColours.Background3,\r\n            },\r\n            new Container\r\n            {\r\n                RelativeSizeAxes = Axes.X,\r\n                AutoSizeAxes = Axes.Y,\r\n                Padding = new MarginPadding(5),\r\n                Child = yearsFlow = new FillFlowContainer\r\n                {\r\n                    RelativeSizeAxes = Axes.X,\r\n                    AutoSizeAxes = Axes.Y,\r\n                    Spacing = new Vector2(0, 5),\r\n                },\r\n            },\r\n        };\r\n    }\r\n\r\n    protected override void LoadComplete()\r\n    {\r\n        base.LoadComplete();\r\n\r\n        metadata.BindValueChanged(_ => recreateDrawables(), true);\r\n    }\r\n\r\n    private void recreateDrawables()\r\n    {\r\n        yearsFlow.Clear();\r\n\r\n        if (metadata.Value == null)\r\n        {\r\n            Hide();\r\n            return;\r\n        }\r\n\r\n        foreach (int y in metadata.Value.Years)\r\n            yearsFlow.Add(new YearButton(y));\r\n\r\n        Show();\r\n    }\r\n\r\n    public partial class YearButton : OsuHoverContainer\r\n    {\r\n        private readonly int year;\r\n        private readonly OsuSpriteText yearText;\r\n\r\n        public YearButton(int year)\r\n        {\r\n            this.year = year;\r\n\r\n            RelativeSizeAxes = Axes.X;\r\n            Width = 0.25f;\r\n            Height = 15;\r\n\r\n            Child = yearText = new OsuSpriteText\r\n            {\r\n                Anchor = Anchor.Centre,\r\n                Origin = Anchor.Centre,\r\n                Text = year.ToString(),\r\n            };\r\n        }\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(OverlayColourProvider colourProvider, Bindable<int> currentYear)\r\n        {\r\n            currentYear.BindValueChanged(e =>\r\n            {\r\n                bool isCurrent = year == e.NewValue;\r\n\r\n                // update hover color.\r\n                Colour = isCurrent ? Color4.White : colourProvider.Light2;\r\n                HoverColour = isCurrent ? Color4.White : colourProvider.Light1;\r\n\r\n                // update font.\r\n                yearText.Font = OsuFont.GetFont(size: 16, weight: isCurrent ? FontWeight.SemiBold : FontWeight.Medium);\r\n            }, true);\r\n\r\n            Action = () =>\r\n            {\r\n                currentYear.Value = year;\r\n            };\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Overlays/Dialog/OkPopupDialog.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Game.Overlays.Dialog;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Overlays.Dialog;\r\n\r\npublic partial class OkPopupDialog : PopupDialog\r\n{\r\n    public OkPopupDialog(Action<bool>? okAction = null)\r\n    {\r\n        Buttons = new PopupDialogButton[]\r\n        {\r\n            new PopupDialogOkButton\r\n            {\r\n                Text = \"OK\",\r\n                Action = () => okAction?.Invoke(true),\r\n            },\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Overlays/KaraokeChangelogOverlay.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing System.Threading;\r\nusing System.Threading.Tasks;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Audio;\r\nusing osu.Framework.Audio.Sample;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Game.Input.Bindings;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Online.API.Requests;\r\nusing osu.Game.Rulesets.Karaoke.Online.API.Requests.Responses;\r\nusing osu.Game.Rulesets.Karaoke.Overlays.Changelog;\r\nusing osu.Game.Rulesets.Karaoke.Overlays.Changelog.Sidebar;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Overlays;\r\n\r\npublic partial class KaraokeChangelogOverlay : OnlineOverlay<ChangelogHeader>\r\n{\r\n    public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks;\r\n\r\n    [Cached]\r\n    public readonly Bindable<APIChangelogBuild?> Current = new();\r\n\r\n    private readonly Container sidebarContainer;\r\n    private readonly ChangelogSidebar sidebar;\r\n    private readonly Container content;\r\n\r\n    private Sample? sampleBack;\r\n\r\n    private APIChangelogIndex? index;\r\n\r\n    private readonly string organizationName;\r\n    private readonly string branchName;\r\n\r\n    private string projectName => $\"{organizationName}.github.io\";\r\n\r\n    public KaraokeChangelogOverlay(string organization, string branch = \"master\")\r\n        : base(OverlayColourScheme.Purple, false)\r\n    {\r\n        organizationName = organization;\r\n        branchName = branch;\r\n\r\n        Child = new GridContainer\r\n        {\r\n            RelativeSizeAxes = Axes.X,\r\n            AutoSizeAxes = Axes.Y,\r\n            RowDimensions = new[]\r\n            {\r\n                new Dimension(GridSizeMode.AutoSize),\r\n            },\r\n            ColumnDimensions = new[]\r\n            {\r\n                new Dimension(GridSizeMode.AutoSize),\r\n                new Dimension(),\r\n            },\r\n            Content = new[]\r\n            {\r\n                new Drawable[]\r\n                {\r\n                    sidebarContainer = new Container\r\n                    {\r\n                        AutoSizeAxes = Axes.X,\r\n                        Child = sidebar = new ChangelogSidebar(),\r\n                    },\r\n                    content = new Container\r\n                    {\r\n                        RelativeSizeAxes = Axes.X,\r\n                        AutoSizeAxes = Axes.Y,\r\n                    },\r\n                },\r\n            },\r\n        };\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(AudioManager audio)\r\n    {\r\n        Header.Build.BindTo(Current);\r\n\r\n        sampleBack = audio.Samples.Get(\"UI/generic-select-soft\");\r\n\r\n        Current.BindValueChanged(e =>\r\n        {\r\n            if (e.NewValue != null)\r\n            {\r\n                loadContent(new ChangelogSingleBuild(e.NewValue));\r\n            }\r\n            else if (index != null)\r\n            {\r\n                loadContent(new ChangelogListing(index.PreviewBuilds));\r\n            }\r\n            else\r\n            {\r\n                // todo: should show oops content.\r\n            }\r\n        });\r\n    }\r\n\r\n    protected override void UpdateAfterChildren()\r\n    {\r\n        base.UpdateAfterChildren();\r\n        sidebarContainer.Height = DrawHeight;\r\n        sidebarContainer.Y = (float)Math.Clamp(ScrollFlow.Current - Header.DrawHeight, 0, Math.Max(ScrollFlow.ScrollContent.DrawHeight - DrawHeight - Header.DrawHeight, 0));\r\n    }\r\n\r\n    protected override ChangelogHeader CreateHeader() => new()\r\n    {\r\n        ListingSelected = ShowListing,\r\n    };\r\n\r\n    protected override Color4 BackgroundColour => ColourProvider.Background4;\r\n\r\n    public void ShowListing()\r\n    {\r\n        Current.Value = null;\r\n        Show();\r\n    }\r\n\r\n    /// <summary>\r\n    /// Fetches and shows a specific build from a specific update stream.\r\n    /// </summary>\r\n    /// <param name=\"build\"> Singer <see cref=\"APIChangelogBuild\"/>.</param>\r\n    public void ShowBuild(APIChangelogBuild build)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(build);\r\n\r\n        Current.Value = build;\r\n        Show();\r\n    }\r\n\r\n    public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)\r\n    {\r\n        switch (e.Action)\r\n        {\r\n            case GlobalAction.Back:\r\n                if (Current.Value == null)\r\n                {\r\n                    Hide();\r\n                }\r\n                else\r\n                {\r\n                    Current.Value = null;\r\n                    sampleBack?.Play();\r\n                }\r\n\r\n                return true;\r\n\r\n            default:\r\n                return false;\r\n        }\r\n    }\r\n\r\n    protected override void PopIn()\r\n    {\r\n        base.PopIn();\r\n\r\n        // fetch and refresh to show listing, if no other request was made via Show methods\r\n        if (initialFetchTask == null)\r\n        {\r\n            performAfterFetch(() =>\r\n            {\r\n                Current.TriggerChange();\r\n\r\n                if (index == null)\r\n                    return;\r\n\r\n                sidebar.Year.Value = index.Years.Max();\r\n                sidebar.Metadata.Value = index;\r\n            });\r\n        }\r\n    }\r\n\r\n    private Task? initialFetchTask;\r\n\r\n    private void performAfterFetch(Action action) => fetchListing()\r\n        .ContinueWith(_ => Schedule(action), TaskContinuationOptions.OnlyOnRanToCompletion);\r\n\r\n    private Task fetchListing()\r\n    {\r\n        if (initialFetchTask != null)\r\n            return initialFetchTask;\r\n\r\n        return initialFetchTask = Task.Run(async () =>\r\n        {\r\n            var tcs = new TaskCompletionSource<bool>();\r\n\r\n            var req = new GetChangelogRequest();\r\n\r\n            req.Success += res => Schedule(() =>\r\n            {\r\n                index = res;\r\n                tcs.SetResult(true);\r\n            });\r\n\r\n            req.Failure += e =>\r\n            {\r\n                initialFetchTask = null;\r\n                tcs.SetException(e);\r\n            };\r\n\r\n            await req.Perform().ConfigureAwait(false);\r\n\r\n            return tcs.Task;\r\n        }).Unwrap();\r\n    }\r\n\r\n    private CancellationTokenSource? loadContentCancellation;\r\n\r\n    private void loadContent(ChangelogContent newContent)\r\n    {\r\n        content.FadeTo(0.2f, 300, Easing.OutQuint);\r\n\r\n        loadContentCancellation?.Cancel();\r\n\r\n        LoadComponentAsync(newContent, c =>\r\n        {\r\n            content.FadeIn(300, Easing.OutQuint);\r\n\r\n            // if content changed view version\r\n            c.BuildSelected = ShowBuild;\r\n            content.Child = c;\r\n        }, (loadContentCancellation = new CancellationTokenSource()).Token);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Replays/KaraokeAutoGenerator.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Replays;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Mods;\r\nusing osu.Game.Rulesets.Replays;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Replays;\r\n\r\npublic class KaraokeAutoGenerator : AutoGenerator\r\n{\r\n    public KaraokeAutoGenerator(IBeatmap beatmap, IReadOnlyList<Mod>? mods = null)\r\n        : base(beatmap)\r\n    {\r\n    }\r\n\r\n    public override Replay Generate()\r\n    {\r\n        var notes = Beatmap.HitObjects.OfType<Note>().Where(x => x.Display).ToArray();\r\n        return new Replay\r\n        {\r\n            Frames = notes.SelectMany((element, index) => getReplayFrames(element, notes.ElementAtOrDefault(index + 1))).ToList(),\r\n        };\r\n    }\r\n\r\n    private IEnumerable<ReplayFrame> getReplayFrames(Note note, Note? next)\r\n    {\r\n        double startTime = note.StartTime;\r\n        double endTime = note.EndTime;\r\n\r\n        // Generate frame each 100ms\r\n        for (double i = startTime; i < endTime; i += 100)\r\n        {\r\n            float scale = note.Tone.Scale + (note.Tone.Half ? 0.5f : 0);\r\n            yield return new KaraokeReplayFrame(i, scale);\r\n        }\r\n\r\n        if ((next?.StartTime ?? int.MaxValue) - note.EndTime > 500)\r\n        {\r\n            yield return new KaraokeReplayFrame(endTime + 1);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Replays/KaraokeAutoGeneratorBySinger.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.IO;\r\nusing System.Linq;\r\nusing System.Threading;\r\nusing System.Threading.Tasks;\r\nusing ManagedBass;\r\nusing NWaves.Features;\r\nusing osu.Framework.Audio.Callbacks;\r\nusing osu.Framework.Audio.Track;\r\nusing osu.Framework.Extensions;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Replays;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Replays;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Replays;\r\n\r\npublic class KaraokeAutoGeneratorBySinger : AutoGenerator\r\n{\r\n    private readonly CancellationTokenSource cancelSource = new();\r\n    private readonly Task<Dictionary<double, float?>> readTask = null!;\r\n\r\n    /// <summary>\r\n    /// Using audio's voice to generate replay frames\r\n    /// Logic is copied from <see cref=\"Waveform\"/>\r\n    /// </summary>\r\n    /// <param name=\"beatmap\"></param>\r\n    /// <param name=\"data\"></param>\r\n    public KaraokeAutoGeneratorBySinger(IBeatmap beatmap, Stream? data)\r\n        : base(beatmap)\r\n    {\r\n        if (data == null)\r\n            return;\r\n\r\n        readTask = Task.Run(() =>\r\n        {\r\n            int decodeStream;\r\n\r\n            using (var fileCallbacks = new FileCallbacks(new DataStreamFileProcedures(data)))\r\n            {\r\n                decodeStream = Bass.CreateStream(StreamSystem.NoBuffer, BassFlags.Decode | BassFlags.Float, fileCallbacks.Callbacks, fileCallbacks.Handle);\r\n            }\r\n\r\n            Bass.ChannelGetInfo(decodeStream, out var info);\r\n\r\n            long totalLength = Bass.ChannelGetLength(decodeStream);\r\n            double trackLength = Bass.ChannelBytes2Seconds(decodeStream, totalLength) * 1000;\r\n            long length = totalLength;\r\n            long lengthSum = 0;\r\n\r\n            const int bytes_per_sample = 4;\r\n\r\n            // Microphone at period 10\r\n            int bytesPerIteration = 3276 * info.Channels * bytes_per_sample;\r\n\r\n            var pitches = new Dictionary<double, float?>();\r\n            float[] sampleBuffer = new float[bytesPerIteration / bytes_per_sample];\r\n\r\n            // Read sample data\r\n            while (length > 0)\r\n            {\r\n                length = Bass.ChannelGetData(decodeStream, sampleBuffer, bytesPerIteration);\r\n                lengthSum += length;\r\n\r\n                // usually sample 1 is vocal\r\n                float[] channel0Sample = sampleBuffer.Where((_, i) => i % 2 == 0).ToArray();\r\n                //var channel1Sample = sampleBuffer.Where((x, i) => i % 2 != 0).ToArray();\r\n\r\n                // Convert buffer to pitch data\r\n                double time = lengthSum * trackLength / totalLength;\r\n                float pitch = Pitch.FromYin(channel0Sample, info.Frequency, low: 40, high: 1000);\r\n                pitches.Add(time, pitch == 0 ? default(float?) : pitch);\r\n            }\r\n\r\n            return pitches;\r\n        }, cancelSource.Token);\r\n    }\r\n\r\n    public override Replay Generate()\r\n    {\r\n        var result = readTask.GetResultSafely();\r\n        return new Replay\r\n        {\r\n            Frames = getReplayFrames(result).ToList(),\r\n        };\r\n    }\r\n\r\n    private IEnumerable<ReplayFrame> getReplayFrames(IDictionary<double, float?> pitches)\r\n    {\r\n        var lastPitch = pitches.FirstOrDefault();\r\n\r\n        foreach (var pitch in pitches)\r\n        {\r\n            if (pitch.Value != null)\r\n            {\r\n                float scale = Beatmap.PitchToScale(pitch.Value ?? 0);\r\n                yield return new KaraokeReplayFrame(pitch.Key, scale);\r\n            }\r\n            else if (lastPitch.Value != null)\r\n                yield return new KaraokeReplayFrame(pitch.Key);\r\n\r\n            lastPitch = pitch;\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Replays/KaraokeFramedReplayInputHandler.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Input.StateChanges;\r\nusing osu.Game.Replays;\r\nusing osu.Game.Rulesets.Replays;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Replays;\r\n\r\npublic class KaraokeFramedReplayInputHandler : FramedReplayInputHandler<KaraokeReplayFrame>\r\n{\r\n    public KaraokeFramedReplayInputHandler(Replay replay)\r\n        : base(replay)\r\n    {\r\n    }\r\n\r\n    protected override bool IsImportant(KaraokeReplayFrame frame) => frame.Sound;\r\n\r\n    protected override void CollectReplayInputs(List<IInput> inputs)\r\n    {\r\n        inputs.Add(new ReplayState<KaraokeScoringAction>\r\n        {\r\n            PressedActions = CurrentFrame?.Sound ?? false\r\n                ? new List<KaraokeScoringAction>\r\n                {\r\n                    new()\r\n                    {\r\n                        Scale = CurrentFrame.Scale,\r\n                    },\r\n                }\r\n                : new List<KaraokeScoringAction>(),\r\n        });\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Replays/KaraokeReplayFrame.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Replays.Legacy;\r\nusing osu.Game.Rulesets.Replays;\r\nusing osu.Game.Rulesets.Replays.Types;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Replays;\r\n\r\npublic class KaraokeReplayFrame : ReplayFrame, IConvertibleReplayFrame\r\n{\r\n    /// <summary>\r\n    /// Use for Scoring playfield\r\n    /// Maybe format will be changed, but i have no idea now.\r\n    /// </summary>\r\n    public float Scale { get; private set; }\r\n\r\n    // To record this frame has sound.\r\n    public bool Sound { get; private set; }\r\n\r\n    public KaraokeReplayFrame()\r\n    {\r\n    }\r\n\r\n    public KaraokeReplayFrame(double time)\r\n        : base(time)\r\n    {\r\n    }\r\n\r\n    public KaraokeReplayFrame(double time, float scale)\r\n        : base(time)\r\n    {\r\n        Scale = scale;\r\n        Sound = true;\r\n    }\r\n\r\n    public override string ToString() => $\"{Time}, {Scale}\";\r\n\r\n    public void FromLegacy(LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame? lastFrame = null)\r\n    {\r\n        Sound = currentFrame.MouseY.HasValue;\r\n        Scale = currentFrame.MouseY.GetValueOrDefault();\r\n    }\r\n\r\n    public LegacyReplayFrame ToLegacy(IBeatmap beatmap)\r\n    {\r\n        float? mouseYPosition = Sound ? Scale : default(float?);\r\n        return new LegacyReplayFrame(Time, null, mouseYPosition, ReplayButtonState.None);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Resources/Skin/Default/default.json",
    "content": "{\n  \"lyric_font_info\": {\n    \"$type\": 0,\n    \"name\": \"Default lyric config\",\n    \"smart_horizon\": 2,\n    \"lyrics_interval\": 4,\n    \"ruby_interval\": 2,\n    \"ruby_margin\": 4,\n    \"main_text_font\": {\n      \"family\": \"Torus\",\n      \"weight\": \"Bold\",\n      \"size\": 48.0\n    },\n    \"ruby_text_font\": {\n      \"family\": \"Torus\",\n      \"weight\": \"Bold\"\n    },\n    \"romanisation_text_font\": {\n      \"family\": \"Torus\",\n      \"weight\": \"Bold\"\n    }\n  },\n  \"lyric_style\": {\n    \"$type\": 2,\n    \"name\": \"Default lyric style\",\n    \"left_lyric_text_shaders\": [\n      {\n        \"$type\": \"StepShader\",\n        \"name\": \"HelloShader\",\n        \"draw\": true,\n        \"step_shaders\": [\n          {\n            \"$type\": \"OutlineShader\",\n            \"radius\": 3,\n            \"outline_colour\": \"#CCA532\"\n          },\n          {\n            \"$type\": \"ShadowShader\",\n            \"shadow_colour\": \"#6B5B2D\",\n            \"shadow_offset\": {\n              \"x\": 3.0,\n              \"y\": 3.0\n            }\n          }\n        ]\n      }\n    ],\n    \"right_lyric_text_shaders\": [\n      {\n        \"$type\": \"StepShader\",\n        \"name\": \"HelloShader\",\n        \"draw\": true,\n        \"step_shaders\": [\n          {\n            \"$type\": \"OutlineShader\",\n            \"radius\": 3,\n            \"outline_colour\": \"#5932CC\"\n          },\n          {\n            \"$type\": \"ShadowShader\",\n            \"shadow_colour\": \"#3D2D6B\",\n            \"shadow_offset\": {\n              \"x\": 3.0,\n              \"y\": 3.0\n            }\n          }\n        ]\n      }\n    ]\n  },\n  \"note_style\": {\n    \"$type\": 3,\n    \"name\": \"Default note style\",\n    \"note_color\": \"#44AADD\",\n    \"blink_color\": \"#FF66AA\",\n    \"text_color\": \"#FFFFFF\",\n    \"bold_text\": true\n  }\n}"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Resources/Skin/Default/lyric-font-infos.json",
    "content": "[]"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Resources/Skin/Default/note-styles.json",
    "content": "[]"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Scoring/KaraokeHitWindows.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Scoring;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Scoring;\r\n\r\npublic abstract class KaraokeHitWindows : HitWindows;\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Scoring/KaraokeLyricHitWindows.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Rulesets.Scoring;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Scoring;\r\n\r\npublic class KaraokeLyricHitWindows : KaraokeHitWindows\r\n{\r\n    private static readonly DifficultyRange perfect_window_range = new(40D, 20D, 10D);\r\n\r\n    private double perfect;\r\n\r\n    public override bool IsHitResultAllowed(HitResult result) =>\r\n        result switch\r\n        {\r\n            HitResult.Perfect => true,\r\n            // todo: add this in order not to throw error in some test cases.\r\n            HitResult.Miss => true,\r\n            _ => false,\r\n        };\r\n\r\n    public override void SetDifficulty(double difficulty)\r\n    {\r\n        perfect = IBeatmapDifficultyInfo.DifficultyRange(difficulty, perfect_window_range) ;\r\n    }\r\n\r\n    public override double WindowFor(HitResult result)\r\n    {\r\n        switch (result)\r\n        {\r\n            case HitResult.Perfect:\r\n                return perfect;\r\n\r\n            // todo: add this in order not to throw error in some test cases.\r\n            case HitResult.Miss:\r\n                return 1000;\r\n\r\n            default:\r\n                throw new ArgumentOutOfRangeException(nameof(result), result, null);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Scoring/KaraokeNoteHitWindows.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Rulesets.Scoring;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Scoring;\r\n\r\npublic class KaraokeNoteHitWindows : KaraokeHitWindows\r\n{\r\n    private static readonly DifficultyRange perfect_window_range = new(80D, 50D, 20D);\r\n    private static readonly DifficultyRange meh_window_range = new(80D, 50D, 20D);\r\n    private static readonly DifficultyRange miss_window_range = new(2000D, 1500D, 1000D);\r\n\r\n    private double perfect;\r\n    private double meh;\r\n    private double miss;\r\n\r\n    public override bool IsHitResultAllowed(HitResult result) =>\r\n        result switch\r\n        {\r\n            HitResult.Perfect => true,\r\n            HitResult.Meh => true,\r\n            HitResult.Miss => true,\r\n            _ => false,\r\n        };\r\n\r\n    public override void SetDifficulty(double difficulty)\r\n    {\r\n        perfect = IBeatmapDifficultyInfo.DifficultyRange(difficulty, perfect_window_range) ;\r\n        meh = IBeatmapDifficultyInfo.DifficultyRange(difficulty, meh_window_range);\r\n        miss = IBeatmapDifficultyInfo.DifficultyRange(difficulty, miss_window_range);\r\n    }\r\n\r\n    public override double WindowFor(HitResult result)\r\n    {\r\n        switch (result)\r\n        {\r\n            case HitResult.Perfect:\r\n                return perfect;\r\n\r\n            case HitResult.Meh:\r\n                return meh;\r\n\r\n            case HitResult.Miss:\r\n                return miss;\r\n\r\n            default:\r\n                throw new ArgumentOutOfRangeException(nameof(result), result, null);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Scoring/KaraokeScoreProcessor.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Scoring;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Scoring;\r\n\r\ninternal partial class KaraokeScoreProcessor : ScoreProcessor\r\n{\r\n    public KaraokeScoreProcessor()\r\n        : base(new KaraokeRuleset())\r\n    {\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/AutoGenerateSection.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Localisation;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit;\r\n\r\npublic abstract partial class AutoGenerateSection : EditorSection\r\n{\r\n    protected sealed override LocalisableString Title => \"Auto generate\";\r\n\r\n    protected AutoGenerateSection()\r\n    {\r\n        Children = new[]\r\n        {\r\n            CreateAutoGenerateSubsection(),\r\n        };\r\n    }\r\n\r\n    protected abstract AutoGenerateSubsection CreateAutoGenerateSubsection();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/AutoGenerateSubsection.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Extensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Cursor;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Graphics.UserInterfaceV2;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Markdown;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit;\r\n\r\npublic abstract partial class AutoGenerateSubsection : FillFlowContainer\r\n{\r\n    private const int horizontal_padding = 20;\r\n\r\n    protected AutoGenerateSubsection()\r\n    {\r\n        RelativeSizeAxes = Axes.X;\r\n        AutoSizeAxes = Axes.Y;\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        // should wait until BDL in the parent class has been loaded.\r\n        Schedule(() =>\r\n        {\r\n            Children = new Drawable[]\r\n            {\r\n                new GridContainer\r\n                {\r\n                    AutoSizeAxes = Axes.Y,\r\n                    RelativeSizeAxes = Axes.X,\r\n                    ColumnDimensions = new[]\r\n                    {\r\n                        new Dimension(),\r\n                        new Dimension(GridSizeMode.Absolute, 5),\r\n                        new Dimension(GridSizeMode.Absolute, 36),\r\n                    },\r\n                    RowDimensions = new[]\r\n                    {\r\n                        new Dimension(GridSizeMode.AutoSize),\r\n                    },\r\n                    Content = new[]\r\n                    {\r\n                        new Drawable?[]\r\n                        {\r\n                            CreateGenerateButton(),\r\n                            null,\r\n                            CreateConfigButton().With(x =>\r\n                            {\r\n                                x.Anchor = Anchor.Centre;\r\n                                x.Origin = Anchor.Centre;\r\n                                x.Size = new Vector2(36);\r\n                            }),\r\n                        },\r\n                    },\r\n                },\r\n                CreateDescriptionTextFlowContainer().With(x =>\r\n                {\r\n                    x.RelativeSizeAxes = Axes.X;\r\n                    x.AutoSizeAxes = Axes.Y;\r\n                    x.Padding = new MarginPadding { Horizontal = horizontal_padding };\r\n                    x.Description = CreateInvalidDescriptionFormat();\r\n                }),\r\n            };\r\n        });\r\n    }\r\n\r\n    protected abstract EditorSectionButton CreateGenerateButton();\r\n\r\n    protected virtual DescriptionTextFlowContainer CreateDescriptionTextFlowContainer() => new();\r\n\r\n    protected abstract DescriptionFormat CreateInvalidDescriptionFormat();\r\n\r\n    protected abstract ConfigButton CreateConfigButton();\r\n\r\n    protected abstract partial class ConfigButton : IconButton, IHasPopover\r\n    {\r\n        protected ConfigButton()\r\n        {\r\n            Icon = FontAwesome.Solid.Cog;\r\n            Action = openConfigSetting;\r\n\r\n            void openConfigSetting()\r\n                => this.ShowPopover();\r\n        }\r\n\r\n        public abstract Popover GetPopover();\r\n    }\r\n\r\n    protected abstract partial class MultiConfigButton : ConfigButton\r\n    {\r\n        private KaraokeRulesetEditGeneratorSetting? selectedSetting;\r\n\r\n        protected MultiConfigButton()\r\n        {\r\n            Action = this.ShowPopover;\r\n        }\r\n\r\n        public sealed override Popover GetPopover()\r\n        {\r\n            if (selectedSetting == null)\r\n                return createSelectionPopover();\r\n\r\n            return new GeneratorConfigPopover(selectedSetting.Value);\r\n        }\r\n\r\n        protected abstract IEnumerable<KaraokeRulesetEditGeneratorSetting> AvailableSettings { get; }\r\n\r\n        protected abstract string GetDisplayName(KaraokeRulesetEditGeneratorSetting setting);\r\n\r\n        private Popover createSelectionPopover()\r\n            => new OsuPopover\r\n            {\r\n                Child = new FillFlowContainer<OsuButton>\r\n                {\r\n                    AutoSizeAxes = Axes.Both,\r\n                    Direction = FillDirection.Vertical,\r\n                    Spacing = new Vector2(10),\r\n                    Children = AvailableSettings.Select(x =>\r\n                    {\r\n                        string name = GetDisplayName(x);\r\n                        return new AutoGenerateButton\r\n                        {\r\n                            Text = name,\r\n                            Width = 150,\r\n                            Action = () =>\r\n                            {\r\n                                selectedSetting = x;\r\n                                this.ShowPopover();\r\n\r\n                                // after show config pop-over, should make the state back for able to show this dialog next time.\r\n                                selectedSetting = null;\r\n                            },\r\n                        };\r\n                    }).ToList(),\r\n                },\r\n            };\r\n\r\n        private partial class AutoGenerateButton : EditorSectionButton;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/BeatmapEditorRoundedScreen.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Game.Overlays;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps;\r\n\r\n/// <summary>\r\n/// Copied from EditorRoundedScreen\r\n/// todo: will remove this screen eventually because new editor design not have round screen style.\r\n/// </summary>\r\npublic partial class BeatmapEditorRoundedScreen : BeatmapEditorScreen\r\n{\r\n    public const int HORIZONTAL_PADDING = 100;\r\n\r\n    private Container roundedContent = null!;\r\n\r\n    protected override Container<Drawable> Content => roundedContent;\r\n\r\n    public BeatmapEditorRoundedScreen(KaraokeBeatmapEditorScreenMode type)\r\n        : base(type)\r\n    {\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OverlayColourProvider colourProvider)\r\n    {\r\n        base.Content.Add(new Container\r\n        {\r\n            RelativeSizeAxes = Axes.Both,\r\n            Padding = new MarginPadding(50),\r\n            Child = new Container\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n                Masking = true,\r\n                CornerRadius = 10,\r\n                Children = new Drawable[]\r\n                {\r\n                    new Box\r\n                    {\r\n                        Colour = colourProvider.Background3,\r\n                        RelativeSizeAxes = Axes.Both,\r\n                    },\r\n                    roundedContent = new Container\r\n                    {\r\n                        RelativeSizeAxes = Axes.Both,\r\n                    },\r\n                },\r\n            },\r\n        });\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/BeatmapEditorScreen.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps;\r\n\r\npublic abstract partial class BeatmapEditorScreen : GenericEditorScreen<KaraokeBeatmapEditorScreenMode>\r\n{\r\n    [Resolved]\r\n    private EditorBeatmap beatmap { get; set; } = null!;\r\n\r\n    protected BeatmapEditorScreen(KaraokeBeatmapEditorScreenMode type)\r\n        : base(type)\r\n    {\r\n    }\r\n\r\n    protected override void PopIn()\r\n    {\r\n        base.PopIn();\r\n\r\n        // should wait until current change handler done.\r\n        // not a good way but ok for now.\r\n        ScheduleAfterChildren(() =>\r\n        {\r\n            // for prevent accidentally change the property in the hit object that is not expected,\r\n            // should clear all selected hit object if user change to the different tab.\r\n            beatmap.SelectedHitObjects.Clear();\r\n        });\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Components/Menus/AutoFocusToEditLyricMenu.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Extensions.IEnumerableExtensions;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Components.Menus;\r\n\r\npublic class AutoFocusToEditLyricMenu : MenuItem\r\n{\r\n    private const int disable_selection_index = -1;\r\n\r\n    private readonly BindableBool bindableAutoFocusToEditLyric = new();\r\n    private readonly BindableInt bindableAutoFocusToEditLyricSkipRows = new();\r\n\r\n    public AutoFocusToEditLyricMenu(KaraokeRulesetLyricEditorConfigManager config, string text)\r\n        : base(text)\r\n    {\r\n        var selections = new List<MenuItem>\r\n        {\r\n            new ToggleMenuItem(getName(disable_selection_index), MenuItemType.Standard, _ => updateAutoFocusToEditLyric()),\r\n        };\r\n        selections.AddRange(Enumerable.Range(0, 4).Select(x => new ToggleMenuItem(getName(x), MenuItemType.Standard, _ => updateAutoFocusToEditLyricSkipRows(x))));\r\n        Items = selections;\r\n\r\n        config.BindWith(KaraokeRulesetLyricEditorSetting.AutoFocusToEditLyric, bindableAutoFocusToEditLyric);\r\n        config.BindWith(KaraokeRulesetLyricEditorSetting.AutoFocusToEditLyricSkipRows, bindableAutoFocusToEditLyricSkipRows);\r\n\r\n        // mark disable as selected option.\r\n        bindableAutoFocusToEditLyric.BindValueChanged(_ =>\r\n        {\r\n            updateSelectionState();\r\n        }, true);\r\n\r\n        // mark line as selected option.\r\n        bindableAutoFocusToEditLyricSkipRows.BindValueChanged(_ =>\r\n        {\r\n            updateSelectionState();\r\n        }, true);\r\n    }\r\n\r\n    private string getName(int number)\r\n    {\r\n        return number switch\r\n        {\r\n            disable_selection_index => \"Disable\",\r\n            0 => \"Enable\",\r\n            _ => $\"Enable (skip {number} rows)\",\r\n        };\r\n    }\r\n\r\n    private void updateAutoFocusToEditLyric()\r\n    {\r\n        bindableAutoFocusToEditLyric.Value = !bindableAutoFocusToEditLyric.Value;\r\n    }\r\n\r\n    private void updateAutoFocusToEditLyricSkipRows(int rows)\r\n    {\r\n        bindableAutoFocusToEditLyric.Value = true;\r\n        bindableAutoFocusToEditLyricSkipRows.Value = rows;\r\n    }\r\n\r\n    private void updateSelectionState()\r\n    {\r\n        int selection = bindableAutoFocusToEditLyric.Value ? bindableAutoFocusToEditLyricSkipRows.Value : disable_selection_index;\r\n        Items.OfType<ToggleMenuItem>().ForEach(x =>\r\n        {\r\n            bool match = x.Text.Value == getName(selection);\r\n            x.State.Value = match;\r\n        });\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Components/Menus/GeneratorConfigMenu.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics.UserInterface;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Components.Menus;\r\n\r\npublic class GeneratorConfigMenu : MenuItem\r\n{\r\n    public GeneratorConfigMenu(string text)\r\n        : base(text)\r\n    {\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Components/Menus/ImportLyricMenu.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Framework.Screens;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Import.Lyrics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Components.Menus;\r\n\r\npublic class ImportLyricMenu : MenuItem\r\n{\r\n    public ImportLyricMenu(IScreen? screen, string text, IImportBeatmapChangeHandler importBeatmapChangeHandler)\r\n        : base(text, () => openLyricImporter(screen, importBeatmapChangeHandler))\r\n    {\r\n    }\r\n\r\n    private static void openLyricImporter(IScreen? screen, IImportBeatmapChangeHandler importBeatmapChangeHandler)\r\n    {\r\n        if (screen == null)\r\n            return;\r\n\r\n        var importer = new LyricImporter\r\n        {\r\n            OnImportFinished = importBeatmapChangeHandler.Import,\r\n        };\r\n        screen.Push(importer);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Components/Menus/LockStateMenu.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.UserInterface;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Types;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Components.Menus;\r\n\r\n/// <summary>\r\n/// If click the lock icon in <see cref=\"LyricEditor\"/>, will apply <see cref=\"LockState.Partial\"/> or <see cref=\"LockState.Full\"/>\r\n/// </summary>\r\npublic class LockStateMenuItem : BindableEnumMenuItem<LockState>\r\n{\r\n    public LockStateMenuItem(string text, KaraokeRulesetLyricEditorConfigManager config)\r\n        : base(text, config.GetBindable<LockState>(KaraokeRulesetLyricEditorSetting.ClickToLockLyricState))\r\n    {\r\n    }\r\n\r\n    protected override IEnumerable<LockState> ValidEnums => new[] { LockState.Partial, LockState.Full };\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Components/Menus/LyricEditorModeMenu.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.UserInterface;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Components.Menus;\r\n\r\npublic class LyricEditorModeMenuItem : BindableEnumMenuItem<LyricEditorMode>\r\n{\r\n    public LyricEditorModeMenuItem(string text, Bindable<LyricEditorMode> config)\r\n        : base(text, config)\r\n    {\r\n    }\r\n\r\n    protected override IEnumerable<LyricEditorMode> ValidEnums => new[]\r\n    {\r\n        LyricEditorMode.View,\r\n        LyricEditorMode.EditText,\r\n        LyricEditorMode.EditReferenceLyric,\r\n        LyricEditorMode.EditLanguage,\r\n        LyricEditorMode.EditRuby,\r\n        LyricEditorMode.EditTimeTag,\r\n        LyricEditorMode.EditRomanisation,\r\n        LyricEditorMode.EditNote,\r\n        LyricEditorMode.EditSinger,\r\n    };\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Components/Menus/LyricEditorPreferLayoutMenu.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.UserInterface;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Components.Menus;\r\n\r\npublic class LyricEditorPreferLayoutMenuItem : BindableEnumMenuItem<LyricEditorLayout>\r\n{\r\n    public LyricEditorPreferLayoutMenuItem(string text, KaraokeRulesetLyricEditorConfigManager config)\r\n        : base(text, config.GetBindable<LyricEditorLayout>(KaraokeRulesetLyricEditorSetting.LyricEditorPreferLayout))\r\n    {\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Components/Menus/LyricEditorTextSizeMenu.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Extensions.IEnumerableExtensions;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Components.Menus;\r\n\r\npublic class LyricEditorTextSizeMenu : MenuItem\r\n{\r\n    private readonly Bindable<float> bindableFontSize = new();\r\n\r\n    public LyricEditorTextSizeMenu(KaraokeRulesetLyricEditorConfigManager config, string text)\r\n        : base(text)\r\n    {\r\n        Items = createMenuItems();\r\n\r\n        config.BindWith(KaraokeRulesetLyricEditorSetting.LyricEditorFontSize, bindableFontSize);\r\n        bindableFontSize.BindValueChanged(e =>\r\n        {\r\n            float newSelection = e.NewValue;\r\n            Items.OfType<ToggleMenuItem>().ForEach(x =>\r\n            {\r\n                bool match = x.Text.Value == FontUtils.GetText(newSelection);\r\n                x.State.Value = match;\r\n            });\r\n        }, true);\r\n    }\r\n\r\n    private ToggleMenuItem[] createMenuItems()\r\n    {\r\n        float[] sizes = FontUtils.DefaultPreviewFontSize();\r\n        return sizes.Select(e =>\r\n        {\r\n            var item = new ToggleMenuItem(FontUtils.GetText(e), MenuItemType.Standard, _ => updateMode(e));\r\n            return item;\r\n        }).ToArray();\r\n    }\r\n\r\n    private void updateMode(float size)\r\n    {\r\n        bindableFontSize.Value = size;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Components/UserInterfaceV2/LyricSelector.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Containers;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.Containers;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Screens.Edit;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Components.UserInterfaceV2;\r\n\r\npublic partial class LyricSelector : CompositeDrawable, IHasCurrentValue<Lyric?>\r\n{\r\n    private readonly LyricSelectionSearchTextBox filter;\r\n\r\n    private readonly BindableWithCurrent<Lyric?> current = new();\r\n\r\n    public Bindable<Lyric?> Current\r\n    {\r\n        get => current.Current;\r\n        set => current.Current = value;\r\n    }\r\n\r\n    public override bool AcceptsFocus => true;\r\n\r\n    public override bool RequestsFocus => true;\r\n\r\n    private readonly RearrangeableLyricListContainer lyricList;\r\n\r\n    public LyricSelector()\r\n    {\r\n        InternalChild = new GridContainer\r\n        {\r\n            RelativeSizeAxes = Axes.Both,\r\n            RowDimensions = new[]\r\n            {\r\n                new Dimension(GridSizeMode.Absolute, 40),\r\n                new Dimension(),\r\n            },\r\n            Content = new[]\r\n            {\r\n                new Drawable[]\r\n                {\r\n                    filter = new LyricSelectionSearchTextBox\r\n                    {\r\n                        RelativeSizeAxes = Axes.X,\r\n                    },\r\n                },\r\n                new Drawable[]\r\n                {\r\n                    lyricList = CreateRearrangeableLyricListContainer().With(x =>\r\n                    {\r\n                        x.RelativeSizeAxes = Axes.Both;\r\n                        x.RequestSelection = item =>\r\n                        {\r\n                            Current.Value = item;\r\n                        };\r\n                    }),\r\n                },\r\n            },\r\n        };\r\n\r\n        filter.Current.BindValueChanged(e => lyricList.Filter(e.NewValue));\r\n        Current.BindValueChanged(e => lyricList.SelectedSet.Value = e.NewValue);\r\n    }\r\n\r\n    protected virtual RearrangeableLyricListContainer CreateRearrangeableLyricListContainer() => new();\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(EditorBeatmap editorBeatmap)\r\n    {\r\n        lyricList.Items.AddRange(editorBeatmap.HitObjects.OfType<Lyric>());\r\n    }\r\n\r\n    protected override void OnFocus(FocusEvent e)\r\n    {\r\n        base.OnFocus(e);\r\n\r\n        GetContainingFocusManager().ChangeFocus(filter);\r\n    }\r\n\r\n    private partial class LyricSelectionSearchTextBox : SearchTextBox\r\n    {\r\n        protected override Color4 SelectionColour => Color4.Gray;\r\n\r\n        public LyricSelectionSearchTextBox()\r\n        {\r\n            PlaceholderText = \"type in keywords...\";\r\n        }\r\n    }\r\n\r\n    protected partial class RearrangeableLyricListContainer : RearrangeableTextFlowListContainer<Lyric?>\r\n    {\r\n        protected override DrawableTextListItem CreateDrawable(Lyric? item)\r\n            => new DrawableLyricListItem(item);\r\n\r\n        protected partial class DrawableLyricListItem : DrawableTextListItem\r\n        {\r\n            [Resolved]\r\n            private OsuColour colours { get; set; } = null!;\r\n\r\n            public DrawableLyricListItem(Lyric? item)\r\n                : base(item)\r\n            {\r\n            }\r\n\r\n            public override IEnumerable<LocalisableString> FilterTerms => new[]\r\n            {\r\n                new LocalisableString(Model?.Text ?? string.Empty),\r\n            };\r\n\r\n            protected override void CreateDisplayContent(OsuTextFlowContainer textFlowContainer, Lyric? model)\r\n            {\r\n                if (model == null)\r\n                {\r\n                    textFlowContainer.AddText(\"<Empty>\");\r\n                }\r\n                else\r\n                {\r\n                    // display the lyric order.\r\n                    textFlowContainer.AddText($\"#{model.Order}\", x => x.Colour = colours.Yellow);\r\n                    textFlowContainer.AddText(\"  \");\r\n\r\n                    // main text\r\n                    textFlowContainer.AddText(model.Text);\r\n                    textFlowContainer.AddText(\" \");\r\n                }\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/ILyricsProvider.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps;\r\n\r\npublic interface ILyricsProvider\r\n{\r\n    BindableList<Lyric> BindableLyrics { get; }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/KaraokeBeatmapEditor.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Framework.Screens;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Debugging;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Export;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Components.Menus;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Pages;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Singers;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Translations;\r\nusing osu.Game.Rulesets.Karaoke.Skinning.Fonts;\r\nusing osu.Game.Screens.Edit.Components.Menus;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps;\r\n\r\npublic partial class KaraokeBeatmapEditor : GenericEditor<KaraokeBeatmapEditorScreenMode>\r\n{\r\n    [Cached]\r\n    private readonly OverlayColourProvider colourProvider = new(OverlayColourScheme.Blue);\r\n\r\n    [Cached]\r\n    private readonly KaraokeRulesetEditConfigManager editConfigManager;\r\n\r\n    [Cached]\r\n    private readonly KaraokeRulesetLyricEditorConfigManager lyricEditorConfigManager;\r\n\r\n    [Cached]\r\n    private readonly KaraokeRulesetEditGeneratorConfigManager generatorConfigManager;\r\n\r\n    [Cached]\r\n    private readonly KaraokeRulesetEditCheckerConfigManager checkerConfigManager;\r\n\r\n    [Cached]\r\n    private readonly FontManager fontManager;\r\n\r\n    [Cached(typeof(IKaraokeBeatmapResourcesProvider))]\r\n    private KaraokeBeatmapResourcesProvider karaokeBeatmapResourcesProvider;\r\n\r\n    [Cached(typeof(ILyricsProvider))]\r\n    private readonly LyricsProvider lyricsProvider;\r\n\r\n    [Cached]\r\n    private readonly ExportLyricManager exportLyricManager;\r\n\r\n    [Cached(typeof(IImportBeatmapChangeHandler))]\r\n    private readonly ImportBeatmapChangeHandler importBeatmapChangeHandler;\r\n\r\n    [Cached]\r\n    private readonly DebugBeatmapManager debugBeatmapManager;\r\n\r\n    [Cached]\r\n    private readonly Bindable<LyricEditorMode> bindableLyricEditorMode = new();\r\n\r\n    public KaraokeBeatmapEditor()\r\n    {\r\n        editConfigManager = new KaraokeRulesetEditConfigManager();\r\n        lyricEditorConfigManager = new KaraokeRulesetLyricEditorConfigManager();\r\n        generatorConfigManager = new KaraokeRulesetEditGeneratorConfigManager();\r\n        checkerConfigManager = new KaraokeRulesetEditCheckerConfigManager();\r\n\r\n        // Duplicated registration because selection handler need to use it.\r\n        AddInternal(fontManager = new FontManager());\r\n        AddInternal(karaokeBeatmapResourcesProvider = new KaraokeBeatmapResourcesProvider());\r\n\r\n        AddInternal(exportLyricManager = new ExportLyricManager());\r\n        AddInternal(lyricsProvider = new LyricsProvider());\r\n\r\n        AddInternal(importBeatmapChangeHandler = new ImportBeatmapChangeHandler());\r\n\r\n        AddInternal(debugBeatmapManager = new DebugBeatmapManager());\r\n    }\r\n\r\n    protected override GenericEditorScreen<KaraokeBeatmapEditorScreenMode> GenerateScreen(KaraokeBeatmapEditorScreenMode screenMode) =>\r\n        screenMode switch\r\n        {\r\n            KaraokeBeatmapEditorScreenMode.Lyric => new LyricEditorScreen(),\r\n            KaraokeBeatmapEditorScreenMode.Singer => new SingerScreen(),\r\n            KaraokeBeatmapEditorScreenMode.Translation => new TranslationScreen(),\r\n            KaraokeBeatmapEditorScreenMode.Page => new PageScreen(),\r\n            _ => throw new InvalidOperationException(\"Editor menu bar switched to an unsupported mode\"),\r\n        };\r\n\r\n    protected override MenuItem[] GenerateMenuItems(KaraokeBeatmapEditorScreenMode screenMode)\r\n    {\r\n        return screenMode switch\r\n        {\r\n            KaraokeBeatmapEditorScreenMode.Lyric => new MenuItem[]\r\n            {\r\n                new(\"File\")\r\n                {\r\n                    Items = new MenuItem[]\r\n                    {\r\n                        new ImportLyricMenu(this, \"Import from text\", importBeatmapChangeHandler),\r\n                        new ImportLyricMenu(this, \"Import from .kar file\", importBeatmapChangeHandler),\r\n                        new OsuMenuItemSpacer(),\r\n                        new EditorMenuItem(\"Export to .kar\", MenuItemType.Standard, () => exportLyricManager.ExportToKar()),\r\n                        new EditorMenuItem(\"Export to text\", MenuItemType.Standard, () => exportLyricManager.ExportToText()),\r\n                    },\r\n                },\r\n                new LyricEditorModeMenuItem(\"Mode\", bindableLyricEditorMode),\r\n                new(\"View\")\r\n                {\r\n                    Items = new MenuItem[]\r\n                    {\r\n                        new LyricEditorPreferLayoutMenuItem(\"Layout\", lyricEditorConfigManager),\r\n                        new LyricEditorTextSizeMenu(lyricEditorConfigManager, \"Text size\"),\r\n                        new AutoFocusToEditLyricMenu(lyricEditorConfigManager, \"Auto focus to edit lyric\"),\r\n                    },\r\n                },\r\n                new(\"Config\")\r\n                {\r\n                    Items = new MenuItem[] { new EditorMenuItem(\"Lyric editor\"), new GeneratorConfigMenu(\"Auto-generator\"), new LockStateMenuItem(\"Lock\", lyricEditorConfigManager) },\r\n                },\r\n                new(\"Debug\")\r\n                {\r\n                    Items = new MenuItem[]\r\n                    {\r\n                        new EditorMenuItem(\"Override beatmap as json format\", MenuItemType.Destructive, () => debugBeatmapManager.OverrideTheBeatmapWithJsonFormat()),\r\n                        new EditorMenuItem(\"Save beatmap to new difficulty as json format\", MenuItemType.Destructive, () => debugBeatmapManager.SaveToNewDifficulty()),\r\n                        new OsuMenuItemSpacer(),\r\n                        new EditorMenuItem(\"Export to json\", MenuItemType.Destructive, () => debugBeatmapManager.ExportToJson()),\r\n                        new EditorMenuItem(\"Export to json beatmap\", MenuItemType.Destructive, () => debugBeatmapManager.ExportToJsonBeatmap()),\r\n                    },\r\n                },\r\n            },\r\n            _ => Array.Empty<MenuItem>(),\r\n        };\r\n    }\r\n\r\n    public override void OnSuspending(ScreenTransitionEvent e)\r\n    {\r\n        // add fade-in/out effect for the lyric importer.\r\n        this.FadeOutFromOne(250);\r\n        base.OnSuspending(e);\r\n    }\r\n\r\n    public override void OnResuming(ScreenTransitionEvent e)\r\n    {\r\n        // add fade-in/out effect for the lyric importer.\r\n        base.OnResuming(e);\r\n        this.FadeInFromZero(250, Easing.OutQuint);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/KaraokeBeatmapEditorScreenMode.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.ComponentModel;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps;\r\n\r\npublic enum KaraokeBeatmapEditorScreenMode\r\n{\r\n    [Description(\"Lyric\")]\r\n    Lyric,\r\n\r\n    [Description(\"Singer\")]\r\n    Singer,\r\n\r\n    [Description(\"Translation\")]\r\n    Translation,\r\n\r\n    [Description(\"Page\")]\r\n    Page,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/BindableBlueprintContainer.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Specialized;\r\nusing System.Diagnostics;\r\nusing System.Linq;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Game.Screens.Edit.Compose.Components;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics;\r\n\r\npublic abstract partial class BindableBlueprintContainer<T> : BlueprintContainer<T> where T : class\r\n{\r\n    private BindableList<T>? bindableList;\r\n\r\n    protected void RegisterBindable(BindableList<T> bindable)\r\n    {\r\n        if (bindableList != null)\r\n            throw new InvalidOperationException(\"Already have bindable.\");\r\n\r\n        bindableList = bindable;\r\n\r\n        // Add time-tag into blueprint container\r\n        bindableList.BindCollectionChanged((_, args) =>\r\n        {\r\n            switch (args.Action)\r\n            {\r\n                case NotifyCollectionChangedAction.Add:\r\n                    Debug.Assert(args.NewItems != null);\r\n\r\n                    foreach (var obj in args.NewItems.OfType<T>())\r\n                        AddBlueprintFor(obj);\r\n\r\n                    break;\r\n\r\n                case NotifyCollectionChangedAction.Remove:\r\n                    Debug.Assert(args.OldItems != null);\r\n\r\n                    foreach (var obj in args.OldItems.OfType<T>())\r\n                        RemoveBlueprintFor(obj);\r\n\r\n                    break;\r\n            }\r\n        }, true);\r\n    }\r\n\r\n    protected override bool OnDragStart(DragStartEvent e)\r\n    {\r\n        if (!base.OnDragStart(e))\r\n            return false;\r\n\r\n        // should clear all selected text-tag if start selecting.\r\n        if (containsSelectionFromOtherBlueprintContainer())\r\n            DeselectAll();\r\n\r\n        return true;\r\n    }\r\n\r\n    protected override bool OnClick(ClickEvent e)\r\n    {\r\n        if (!base.OnClick(e))\r\n            return false;\r\n\r\n        // should clear all selected text-tag if start selecting.\r\n        if (containsSelectionFromOtherBlueprintContainer())\r\n            DeselectAll();\r\n\r\n        return true;\r\n    }\r\n\r\n    protected override void SelectAll()\r\n    {\r\n        SelectedItems.AddRange(bindableList);\r\n    }\r\n\r\n    private bool containsSelectionFromOtherBlueprintContainer()\r\n    {\r\n        var items = SelectionBlueprints.Select(x => x.Item);\r\n\r\n        // check any selected items that is not in current blueprint container.\r\n        return SelectedItems.Any(x => !items.Contains(x));\r\n    }\r\n\r\n    public abstract partial class BindableSelectionHandler : SelectionHandler<T>\r\n    {\r\n        protected override void OnSelectionChanged()\r\n        {\r\n            base.OnSelectionChanged();\r\n\r\n            updateVisibility();\r\n        }\r\n\r\n        /// <summary>\r\n        /// Updates whether this <see cref=\"SelectionHandler{T}\"/> is visible.\r\n        /// </summary>\r\n        private void updateVisibility()\r\n        {\r\n            bool visible = containsSelectionInCurrentBlueprintContainer();\r\n            SelectionBox.FadeTo(visible ? 1f : 0.0f);\r\n        }\r\n\r\n        private bool containsSelectionInCurrentBlueprintContainer()\r\n        {\r\n            var items = SelectedBlueprints.Select(x => x.Item);\r\n\r\n            // check any selected items that is in current blueprint container.\r\n            return SelectedItems.Any(x => items.Contains(x));\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/CaretPosition/Algorithms/CaretPositionAlgorithm.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Diagnostics;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition.Algorithms;\r\n\r\n/// <summary>\r\n/// Base class for move the <see cref=\"ICaretPosition\"/> to the previous or next position.\r\n/// </summary>\r\n/// <typeparam name=\"TCaretPosition\"></typeparam>\r\npublic abstract class CaretPositionAlgorithm<TCaretPosition> : ICaretPositionAlgorithm where TCaretPosition : struct, ICaretPosition\r\n{\r\n    // Lyrics is not lock and can be accessible.\r\n    protected readonly Lyric[] Lyrics;\r\n\r\n    protected CaretPositionAlgorithm(Lyric[] lyrics)\r\n    {\r\n        Lyrics = lyrics;\r\n    }\r\n\r\n    protected abstract bool PositionMovable(TCaretPosition position);\r\n\r\n    protected abstract TCaretPosition? MoveToPreviousLyric(TCaretPosition currentPosition);\r\n\r\n    protected abstract TCaretPosition? MoveToNextLyric(TCaretPosition currentPosition);\r\n\r\n    protected abstract TCaretPosition? MoveToFirstLyric();\r\n\r\n    protected abstract TCaretPosition? MoveToLastLyric();\r\n\r\n    protected abstract TCaretPosition? MoveToTargetLyric(Lyric lyric);\r\n\r\n    public ICaretPosition? MoveToPreviousLyric(ICaretPosition currentPosition)\r\n    {\r\n        if (currentPosition is not TCaretPosition tCaretPosition)\r\n            throw new InvalidCastException(nameof(currentPosition));\r\n\r\n        PreValidate(tCaretPosition);\r\n\r\n        var movedCaretPosition = MoveToPreviousLyric(tCaretPosition);\r\n        return PostValidate(movedCaretPosition);\r\n    }\r\n\r\n    public ICaretPosition? MoveToNextLyric(ICaretPosition currentPosition)\r\n    {\r\n        if (currentPosition is not TCaretPosition tCaretPosition)\r\n            throw new InvalidCastException(nameof(currentPosition));\r\n\r\n        PreValidate(tCaretPosition);\r\n\r\n        var movedCaretPosition = MoveToNextLyric(tCaretPosition);\r\n        return PostValidate(movedCaretPosition);\r\n    }\r\n\r\n    ICaretPosition? ICaretPositionAlgorithm.MoveToFirstLyric()\r\n    {\r\n        var movedCaretPosition = MoveToFirstLyric();\r\n        return PostValidate(movedCaretPosition);\r\n    }\r\n\r\n    ICaretPosition? ICaretPositionAlgorithm.MoveToLastLyric()\r\n    {\r\n        var movedCaretPosition = MoveToLastLyric();\r\n        return PostValidate(movedCaretPosition);\r\n    }\r\n\r\n    ICaretPosition? ICaretPositionAlgorithm.MoveToTargetLyric(Lyric lyric)\r\n    {\r\n        var movedCaretPosition = MoveToTargetLyric(lyric);\r\n        return PostValidate(movedCaretPosition);\r\n    }\r\n\r\n    protected virtual void PreValidate(TCaretPosition input)\r\n    {\r\n        Debug.Assert(PositionMovable(input));\r\n    }\r\n\r\n    protected TCaretPosition? PostValidate(TCaretPosition? movedCaretPosition)\r\n    {\r\n        if (movedCaretPosition == null)\r\n            return null;\r\n\r\n        if (!PositionMovable(movedCaretPosition.Value))\r\n            return null;\r\n\r\n        PreValidate(movedCaretPosition.Value);\r\n\r\n        return movedCaretPosition;\r\n    }\r\n\r\n    public Type GetCaretPositionType() => typeof(TCaretPosition);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/CaretPosition/Algorithms/CharGapCaretPositionAlgorithm.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing osu.Game.Rulesets.Karaoke.Extensions;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition.Algorithms;\r\n\r\n/// <summary>\r\n/// Base class for those algorithms which use char gap as index.\r\n/// </summary>\r\n/// <typeparam name=\"TCaretPosition\"></typeparam>\r\npublic abstract class CharGapCaretPositionAlgorithm<TCaretPosition> : IndexCaretPositionAlgorithm<TCaretPosition, int>\r\n    where TCaretPosition : struct, ICharGapCaretPosition\r\n{\r\n    protected CharGapCaretPositionAlgorithm(Lyric[] lyrics)\r\n        : base(lyrics)\r\n    {\r\n    }\r\n\r\n    protected sealed override bool PositionMovable(TCaretPosition position)\r\n    {\r\n        return indexInTextRange(position.CharGap, position.Lyric);\r\n    }\r\n\r\n    protected sealed override TCaretPosition? MoveToPreviousLyric(TCaretPosition currentPosition)\r\n    {\r\n        var lyric = Lyrics.GetPreviousMatch(currentPosition.Lyric, lyricMovable);\r\n        if (lyric == null)\r\n            return null;\r\n\r\n        int minIndex = GetMinIndex(lyric.Text);\r\n        int maxIndex = GetMaxIndex(lyric.Text);\r\n        if (maxIndex < minIndex)\r\n            return null;\r\n\r\n        int index = Math.Clamp(currentPosition.CharGap, minIndex, maxIndex);\r\n\r\n        return CreateCaretPosition(lyric, index);\r\n    }\r\n\r\n    protected sealed override TCaretPosition? MoveToNextLyric(TCaretPosition currentPosition)\r\n    {\r\n        var lyric = Lyrics.GetNextMatch(currentPosition.Lyric, lyricMovable);\r\n        if (lyric == null)\r\n            return null;\r\n\r\n        int index = Math.Clamp(currentPosition.CharGap, GetMinIndex(lyric.Text), GetMaxIndex(lyric.Text));\r\n\r\n        return CreateCaretPosition(lyric, index);\r\n    }\r\n\r\n    protected sealed override TCaretPosition? MoveToFirstLyric()\r\n    {\r\n        var lyric = Lyrics.FirstOrDefault(lyricMovable);\r\n        if (lyric == null)\r\n            return null;\r\n\r\n        return CreateCaretPosition(lyric, GetMinIndex(lyric.Text));\r\n    }\r\n\r\n    protected sealed override TCaretPosition? MoveToLastLyric()\r\n    {\r\n        var lyric = Lyrics.LastOrDefault(lyricMovable);\r\n        if (lyric == null)\r\n            return null;\r\n\r\n        return CreateCaretPosition(lyric, GetMaxIndex(lyric.Text));\r\n    }\r\n\r\n    protected sealed override TCaretPosition? MoveToTargetLyric(Lyric lyric)\r\n        => CreateCaretPosition(lyric, GetMinIndex(lyric.Text));\r\n\r\n    protected sealed override TCaretPosition? MoveToPreviousIndex(TCaretPosition currentPosition)\r\n    {\r\n        // get previous caret and make a check is need to change line.\r\n        var lyric = currentPosition.Lyric;\r\n        int previousIndex = currentPosition.CharGap - 1;\r\n\r\n        if (!indexInTextRange(previousIndex, lyric))\r\n            return null;\r\n\r\n        return CreateCaretPosition(lyric, previousIndex);\r\n    }\r\n\r\n    protected sealed override TCaretPosition? MoveToNextIndex(TCaretPosition currentPosition)\r\n    {\r\n        // get next caret and make a check is need to change line.\r\n        var lyric = currentPosition.Lyric;\r\n        int nextIndex = currentPosition.CharGap + 1;\r\n\r\n        if (!indexInTextRange(nextIndex, lyric))\r\n            return null;\r\n\r\n        return CreateCaretPosition(lyric, nextIndex);\r\n    }\r\n\r\n    protected sealed override TCaretPosition? MoveToFirstIndex(Lyric lyric)\r\n    {\r\n        int index = GetMinIndex(lyric.Text);\r\n\r\n        return CreateCaretPosition(lyric, index);\r\n    }\r\n\r\n    protected sealed override TCaretPosition? MoveToLastIndex(Lyric lyric)\r\n    {\r\n        int index = GetMaxIndex(lyric.Text);\r\n\r\n        return CreateCaretPosition(lyric, index);\r\n    }\r\n\r\n    private bool lyricMovable(Lyric lyric)\r\n    {\r\n        int minIndex = GetMinIndex(lyric.Text);\r\n        return indexInTextRange(minIndex, lyric);\r\n    }\r\n\r\n    private bool indexInTextRange(int index, Lyric lyric)\r\n    {\r\n        string text = lyric.Text;\r\n        return index >= GetMinIndex(text) && index <= GetMaxIndex(text);\r\n    }\r\n\r\n    protected abstract int GetMinIndex(string text);\r\n\r\n    protected abstract int GetMaxIndex(string text);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/CaretPosition/Algorithms/CharIndexCaretPositionAlgorithm.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing osu.Game.Rulesets.Karaoke.Extensions;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition.Algorithms;\r\n\r\n/// <summary>\r\n/// Base class for those algorithms which use char index as index.\r\n/// </summary>\r\n/// <typeparam name=\"TCaretPosition\"></typeparam>\r\npublic abstract class CharIndexCaretPositionAlgorithm<TCaretPosition> : IndexCaretPositionAlgorithm<TCaretPosition, int>\r\n    where TCaretPosition : struct, ICharIndexCaretPosition\r\n{\r\n    protected CharIndexCaretPositionAlgorithm(Lyric[] lyrics)\r\n        : base(lyrics)\r\n    {\r\n    }\r\n\r\n    protected sealed override bool PositionMovable(TCaretPosition position)\r\n    {\r\n        return indexInTextRange(position.CharIndex, position.Lyric);\r\n    }\r\n\r\n    protected sealed override TCaretPosition? MoveToPreviousLyric(TCaretPosition currentPosition)\r\n    {\r\n        var lyric = Lyrics.GetPreviousMatch(currentPosition.Lyric, lyricMovable);\r\n        if (lyric == null)\r\n            return null;\r\n\r\n        int minIndex = getMinIndex(lyric.Text);\r\n        int maxIndex = getMaxIndex(lyric.Text);\r\n        if (maxIndex < minIndex)\r\n            return null;\r\n\r\n        int index = Math.Clamp(currentPosition.CharIndex, minIndex, maxIndex);\r\n\r\n        return CreateCaretPosition(lyric, index);\r\n    }\r\n\r\n    protected sealed override TCaretPosition? MoveToNextLyric(TCaretPosition currentPosition)\r\n    {\r\n        var lyric = Lyrics.GetNextMatch(currentPosition.Lyric, lyricMovable);\r\n        if (lyric == null)\r\n            return null;\r\n\r\n        int index = Math.Clamp(currentPosition.CharIndex, getMinIndex(lyric.Text), getMaxIndex(lyric.Text));\r\n\r\n        return CreateCaretPosition(lyric, index);\r\n    }\r\n\r\n    protected sealed override TCaretPosition? MoveToFirstLyric()\r\n    {\r\n        var lyric = Lyrics.FirstOrDefault(lyricMovable);\r\n        if (lyric == null)\r\n            return null;\r\n\r\n        return CreateCaretPosition(lyric, getMinIndex(lyric.Text));\r\n    }\r\n\r\n    protected sealed override TCaretPosition? MoveToLastLyric()\r\n    {\r\n        var lyric = Lyrics.LastOrDefault(lyricMovable);\r\n        if (lyric == null)\r\n            return null;\r\n\r\n        return CreateCaretPosition(lyric, getMaxIndex(lyric.Text));\r\n    }\r\n\r\n    protected sealed override TCaretPosition? MoveToTargetLyric(Lyric lyric)\r\n        => CreateCaretPosition(lyric, getMinIndex(lyric.Text));\r\n\r\n    protected sealed override TCaretPosition? MoveToPreviousIndex(TCaretPosition currentPosition)\r\n    {\r\n        // get previous caret and make a check is need to change line.\r\n        var lyric = currentPosition.Lyric;\r\n        int previousIndex = currentPosition.CharIndex - 1;\r\n\r\n        if (!indexInTextRange(previousIndex, lyric))\r\n            return null;\r\n\r\n        return CreateCaretPosition(lyric, previousIndex);\r\n    }\r\n\r\n    protected sealed override TCaretPosition? MoveToNextIndex(TCaretPosition currentPosition)\r\n    {\r\n        // get next caret and make a check is need to change line.\r\n        var lyric = currentPosition.Lyric;\r\n        int nextIndex = currentPosition.CharIndex + 1;\r\n\r\n        if (!indexInTextRange(nextIndex, lyric))\r\n            return null;\r\n\r\n        return CreateCaretPosition(lyric, nextIndex);\r\n    }\r\n\r\n    protected sealed override TCaretPosition? MoveToFirstIndex(Lyric lyric)\r\n    {\r\n        int index = getMinIndex(lyric.Text);\r\n\r\n        return CreateCaretPosition(lyric, index);\r\n    }\r\n\r\n    protected sealed override TCaretPosition? MoveToLastIndex(Lyric lyric)\r\n    {\r\n        int index = getMaxIndex(lyric.Text);\r\n\r\n        return CreateCaretPosition(lyric, index);\r\n    }\r\n\r\n    private bool lyricMovable(Lyric lyric)\r\n    {\r\n        int minIndex = getMinIndex(lyric.Text);\r\n        return indexInTextRange(minIndex, lyric);\r\n    }\r\n\r\n    private static bool indexInTextRange(int index, Lyric lyric)\r\n    {\r\n        string text = lyric.Text;\r\n        return index >= getMinIndex(text) && index <= getMaxIndex(text);\r\n    }\r\n\r\n    private static int getMinIndex(string text) => 0;\r\n\r\n    private static int getMaxIndex(string text) => text.Length - 1;\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/CaretPosition/Algorithms/ClickingCaretPositionAlgorithm.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition.Algorithms;\r\n\r\n/// <summary>\r\n/// Algorithm for only accept navigate to the target lyric by mouse click.\r\n/// </summary>\r\npublic class ClickingCaretPositionAlgorithm : CaretPositionAlgorithm<ClickingCaretPosition>\r\n{\r\n    public ClickingCaretPositionAlgorithm(Lyric[] lyrics)\r\n        : base(lyrics)\r\n    {\r\n    }\r\n\r\n    protected override bool PositionMovable(ClickingCaretPosition position)\r\n    {\r\n        return true;\r\n    }\r\n\r\n    protected override ClickingCaretPosition? MoveToPreviousLyric(ClickingCaretPosition currentPosition)\r\n    {\r\n        return null;\r\n    }\r\n\r\n    protected override ClickingCaretPosition? MoveToNextLyric(ClickingCaretPosition currentPosition)\r\n    {\r\n        return null;\r\n    }\r\n\r\n    protected override ClickingCaretPosition? MoveToFirstLyric()\r\n    {\r\n        return null;\r\n    }\r\n\r\n    protected override ClickingCaretPosition? MoveToLastLyric()\r\n    {\r\n        return null;\r\n    }\r\n\r\n    protected override ClickingCaretPosition? MoveToTargetLyric(Lyric lyric) => new(lyric);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/CaretPosition/Algorithms/CreateRemoveTimeTagCaretPositionAlgorithm.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition.Algorithms;\r\n\r\n/// <summary>\r\n/// Algorithm for able to create/remove the time-tag by lyric char index.\r\n/// </summary>\r\npublic class CreateRemoveTimeTagCaretPositionAlgorithm : CharIndexCaretPositionAlgorithm<CreateRemoveTimeTagCaretPosition>\r\n{\r\n    public CreateRemoveTimeTagCaretPositionAlgorithm(Lyric[] lyrics)\r\n        : base(lyrics)\r\n    {\r\n    }\r\n\r\n    protected override CreateRemoveTimeTagCaretPosition CreateCaretPosition(Lyric lyric, int index) => new(lyric, index);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/CaretPosition/Algorithms/CreateRubyTagCaretPositionAlgorithm.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition.Algorithms;\r\n\r\npublic class CreateRubyTagCaretPositionAlgorithm : CharIndexCaretPositionAlgorithm<CreateRubyTagCaretPosition>\r\n{\r\n    public CreateRubyTagCaretPositionAlgorithm(Lyric[] lyrics)\r\n        : base(lyrics)\r\n    {\r\n    }\r\n\r\n    protected override CreateRubyTagCaretPosition CreateCaretPosition(Lyric lyric, int index) => new(lyric, index);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/CaretPosition/Algorithms/CuttingCaretPositionAlgorithm.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition.Algorithms;\r\n\r\n/// <summary>\r\n/// Algorithm for move the caret position indicate the position that cut the <see cref=\"Lyric.Text\"/>.\r\n/// </summary>\r\npublic class CuttingCaretPositionAlgorithm : CharGapCaretPositionAlgorithm<CuttingCaretPosition>\r\n{\r\n    public CuttingCaretPositionAlgorithm(Lyric[] lyrics)\r\n        : base(lyrics)\r\n    {\r\n    }\r\n\r\n    protected override CuttingCaretPosition CreateCaretPosition(Lyric lyric, int index) => new(lyric, index);\r\n\r\n    protected override int GetMinIndex(string text) => 1;\r\n\r\n    protected override int GetMaxIndex(string text) => text.Length - 1;\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/CaretPosition/Algorithms/ICaretPositionAlgorithm.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition.Algorithms;\r\n\r\npublic interface ICaretPositionAlgorithm\r\n{\r\n    ICaretPosition? MoveToPreviousLyric(ICaretPosition currentPosition);\r\n\r\n    ICaretPosition? MoveToNextLyric(ICaretPosition currentPosition);\r\n\r\n    ICaretPosition? MoveToFirstLyric();\r\n\r\n    ICaretPosition? MoveToLastLyric();\r\n\r\n    ICaretPosition? MoveToTargetLyric(Lyric lyric);\r\n\r\n    Type GetCaretPositionType();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/CaretPosition/Algorithms/IIndexCaretPositionAlgorithm.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition.Algorithms;\r\n\r\npublic interface IIndexCaretPositionAlgorithm : ICaretPositionAlgorithm\r\n{\r\n    IIndexCaretPosition? MoveToPreviousIndex(IIndexCaretPosition currentPosition);\r\n\r\n    IIndexCaretPosition? MoveToNextIndex(IIndexCaretPosition currentPosition);\r\n\r\n    IIndexCaretPosition? MoveToFirstIndex(Lyric lyric);\r\n\r\n    IIndexCaretPosition? MoveToLastIndex(Lyric lyric);\r\n\r\n    IIndexCaretPosition? MoveToTargetLyric<TIndex>(Lyric lyric, TIndex index) where TIndex : notnull;\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/CaretPosition/Algorithms/IndexCaretPositionAlgorithm.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition.Algorithms;\r\n\r\n/// <summary>\r\n/// Base class for move the <see cref=\"ICaretPosition\"/> to the previous or next position with target index.\r\n/// </summary>\r\n/// <typeparam name=\"TCaretPosition\"></typeparam>\r\n/// <typeparam name=\"TCaretIndex\"></typeparam>\r\npublic abstract class IndexCaretPositionAlgorithm<TCaretPosition, TCaretIndex> : CaretPositionAlgorithm<TCaretPosition>, IIndexCaretPositionAlgorithm\r\n    where TCaretPosition : struct, IIndexCaretPosition\r\n    where TCaretIndex : notnull\r\n{\r\n    protected IndexCaretPositionAlgorithm(Lyric[] lyrics)\r\n        : base(lyrics)\r\n    {\r\n    }\r\n\r\n    protected abstract TCaretPosition? MoveToPreviousIndex(TCaretPosition currentPosition);\r\n\r\n    protected abstract TCaretPosition? MoveToNextIndex(TCaretPosition currentPosition);\r\n\r\n    protected abstract TCaretPosition? MoveToFirstIndex(Lyric lyric);\r\n\r\n    protected abstract TCaretPosition? MoveToLastIndex(Lyric lyric);\r\n\r\n    protected abstract TCaretPosition CreateCaretPosition(Lyric lyric, TCaretIndex index);\r\n\r\n    public IIndexCaretPosition? MoveToPreviousIndex(IIndexCaretPosition currentPosition)\r\n    {\r\n        if (currentPosition is not TCaretPosition tCaretPosition)\r\n            throw new InvalidCastException(nameof(currentPosition));\r\n\r\n        PreValidate(tCaretPosition);\r\n\r\n        var movedCaretPosition = MoveToPreviousIndex(tCaretPosition);\r\n        return PostValidate(movedCaretPosition);\r\n    }\r\n\r\n    public IIndexCaretPosition? MoveToNextIndex(IIndexCaretPosition currentPosition)\r\n    {\r\n        if (currentPosition is not TCaretPosition tCaretPosition)\r\n            throw new InvalidCastException(nameof(currentPosition));\r\n\r\n        PreValidate(tCaretPosition);\r\n\r\n        var movedCaretPosition = MoveToNextIndex(tCaretPosition);\r\n        return PostValidate(movedCaretPosition);\r\n    }\r\n\r\n    IIndexCaretPosition? IIndexCaretPositionAlgorithm.MoveToFirstIndex(Lyric lyric)\r\n    {\r\n        var movedCaretPosition = MoveToFirstIndex(lyric);\r\n        return PostValidate(movedCaretPosition);\r\n    }\r\n\r\n    IIndexCaretPosition? IIndexCaretPositionAlgorithm.MoveToLastIndex(Lyric lyric)\r\n    {\r\n        var movedCaretPosition = MoveToLastIndex(lyric);\r\n        return PostValidate(movedCaretPosition);\r\n    }\r\n\r\n    IIndexCaretPosition? IIndexCaretPositionAlgorithm.MoveToTargetLyric<TIndex>(Lyric lyric, TIndex index)\r\n    {\r\n        if (index is not TCaretIndex caretIndex)\r\n            throw new InvalidCastException();\r\n\r\n        var movedCaretPosition = CreateCaretPosition(lyric, caretIndex);\r\n        return PostValidate(movedCaretPosition);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/CaretPosition/Algorithms/NavigateCaretPositionAlgorithm.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing osu.Framework.Extensions.IEnumerableExtensions;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition.Algorithms;\r\n\r\n/// <summary>\r\n/// Algorithm for navigate to the previous/next <see cref=\"Lyric\"/>.\r\n/// </summary>\r\npublic class NavigateCaretPositionAlgorithm : CaretPositionAlgorithm<NavigateCaretPosition>\r\n{\r\n    public NavigateCaretPositionAlgorithm(Lyric[] lyrics)\r\n        : base(lyrics)\r\n    {\r\n    }\r\n\r\n    protected override bool PositionMovable(NavigateCaretPosition position)\r\n    {\r\n        return true;\r\n    }\r\n\r\n    protected override NavigateCaretPosition? MoveToPreviousLyric(NavigateCaretPosition currentPosition)\r\n    {\r\n        var lyric = Lyrics.GetPrevious(currentPosition.Lyric);\r\n        if (lyric == null)\r\n            return null;\r\n\r\n        return new NavigateCaretPosition(lyric);\r\n    }\r\n\r\n    protected override NavigateCaretPosition? MoveToNextLyric(NavigateCaretPosition currentPosition)\r\n    {\r\n        var lyric = Lyrics.GetNext(currentPosition.Lyric);\r\n        if (lyric == null)\r\n            return null;\r\n\r\n        return new NavigateCaretPosition(lyric);\r\n    }\r\n\r\n    protected override NavigateCaretPosition? MoveToFirstLyric()\r\n    {\r\n        var lyric = Lyrics.FirstOrDefault();\r\n        if (lyric == null)\r\n            return null;\r\n\r\n        return new NavigateCaretPosition(lyric);\r\n    }\r\n\r\n    protected override NavigateCaretPosition? MoveToLastLyric()\r\n    {\r\n        var lyric = Lyrics.LastOrDefault();\r\n        if (lyric == null)\r\n            return null;\r\n\r\n        return new NavigateCaretPosition(lyric);\r\n    }\r\n\r\n    protected override NavigateCaretPosition? MoveToTargetLyric(Lyric lyric) => new(lyric);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/CaretPosition/Algorithms/RecordingTimeTagCaretMoveMode.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition.Algorithms;\r\n\r\npublic enum RecordingTimeTagCaretMoveMode\r\n{\r\n    /// <summary>\r\n    /// Move to any tag\r\n    /// </summary>\r\n    None,\r\n\r\n    /// <summary>\r\n    /// Only move to next start tag.\r\n    /// </summary>\r\n    OnlyStartTag,\r\n\r\n    /// <summary>\r\n    /// Only move to next end tag.\r\n    /// </summary>\r\n    OnlyEndTag,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/CaretPosition/Algorithms/RecordingTimeTagCaretPositionAlgorithm.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Diagnostics;\r\nusing System.Linq;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Extensions;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition.Algorithms;\r\n\r\n/// <summary>\r\n/// Algorithm for navigate to the <see cref=\"TimeTag\"/> position inside the <see cref=\"Lyric\"/>.\r\n/// </summary>\r\npublic class RecordingTimeTagCaretPositionAlgorithm : IndexCaretPositionAlgorithm<RecordingTimeTagCaretPosition, TimeTag>\r\n{\r\n    public RecordingTimeTagCaretMoveMode Mode { get; set; }\r\n\r\n    public RecordingTimeTagCaretPositionAlgorithm(Lyric[] lyrics)\r\n        : base(lyrics)\r\n    {\r\n    }\r\n\r\n    protected override void PreValidate(RecordingTimeTagCaretPosition input)\r\n    {\r\n        var timeTag = input.TimeTag;\r\n        var lyric = input.Lyric;\r\n\r\n        // should only check if the time-tag is in the lyric because previous time-tag position might not match to the mode.\r\n        Debug.Assert(lyric.TimeTags.Contains(timeTag));\r\n    }\r\n\r\n    protected override bool PositionMovable(RecordingTimeTagCaretPosition position)\r\n    {\r\n        var lyric = position.Lyric;\r\n        var timeTag = position.TimeTag;\r\n\r\n        return lyric.TimeTags.Contains(timeTag)\r\n               && timeTagMovable(timeTag);\r\n    }\r\n\r\n    protected override RecordingTimeTagCaretPosition? MoveToPreviousLyric(RecordingTimeTagCaretPosition currentPosition)\r\n    {\r\n        var currentLyric = currentPosition.Lyric;\r\n        var previousLyric = Lyrics.GetPreviousMatch(currentLyric, l => l.TimeTags.Any(timeTagMovable));\r\n        if (previousLyric == null)\r\n            return null;\r\n\r\n        // always goes to fist time-tag for recording.\r\n        var timeTags = previousLyric.TimeTags.Where(timeTagMovable).ToArray();\r\n        var upTimeTag = timeTags.FirstOrDefault();\r\n\r\n        if (upTimeTag == null)\r\n            return null;\r\n\r\n        return CreateCaretPosition(previousLyric, upTimeTag);\r\n    }\r\n\r\n    protected override RecordingTimeTagCaretPosition? MoveToNextLyric(RecordingTimeTagCaretPosition currentPosition)\r\n    {\r\n        var currentLyric = currentPosition.Lyric;\r\n        var nextLyric = Lyrics.GetNextMatch(currentLyric, l => l.TimeTags.Any(timeTagMovable));\r\n        if (nextLyric == null)\r\n            return null;\r\n\r\n        // always goes to fist time-tag for recording.\r\n        var timeTags = nextLyric.TimeTags.Where(timeTagMovable).ToArray();\r\n        var downTimeTag = timeTags.FirstOrDefault();\r\n\r\n        if (downTimeTag == null)\r\n            return null;\r\n\r\n        return CreateCaretPosition(nextLyric, downTimeTag);\r\n    }\r\n\r\n    protected override RecordingTimeTagCaretPosition? MoveToFirstLyric()\r\n    {\r\n        var firstLyric = Lyrics.FirstOrDefault(x => x.TimeTags.Any(timeTagMovable));\r\n        var firstTimeTag = firstLyric?.TimeTags.FirstOrDefault(timeTagMovable);\r\n        if (firstLyric == null || firstTimeTag == null)\r\n            return null;\r\n\r\n        return CreateCaretPosition(firstLyric, firstTimeTag);\r\n    }\r\n\r\n    protected override RecordingTimeTagCaretPosition? MoveToLastLyric()\r\n    {\r\n        var lastLyric = Lyrics.LastOrDefault(x => x.TimeTags.Any(timeTagMovable));\r\n        var lastTimeTag = lastLyric?.TimeTags.LastOrDefault(timeTagMovable);\r\n        if (lastLyric == null || lastTimeTag == null)\r\n            return null;\r\n\r\n        return CreateCaretPosition(lastLyric, lastTimeTag);\r\n    }\r\n\r\n    protected override RecordingTimeTagCaretPosition? MoveToTargetLyric(Lyric lyric)\r\n    {\r\n        var targetTimeTag = lyric.TimeTags.FirstOrDefault(timeTagMovable);\r\n        if (targetTimeTag == null)\r\n            return null;\r\n\r\n        return CreateCaretPosition(lyric, targetTimeTag);\r\n    }\r\n\r\n    protected override RecordingTimeTagCaretPosition? MoveToPreviousIndex(RecordingTimeTagCaretPosition currentPosition)\r\n    {\r\n        var lyric = currentPosition.Lyric;\r\n        var timeTags = lyric.TimeTags;\r\n        var previousTimeTag = timeTags.GetPreviousMatch(currentPosition.TimeTag, timeTagMovable);\r\n        if (previousTimeTag == null)\r\n            return null;\r\n\r\n        return CreateCaretPosition(lyric, previousTimeTag);\r\n    }\r\n\r\n    protected override RecordingTimeTagCaretPosition? MoveToNextIndex(RecordingTimeTagCaretPosition currentPosition)\r\n    {\r\n        var lyric = currentPosition.Lyric;\r\n        var timeTags = lyric.TimeTags;\r\n        var nextTimeTag = timeTags.GetNextMatch(currentPosition.TimeTag, timeTagMovable);\r\n        if (nextTimeTag == null)\r\n            return null;\r\n\r\n        return CreateCaretPosition(lyric, nextTimeTag);\r\n    }\r\n\r\n    protected override RecordingTimeTagCaretPosition? MoveToFirstIndex(Lyric lyric)\r\n    {\r\n        var firstTimeTag = lyric.TimeTags.FirstOrDefault();\r\n        if (firstTimeTag == null)\r\n            return null;\r\n\r\n        var caret = CreateCaretPosition(lyric, firstTimeTag);\r\n        if (!timeTagMovable(firstTimeTag))\r\n            return MoveToNextIndex(caret);\r\n\r\n        return caret;\r\n    }\r\n\r\n    protected override RecordingTimeTagCaretPosition? MoveToLastIndex(Lyric lyric)\r\n    {\r\n        var lastTimeTag = lyric.TimeTags.LastOrDefault();\r\n        if (lastTimeTag == null)\r\n            return null;\r\n\r\n        var caret = CreateCaretPosition(lyric, lastTimeTag);\r\n        if (!timeTagMovable(lastTimeTag))\r\n            return MoveToPreviousIndex(caret);\r\n\r\n        return caret;\r\n    }\r\n\r\n    protected override RecordingTimeTagCaretPosition CreateCaretPosition(Lyric lyric, TimeTag index) => new(lyric, index);\r\n\r\n    private bool timeTagMovable(TimeTag timeTag)\r\n    {\r\n        return Mode switch\r\n        {\r\n            RecordingTimeTagCaretMoveMode.None => true,\r\n            RecordingTimeTagCaretMoveMode.OnlyStartTag => timeTag.Index.State == TextIndex.IndexState.Start,\r\n            RecordingTimeTagCaretMoveMode.OnlyEndTag => timeTag.Index.State == TextIndex.IndexState.End,\r\n            _ => throw new InvalidOperationException(nameof(RecordingTimeTagCaretMoveMode)),\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/CaretPosition/Algorithms/TypingCaretPositionAlgorithm.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition.Algorithms;\r\n\r\n/// <summary>\r\n/// Algorithm for move the caret position indicate the position that type to the <see cref=\"Lyric.Text\"/>.\r\n/// </summary>\r\npublic class TypingCaretPositionAlgorithm : CharGapCaretPositionAlgorithm<TypingCaretPosition>\r\n{\r\n    public TypingCaretPositionAlgorithm(Lyric[] lyrics)\r\n        : base(lyrics)\r\n    {\r\n    }\r\n\r\n    protected override TypingCaretPosition CreateCaretPosition(Lyric lyric, int index) => new(lyric, index);\r\n\r\n    protected override int GetMinIndex(string text) => 0;\r\n\r\n    protected override int GetMaxIndex(string text) => text.Length;\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/CaretPosition/ClickingCaretPosition.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\n\r\npublic readonly struct ClickingCaretPosition : ICaretPosition\r\n{\r\n    public ClickingCaretPosition(Lyric lyric)\r\n    {\r\n        Lyric = lyric;\r\n    }\r\n\r\n    public Lyric Lyric { get; }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/CaretPosition/CreateRemoveTimeTagCaretPosition.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\n\r\npublic readonly struct CreateRemoveTimeTagCaretPosition : ICharIndexCaretPosition, IComparable<CreateRemoveTimeTagCaretPosition>\r\n{\r\n    public CreateRemoveTimeTagCaretPosition(Lyric lyric, int charIndex)\r\n    {\r\n        Lyric = lyric;\r\n        CharIndex = charIndex;\r\n    }\r\n\r\n    public Lyric Lyric { get; }\r\n\r\n    public int CharIndex { get; }\r\n\r\n    public TimeTag[] GetTimeTagsWithState(TextIndex.IndexState state)\r\n    {\r\n        int charIndex = CharIndex;\r\n        return Lyric.TimeTags.Where(x => TextIndexUtils.ToCharIndex(x.Index) == charIndex && x.Index.State == state).ToArray();\r\n    }\r\n\r\n    public int CompareTo(CreateRemoveTimeTagCaretPosition other)\r\n    {\r\n        if (Lyric != other.Lyric)\r\n            throw new InvalidOperationException();\r\n\r\n        return CharIndex.CompareTo(other.CharIndex);\r\n    }\r\n\r\n    public int CompareTo(IIndexCaretPosition? other)\r\n    {\r\n        if (other is not CreateRemoveTimeTagCaretPosition timeTagIndexCaretPosition)\r\n            throw new InvalidOperationException();\r\n\r\n        return CompareTo(timeTagIndexCaretPosition);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/CaretPosition/CreateRubyTagCaretPosition.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\n\r\npublic readonly struct CreateRubyTagCaretPosition : ICharIndexCaretPosition, IComparable<CreateRubyTagCaretPosition>\r\n{\r\n    public CreateRubyTagCaretPosition(Lyric lyric, int charIndex)\r\n    {\r\n        Lyric = lyric;\r\n        CharIndex = charIndex;\r\n    }\r\n\r\n    public Lyric Lyric { get; }\r\n\r\n    public int CharIndex { get; }\r\n\r\n    public int CompareTo(CreateRubyTagCaretPosition other)\r\n    {\r\n        if (Lyric != other.Lyric)\r\n            throw new InvalidOperationException();\r\n\r\n        return CharIndex.CompareTo(other.CharIndex);\r\n    }\r\n\r\n    public int CompareTo(IIndexCaretPosition? other)\r\n    {\r\n        if (other is not CreateRubyTagCaretPosition createRubyTagCaretPosition)\r\n            throw new InvalidOperationException();\r\n\r\n        return CompareTo(createRubyTagCaretPosition);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/CaretPosition/CuttingCaretPosition.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\n\r\npublic readonly struct CuttingCaretPosition : ICharGapCaretPosition, IComparable<CuttingCaretPosition>\r\n{\r\n    public CuttingCaretPosition(Lyric lyric, int index)\r\n    {\r\n        Lyric = lyric;\r\n        CharGap = index;\r\n    }\r\n\r\n    public Lyric Lyric { get; }\r\n\r\n    public int CharGap { get; }\r\n\r\n    public int CompareTo(CuttingCaretPosition other)\r\n    {\r\n        if (Lyric != other.Lyric)\r\n            throw new InvalidOperationException();\r\n\r\n        return CharGap.CompareTo(other.CharGap);\r\n    }\r\n\r\n    public int CompareTo(IIndexCaretPosition? other)\r\n    {\r\n        if (other is not CuttingCaretPosition cuttingCaretPosition)\r\n            throw new InvalidOperationException();\r\n\r\n        return CompareTo(cuttingCaretPosition);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/CaretPosition/ICaretPosition.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\n\r\npublic interface ICaretPosition\r\n{\r\n    public Lyric Lyric { get; }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/CaretPosition/ICharGapCaretPosition.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\n\r\npublic interface ICharGapCaretPosition : IIndexCaretPosition\r\n{\r\n    public int CharGap { get; }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/CaretPosition/ICharIndexCaretPosition.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\n\r\npublic interface ICharIndexCaretPosition : IIndexCaretPosition\r\n{\r\n    public int CharIndex { get; }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/CaretPosition/IIndexCaretPosition.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\n\r\npublic interface IIndexCaretPosition : ICaretPosition, IComparable<IIndexCaretPosition>\r\n{\r\n    public static bool operator >(IIndexCaretPosition caretPosition1, IIndexCaretPosition caretPosition2)\r\n        => caretPosition1.CompareTo(caretPosition2) > 0;\r\n\r\n    public static bool operator >=(IIndexCaretPosition caretPosition1, IIndexCaretPosition caretPosition2)\r\n        => caretPosition1.CompareTo(caretPosition2) >= 0;\r\n\r\n    public static bool operator <(IIndexCaretPosition caretPosition1, IIndexCaretPosition caretPosition2)\r\n        => caretPosition1.CompareTo(caretPosition2) < 0;\r\n\r\n    public static bool operator <=(IIndexCaretPosition caretPosition1, IIndexCaretPosition caretPosition2)\r\n        => caretPosition1.CompareTo(caretPosition2) <= 0;\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/CaretPosition/NavigateCaretPosition.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\n\r\npublic readonly struct NavigateCaretPosition : ICaretPosition\r\n{\r\n    public NavigateCaretPosition(Lyric lyric)\r\n    {\r\n        Lyric = lyric;\r\n    }\r\n\r\n    public Lyric Lyric { get; }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/CaretPosition/RecordingTimeTagCaretPosition.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Extensions;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\n\r\npublic readonly struct RecordingTimeTagCaretPosition : IIndexCaretPosition, IComparable<RecordingTimeTagCaretPosition>\r\n{\r\n    public RecordingTimeTagCaretPosition(Lyric lyric, TimeTag timeTag)\r\n    {\r\n        Lyric = lyric;\r\n        TimeTag = timeTag;\r\n    }\r\n\r\n    public Lyric Lyric { get; }\r\n\r\n    public TimeTag TimeTag { get; }\r\n\r\n    public int CompareTo(RecordingTimeTagCaretPosition other)\r\n    {\r\n        if (Lyric != other.Lyric)\r\n            throw new InvalidOperationException();\r\n\r\n        return TimeTag.Index.CompareTo(other.TimeTag.Index);\r\n    }\r\n\r\n    public int CompareTo(IIndexCaretPosition? other)\r\n    {\r\n        if (other is not RecordingTimeTagCaretPosition recordingTagCaretPosition)\r\n            throw new InvalidOperationException();\r\n\r\n        return CompareTo(recordingTagCaretPosition);\r\n    }\r\n\r\n    public int GetTotalTimeTags()\r\n    {\r\n        return Lyric.TimeTags.Count;\r\n    }\r\n\r\n    public int GetCurrentTimeTagIndex()\r\n    {\r\n        return Lyric.TimeTags.IndexOf(TimeTag);\r\n    }\r\n\r\n    public int GetPaddingTextIndex()\r\n    {\r\n        var currentTimeTag = TimeTag;\r\n        return Lyric.TimeTags.SkipWhile(x => x != currentTimeTag).Skip(1).TakeWhile(x => x.Index == currentTimeTag.Index).Count();\r\n    }\r\n\r\n    public TimeTag[] GetRecordedTimeTags()\r\n    {\r\n        var currentTimeTag = TimeTag;\r\n        return Lyric.TimeTags.TakeWhile(tag => tag != currentTimeTag).ToArray();\r\n    }\r\n\r\n    public TimeTag[] GetPendingTimeTags()\r\n    {\r\n        var currentTimeTag = TimeTag;\r\n        return Lyric.TimeTags.SkipWhile(tag => tag != currentTimeTag).Skip(1).ToArray();\r\n    }\r\n\r\n    public Tuple<int, int> GetLyricCharRange()\r\n    {\r\n        return GetLyricCharRange(TimeTag);\r\n    }\r\n\r\n    public Tuple<int, int> GetLyricCharRange(TimeTag timeTag)\r\n    {\r\n        if (!Lyric.TimeTags.Contains(timeTag))\r\n            throw new InvalidOperationException();\r\n\r\n        switch (timeTag.Index.State)\r\n        {\r\n            case TextIndex.IndexState.Start:\r\n            {\r\n                var nextTimeTag = Lyric.TimeTags.GetNextMatch(timeTag, x => x.Index != timeTag.Index);\r\n\r\n                int startGapIndex = TextIndexUtils.ToGapIndex(timeTag.Index);\r\n                int endGapIndex = TextIndexUtils.ToGapIndex(nextTimeTag?.Index ?? timeTag.Index);\r\n\r\n                return new Tuple<int, int>(startGapIndex, endGapIndex - 1);\r\n            }\r\n\r\n            case TextIndex.IndexState.End:\r\n            {\r\n                var previousTimeTag = Lyric.TimeTags.GetPreviousMatch(timeTag, x => x.Index != timeTag.Index);\r\n\r\n                int startGapIndex = TextIndexUtils.ToGapIndex(previousTimeTag?.Index ?? timeTag.Index);\r\n                int endGapIndex = TextIndexUtils.ToGapIndex(timeTag.Index);\r\n\r\n                return new Tuple<int, int>(startGapIndex, endGapIndex - 1);\r\n            }\r\n\r\n            default:\r\n                throw new ArgumentOutOfRangeException();\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/CaretPosition/TypingCaretPosition.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\n\r\npublic readonly struct TypingCaretPosition : ICharGapCaretPosition, IComparable<TypingCaretPosition>\r\n{\r\n    public TypingCaretPosition(Lyric lyric, int charGap)\r\n    {\r\n        Lyric = lyric;\r\n        CharGap = charGap;\r\n    }\r\n\r\n    public Lyric Lyric { get; }\r\n\r\n    public int CharGap { get; }\r\n\r\n    public int CompareTo(TypingCaretPosition other)\r\n    {\r\n        if (Lyric != other.Lyric)\r\n            throw new InvalidOperationException();\r\n\r\n        return CharGap.CompareTo(other.CharGap);\r\n    }\r\n\r\n    public int CompareTo(IIndexCaretPosition? other)\r\n    {\r\n        if (other is not TypingCaretPosition typingCaretPosition)\r\n            throw new InvalidOperationException();\r\n\r\n        return CompareTo(typingCaretPosition);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/ClipboardToast.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.ComponentModel;\r\nusing osu.Framework.Extensions;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Overlays.OSD;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics;\r\n\r\npublic partial class ClipboardToast : Toast\r\n{\r\n    public ClipboardToast(LyricEditorMode mode, ClipboardAction action)\r\n        : base(getDescription(), getValue(action))\r\n    {\r\n    }\r\n\r\n    private static LocalisableString getDescription()\r\n        => \"Lyric editor\";\r\n\r\n    private static LocalisableString getValue(ClipboardAction action)\r\n        => action.GetDescription();\r\n}\r\n\r\npublic enum ClipboardAction\r\n{\r\n    [Description(\"Cut\")]\r\n    Cut,\r\n\r\n    [Description(\"Copy\")]\r\n    Copy,\r\n\r\n    [Description(\"Paste\")]\r\n    Paste,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/ApplySelectingArea.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content;\r\n\r\npublic partial class ApplySelectingArea : CompositeDrawable\r\n{\r\n    private const float spacing = 10;\r\n    private const float button_width = 100;\r\n\r\n    private readonly IBindableList<Lyric> selectedLyrics = new BindableList<Lyric>();\r\n\r\n    private readonly Box background;\r\n    private readonly GridContainer gridContainer;\r\n    private readonly ActionButton applyButton;\r\n    private readonly ActionButton cancelButton;\r\n    private readonly ActionButton previewButton;\r\n\r\n    public ApplySelectingArea()\r\n    {\r\n        RelativeSizeAxes = Axes.X;\r\n        Height = 45;\r\n\r\n        InternalChildren = new Drawable[]\r\n        {\r\n            background = new Box\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n            },\r\n            gridContainer = new GridContainer\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n                ColumnDimensions = new[]\r\n                {\r\n                    new Dimension(GridSizeMode.Absolute, LyricList.LYRIC_LIST_PADDING),\r\n                    new Dimension(GridSizeMode.Absolute, Row.SELECT_AREA_WIDTH),\r\n                    new Dimension(),\r\n                    new Dimension(GridSizeMode.Absolute, spacing),\r\n                    new Dimension(GridSizeMode.Absolute, button_width),\r\n                    new Dimension(GridSizeMode.Absolute, spacing),\r\n                    new Dimension(GridSizeMode.Absolute, button_width),\r\n                    new Dimension(GridSizeMode.Absolute, spacing),\r\n                    new Dimension(GridSizeMode.Absolute, button_width),\r\n                    new Dimension(GridSizeMode.Absolute, spacing),\r\n                },\r\n                Content = new[]\r\n                {\r\n                    new[]\r\n                    {\r\n                        Empty(),\r\n                        new SelectAllArea\r\n                        {\r\n                            RelativeSizeAxes = Axes.Both,\r\n                        },\r\n                        new Container\r\n                        {\r\n                            RelativeSizeAxes = Axes.Both,\r\n                        },\r\n                        Empty(),\r\n                        applyButton = new ActionButton\r\n                        {\r\n                            Text = \"Apply\",\r\n                        },\r\n                        Empty(),\r\n                        cancelButton = new ActionButton\r\n                        {\r\n                            Text = \"Cancel\",\r\n                        },\r\n                        Empty(),\r\n                        previewButton = new ActionButton\r\n                        {\r\n                            Text = \"Preview\",\r\n                        },\r\n                        Empty(),\r\n                    },\r\n                },\r\n            },\r\n        };\r\n\r\n        selectedLyrics.BindCollectionChanged((_, _) =>\r\n        {\r\n            bool selectAny = selectedLyrics.Any();\r\n            applyButton.Enabled.Value = selectAny;\r\n        }, true);\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OsuColour colours, ILyricSelectionState lyricSelectionState)\r\n    {\r\n        background.Colour = colours.Gray2;\r\n\r\n        selectedLyrics.BindTo(lyricSelectionState.SelectedLyrics);\r\n\r\n        applyButton.BackgroundColour = colours.Red;\r\n        applyButton.Action = () =>\r\n        {\r\n            lyricSelectionState.EndSelecting(LyricEditorSelectingAction.Apply);\r\n        };\r\n\r\n        cancelButton.BackgroundColour = colours.Gray2;\r\n        cancelButton.Action = () =>\r\n        {\r\n            lyricSelectionState.EndSelecting(LyricEditorSelectingAction.Cancel);\r\n        };\r\n\r\n        previewButton.BackgroundColour = colours.Purple;\r\n        previewButton.Action = () =>\r\n        {\r\n            // todo : implement\r\n        };\r\n    }\r\n\r\n    public partial class SelectAllArea : CompositeDrawable\r\n    {\r\n        private readonly IBindable<LyricEditorMode> bindableMode = new Bindable<LyricEditorMode>();\r\n        private readonly IBindableDictionary<Lyric, LocalisableString> disableSelectingLyrics = new BindableDictionary<Lyric, LocalisableString>();\r\n        private readonly IBindableList<Lyric> selectedLyrics = new BindableList<Lyric>();\r\n\r\n        private readonly Box background;\r\n        private readonly CircleCheckbox allSelectedCheckbox;\r\n\r\n        public SelectAllArea()\r\n        {\r\n            InternalChildren = new Drawable[]\r\n            {\r\n                background = new Box\r\n                {\r\n                    RelativeSizeAxes = Axes.Both,\r\n                },\r\n                allSelectedCheckbox = new CircleCheckbox\r\n                {\r\n                    Anchor = Anchor.Centre,\r\n                    Origin = Anchor.Centre,\r\n                },\r\n            };\r\n        }\r\n\r\n        protected override bool OnClick(ClickEvent e)\r\n        {\r\n            // trigger checkbox click if click this area.\r\n            allSelectedCheckbox.TriggerEvent(e);\r\n            return base.OnClick(e);\r\n        }\r\n\r\n        private bool selectedLyricsTriggering;\r\n        private bool checkboxClicking;\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(ILyricEditorState state, ILyricSelectionState lyricSelectionState, LyricEditorColourProvider colourProvider, EditorBeatmap beatmap)\r\n        {\r\n            bindableMode.BindTo(state.BindableMode);\r\n            disableSelectingLyrics.BindTo(lyricSelectionState.DisableSelectingLyric);\r\n            selectedLyrics.BindTo(lyricSelectionState.SelectedLyrics);\r\n\r\n            // should update background if mode changed.\r\n            bindableMode.BindValueChanged(e =>\r\n            {\r\n                background.Colour = colourProvider.Dark2(e.NewValue);\r\n                allSelectedCheckbox.AccentColour = colourProvider.Colour2(e.NewValue);\r\n            }, true);\r\n\r\n            // should disable selection if current lyric is disabled.\r\n            disableSelectingLyrics.BindCollectionChanged((_, _) =>\r\n            {\r\n                int disabledLyricNumber = lyricSelectionState.DisableSelectingLyric.Count;\r\n                int totalLyrics = beatmap.HitObjects.OfType<Lyric>().Count();\r\n                bool disabled = disabledLyricNumber == totalLyrics;\r\n\r\n                allSelectedCheckbox.Current.Disabled = disabled;\r\n                allSelectedCheckbox.TooltipText = disabled ? \"Seems all selection are disabled\" : string.Empty;\r\n            });\r\n\r\n            // get bindable and update bindable if select or not select all.\r\n            selectedLyrics.BindCollectionChanged((_, _) =>\r\n            {\r\n                if (checkboxClicking)\r\n                    return;\r\n\r\n                selectedLyricsTriggering = true;\r\n\r\n                var lyrics = beatmap.HitObjects.OfType<Lyric>();\r\n\r\n                bool selectAll = lyrics.All(x => selectedLyrics.Contains(x));\r\n                allSelectedCheckbox.Current.Value = selectAll;\r\n\r\n                selectedLyricsTriggering = false;\r\n            }, true);\r\n\r\n            allSelectedCheckbox.Current.BindValueChanged(e =>\r\n            {\r\n                if (selectedLyricsTriggering)\r\n                    return;\r\n\r\n                checkboxClicking = true;\r\n\r\n                if (e.NewValue)\r\n                    lyricSelectionState.SelectAll();\r\n                else\r\n                    lyricSelectionState.UnSelectAll();\r\n\r\n                checkboxClicking = false;\r\n            });\r\n        }\r\n    }\r\n\r\n    public partial class ActionButton : OsuButton\r\n    {\r\n        public ActionButton()\r\n        {\r\n            Anchor = Anchor.Centre;\r\n            Origin = Anchor.Centre;\r\n            RelativeSizeAxes = Axes.X;\r\n            Height = 45 - spacing * 2;\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/CircleCheckbox.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Audio;\r\nusing osu.Framework.Audio.Sample;\r\nusing osu.Framework.Extensions.Color4Extensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Cursor;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osuTK;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content;\r\n\r\npublic partial class CircleCheckbox : Checkbox, IHasAccentColour, IHasTooltip\r\n{\r\n    private const float expanded_size = 24;\r\n\r\n    private readonly Circle background;\r\n    private readonly SpriteIcon border;\r\n    private readonly SpriteIcon selectedIcon;\r\n\r\n    private Sample? sampleChecked;\r\n    private Sample? sampleUnchecked;\r\n\r\n    public CircleCheckbox()\r\n    {\r\n        Size = new Vector2(expanded_size);\r\n\r\n        Children = new Drawable[]\r\n        {\r\n            background = new Circle\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n                Alpha = 0.5f,\r\n            },\r\n            border = new SpriteIcon\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n                Icon = FontAwesome.Regular.Circle,\r\n            },\r\n            selectedIcon = new SpriteIcon\r\n            {\r\n                Anchor = Anchor.Centre,\r\n                Origin = Anchor.Centre,\r\n                RelativeSizeAxes = Axes.Both,\r\n                Icon = FontAwesome.Solid.Check,\r\n                Scale = new Vector2(0),\r\n            },\r\n            new HoverSounds(),\r\n        };\r\n\r\n        Current.DisabledChanged += disabled =>\r\n        {\r\n            background.Alpha = disabled ? 0.2f : 0.5f;\r\n            border.Alpha = selectedIcon.Alpha = disabled ? 0.2f : 1;\r\n        };\r\n\r\n        Current.ValueChanged += e =>\r\n        {\r\n            selectedIcon.ScaleTo(e.NewValue ? 0.6f : 0, 200, Easing.OutElastic);\r\n        };\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(AudioManager audio)\r\n    {\r\n        sampleChecked = audio.Samples.Get(\"UI/check-on\");\r\n        sampleUnchecked = audio.Samples.Get(\"UI/check-off\");\r\n    }\r\n\r\n    private Color4 accentColour;\r\n\r\n    public Color4 AccentColour\r\n    {\r\n        get => accentColour;\r\n        set\r\n        {\r\n            accentColour = value;\r\n\r\n            background.Colour = AccentColour.Darken(1.5f);\r\n            border.Colour = AccentColour;\r\n            selectedIcon.Colour = AccentColour;\r\n        }\r\n    }\r\n\r\n    protected override void OnUserChange(bool value)\r\n    {\r\n        base.OnUserChange(value);\r\n\r\n        if (value)\r\n            sampleChecked?.Play();\r\n        else\r\n            sampleUnchecked?.Play();\r\n    }\r\n\r\n    public LocalisableString TooltipText { get; set; }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Components/Badges/Badge.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Colour;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Components.Badges;\r\n\r\npublic abstract partial class Badge : CompositeDrawable\r\n{\r\n    private readonly Box background;\r\n    private readonly OsuSpriteText text;\r\n\r\n    protected Lyric Lyric { get; }\r\n\r\n    [Resolved]\r\n    private ILyricCaretState lyricCaretState { get; set; } = null!;\r\n\r\n    protected Badge(Lyric lyric)\r\n    {\r\n        Lyric = lyric;\r\n\r\n        AutoSizeAxes = Axes.Both;\r\n        Masking = true;\r\n        CornerRadius = 3;\r\n        InternalChildren = new Drawable[]\r\n        {\r\n            background = new Box\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n            },\r\n            text = new OsuSpriteText\r\n            {\r\n                Margin = new MarginPadding\r\n                {\r\n                    Vertical = 2,\r\n                    Horizontal = 5,\r\n                },\r\n                Text = \"Badge\",\r\n            },\r\n        };\r\n    }\r\n\r\n    protected LocalisableString Text\r\n    {\r\n        get => text.Text;\r\n        set => text.Text = value;\r\n    }\r\n\r\n    protected ColourInfo BackgroundColour\r\n    {\r\n        get => background.Colour;\r\n        set => background.Colour = value;\r\n    }\r\n\r\n    protected override bool OnClick(ClickEvent e)\r\n    {\r\n        lyricCaretState.MoveCaretToTargetPosition(Lyric);\r\n        return base.OnClick(e);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Components/Badges/LanguageBadge.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Globalization;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Extensions;\r\nusing osu.Framework.Graphics.Cursor;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.UserInterfaceV2;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Components.Badges;\r\n\r\npublic partial class LanguageBadge : Badge, IHasPopover\r\n{\r\n    private readonly Bindable<CultureInfo?> languageBindable;\r\n\r\n    public LanguageBadge(Lyric lyric)\r\n        : base(lyric)\r\n    {\r\n        languageBindable = lyric.LanguageBindable.GetBoundCopy();\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(ILyricLanguageChangeHandler lyricLanguageChangeHandler, ILyricSelectionState lyricSelectionState, OsuColour colours)\r\n    {\r\n        languageBindable.BindValueChanged(value =>\r\n        {\r\n            var language = value.NewValue;\r\n            updateBadgeText(language);\r\n\r\n            if (lyricSelectionState.Selecting.Value)\r\n                return;\r\n\r\n            lyricLanguageChangeHandler.SetLanguage(language);\r\n        });\r\n        updateBadgeText(Lyric.Language);\r\n\r\n        BackgroundColour = colours.BlueDarker;\r\n\r\n        void updateBadgeText(CultureInfo? language)\r\n            => Text = CultureInfoUtils.GetLanguageDisplayText(language);\r\n    }\r\n\r\n    protected override bool OnClick(ClickEvent e)\r\n    {\r\n        this.ShowPopover();\r\n\r\n        return base.OnClick(e);\r\n    }\r\n\r\n    public Popover GetPopover()\r\n        => new LanguageSelectorPopover(languageBindable)\r\n        {\r\n            EnableEmptyOption = true,\r\n        };\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Components/Badges/ReferenceLyricBadge.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Components.Badges;\r\n\r\npublic partial class ReferenceLyricBadge : Badge\r\n{\r\n    private readonly IBindable<Lyric?> bindableReferenceLyric;\r\n\r\n    [Resolved]\r\n    private ILyricCaretState lyricCaretState { get; set; } = null!;\r\n\r\n    public ReferenceLyricBadge(Lyric lyric)\r\n        : base(lyric)\r\n    {\r\n        bindableReferenceLyric = lyric.ReferenceLyricBindable.GetBoundCopy();\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OsuColour colours)\r\n    {\r\n        BackgroundColour = colours.Red;\r\n\r\n        bindableReferenceLyric.BindValueChanged(e =>\r\n        {\r\n            if (e.NewValue == null)\r\n            {\r\n                Hide();\r\n            }\r\n            else\r\n            {\r\n                Show();\r\n\r\n                // note: there's no need to worry about referenced lyric change the order because there's no possible to change hhe order in reference lyric mode.\r\n                Text = $\"Ref: #{e.NewValue.Order}\";\r\n            }\r\n        }, true);\r\n    }\r\n\r\n    protected override bool OnClick(ClickEvent e)\r\n    {\r\n        if (bindableReferenceLyric.Value == null)\r\n            return base.OnClick(e);\r\n\r\n        lyricCaretState.MoveCaretToTargetPosition(bindableReferenceLyric.Value);\r\n        return true;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Components/Badges/SingerBadge.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.Drawables;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Components.Badges;\r\n\r\npublic partial class SingerBadge : CompositeDrawable\r\n{\r\n    private readonly SingerDisplay singerDisplay;\r\n\r\n    private readonly IBindableDictionary<Singer, SingerState[]> singerIndexesBindable;\r\n\r\n    public SingerBadge(Lyric lyric)\r\n    {\r\n        singerIndexesBindable = lyric.SingersBindable.GetBoundCopy();\r\n\r\n        AutoSizeAxes = Axes.Both;\r\n\r\n        InternalChild = singerDisplay = new SingerDisplay\r\n        {\r\n            Anchor = Anchor.TopRight,\r\n            Origin = Anchor.TopRight,\r\n        };\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        singerIndexesBindable.BindCollectionChanged((_, _) =>\r\n        {\r\n            // todo: maybe should display the singer state also?\r\n            singerDisplay.Current.Value = singerIndexesBindable.Keys.ToArray();\r\n        }, true);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Components/Badges/TimeTagBadge.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Components.Badges;\r\n\r\npublic partial class TimeTagBadge : Badge\r\n{\r\n    private readonly IBindable<int> bindableTimeTagsTimingVersion;\r\n    private readonly IBindableList<TimeTag> bindableTimeTags;\r\n\r\n    public TimeTagBadge(Lyric lyric)\r\n        : base(lyric)\r\n    {\r\n        bindableTimeTagsTimingVersion = lyric.TimeTagsTimingVersion.GetBoundCopy();\r\n        bindableTimeTags = lyric.TimeTagsBindable.GetBoundCopy();\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OsuColour colours)\r\n    {\r\n        BackgroundColour = colours.Green;\r\n\r\n        bindableTimeTagsTimingVersion.BindValueChanged(_ => updateBadgeText());\r\n        bindableTimeTags.BindCollectionChanged((_, _) => updateBadgeText());\r\n\r\n        updateBadgeText();\r\n\r\n        void updateBadgeText()\r\n            => Text = LyricUtils.TimeTagTimeFormattedString(Lyric);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Components/FixedInfo/InvalidInfo.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Extensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Cursor;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Graphics.UserInterfaceV2;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Issues;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Components.FixedInfo;\r\n\r\npublic partial class InvalidInfo : SpriteIcon, IHasCustomTooltip<Issue[]>, IHasPopover\r\n{\r\n    private readonly IBindableList<Issue> bindableIssues = new BindableList<Issue>();\r\n    private readonly Lyric lyric;\r\n\r\n    public InvalidInfo(Lyric lyric)\r\n    {\r\n        this.lyric = lyric;\r\n\r\n        Size = new Vector2(12);\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OsuColour colours, ILyricEditorVerifier verifier)\r\n    {\r\n        bindableIssues.BindTo(verifier.GetBindable(lyric));\r\n        bindableIssues.BindCollectionChanged((_, args) =>\r\n        {\r\n            TooltipContent = bindableIssues.ToArray();\r\n\r\n            var issue = getDisplayIssue(bindableIssues);\r\n\r\n            if (issue == null)\r\n            {\r\n                Icon = FontAwesome.Solid.CheckCircle;\r\n                Colour = colours.Green;\r\n                return;\r\n            }\r\n\r\n            var displayIssueType = issue.Template.Type;\r\n            var targetColour = issue.Template.Colour;\r\n\r\n            Icon = displayIssueType switch\r\n            {\r\n                IssueType.Problem => FontAwesome.Solid.TimesCircle,\r\n                IssueType.Warning => FontAwesome.Solid.ExclamationCircle,\r\n                // it's caused by internal error.\r\n                IssueType.Error => FontAwesome.Solid.ExclamationTriangle,\r\n                IssueType.Negligible => FontAwesome.Solid.InfoCircle,\r\n                _ => throw new ArgumentOutOfRangeException(),\r\n            };\r\n            Colour = targetColour;\r\n        }, true);\r\n    }\r\n\r\n    private static Issue? getDisplayIssue(IReadOnlyList<Issue> issues)\r\n    {\r\n        if (!issues.Any())\r\n            return null;\r\n\r\n        return issues.OrderByDescending(x => x.Template.Type).First();\r\n    }\r\n\r\n    protected override bool OnClick(ClickEvent e)\r\n    {\r\n        if (bindableIssues.Any())\r\n        {\r\n            this.ShowPopover();\r\n        }\r\n\r\n        return base.OnClick(e);\r\n    }\r\n\r\n    public ITooltip<Issue[]> GetCustomTooltip()\r\n        => new IssuesToolTip();\r\n\r\n    public Issue[]? TooltipContent { get; private set; }\r\n\r\n    public Popover GetPopover()\r\n    {\r\n        if (Parent == null)\r\n            throw new InvalidCastException(\"Should attach parent before get popover.\");\r\n\r\n        return new IssueTablePopover(Parent.Dependencies, bindableIssues);\r\n    }\r\n\r\n    private partial class IssueTablePopover : OsuPopover\r\n    {\r\n        private readonly IReadOnlyDependencyContainer dependencyContainer;\r\n\r\n        public IssueTablePopover(IReadOnlyDependencyContainer dependencyContainer, IReadOnlyCollection<Issue> issues)\r\n        {\r\n            this.dependencyContainer = dependencyContainer;\r\n\r\n            Child = new Container\r\n            {\r\n                Width = 300,\r\n                AutoSizeAxes = Axes.Y,\r\n                Children = new Drawable[]\r\n                {\r\n                    new SingleLyricIssueTable\r\n                    {\r\n                        Issues = issues,\r\n                    },\r\n                    new IconButton\r\n                    {\r\n                        Anchor = Anchor.TopRight,\r\n                        Origin = Anchor.TopRight,\r\n                        Icon = FontAwesome.Solid.Redo,\r\n                        Scale = new Vector2(0.7f),\r\n                        Action = () =>\r\n                        {\r\n                            var verifier = dependencyContainer.Get<ILyricEditorVerifier>();\r\n                            verifier.Refresh();\r\n\r\n                            // should close the popover if has no issue.\r\n                            if (!issues.Any())\r\n                                this.HidePopover();\r\n                        },\r\n                    },\r\n                },\r\n            };\r\n        }\r\n\r\n        protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)\r\n        {\r\n            var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));\r\n\r\n            dependencies.Cache(dependencyContainer.Get<LyricEditorColourProvider>());\r\n            dependencies.CacheAs(dependencyContainer.Get<ILyricEditorState>());\r\n            dependencies.CacheAs(dependencyContainer.Get<IIssueNavigator>());\r\n\r\n            return dependencies;\r\n        }\r\n\r\n        private partial class SingleLyricIssueTable : LyricEditorIssueTable\r\n        {\r\n            protected override TableColumn[] CreateHeaders() => new[]\r\n            {\r\n                new TableColumn(string.Empty, Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize, minSize: 30)),\r\n                new TableColumn(\"Message\", Anchor.CentreLeft),\r\n            };\r\n\r\n            protected override Drawable[] CreateContent(Issue issue) =>\r\n                new Drawable[]\r\n                {\r\n                    new IssueIcon\r\n                    {\r\n                        Origin = Anchor.Centre,\r\n                        Size = new Vector2(10),\r\n                        Margin = new MarginPadding { Left = 10 },\r\n                        Issue = issue,\r\n                    },\r\n                    new TruncatingSpriteText\r\n                    {\r\n                        Text = issue.ToString(),\r\n                        RelativeSizeAxes = Axes.X,\r\n                        Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Medium),\r\n                        ShowTooltip = false,\r\n                    },\r\n                };\r\n\r\n            protected override void OnIssueClicked(Issue issue)\r\n            {\r\n                base.OnIssueClicked(issue);\r\n\r\n                this.HidePopover();\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Components/FixedInfo/LockInfo.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics.Cursor;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Components.ContextMenu;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Types;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Components.FixedInfo;\r\n\r\npublic partial class LockInfo : SpriteIcon, IHasContextMenu\r\n{\r\n    [Resolved]\r\n    private ILockChangeHandler lockChangeHandler { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private ILyricCaretState lyricCaretState { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private KaraokeRulesetLyricEditorConfigManager configManager { get; set; } = null!;\r\n\r\n    public MenuItem[] ContextMenuItems => new LyricLockContextMenu(lockChangeHandler, lyric, \"Lock\").Items.ToArray();\r\n\r\n    private readonly Lyric lyric;\r\n\r\n    private readonly IBindable<LockState> bindableLockState;\r\n\r\n    public LockInfo(Lyric lyric)\r\n    {\r\n        this.lyric = lyric;\r\n        bindableLockState = lyric.LockBindable.GetBoundCopy();\r\n\r\n        Size = new Vector2(12);\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OsuColour colours)\r\n    {\r\n        bindableLockState.BindValueChanged(value =>\r\n        {\r\n            switch (value.NewValue)\r\n            {\r\n                case LockState.None:\r\n                    Icon = FontAwesome.Solid.Unlock;\r\n                    Colour = colours.Green;\r\n                    break;\r\n\r\n                case LockState.Partial:\r\n                    Icon = FontAwesome.Solid.Lock;\r\n                    Colour = colours.Yellow;\r\n                    break;\r\n\r\n                case LockState.Full:\r\n                    Icon = FontAwesome.Solid.Lock;\r\n                    Colour = colours.Red;\r\n                    return;\r\n\r\n                default:\r\n                    throw new ArgumentOutOfRangeException(nameof(value.NewValue));\r\n            }\r\n        }, true);\r\n    }\r\n\r\n    protected override bool OnClick(ClickEvent e)\r\n    {\r\n        // should mark lyric as selected for able to apply the lock state.\r\n        lyricCaretState.MoveCaretToTargetPosition(lyric);\r\n\r\n        if (bindableLockState.Value == LockState.None)\r\n        {\r\n            // change the state by config.\r\n            var newLockState = configManager.Get<LockState>(KaraokeRulesetLyricEditorSetting.ClickToLockLyricState);\r\n            lockChangeHandler.Lock(newLockState);\r\n        }\r\n        else\r\n        {\r\n            lockChangeHandler.Unlock();\r\n        }\r\n\r\n        return base.OnClick(e);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Components/FixedInfo/OrderInfo.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Components.FixedInfo;\r\n\r\npublic partial class OrderInfo : OsuSpriteText\r\n{\r\n    private readonly IBindable<int> bindableOrder;\r\n\r\n    public OrderInfo(Lyric lyric)\r\n    {\r\n        bindableOrder = lyric.OrderBindable.GetBoundCopy();\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        bindableOrder.BindValueChanged(value =>\r\n        {\r\n            int order = value.NewValue;\r\n            Text = $\"#{order}\";\r\n        }, true);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Components/Lyrics/BlueprintLayer.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Components.Lyrics.Blueprints;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Components.Lyrics;\r\n\r\npublic partial class BlueprintLayer : Layer\r\n{\r\n    private readonly IBindable<EditorModeWithEditStep> bindableModeWithEditStep = new Bindable<EditorModeWithEditStep>();\r\n\r\n    // todo: make better way to handle this.\r\n    private readonly IBindable<RubyTagEditMode> bindableRubyTagEditMode = new Bindable<RubyTagEditMode>();\r\n\r\n    // should block all blueprint action if not editable.\r\n    public override bool PropagatePositionalInputSubTree => base.PropagatePositionalInputSubTree && editable;\r\n\r\n    private bool editable = true;\r\n\r\n    public BlueprintLayer(Lyric lyric)\r\n        : base(lyric)\r\n    {\r\n        bindableModeWithEditStep.BindValueChanged(_ =>\r\n        {\r\n            // Initial blueprint container.\r\n            InitializeBlueprint();\r\n        });\r\n\r\n        bindableRubyTagEditMode.BindValueChanged(_ =>\r\n        {\r\n            // Initial blueprint container.\r\n            InitializeBlueprint();\r\n        });\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(ILyricEditorState state, IEditRubyModeState editRubyModeState)\r\n    {\r\n        bindableModeWithEditStep.BindTo(state.BindableModeWithEditStep);\r\n\r\n        bindableRubyTagEditMode.BindTo(editRubyModeState.BindableRubyTagEditMode);\r\n    }\r\n\r\n    protected void InitializeBlueprint()\r\n    {\r\n        // remove all exist blueprint container\r\n        ClearInternal();\r\n\r\n        // create preview and real caret\r\n        var modeWithEditStep = bindableModeWithEditStep.Value;\r\n        var rubyTagEditMode = bindableRubyTagEditMode.Value;\r\n\r\n        var blueprintContainer = createBlueprintContainer(modeWithEditStep, rubyTagEditMode, Lyric);\r\n        if (blueprintContainer == null)\r\n            return;\r\n\r\n        AddInternal(blueprintContainer);\r\n\r\n        static Drawable? createBlueprintContainer(EditorModeWithEditStep modeWithEditStep, RubyTagEditMode rubyTagEditMode, Lyric lyric) =>\r\n            modeWithEditStep.Mode switch\r\n            {\r\n                LyricEditorMode.EditRuby => rubyTagEditMode == RubyTagEditMode.Create ? null : new RubyBlueprintContainer(lyric),\r\n                LyricEditorMode.EditTimeTag => modeWithEditStep.EditStep is TimeTagEditStep.Adjust ? new TimeTagBlueprintContainer(lyric) : null,\r\n                _ => null,\r\n            };\r\n    }\r\n\r\n    public override void UpdateDisableEditState(bool editable)\r\n    {\r\n        this.editable = editable;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Components/Lyrics/Blueprints/LyricPropertyBlueprintContainer.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Components.Lyrics.Blueprints;\r\n\r\npublic abstract partial class LyricPropertyBlueprintContainer<T> : BindableBlueprintContainer<T> where T : class\r\n{\r\n    [Resolved]\r\n    private ILyricCaretState lyricCaretState { get; set; } = null!;\r\n\r\n    private readonly BindableList<T> lyricListProperties;\r\n\r\n    protected readonly Lyric Lyric;\r\n\r\n    protected LyricPropertyBlueprintContainer(Lyric lyric)\r\n    {\r\n        lyricListProperties = GetProperties(lyric);\r\n        Lyric = lyric;\r\n    }\r\n\r\n    protected abstract BindableList<T> GetProperties(Lyric lyric);\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        // Make it auto create or remove the blueprint by the list.\r\n        RegisterBindable(lyricListProperties);\r\n    }\r\n\r\n    protected override bool OnClick(ClickEvent e)\r\n    {\r\n        lyricCaretState.MoveCaretToTargetPosition(Lyric);\r\n        return base.OnClick(e);\r\n    }\r\n\r\n    protected override bool OnDragStart(DragStartEvent e)\r\n    {\r\n        lyricCaretState.MoveCaretToTargetPosition(Lyric);\r\n        return base.OnDragStart(e);\r\n    }\r\n\r\n    protected abstract partial class LyricPropertySelectionHandler<TModeState> : BindableSelectionHandler\r\n        where TModeState : IHasBlueprintSelection<T>\r\n    {\r\n        [BackgroundDependencyLoader]\r\n        private void load(TModeState modeState)\r\n        {\r\n            SelectedItems.BindTo(modeState.SelectedItems);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Components/Lyrics/Blueprints/RubyBlueprintContainer.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Primitives;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Framework.Logging;\r\nusing osu.Game.Rulesets.Edit;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\nusing osu.Game.Screens.Edit.Compose.Components;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Components.Lyrics.Blueprints;\r\n\r\npublic partial class RubyBlueprintContainer : LyricPropertyBlueprintContainer<RubyTag>\r\n{\r\n    public RubyBlueprintContainer(Lyric lyric)\r\n        : base(lyric)\r\n    {\r\n    }\r\n\r\n    protected override BindableList<RubyTag> GetProperties(Lyric lyric)\r\n        => lyric.RubyTagsBindable.GetBoundCopy();\r\n\r\n    protected override SelectionHandler<RubyTag> CreateSelectionHandler()\r\n        => new RubyTagSelectionHandler();\r\n\r\n    protected override SelectionBlueprint<RubyTag> CreateBlueprintFor(RubyTag item)\r\n        => new RubyTagSelectionBlueprint(item);\r\n\r\n    protected override IEnumerable<SelectionBlueprint<RubyTag>> SortForMovement(IReadOnlyList<SelectionBlueprint<RubyTag>> blueprints)\r\n        => blueprints.OrderBy(b => b.Item.StartIndex);\r\n\r\n    protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint<RubyTag> blueprint, Vector2[] originalSnapPositions)> blueprints)\r\n        => false;\r\n\r\n    protected partial class RubyTagSelectionHandler : LyricPropertySelectionHandler<IEditRubyModeState>\r\n    {\r\n        [Resolved]\r\n        private ILyricRubyTagsChangeHandler rubyTagsChangeHandler { get; set; } = null!;\r\n\r\n        [Resolved]\r\n        private IPreviewLyricPositionProvider previewLyricPositionProvider { get; set; } = null!;\r\n\r\n        // need to implement this because blueprint container support change the x scale.\r\n        public override SelectionScaleHandler CreateScaleHandler()\r\n            => new RubyTagSelectionScaleHandler();\r\n\r\n        #region User Input Handling\r\n\r\n        // for now we always allow movement. snapping is provided by the Timeline's \"distance\" snap implementation\r\n        public override bool HandleMovement(MoveSelectionEvent<RubyTag> moveEvent)\r\n        {\r\n            if (!SelectedItems.Any())\r\n                throw new InvalidOperationException(\"Should have at least one selected item.\");\r\n\r\n            float deltaXPosition = moveEvent.ScreenSpaceDelta.X;\r\n            Logger.LogPrint($\"position: {deltaXPosition}\", LoggingTarget.Information);\r\n\r\n            if (deltaXPosition < 0)\r\n            {\r\n                var firstTimeTag = SelectedItems.MinBy(x => x.StartIndex) ?? throw new InvalidOperationException();\r\n                int? newStartIndex = calculateNewIndex(firstTimeTag, deltaXPosition, Anchor.CentreLeft);\r\n                int? offset = newStartIndex - firstTimeTag.StartIndex;\r\n                if (offset is null or 0)\r\n                    return false;\r\n\r\n                setRubyTagShifting(SelectedItems, -1);\r\n            }\r\n            else\r\n            {\r\n                var lastTimeTag = SelectedItems.MaxBy(x => x.EndIndex) ?? throw new InvalidOperationException();\r\n                int? newEndIndex = calculateNewIndex(lastTimeTag, deltaXPosition, Anchor.CentreRight);\r\n                int? offset = newEndIndex - lastTimeTag.EndIndex;\r\n                if (offset is null or 0)\r\n                    return false;\r\n\r\n                setRubyTagShifting(SelectedItems, 1);\r\n            }\r\n\r\n            return true;\r\n\r\n            void setRubyTagShifting(IEnumerable<RubyTag> rubyTags, int offset)\r\n                => rubyTagsChangeHandler.ShiftingIndex(rubyTags, offset);\r\n        }\r\n\r\n        private int? calculateNewIndex(RubyTag rubyTag, float offset, Anchor anchor)\r\n        {\r\n            // get real left-side and right-side position\r\n            var rect = previewLyricPositionProvider.GetRubyTagByPosition(rubyTag);\r\n\r\n            // todo: need to think about how to handle the case if the text-tag already out of the range of the text.\r\n            if (rect == null)\r\n                throw new InvalidOperationException($\"{nameof(rubyTag)} not in the range of the text.\");\r\n\r\n            switch (anchor)\r\n            {\r\n                case Anchor.CentreLeft:\r\n                    var leftPosition = rect.Value.BottomLeft + new Vector2(offset, 0);\r\n                    return previewLyricPositionProvider.GetCharIndexByPosition(leftPosition);\r\n\r\n                case Anchor.CentreRight:\r\n                    var rightPosition = rect.Value.BottomRight + new Vector2(offset, 0);\r\n                    return previewLyricPositionProvider.GetCharIndexByPosition(rightPosition);\r\n\r\n                default:\r\n                    throw new ArgumentOutOfRangeException(nameof(anchor));\r\n            }\r\n        }\r\n\r\n        #endregion\r\n\r\n        protected override void DeleteItems(IEnumerable<RubyTag> items)\r\n            => rubyTagsChangeHandler.RemoveRange(items);\r\n    }\r\n\r\n    private partial class RubyTagSelectionScaleHandler : SelectionScaleHandler\r\n    {\r\n        [Resolved]\r\n        private IPreviewLyricPositionProvider previewLyricPositionProvider { get; set; } = null!;\r\n\r\n        [Resolved]\r\n        private ILyricRubyTagsChangeHandler rubyTagsChangeHandler { get; set; } = null!;\r\n\r\n        private BindableList<RubyTag> selectedItems { get; } = new();\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(IEditRubyModeState editRubyModeState)\r\n        {\r\n            selectedItems.BindTo(editRubyModeState.SelectedItems);\r\n        }\r\n\r\n        protected override void LoadComplete()\r\n        {\r\n            base.LoadComplete();\r\n\r\n            selectedItems.CollectionChanged += (_, __) => updateState();\r\n            updateState();\r\n        }\r\n\r\n        private void updateState()\r\n        {\r\n            // only select one ruby tag can let user drag to change start and end index.\r\n            CanScaleX.Value = selectedItems.Count == 1;\r\n        }\r\n\r\n        public override void Begin()\r\n        {\r\n            base.Begin();\r\n\r\n            var selectedItemsRect = selectedItems\r\n                                    .Select(x => previewLyricPositionProvider.GetRubyTagByPosition(x))\r\n                                    .Where(rect => rect != null)\r\n                                    .OfType<RectangleF>();\r\n\r\n            var rect = selectedItemsRect.Aggregate(new RectangleF(), RectangleF.Union);\r\n\r\n            // Update the quad\r\n            OriginalSurroundingQuad = Quad.FromRectangle(rect);\r\n        }\r\n\r\n        public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both, float axisRotation = 0)\r\n        {\r\n            // this feature only works if only select one ruby tag.\r\n            var selectedRubyTag = selectedItems.FirstOrDefault();\r\n            if (selectedRubyTag == null)\r\n                return;\r\n\r\n            if (adjustAxis != Axes.X)\r\n                throw new InvalidOperationException(\"Only can adjust x axes\");\r\n\r\n            if (origin == null || OriginalSurroundingQuad == null)\r\n                return;\r\n\r\n            float offset = OriginalSurroundingQuad.Value.Width * (scale.X - 1);\r\n\r\n            if (origin.Value.X > OriginalSurroundingQuad.Value.Centre.X)\r\n            {\r\n                int? newStartIndex = calculateNewIndex(selectedRubyTag, -offset, Anchor.CentreLeft);\r\n                if (newStartIndex == null || !RubyTagUtils.ValidNewStartIndex(selectedRubyTag, newStartIndex.Value))\r\n                    return;\r\n\r\n                setRubyTagIndex(selectedRubyTag, newStartIndex, null);\r\n            }\r\n            else\r\n            {\r\n                int? newEndIndex = calculateNewIndex(selectedRubyTag, offset, Anchor.CentreRight);\r\n                if (newEndIndex == null || !RubyTagUtils.ValidNewEndIndex(selectedRubyTag, newEndIndex.Value))\r\n                    return;\r\n\r\n                setRubyTagIndex(selectedRubyTag, null, newEndIndex);\r\n            }\r\n\r\n            return;\r\n\r\n            void setRubyTagIndex(RubyTag rubyTag, int? startPosition, int? endPosition)\r\n                => rubyTagsChangeHandler.SetIndex(rubyTag, startPosition, endPosition);\r\n        }\r\n\r\n        public override void Commit()\r\n        {\r\n            base.Commit();\r\n\r\n            OriginalSurroundingQuad = null;\r\n        }\r\n\r\n        private int? calculateNewIndex(RubyTag rubyTag, float offset, Anchor anchor)\r\n        {\r\n            // get real left-side and right-side position\r\n            var rect = previewLyricPositionProvider.GetRubyTagByPosition(rubyTag);\r\n\r\n            // todo: need to think about how to handle the case if the text-tag already out of the range of the text.\r\n            if (rect == null)\r\n                throw new InvalidOperationException($\"{nameof(rubyTag)} not in the range of the text.\");\r\n\r\n            switch (anchor)\r\n            {\r\n                case Anchor.CentreLeft:\r\n                    var leftPosition = rect.Value.BottomLeft + new Vector2(offset, 0);\r\n                    return previewLyricPositionProvider.GetCharIndexByPosition(leftPosition);\r\n\r\n                case Anchor.CentreRight:\r\n                    var rightPosition = rect.Value.BottomRight + new Vector2(offset, 0);\r\n                    return previewLyricPositionProvider.GetCharIndexByPosition(rightPosition);\r\n\r\n                default:\r\n                    throw new ArgumentOutOfRangeException(nameof(anchor));\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Components/Lyrics/Blueprints/RubyTagSelectionBlueprint.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing JetBrains.Annotations;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Extensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Cursor;\r\nusing osu.Framework.Graphics.Primitives;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.UserInterfaceV2;\r\nusing osu.Game.Rulesets.Edit;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\nusing osuTK;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Components.Lyrics.Blueprints;\r\n\r\npublic partial class RubyTagSelectionBlueprint : SelectionBlueprint<RubyTag>, IHasPopover\r\n{\r\n    [Resolved]\r\n    private IEditRubyModeState editRubyModeState { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private IPreviewLyricPositionProvider previewLyricPositionProvider { get; set; } = null!;\r\n\r\n    [UsedImplicitly]\r\n    private readonly Bindable<string> text;\r\n\r\n    [UsedImplicitly]\r\n    private readonly BindableNumber<int> startIndex;\r\n\r\n    [UsedImplicitly]\r\n    private readonly BindableNumber<int> endIndex;\r\n\r\n    private readonly Container previewTextArea;\r\n    private readonly Container indexRangeBackground;\r\n\r\n    public RubyTagSelectionBlueprint(RubyTag item)\r\n        : base(item)\r\n    {\r\n        // Instead of adding the margin to the popover, use this way to make the popover not block the lyric text.\r\n        RelativeSizeAxes = Axes.Y;\r\n        AutoSizeAxes = Axes.X;\r\n\r\n        text = item.TextBindable.GetBoundCopy();\r\n        startIndex = item.StartIndexBindable.GetBoundCopy();\r\n        endIndex = item.EndIndexBindable.GetBoundCopy();\r\n\r\n        InternalChildren = new[]\r\n        {\r\n            previewTextArea = new Container\r\n            {\r\n                Alpha = 0,\r\n            },\r\n            indexRangeBackground = new Container\r\n            {\r\n                Masking = true,\r\n                BorderThickness = 3,\r\n                Alpha = 0,\r\n                BorderColour = Color4.White,\r\n                Children = new Drawable[]\r\n                {\r\n                    new Box\r\n                    {\r\n                        RelativeSizeAxes = Axes.Both,\r\n                        Alpha = 0f,\r\n                        AlwaysPresent = true,\r\n                    },\r\n                },\r\n            },\r\n        };\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OsuColour colours)\r\n    {\r\n        indexRangeBackground.Colour = colours.Pink;\r\n\r\n        UpdatePositionAndSize();\r\n        text.BindValueChanged(_ => UpdatePositionAndSize());\r\n        startIndex.BindValueChanged(_ => UpdatePositionAndSize());\r\n        endIndex.BindValueChanged(_ => UpdatePositionAndSize());\r\n    }\r\n\r\n    protected void UpdatePositionAndSize()\r\n    {\r\n        // wait until lyric update ruby position.\r\n        ScheduleAfterChildren(() =>\r\n        {\r\n            var rubyTagRect = previewLyricPositionProvider.GetRubyTagByPosition(Item);\r\n\r\n            if (rubyTagRect == null)\r\n            {\r\n                return;\r\n            }\r\n\r\n            var startRect = previewLyricPositionProvider.GetRectByCharIndex(Item.StartIndex);\r\n            var endRect = previewLyricPositionProvider.GetRectByCharIndex(Item.EndIndex);\r\n\r\n            // update select position\r\n            updateDrawableRect(previewTextArea, rubyTagRect.Value);\r\n\r\n            // update index range position.\r\n            var indexRangePosition = new Vector2(startRect.Left, rubyTagRect.Value.Y);\r\n            var indexRangeSize = new Vector2(endRect.Right - startRect.Left, rubyTagRect.Value.Height);\r\n            updateDrawableRect(indexRangeBackground, new RectangleF(indexRangePosition, indexRangeSize));\r\n        });\r\n\r\n        static void updateDrawableRect(Drawable target, RectangleF rect)\r\n        {\r\n            target.X = rect.X;\r\n            target.Y = rect.Y;\r\n            target.Width = rect.Width;\r\n            target.Height = rect.Height;\r\n        }\r\n    }\r\n\r\n    public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)\r\n        => previewTextArea.ReceivePositionalInputAt(screenSpacePos);\r\n\r\n    public override Vector2 ScreenSpaceSelectionPoint => previewTextArea.ScreenSpaceDrawQuad.Centre;\r\n\r\n    public override Quad SelectionQuad => previewTextArea.ScreenSpaceDrawQuad;\r\n\r\n    protected override void OnSelected()\r\n    {\r\n        indexRangeBackground.FadeIn(500);\r\n    }\r\n\r\n    protected override void OnDeselected()\r\n    {\r\n        indexRangeBackground.FadeOut(500);\r\n    }\r\n\r\n    public Popover GetPopover() => new RubyEditPopover(Item);\r\n\r\n    protected override bool OnClick(ClickEvent e)\r\n    {\r\n        Schedule(() =>\r\n        {\r\n            // should select the current item after popover opened, or other popover closed.\r\n            editRubyModeState.Select(Item);\r\n        });\r\n\r\n        this.ShowPopover();\r\n        return base.OnClick(e);\r\n    }\r\n\r\n    private partial class RubyEditPopover : OsuPopover\r\n    {\r\n        [Resolved]\r\n        private ILyricRubyTagsChangeHandler lyricRubyTagsChangeHandler { get; set; } = null!;\r\n\r\n        private readonly LabelledTextBox labelledRubyTextBox;\r\n\r\n        public RubyEditPopover(RubyTag rubyTag)\r\n        {\r\n            AllowableAnchors = new[] { Anchor.TopCentre, Anchor.BottomCentre };\r\n            Child = new FillFlowContainer\r\n            {\r\n                Width = 200,\r\n                Direction = FillDirection.Vertical,\r\n                AutoSizeAxes = Axes.Y,\r\n                Spacing = new Vector2(0, 10),\r\n                Children = new Drawable[]\r\n                {\r\n                    labelledRubyTextBox = new LabelledTextBox\r\n                    {\r\n                        Label = \"Ruby\",\r\n                        Current =\r\n                        {\r\n                            Value = rubyTag.Text,\r\n                        },\r\n                    },\r\n                    new DeleteRubyButton\r\n                    {\r\n                        Text = \"Delete\",\r\n                        Action = deleteRubyText,\r\n                    },\r\n                },\r\n            };\r\n\r\n            labelledRubyTextBox.OnCommit += (_, newText) =>\r\n            {\r\n                if (newText)\r\n                {\r\n                    editRubyText();\r\n                }\r\n            };\r\n\r\n            return;\r\n\r\n            void editRubyText()\r\n            {\r\n                lyricRubyTagsChangeHandler.SetText(rubyTag, labelledRubyTextBox.Text);\r\n                this.HidePopover();\r\n            }\r\n\r\n            void deleteRubyText()\r\n            {\r\n                lyricRubyTagsChangeHandler.Remove(rubyTag);\r\n                this.HidePopover();\r\n            }\r\n        }\r\n\r\n        protected override void LoadComplete()\r\n        {\r\n            base.LoadComplete();\r\n            ScheduleAfterChildren(() => GetContainingFocusManager().ChangeFocus(labelledRubyTextBox));\r\n        }\r\n\r\n        private partial class DeleteRubyButton : EditorSectionButton\r\n        {\r\n            [BackgroundDependencyLoader]\r\n            private void load(OsuColour colours)\r\n            {\r\n                BackgroundColour = colours.Pink3;\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Components/Lyrics/Blueprints/TimeTagBlueprintContainer.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Game.Rulesets.Edit;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\nusing osu.Game.Screens.Edit.Compose.Components;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Components.Lyrics.Blueprints;\r\n\r\npublic partial class TimeTagBlueprintContainer : LyricPropertyBlueprintContainer<TimeTag>\r\n{\r\n    public TimeTagBlueprintContainer(Lyric lyric)\r\n        : base(lyric)\r\n    {\r\n    }\r\n\r\n    protected override BindableList<TimeTag> GetProperties(Lyric lyric)\r\n        => lyric.TimeTagsBindable.GetBoundCopy();\r\n\r\n    protected override SelectionHandler<TimeTag> CreateSelectionHandler()\r\n        => new TimeTagSelectionHandler();\r\n\r\n    protected override SelectionBlueprint<TimeTag> CreateBlueprintFor(TimeTag item)\r\n        => new TimeTagSelectionBlueprint(item);\r\n\r\n    protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint<TimeTag> blueprint, Vector2[] originalSnapPositions)> blueprints)\r\n        => false;\r\n\r\n    protected partial class TimeTagSelectionHandler : LyricPropertySelectionHandler<IEditTimeTagModeState>\r\n    {\r\n        [Resolved]\r\n        private ILyricTimeTagsChangeHandler lyricTimeTagsChangeHandler { get; set; } = null!;\r\n\r\n        // for now we always allow movement. snapping is provided by the Timeline's \"distance\" snap implementation\r\n        public override bool HandleMovement(MoveSelectionEvent<TimeTag> moveEvent) => true;\r\n\r\n        protected override void DeleteItems(IEnumerable<TimeTag> items)\r\n        {\r\n            lyricTimeTagsChangeHandler.RemoveRange(items);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Components/Lyrics/Blueprints/TimeTagSelectionBlueprint.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Rulesets.Edit;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Components.Lyrics.Blueprints;\r\n\r\npublic partial class TimeTagSelectionBlueprint : SelectionBlueprint<TimeTag>\r\n{\r\n    private const float time_tag_size = 10;\r\n\r\n    [Resolved]\r\n    private IPreviewLyricPositionProvider previewLyricPositionProvider { get; set; } = null!;\r\n\r\n    public TimeTagSelectionBlueprint(TimeTag item)\r\n        : base(item)\r\n    {\r\n        RelativeSizeAxes = Axes.None;\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        updatePosition();\r\n    }\r\n\r\n    private void updatePosition()\r\n    {\r\n        var size = new Vector2(time_tag_size);\r\n        var position = previewLyricPositionProvider.GetPositionByTimeTag(Item) - size / 2;\r\n\r\n        X = position.X;\r\n        Y = position.Y;\r\n        Width = time_tag_size;\r\n        Height = time_tag_size;\r\n    }\r\n\r\n    public override Vector2 ScreenSpaceSelectionPoint => ScreenSpaceDrawQuad.TopLeft;\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Components/Lyrics/CaretLayer.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Extensions.IEnumerableExtensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition.Algorithms;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Components.Lyrics.Carets;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Components.Lyrics;\r\n\r\npublic partial class CaretLayer : Layer\r\n{\r\n    private readonly IBindable<ICaretPositionAlgorithm?> bindableCaretPositionAlgorithm = new Bindable<ICaretPositionAlgorithm?>();\r\n\r\n    private readonly IBindable<ICaretPosition?> bindableHoverCaretPosition = new Bindable<ICaretPosition?>();\r\n    private readonly IBindable<ICaretPosition?> bindableCaretPosition = new Bindable<ICaretPosition?>();\r\n    private readonly IBindable<RangeCaretPosition?> bindableRangeCaretPosition = new Bindable<RangeCaretPosition?>();\r\n\r\n    public CaretLayer(Lyric lyric)\r\n        : base(lyric)\r\n    {\r\n        bindableCaretPositionAlgorithm.BindValueChanged(e =>\r\n        {\r\n            updateDrawableCaret(e.NewValue, DrawableCaretState.Idle);\r\n            updateDrawableCaret(e.NewValue, DrawableCaretState.Hover);\r\n        }, true);\r\n\r\n        bindableHoverCaretPosition.BindValueChanged(e =>\r\n        {\r\n            applyTheCaretPosition(e.NewValue, DrawableCaretState.Hover);\r\n        }, true);\r\n\r\n        bindableCaretPosition.BindValueChanged(e =>\r\n        {\r\n            applyTheCaretPosition(e.NewValue, DrawableCaretState.Idle);\r\n        }, true);\r\n\r\n        bindableRangeCaretPosition.BindValueChanged(e =>\r\n        {\r\n            applyRangeCaretPosition(e.NewValue, DrawableCaretState.Idle);\r\n        }, true);\r\n    }\r\n\r\n    private void updateDrawableCaret(ICaretPositionAlgorithm? algorithm, DrawableCaretState state)\r\n    {\r\n        var oldCaret = getDrawableCaret(state);\r\n        if (oldCaret != null)\r\n            RemoveInternal(oldCaret, true);\r\n\r\n        var caret = createCaret(algorithm, state);\r\n        if (caret == null)\r\n            return;\r\n\r\n        caret.Hide();\r\n\r\n        AddInternal(caret);\r\n\r\n        static DrawableCaret? createCaret(ICaretPositionAlgorithm? algorithm, DrawableCaretState state) =>\r\n            algorithm?.GetCaretPositionType() switch\r\n            {\r\n                Type t when t == typeof(CreateRubyTagCaretPosition) => new DrawableCreateRubyTagCaret(state),\r\n                Type t when t == typeof(CuttingCaretPosition) => new DrawableCuttingCaret(state),\r\n                Type t when t == typeof(RecordingTimeTagCaretPosition) => new DrawableRecordingTimeTagCaret(state),\r\n                Type t when t == typeof(CreateRemoveTimeTagCaretPosition) => new DrawableCreateRemoveTimeTagCaret(state),\r\n                Type t when t == typeof(TypingCaretPosition) => new DrawableTypingCaret(state),\r\n                _ => null,\r\n            };\r\n    }\r\n\r\n    private void applyTheCaretPosition(ICaretPosition? position, DrawableCaretState state)\r\n    {\r\n        if (position == null)\r\n            return;\r\n\r\n        var caret = getDrawableCaret(state);\r\n        if (caret == null)\r\n            return;\r\n\r\n        if (position.Lyric != Lyric)\r\n        {\r\n            caret.Hide();\r\n            return;\r\n        }\r\n\r\n        caret.Show();\r\n        caret.ApplyCaretPosition(position);\r\n    }\r\n\r\n    private void applyRangeCaretPosition(RangeCaretPosition? rangeCaretPosition, DrawableCaretState state)\r\n    {\r\n        if (rangeCaretPosition == null)\r\n            return;\r\n\r\n        var caret = getDrawableCaret(state);\r\n        if (caret == null)\r\n            throw new InvalidOperationException(\"Should be able to get the drawable caret.\");\r\n\r\n        if (rangeCaretPosition.Start.Lyric != Lyric || rangeCaretPosition.End.Lyric != Lyric)\r\n        {\r\n            caret.Hide();\r\n            return;\r\n        }\r\n\r\n        caret.Show();\r\n\r\n        if (caret is not ICanAcceptRangeIndex rangeIndexDrawableCaret)\r\n            throw new InvalidOperationException(\"Caret should be able to accept range index.\");\r\n\r\n        rangeIndexDrawableCaret.ApplyRangeCaretPosition(rangeCaretPosition);\r\n    }\r\n\r\n    private DrawableCaret? getDrawableCaret(DrawableCaretState state)\r\n        => InternalChildren.OfType<DrawableCaret>().FirstOrDefault(x => x.State == state);\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(ILyricCaretState lyricCaretState)\r\n    {\r\n        bindableCaretPositionAlgorithm.BindTo(lyricCaretState.BindableCaretPositionAlgorithm);\r\n\r\n        bindableHoverCaretPosition.BindTo(lyricCaretState.BindableHoverCaretPosition);\r\n        bindableCaretPosition.BindTo(lyricCaretState.BindableCaretPosition);\r\n        bindableRangeCaretPosition.BindTo(lyricCaretState.BindableRangeCaretPosition);\r\n    }\r\n\r\n    public override void UpdateDisableEditState(bool editable)\r\n    {\r\n        this.FadeTo(editable ? 1 : 0.7f, 100);\r\n    }\r\n\r\n    public override void TriggerDisallowEditEffect(LyricEditorMode editorMode)\r\n    {\r\n        InternalChildren.OfType<DrawableCaret>().ForEach(x => x.TriggerDisallowEditEffect());\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Components/Lyrics/Carets/DrawableCaret.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Components.Lyrics.Carets;\r\n\r\npublic abstract partial class DrawableCaret<TCaretPosition> : DrawableCaret\r\n    where TCaretPosition : struct, ICaretPosition\r\n{\r\n    protected DrawableCaret(DrawableCaretState state)\r\n        : base(state)\r\n    {\r\n    }\r\n\r\n    public sealed override void ApplyCaretPosition(ICaretPosition caret)\r\n    {\r\n        if (caret is not TCaretPosition tCaret)\r\n            throw new InvalidCastException();\r\n\r\n        ApplyCaretPosition(tCaret);\r\n    }\r\n\r\n    protected abstract void ApplyCaretPosition(TCaretPosition caret);\r\n}\r\n\r\npublic abstract partial class DrawableCaret : CompositeDrawable\r\n{\r\n    [Resolved]\r\n    protected OsuColour Colours { get; private set; } = null!;\r\n\r\n    [Resolved]\r\n    protected IPreviewLyricPositionProvider LyricPositionProvider { get; private set; } = null!;\r\n\r\n    public readonly DrawableCaretState State;\r\n\r\n    protected DrawableCaret(DrawableCaretState state)\r\n    {\r\n        State = state;\r\n    }\r\n\r\n    protected static float GetAlpha(DrawableCaretState state) =>\r\n        state switch\r\n        {\r\n            DrawableCaretState.Idle => 1,\r\n            DrawableCaretState.Hover => 0.5f,\r\n            _ => throw new ArgumentOutOfRangeException(nameof(state), state, null),\r\n        };\r\n\r\n    public abstract void ApplyCaretPosition(ICaretPosition caret);\r\n\r\n    public void TriggerDisallowEditEffect()\r\n    {\r\n        TriggerDisallowEditEffect(Colours);\r\n    }\r\n\r\n    protected abstract void TriggerDisallowEditEffect(OsuColour colour);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Components/Lyrics/Carets/DrawableCaretState.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Components.Lyrics.Carets;\r\n\r\npublic enum DrawableCaretState\r\n{\r\n    Idle,\r\n\r\n    Hover,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Components/Lyrics/Carets/DrawableCreateRemoveTimeTagCaret.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Cursor;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Components.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Components.Lyrics.Carets;\r\n\r\npublic partial class DrawableCreateRemoveTimeTagCaret : DrawableCaret<CreateRemoveTimeTagCaretPosition>\r\n{\r\n    private const float border_spacing = 5;\r\n\r\n    private readonly TimeTagsInfo? startTimeTagInfo;\r\n    private readonly TimeTagsInfo? endTimeTagInfo;\r\n\r\n    public DrawableCreateRemoveTimeTagCaret(DrawableCaretState state)\r\n        : base(state)\r\n    {\r\n        // todo: should re-design the drawable caret.\r\n        InternalChildren = new Drawable[]\r\n        {\r\n            new Container\r\n            {\r\n                Masking = true,\r\n                BorderThickness = border_spacing,\r\n                BorderColour = Colour4.White,\r\n                RelativeSizeAxes = Axes.Both,\r\n                Alpha = GetAlpha(state),\r\n                Child = new Box\r\n                {\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    Colour = Colour4.White,\r\n                    Alpha = 0.1f,\r\n                },\r\n            },\r\n        };\r\n\r\n        if (state != DrawableCaretState.Idle)\r\n            return;\r\n\r\n        AddRangeInternal(new[]\r\n        {\r\n            startTimeTagInfo = new TimeTagsInfo(TextIndex.IndexState.Start)\r\n            {\r\n                X = 18,\r\n                Anchor = Anchor.BottomLeft,\r\n                Origin = Anchor.CentreRight,\r\n                Alpha = GetAlpha(state),\r\n            },\r\n            endTimeTagInfo = new TimeTagsInfo(TextIndex.IndexState.End)\r\n            {\r\n                X = -18,\r\n                Anchor = Anchor.BottomRight,\r\n                Origin = Anchor.CentreLeft,\r\n                Alpha = GetAlpha(state),\r\n            },\r\n        });\r\n    }\r\n\r\n    protected override void ApplyCaretPosition(CreateRemoveTimeTagCaretPosition caret)\r\n    {\r\n        var rect = LyricPositionProvider.GetRectByCharIndex(caret.CharIndex);\r\n\r\n        Position = rect.TopLeft - new Vector2(border_spacing);\r\n        Size = rect.Size + new Vector2(border_spacing * 2);\r\n\r\n        startTimeTagInfo?.UpdateCaretPosition(caret);\r\n        endTimeTagInfo?.UpdateCaretPosition(caret);\r\n    }\r\n\r\n    protected override void TriggerDisallowEditEffect(OsuColour colour)\r\n    {\r\n        this.FlashColour(colour.Red, 200);\r\n    }\r\n\r\n    /// <summary>\r\n    /// List of time-tags info.\r\n    /// Provide the button for able to create and remove the time-tag.\r\n    /// </summary>\r\n    private partial class TimeTagsInfo : CompositeDrawable\r\n    {\r\n        private const int border_radius = 5;\r\n\r\n        private Container createButtonArea = null!;\r\n        private FillFlowContainer contents = null!;\r\n        private FillFlowContainer<TimeTagVisualization> drawableTimeTags = null!;\r\n\r\n        private readonly TextIndex.IndexState indexState;\r\n        private readonly IBindableList<TimeTag> bindableTimeTags = new BindableList<TimeTag>();\r\n\r\n        public TimeTagsInfo(TextIndex.IndexState indexState)\r\n        {\r\n            this.indexState = indexState;\r\n\r\n            Masking = true;\r\n            CornerRadius = border_radius;\r\n\r\n            AutoSizeAxes = Axes.Both;\r\n        }\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(LyricEditorColourProvider colourProvider,\r\n                          ILyricEditorState state,\r\n                          ILyricTimeTagsChangeHandler lyricTimeTagsChangeHandler,\r\n                          IEditTimeTagModeState editTimeTagModeState)\r\n        {\r\n            InternalChildren = new Drawable[]\r\n            {\r\n                new Box\r\n                {\r\n                    Name = \"Background\",\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    Colour = colourProvider.Background2(state.Mode),\r\n                },\r\n                new Container\r\n                {\r\n                    Name = \"Background for exist time-tags\",\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    Padding = new MarginPadding\r\n                    {\r\n                        Horizontal = 3,\r\n                        Vertical = 5,\r\n                    },\r\n                    Child = new Container\r\n                    {\r\n                        Masking = true,\r\n                        CornerRadius = border_radius,\r\n                        RelativeSizeAxes = Axes.Both,\r\n                        Child = new Box\r\n                        {\r\n                            RelativeSizeAxes = Axes.Both,\r\n                            Colour = colourProvider.Background3(state.Mode),\r\n                        },\r\n                    },\r\n                },\r\n                contents = new FillFlowContainer\r\n                {\r\n                    AutoSizeAxes = Axes.Both,\r\n                    Margin = new MarginPadding(3),\r\n                    Direction = FillDirection.Horizontal,\r\n                    Children = new Drawable[]\r\n                    {\r\n                        createButtonArea = new Container\r\n                        {\r\n                            AutoSizeAxes = Axes.Both,\r\n                            Masking = true,\r\n                            CornerRadius = border_radius,\r\n                            Children = new Drawable[]\r\n                            {\r\n                                new Box\r\n                                {\r\n                                    RelativeSizeAxes = Axes.Both,\r\n                                    Colour = colourProvider.Background5(state.Mode),\r\n                                },\r\n                                new IconButton\r\n                                {\r\n                                    Icon = FontAwesome.Solid.Plus,\r\n                                    Anchor = Anchor.Centre,\r\n                                    Origin = Anchor.Centre,\r\n                                    Margin = new MarginPadding(5),\r\n                                    Size = new Vector2(15),\r\n                                    Action = () =>\r\n                                    {\r\n                                        if (previousCaret == null)\r\n                                            throw new InvalidOperationException();\r\n\r\n                                        var textIndex = new TextIndex(previousCaret.Value.CharIndex, indexState);\r\n                                        lyricTimeTagsChangeHandler.AddByPosition(textIndex);\r\n                                        editTimeTagModeState.BindableCreateType.Value = CreateTimeTagType.Mouse;\r\n                                    },\r\n                                },\r\n                            },\r\n                        },\r\n                        drawableTimeTags = new FillFlowContainer<TimeTagVisualization>\r\n                        {\r\n                            Anchor = Anchor.CentreLeft,\r\n                            Origin = Anchor.CentreLeft,\r\n                            AutoSizeAxes = Axes.Both,\r\n                            Spacing = new Vector2(5),\r\n                            Direction = FillDirection.Horizontal,\r\n                        },\r\n                    },\r\n                },\r\n            };\r\n\r\n            // update the create button by state.\r\n            float position = TextIndexUtils.GetValueByState(indexState, float.MaxValue, float.MinValue);\r\n            contents.SetLayoutPosition(createButtonArea, position);\r\n\r\n            // should update the time-tag visualization if the time-tags are changed.\r\n            bindableTimeTags.BindCollectionChanged((_, _) =>\r\n            {\r\n                redrawTimeTags();\r\n            });\r\n        }\r\n\r\n        private CreateRemoveTimeTagCaretPosition? previousCaret;\r\n\r\n        public void UpdateCaretPosition(CreateRemoveTimeTagCaretPosition caret)\r\n        {\r\n            if (previousCaret?.Lyric != caret.Lyric)\r\n            {\r\n                // should wait until previous caret is updated.\r\n                Schedule(() =>\r\n                {\r\n                    bindableTimeTags.UnbindBindings();\r\n                    bindableTimeTags.BindTo(caret.Lyric.TimeTagsBindable);\r\n                });\r\n            }\r\n\r\n            previousCaret = caret;\r\n            redrawTimeTags();\r\n        }\r\n\r\n        private void redrawTimeTags()\r\n        {\r\n            if (previousCaret == null)\r\n                throw new InvalidOperationException();\r\n\r\n            var timeTags = previousCaret.Value.GetTimeTagsWithState(indexState);\r\n\r\n            drawableTimeTags.Clear();\r\n\r\n            for (int i = 0; i < timeTags.Length; i++)\r\n            {\r\n                bool isFirst = i == 0;\r\n                bool isLast = i == timeTags.Length - 1;\r\n\r\n                drawableTimeTags.Add(new TimeTagVisualization(timeTags[i])\r\n                {\r\n                    Anchor = Anchor.Centre,\r\n                    Origin = Anchor.Centre,\r\n                    Size = new Vector2(12),\r\n                    Margin = new MarginPadding(5)\r\n                    {\r\n                        Left = isFirst ? 5 : 0,\r\n                        Right = isLast ? 5 : 0,\r\n                    },\r\n                    Alpha = 0.5f,\r\n                });\r\n            }\r\n        }\r\n\r\n        /// <summary>\r\n        /// Exist time-tag visualization.\r\n        /// Easy to delete if click on it.\r\n        /// </summary>\r\n        private partial class TimeTagVisualization : CompositeDrawable, IHasTooltip\r\n        {\r\n            [Resolved]\r\n            private ILyricTimeTagsChangeHandler lyricTimeTagsChangeHandler { get; set; } = null!;\r\n\r\n            [Resolved]\r\n            private IEditTimeTagModeState editTimeTagModeState { get; set; } = null!;\r\n\r\n            private readonly TimeTag timeTag;\r\n\r\n            public TimeTagVisualization(TimeTag timeTag)\r\n            {\r\n                this.timeTag = timeTag;\r\n\r\n                InternalChildren = new Drawable[]\r\n                {\r\n                    new DrawableTimeTag\r\n                    {\r\n                        Name = \"Time tag triangle\",\r\n                        Anchor = Anchor.Centre,\r\n                        Origin = Anchor.Centre,\r\n                        RelativeSizeAxes = Axes.Both,\r\n                        TimeTag = timeTag,\r\n                    },\r\n                    // todo: should have delete icon in here?\r\n                };\r\n            }\r\n\r\n            protected override bool OnClick(ClickEvent e)\r\n            {\r\n                lyricTimeTagsChangeHandler.Remove(timeTag);\r\n                editTimeTagModeState.BindableCreateType.Value = CreateTimeTagType.Mouse;\r\n\r\n                return true;\r\n            }\r\n\r\n            public LocalisableString TooltipText => \"Click to remove the time-tag.\";\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Components/Lyrics/Carets/DrawableCreateRubyTagCaret.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Extensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Cursor;\r\nusing osu.Framework.Graphics.Primitives;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Graphics.UserInterfaceV2;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Components.Lyrics.Carets;\r\n\r\npublic partial class DrawableCreateRubyTagCaret : DrawableRangeCaret<CreateRubyTagCaretPosition>, IHasPopover\r\n{\r\n    private const float border_spacing = 5;\r\n    private const float caret_move_time = 60;\r\n    private const float caret_resize_time = 60;\r\n\r\n    private readonly SpriteIcon icon;\r\n\r\n    public DrawableCreateRubyTagCaret(DrawableCaretState state)\r\n        : base(state)\r\n    {\r\n        InternalChildren = new Drawable[]\r\n        {\r\n            new Container\r\n            {\r\n                Masking = true,\r\n                BorderThickness = border_spacing,\r\n                BorderColour = Colour4.White,\r\n                RelativeSizeAxes = Axes.Both,\r\n                Alpha = GetAlpha(state),\r\n                Child = new Box\r\n                {\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    Colour = Colour4.White,\r\n                    Alpha = 0.1f,\r\n                },\r\n            },\r\n            icon = new SpriteIcon\r\n            {\r\n                Anchor = Anchor.TopRight,\r\n                Origin = Anchor.BottomLeft,\r\n                Icon = FontAwesome.Solid.PlusCircle,\r\n                Size = new Vector2(15),\r\n                Alpha = GetAlpha(state),\r\n            },\r\n        };\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OsuColour colours)\r\n    {\r\n        icon.Colour = colours.Green;\r\n    }\r\n\r\n    private int startCharIndex;\r\n    private int endCharIndex;\r\n\r\n    protected override void ApplyCaretPosition(CreateRubyTagCaretPosition caret)\r\n    {\r\n        startCharIndex = caret.CharIndex;\r\n        endCharIndex = caret.CharIndex;\r\n\r\n        var rect = LyricPositionProvider.GetRectByCharIndex(caret.CharIndex);\r\n        changeTheSizeByRect(rect);\r\n\r\n        // should not continuous showing the caret position if move the caret by keyboard.\r\n        if (State == DrawableCaretState.Idle)\r\n        {\r\n            // todo: should wait until layer is attached to the parent.\r\n            // use quick way to fix this because it will cause crash if open the\r\n            Schedule(this.HidePopover);\r\n        }\r\n    }\r\n\r\n    protected override bool OnClick(ClickEvent e)\r\n    {\r\n        if (State == DrawableCaretState.Hover)\r\n            return false;\r\n\r\n        this.ShowPopover();\r\n        return true;\r\n    }\r\n\r\n    protected override void ApplyRangeCaretPosition(RangeCaretPosition<CreateRubyTagCaretPosition> caret)\r\n    {\r\n        startCharIndex = caret.GetRangeCaretPosition().Item1.CharIndex;\r\n        endCharIndex = caret.GetRangeCaretPosition().Item2.CharIndex;\r\n\r\n        var rect = RectangleF.Union(LyricPositionProvider.GetRectByCharIndex(startCharIndex), LyricPositionProvider.GetRectByCharIndex(endCharIndex));\r\n        changeTheSizeByRect(rect);\r\n\r\n        if (State == DrawableCaretState.Idle && caret.DraggingState == RangeCaretDraggingState.EndDrag)\r\n            this.ShowPopover();\r\n    }\r\n\r\n    private void changeTheSizeByRect(RectangleF rect)\r\n    {\r\n        var position = rect.TopLeft - new Vector2(border_spacing);\r\n        float width = rect.Width + border_spacing * 2;\r\n\r\n        this.MoveTo(position, caret_move_time, Easing.Out);\r\n        this.ResizeWidthTo(width, caret_resize_time, Easing.Out);\r\n        Height = rect.Height + border_spacing * 2;\r\n    }\r\n\r\n    protected override void TriggerDisallowEditEffect(OsuColour colour)\r\n    {\r\n        this.FlashColour(colour.Red, 200);\r\n    }\r\n\r\n    public Popover GetPopover() => new CreateRubyPopover(startCharIndex, endCharIndex);\r\n\r\n    private partial class CreateRubyPopover : OsuPopover\r\n    {\r\n        [Resolved]\r\n        private ILyricRubyTagsChangeHandler lyricRubyTagsChangeHandler { get; set; } = null!;\r\n\r\n        private readonly int startCharIndex;\r\n        private readonly int endCharIndex;\r\n\r\n        private readonly LabelledTextBox labelledRubyTextBox;\r\n\r\n        public CreateRubyPopover(int startCharIndex, int endCharIndex)\r\n        {\r\n            this.startCharIndex = startCharIndex;\r\n            this.endCharIndex = endCharIndex;\r\n\r\n            AllowableAnchors = new[] { Anchor.TopCentre, Anchor.BottomCentre };\r\n\r\n            Child = new FillFlowContainer\r\n            {\r\n                Width = 200,\r\n                Direction = FillDirection.Vertical,\r\n                AutoSizeAxes = Axes.Y,\r\n                Spacing = new Vector2(0, 10),\r\n                Children = new Drawable[]\r\n                {\r\n                    labelledRubyTextBox = new CreateRubyLabelledTextBox\r\n                    {\r\n                        Label = \"Ruby\",\r\n                        PlaceholderText = \"Ruby text\",\r\n                    },\r\n                    new CreateRubyButton\r\n                    {\r\n                        Text = \"Create\",\r\n                        Action = addRubyText,\r\n                    },\r\n                },\r\n            };\r\n\r\n            labelledRubyTextBox.OnCommit += (_, _) =>\r\n            {\r\n                addRubyText();\r\n            };\r\n        }\r\n\r\n        private void addRubyText()\r\n        {\r\n            string? rubyText = labelledRubyTextBox.Text;\r\n\r\n            if (string.IsNullOrEmpty(rubyText))\r\n            {\r\n                labelledRubyTextBox.Description = \"Please enter the ruby text\";\r\n                GetContainingFocusManager().ChangeFocus(labelledRubyTextBox);\r\n                return;\r\n            }\r\n\r\n            lyricRubyTagsChangeHandler.Add(new RubyTag\r\n            {\r\n                StartIndex = startCharIndex,\r\n                EndIndex = endCharIndex,\r\n                Text = labelledRubyTextBox.Text,\r\n            });\r\n\r\n            this.HidePopover();\r\n        }\r\n\r\n        protected override void LoadComplete()\r\n        {\r\n            base.LoadComplete();\r\n            ScheduleAfterChildren(() => GetContainingFocusManager().ChangeFocus(labelledRubyTextBox));\r\n        }\r\n\r\n        private partial class CreateRubyLabelledTextBox : LabelledTextBox\r\n        {\r\n            protected override OsuTextBox CreateComponent()\r\n            {\r\n                return base.CreateComponent().With(x =>\r\n                {\r\n                    x.CommitOnFocusLost = false;\r\n                });\r\n            }\r\n        }\r\n\r\n        private partial class CreateRubyButton : EditorSectionButton;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Components/Lyrics/Carets/DrawableCuttingCaret.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Components.Lyrics.Carets;\r\n\r\npublic partial class DrawableCuttingCaret : DrawableCaret<CuttingCaretPosition>\r\n{\r\n    private const float caret_width = 10;\r\n\r\n    private readonly Container splitter;\r\n    private readonly SpriteIcon splitIcon;\r\n\r\n    public DrawableCuttingCaret(DrawableCaretState state)\r\n        : base(state)\r\n    {\r\n        Width = caret_width;\r\n        Origin = Anchor.TopCentre;\r\n\r\n        InternalChildren = new Drawable[]\r\n        {\r\n            splitIcon = new SpriteIcon\r\n            {\r\n                Anchor = Anchor.TopCentre,\r\n                Origin = Anchor.BottomCentre,\r\n                Icon = FontAwesome.Solid.HandScissors,\r\n                X = 7,\r\n                Y = -5,\r\n                Size = new Vector2(10),\r\n            },\r\n            splitter = new Container\r\n            {\r\n                RelativeSizeAxes = Axes.Y,\r\n                AutoSizeAxes = Axes.X,\r\n                Alpha = GetAlpha(state),\r\n                Children = new Drawable[]\r\n                {\r\n                    new Triangle\r\n                    {\r\n                        Anchor = Anchor.TopCentre,\r\n                        Origin = Anchor.BottomCentre,\r\n                        Scale = new Vector2(1, -1),\r\n                        Size = new Vector2(10, 5),\r\n                    },\r\n                    new Box\r\n                    {\r\n                        Anchor = Anchor.Centre,\r\n                        Origin = Anchor.Centre,\r\n                        RelativeSizeAxes = Axes.Y,\r\n                        Width = 2,\r\n                        EdgeSmoothness = new Vector2(1, 0),\r\n                    },\r\n                },\r\n            },\r\n        };\r\n\r\n        switch (state)\r\n        {\r\n            case DrawableCaretState.Idle:\r\n                splitIcon.Hide();\r\n                break;\r\n\r\n            case DrawableCaretState.Hover:\r\n                splitIcon.Show();\r\n                break;\r\n\r\n            default:\r\n                throw new ArgumentOutOfRangeException(nameof(state), state, null);\r\n        }\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OsuColour colours)\r\n    {\r\n        splitter.Colour = colours.Red;\r\n        splitIcon.Colour = colours.Yellow;\r\n    }\r\n\r\n    protected override void ApplyCaretPosition(CuttingCaretPosition caret)\r\n    {\r\n        var rect = LyricPositionProvider.GetRectByCharIndicator(caret.CharGap);\r\n\r\n        Position = rect.TopLeft + new Vector2(rect.Width / 2 - caret_width / 2, 0);\r\n        Height = rect.Height;\r\n    }\r\n\r\n    protected override void TriggerDisallowEditEffect(OsuColour colour)\r\n    {\r\n        this.FlashColour(colour.Red, 200);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Components/Lyrics/Carets/DrawableRangeCaret.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Components.Lyrics.Carets;\r\n\r\npublic abstract partial class DrawableRangeCaret<TCaretPosition> : DrawableCaret, ICanAcceptRangeIndex\r\n    where TCaretPosition : struct, IIndexCaretPosition\r\n{\r\n    private readonly IBindable<RangeCaretPosition?> bindableRangeCaretPosition = new Bindable<RangeCaretPosition?>();\r\n\r\n    protected DrawableRangeCaret(DrawableCaretState state)\r\n        : base(state)\r\n    {\r\n        // should auto hide the hover caret if selecting.\r\n        bindableRangeCaretPosition.BindValueChanged(x =>\r\n        {\r\n            if (State != DrawableCaretState.Hover)\r\n                return;\r\n\r\n            if (x.NewValue != null)\r\n                Hide();\r\n        });\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(ILyricCaretState lyricCaretState)\r\n    {\r\n        bindableRangeCaretPosition.BindTo(lyricCaretState.BindableRangeCaretPosition);\r\n    }\r\n\r\n    public sealed override void ApplyCaretPosition(ICaretPosition caret)\r\n    {\r\n        if (caret is not TCaretPosition tCaret)\r\n            throw new InvalidCastException();\r\n\r\n        ApplyCaretPosition(tCaret);\r\n    }\r\n\r\n    public void ApplyRangeCaretPosition(RangeCaretPosition rangeCaretPosition)\r\n    {\r\n        ApplyRangeCaretPosition(rangeCaretPosition.GetRangeCaretPositionWithType<TCaretPosition>());\r\n    }\r\n\r\n    protected abstract void ApplyCaretPosition(TCaretPosition caret);\r\n\r\n    protected abstract void ApplyRangeCaretPosition(RangeCaretPosition<TCaretPosition> caret);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Components/Lyrics/Carets/DrawableRecordingTimeTagCaret.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Primitives;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Components.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Components.Lyrics.Carets;\r\n\r\npublic partial class DrawableRecordingTimeTagCaret : DrawableCaret<RecordingTimeTagCaretPosition>\r\n{\r\n    private const float border_spacing = 5;\r\n    private const float caret_move_time = 60;\r\n    private const float caret_resize_time = 60;\r\n\r\n    // should be list of indexes.\r\n    private readonly TextIndexInfo? textIndexInfo;\r\n    private readonly Box? indicator;\r\n\r\n    public DrawableRecordingTimeTagCaret(DrawableCaretState state)\r\n        : base(state)\r\n    {\r\n        InternalChildren = new Drawable[]\r\n        {\r\n            new Container\r\n            {\r\n                Masking = true,\r\n                BorderThickness = border_spacing,\r\n                BorderColour = Colour4.White,\r\n                RelativeSizeAxes = Axes.Both,\r\n                Alpha = GetAlpha(state),\r\n                Child = new Box\r\n                {\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    Colour = Colour4.White,\r\n                    Alpha = 0.1f,\r\n                },\r\n            },\r\n        };\r\n\r\n        if (state == DrawableCaretState.Idle)\r\n        {\r\n            AddRangeInternal(new Drawable[]\r\n            {\r\n                textIndexInfo = new TextIndexInfo\r\n                {\r\n                    Y = -10,\r\n                    Alpha = GetAlpha(state),\r\n                },\r\n                indicator = new Box\r\n                {\r\n                    Width = border_spacing,\r\n                    RelativeSizeAxes = Axes.Y,\r\n                    Alpha = GetAlpha(state),\r\n                },\r\n            });\r\n        }\r\n    }\r\n\r\n    protected override void ApplyCaretPosition(RecordingTimeTagCaretPosition caret)\r\n    {\r\n        var timeTag = caret.TimeTag;\r\n        var charRange = caret.GetLyricCharRange();\r\n        var rect = RectangleF.Union(\r\n            LyricPositionProvider.GetRectByCharIndex(charRange.Item1),\r\n            LyricPositionProvider.GetRectByCharIndex(charRange.Item2));\r\n\r\n        // update the caret.\r\n        changeTheSizeByRect(rect);\r\n\r\n        if (textIndexInfo != null)\r\n        {\r\n            textIndexInfo.Anchor = TextIndexUtils.GetValueByState(timeTag.Index, Anchor.TopLeft, Anchor.TopRight);\r\n            textIndexInfo.Origin = TextIndexUtils.GetValueByState(timeTag.Index, Anchor.BottomLeft, Anchor.BottomRight);\r\n            textIndexInfo.X = TextIndexUtils.GetValueByState(timeTag.Index, -10, 10);\r\n            textIndexInfo.UpdateCaret(caret);\r\n        }\r\n\r\n        if (indicator != null)\r\n        {\r\n            indicator.Colour = Colours.GetRecordingTimeTagCaretColour(timeTag);\r\n            indicator.Anchor = TextIndexUtils.GetValueByState(timeTag.Index, Anchor.CentreLeft, Anchor.CentreRight);\r\n            indicator.Origin = TextIndexUtils.GetValueByState(timeTag.Index, Anchor.CentreLeft, Anchor.CentreRight);\r\n        }\r\n    }\r\n\r\n    private void changeTheSizeByRect(RectangleF rect)\r\n    {\r\n        var position = rect.TopLeft - new Vector2(border_spacing);\r\n        float width = rect.Width + border_spacing * 2;\r\n\r\n        this.MoveTo(position, caret_move_time, Easing.Out);\r\n        this.ResizeWidthTo(width, caret_resize_time, Easing.Out);\r\n        Height = rect.Height + border_spacing * 2;\r\n    }\r\n\r\n    protected override void TriggerDisallowEditEffect(OsuColour colour)\r\n    {\r\n        this.FlashColour(colour.Red, 200);\r\n    }\r\n\r\n    private partial class TextIndexInfo : CompositeDrawable\r\n    {\r\n        private const int border_radius = 5;\r\n\r\n        private DrawableTimeTag currentTimeTag = null!;\r\n        private FillFlowContainer<DrawableTimeTag> pendingTimeTags = null!;\r\n\r\n        [Resolved]\r\n        private OsuColour colours { get; set; } = null!;\r\n\r\n        public TextIndexInfo()\r\n        {\r\n            Masking = true;\r\n            CornerRadius = border_radius;\r\n\r\n            AutoSizeAxes = Axes.Both;\r\n        }\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(LyricEditorColourProvider colourProvider, ILyricEditorState state)\r\n        {\r\n            InternalChildren = new Drawable[]\r\n            {\r\n                new Box\r\n                {\r\n                    Name = \"Background\",\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    Colour = colourProvider.Background2(state.Mode),\r\n                },\r\n                new Container\r\n                {\r\n                    Name = \"Background for pending text indexes\",\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    Padding = new MarginPadding\r\n                    {\r\n                        Horizontal = 5,\r\n                        Vertical = 7,\r\n                    },\r\n                    Child = new Container\r\n                    {\r\n                        Masking = true,\r\n                        CornerRadius = border_radius,\r\n                        RelativeSizeAxes = Axes.Both,\r\n                        Child = new Box\r\n                        {\r\n                            RelativeSizeAxes = Axes.Both,\r\n                            Colour = colourProvider.Background3(state.Mode),\r\n                        },\r\n                    },\r\n                },\r\n                new FillFlowContainer\r\n                {\r\n                    AutoSizeAxes = Axes.Both,\r\n                    Margin = new MarginPadding(5),\r\n                    Direction = FillDirection.Horizontal,\r\n                    Children = new Drawable[]\r\n                    {\r\n                        new Container\r\n                        {\r\n                            AutoSizeAxes = Axes.Both,\r\n                            Masking = true,\r\n                            CornerRadius = border_radius,\r\n                            Children = new Drawable[]\r\n                            {\r\n                                new Box\r\n                                {\r\n                                    RelativeSizeAxes = Axes.Both,\r\n                                    Colour = colourProvider.Background5(state.Mode),\r\n                                },\r\n                                currentTimeTag = new DrawableTimeTag\r\n                                {\r\n                                    Anchor = Anchor.Centre,\r\n                                    Origin = Anchor.Centre,\r\n                                    Margin = new MarginPadding(5),\r\n                                    Size = new Vector2(15),\r\n                                },\r\n                            },\r\n                        },\r\n                        pendingTimeTags = new FillFlowContainer<DrawableTimeTag>\r\n                        {\r\n                            Anchor = Anchor.CentreLeft,\r\n                            Origin = Anchor.CentreLeft,\r\n                            AutoSizeAxes = Axes.Both,\r\n                            Spacing = new Vector2(5),\r\n                            Direction = FillDirection.Horizontal,\r\n                        },\r\n                    },\r\n                },\r\n            };\r\n        }\r\n\r\n        public void UpdateCaret(RecordingTimeTagCaretPosition caret)\r\n        {\r\n            currentTimeTag.TimeTag = caret.TimeTag;\r\n\r\n            int paddingIndicator = caret.GetPaddingTextIndex();\r\n            pendingTimeTags.Clear();\r\n\r\n            for (int i = 0; i < paddingIndicator; i++)\r\n            {\r\n                bool isFirst = i == 0;\r\n                bool isLast = i == paddingIndicator - 1;\r\n\r\n                pendingTimeTags.Add(new DrawableTimeTag\r\n                {\r\n                    Anchor = Anchor.Centre,\r\n                    Origin = Anchor.Centre,\r\n                    Size = new Vector2(12),\r\n                    Margin = new MarginPadding(5)\r\n                    {\r\n                        Left = isFirst ? 5 : 0,\r\n                        Right = isLast ? 5 : 0,\r\n                    },\r\n                    TimeTag = caret.TimeTag,\r\n                    Alpha = 0.5f,\r\n                });\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Components/Lyrics/Carets/DrawableTypingCaret.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Primitives;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Framework.Input;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\nusing osuTK;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Components.Lyrics.Carets;\r\n\r\npublic partial class DrawableTypingCaret : DrawableRangeCaret<TypingCaretPosition>\r\n{\r\n    private const float fading_time = 200;\r\n    private const float caret_move_time = 60;\r\n    private const float caret_resize_time = 60;\r\n    private const float caret_width = 2;\r\n\r\n    private readonly Box drawableCaret;\r\n    private readonly TypingCaretEventHandler? typingCaretEventHandler;\r\n\r\n    public DrawableTypingCaret(DrawableCaretState state)\r\n        : base(state)\r\n    {\r\n        drawableCaret = new Box\r\n        {\r\n            RelativeSizeAxes = Axes.Both,\r\n            Colour = Color4.White,\r\n            Alpha = GetAlpha(State),\r\n        };\r\n        AddInternal(drawableCaret);\r\n\r\n        if (State != DrawableCaretState.Idle)\r\n            return;\r\n\r\n        var inputCaretTextBox = new InputCaretTextBox\r\n        {\r\n            Anchor = Anchor.TopRight,\r\n            Origin = Anchor.TopLeft,\r\n            Width = 50,\r\n            Height = 20,\r\n            ReleaseFocusOnCommit = false,\r\n        };\r\n        typingCaretEventHandler = new TypingCaretEventHandler(inputCaretTextBox);\r\n\r\n        AddInternal(inputCaretTextBox);\r\n        AddInternal(typingCaretEventHandler);\r\n    }\r\n\r\n    protected override void LoadComplete()\r\n    {\r\n        base.LoadComplete();\r\n\r\n        drawableCaret\r\n            .Loop(c => c.FadeTo(0.7f).FadeTo(0.4f, 500, Easing.InOutSine));\r\n    }\r\n\r\n    public override void Show() => this.FadeIn(fading_time);\r\n\r\n    public override void Hide() => this.FadeOut(fading_time);\r\n\r\n    protected override void ApplyCaretPosition(TypingCaretPosition caret)\r\n    {\r\n        typingCaretEventHandler?.ChangeLyric(caret.Lyric);\r\n        typingCaretEventHandler?.ChangeCharGapAndOffset(caret.CharGap);\r\n        typingCaretEventHandler?.FocusInputCaretTextBox();\r\n\r\n        var rect = toRectWithFixedWidth(LyricPositionProvider.GetRectByCharIndicator(caret.CharGap));\r\n        changeTheSizeByRect(rect);\r\n    }\r\n\r\n    protected override void ApplyRangeCaretPosition(RangeCaretPosition<TypingCaretPosition> caret)\r\n    {\r\n        int minGap = caret.GetRangeCaretPosition().Item1.CharGap;\r\n        int maxGap = caret.GetRangeCaretPosition().Item2.CharGap;\r\n\r\n        typingCaretEventHandler?.ChangeLyric(caret.Start.Lyric);\r\n        typingCaretEventHandler?.ChangeCharGapAndOffset(maxGap, maxGap - minGap);\r\n        typingCaretEventHandler?.FocusInputCaretTextBox();\r\n\r\n        var minGapRect = toRectWithFixedWidth(LyricPositionProvider.GetRectByCharIndicator(minGap));\r\n        var maxGapRect = toRectWithFixedWidth(LyricPositionProvider.GetRectByCharIndicator(maxGap));\r\n\r\n        var rect = RectangleF.Union(minGapRect, maxGapRect);\r\n        changeTheSizeByRect(rect);\r\n    }\r\n\r\n    private RectangleF toRectWithFixedWidth(RectangleF rect)\r\n    {\r\n        var position = rect.TopLeft + new Vector2(rect.Width / 2 - caret_width / 2, 0);\r\n        var size = new Vector2(caret_width, rect.Height);\r\n\r\n        return new RectangleF(position, size);\r\n    }\r\n\r\n    private void changeTheSizeByRect(RectangleF rect)\r\n    {\r\n        var position = rect.TopLeft - new Vector2(caret_width / 2, 0);\r\n        float width = rect.Width + caret_width;\r\n\r\n        this.MoveTo(position, caret_move_time, Easing.Out);\r\n        this.ResizeWidthTo(width, caret_resize_time, Easing.Out);\r\n        Height = rect.Height;\r\n    }\r\n\r\n    protected override void TriggerDisallowEditEffect(OsuColour colour)\r\n    {\r\n        this.FlashColour(colour.Red, 200);\r\n    }\r\n\r\n    private partial class TypingCaretEventHandler : Component\r\n    {\r\n        private readonly InputCaretTextBox inputCaretTextBox;\r\n\r\n        public TypingCaretEventHandler(InputCaretTextBox inputCaretTextBox)\r\n        {\r\n            this.inputCaretTextBox = inputCaretTextBox;\r\n        }\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(ILyricTextChangeHandler lyricTextChangeHandler, ILyricCaretState lyricCaretState, IInteractableLyricState interactableLyricState)\r\n        {\r\n            inputCaretTextBox.NewCommitText = text =>\r\n            {\r\n                if (lyricTextChangeHandler.IsSelectionsLocked())\r\n                {\r\n                    interactableLyricState.TriggerDisallowEditEffect();\r\n                    return;\r\n                }\r\n\r\n                lyricTextChangeHandler.InsertText(charGap ?? throw new ArgumentNullException(nameof(charGap)), text);\r\n\r\n                moveCaret(text.Length);\r\n            };\r\n            inputCaretTextBox.DeleteText = () =>\r\n            {\r\n                if (lyricTextChangeHandler.IsSelectionsLocked())\r\n                {\r\n                    interactableLyricState.TriggerDisallowEditEffect();\r\n                    return;\r\n                }\r\n\r\n                if (charGap == 0)\r\n                    return;\r\n\r\n                lyricTextChangeHandler.DeleteLyricText(charGap ?? throw new ArgumentNullException(nameof(charGap)), removeCharAmount);\r\n\r\n                moveCaret(-removeCharAmount);\r\n            };\r\n\r\n            void moveCaret(int offset)\r\n            {\r\n                // calculate new caret position.\r\n                var targetLyric = lyric ?? throw new ArgumentNullException(nameof(lyric));\r\n                int targetIndex = (charGap ?? throw new ArgumentNullException(nameof(charGap))) + offset;\r\n                lyricCaretState.MoveCaretToTargetPosition(targetLyric, targetIndex);\r\n            }\r\n        }\r\n\r\n        private Lyric? lyric;\r\n\r\n        public void ChangeLyric(Lyric lyric)\r\n        {\r\n            this.lyric = lyric;\r\n        }\r\n\r\n        private int? charGap;\r\n        private int removeCharAmount = 1;\r\n\r\n        public void ChangeCharGapAndOffset(int charGap, int removeCharAmount = 1)\r\n        {\r\n            this.charGap = charGap;\r\n            this.removeCharAmount = removeCharAmount;\r\n        }\r\n\r\n        public void FocusInputCaretTextBox()\r\n        {\r\n            // Should wait for a while after click event finished in the outside.\r\n            Schedule(() =>\r\n            {\r\n                inputCaretTextBox.Text = string.Empty;\r\n                GetContainingFocusManager().ChangeFocus(inputCaretTextBox);\r\n            });\r\n        }\r\n    }\r\n\r\n    private partial class InputCaretTextBox : BasicTextBox\r\n    {\r\n        public Action<string>? NewCommitText;\r\n\r\n        public Action? DeleteText;\r\n\r\n        // should not accept tab event because all focus/unfocus should be controlled by caret.\r\n        public override bool CanBeTabbedTo => false;\r\n\r\n        // should not allow cursor index change because press left/right event is handled by parent caret.\r\n        protected override bool AllowWordNavigation => false;\r\n\r\n        public InputCaretTextBox()\r\n        {\r\n            OnCommit += (sender, newText) =>\r\n            {\r\n                if (!newText)\r\n                    return;\r\n\r\n                string text = sender.Text;\r\n\r\n                if (string.IsNullOrEmpty(text))\r\n                    return;\r\n\r\n                NewCommitText?.Invoke(text);\r\n\r\n                sender.Text = string.Empty;\r\n            };\r\n        }\r\n\r\n        public override bool OnPressed(KeyBindingPressEvent<PlatformAction> e)\r\n        {\r\n            bool triggerDeleteText = processTriggerDeleteText(e.Action);\r\n            bool triggerMoveTextCaretIndex = processTriggerMoveText(e.Action);\r\n\r\n            // should trigger delete the main text in lyric if there's not pending text.\r\n            if (triggerDeleteText && string.IsNullOrEmpty(Text))\r\n            {\r\n                DeleteText?.Invoke();\r\n                return true;\r\n            }\r\n\r\n            // should not block the move left/right event if there's on text in the text box.\r\n            if (triggerMoveTextCaretIndex && string.IsNullOrEmpty(Text))\r\n            {\r\n                return false;\r\n            }\r\n\r\n            return base.OnPressed(e);\r\n\r\n            static bool processTriggerDeleteText(PlatformAction action) =>\r\n                action switch\r\n                {\r\n                    // Deletion\r\n                    PlatformAction.DeleteBackwardChar => true,\r\n                    _ => false,\r\n                };\r\n\r\n            static bool processTriggerMoveText(PlatformAction action) =>\r\n                action switch\r\n                {\r\n                    // Move left/right actions.\r\n                    PlatformAction.MoveBackwardChar => true,\r\n                    PlatformAction.MoveForwardChar => true,\r\n                    PlatformAction.MoveBackwardWord => true,\r\n                    PlatformAction.MoveForwardWord => true,\r\n                    PlatformAction.MoveBackwardLine => true,\r\n                    PlatformAction.MoveForwardLine => true,\r\n                    _ => false,\r\n                };\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Components/Lyrics/Carets/ICanAcceptRangeIndex.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Components.Lyrics.Carets;\r\n\r\npublic interface ICanAcceptRangeIndex\r\n{\r\n    public void ApplyRangeCaretPosition(RangeCaretPosition rangeCaretPosition);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Components/Lyrics/EditLyricLayer.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition.Algorithms;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Components.Lyrics;\r\n\r\n/// <summary>\r\n/// Compare to <see cref=\"InteractLyricLayer\"/>, this layer can do more things:\r\n/// 1. Change the hover caret and index.\r\n/// 2. Change the caret and index.\r\n/// 3. Change the drag caret and index.\r\n/// 4. Do some special action like cut the lyric by double click.\r\n/// </summary>\r\npublic partial class EditLyricLayer : UIEventLayer\r\n{\r\n    [Resolved]\r\n    private IPreviewLyricPositionProvider previewLyricPositionProvider { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private ILyricCaretState lyricCaretState { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private ILyricsChangeHandler lyricsChangeHandler { get; set; } = null!;\r\n\r\n    public EditLyricLayer(Lyric lyric)\r\n        : base(lyric)\r\n    {\r\n    }\r\n\r\n    #region Hover\r\n\r\n    protected override bool OnMouseMove(MouseMoveEvent e)\r\n    {\r\n        if (!lyricCaretState.CaretEnabled)\r\n            return false;\r\n\r\n        // should not trigger the hover caret event if dragging other lyric.\r\n        if (e.HasAnyButtonPressed)\r\n            return false;\r\n\r\n        object? caretIndex = getCaretIndexByPosition(e);\r\n\r\n        if (caretIndex != null)\r\n        {\r\n            lyricCaretState.MoveHoverCaretToTargetPosition(Lyric, caretIndex);\r\n        }\r\n        else if (lyricCaretState.CaretPosition is not IIndexCaretPosition)\r\n        {\r\n            // still need to handle the case with non-index caret position.\r\n            lyricCaretState.MoveHoverCaretToTargetPosition(Lyric);\r\n        }\r\n\r\n        return base.OnMouseMove(e);\r\n    }\r\n\r\n    protected override void OnHoverLost(HoverLostEvent e)\r\n    {\r\n        base.OnHoverLost(e);\r\n\r\n        if (!lyricCaretState.CaretEnabled)\r\n            return;\r\n\r\n        // lost hover caret and time-tag caret\r\n        lyricCaretState.ClearHoverCaretPosition();\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Click\r\n\r\n    protected override bool OnClick(ClickEvent e)\r\n    {\r\n        return lyricCaretState.ConfirmHoverCaretPosition();\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Drag\r\n\r\n    protected override bool OnDragStart(DragStartEvent e)\r\n    {\r\n        // should not handle the drag event if the caret algorithm is able to handle it.\r\n        if (!lyricCaretState.CaretDraggable)\r\n            return false;\r\n\r\n        // confirm the hover caret position before drag start.\r\n        return lyricCaretState.StartDragging();\r\n    }\r\n\r\n    protected override void OnDrag(DragEvent e)\r\n    {\r\n        object? caretIndex = getCaretIndexByPosition(e);\r\n\r\n        if (caretIndex != null)\r\n        {\r\n            lyricCaretState.MoveDraggingCaretIndex(caretIndex);\r\n        }\r\n\r\n        base.OnDrag(e);\r\n    }\r\n\r\n    protected override void OnDragEnd(DragEndEvent e)\r\n    {\r\n        lyricCaretState.EndDragging();\r\n\r\n        base.OnDragEnd(e);\r\n    }\r\n\r\n    #endregion\r\n\r\n    private object? getCaretIndexByPosition(UIEvent mouseEvent)\r\n    {\r\n        var algorithm = lyricCaretState.CaretPositionAlgorithm;\r\n        var position = ToLocalSpace(mouseEvent.ScreenSpaceMousePosition);\r\n        return algorithm switch\r\n        {\r\n            CuttingCaretPositionAlgorithm => previewLyricPositionProvider.GetCharIndicatorByPosition(position),\r\n            TypingCaretPositionAlgorithm => previewLyricPositionProvider.GetCharIndicatorByPosition(position),\r\n            NavigateCaretPositionAlgorithm => null,\r\n            CreateRubyTagCaretPositionAlgorithm => previewLyricPositionProvider.GetCharIndexByPosition(position),\r\n            RecordingTimeTagCaretPositionAlgorithm => getTimeTagByIndex(Lyric, previewLyricPositionProvider.GetCharIndexByPosition(position)),\r\n            CreateRemoveTimeTagCaretPositionAlgorithm => previewLyricPositionProvider.GetCharIndexByPosition(position),\r\n            _ => null,\r\n        };\r\n\r\n        TimeTag? getTimeTagByIndex(Lyric lyric, int? charIndex)\r\n            => lyric.TimeTags.FirstOrDefault(tag => tag.Index.Index == charIndex);\r\n    }\r\n\r\n    #region Double click\r\n\r\n    protected override bool OnDoubleClick(DoubleClickEvent e)\r\n    {\r\n        var position = lyricCaretState.CaretPosition;\r\n\r\n        switch (position)\r\n        {\r\n            case CuttingCaretPosition cuttingCaretPosition:\r\n                lyricsChangeHandler.Split(cuttingCaretPosition.CharGap);\r\n                return true;\r\n\r\n            default:\r\n                return false;\r\n        }\r\n    }\r\n\r\n    #endregion\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Components/Lyrics/GridLayer.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Screens.Edit.Compose.Components;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Components.Lyrics;\r\n\r\npublic partial class GridLayer : Layer\r\n{\r\n    private readonly RectangularPositionSnapGrid rectangularPositionSnapGrid;\r\n\r\n    public GridLayer(Lyric lyric)\r\n        : base(lyric)\r\n    {\r\n        InternalChild = rectangularPositionSnapGrid = new RectangularPositionSnapGrid\r\n        {\r\n            RelativeSizeAxes = Axes.Both,\r\n        };\r\n    }\r\n\r\n    public int Spacing\r\n    {\r\n        get => rectangularPositionSnapGrid.Spacing;\r\n        set => rectangularPositionSnapGrid.Spacing = value;\r\n    }\r\n\r\n    public override void UpdateDisableEditState(bool editable)\r\n    {\r\n        this.FadeTo(editable ? 1 : 0.5f, 100);\r\n    }\r\n\r\n    private partial class RectangularPositionSnapGrid : LinedPositionSnapGrid\r\n    {\r\n        protected override void CreateContent()\r\n        {\r\n            GenerateGridLines(new Vector2(0, -Spacing), DrawSize);\r\n            GenerateGridLines(new Vector2(0, Spacing), DrawSize);\r\n\r\n            GenerateGridLines(new Vector2(-Spacing, 0), DrawSize);\r\n            GenerateGridLines(new Vector2(Spacing, 0), DrawSize);\r\n\r\n            GenerateOutline(DrawSize);\r\n        }\r\n\r\n        public override Vector2 GetSnappedPosition(Vector2 original)\r\n        {\r\n            return StartPosition.Value + original;\r\n        }\r\n\r\n        private int spacing;\r\n\r\n        public int Spacing\r\n        {\r\n            get => spacing;\r\n            set\r\n            {\r\n                spacing = value;\r\n                GridCache.Invalidate();\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Components/Lyrics/IInteractableLyricState.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Components.Lyrics;\r\n\r\npublic interface IInteractableLyricState\r\n{\r\n    void TriggerDisallowEditEffect();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Components/Lyrics/IPreviewLyricPositionProvider.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics.Primitives;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Components.Lyrics;\r\n\r\npublic interface IPreviewLyricPositionProvider\r\n{\r\n    int? GetCharIndexByPosition(Vector2 position);\r\n\r\n    RectangleF GetRectByCharIndex(int charIndex);\r\n\r\n    int? GetCharIndicatorByPosition(Vector2 position);\r\n\r\n    RectangleF GetRectByCharIndicator(int gapIndex);\r\n\r\n    RectangleF? GetRubyTagByPosition(RubyTag rubyTag);\r\n\r\n    TimeTag? GetTimeTagByPosition(Vector2 position);\r\n\r\n    Vector2 GetPositionByTimeTag(TimeTag timeTag);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Components/Lyrics/InteractLyricLayer.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Components.Lyrics;\r\n\r\n/// <summary>\r\n/// Compare to <see cref=\"EditLyricLayer\"/>, this layer only allows to:\r\n/// 1. Hover the whole lyric.\r\n/// 2. Selec the whole lyric.\r\n/// </summary>\r\npublic partial class InteractLyricLayer : UIEventLayer\r\n{\r\n    [Resolved]\r\n    private ILyricCaretState lyricCaretState { get; set; } = null!;\r\n\r\n    public InteractLyricLayer(Lyric lyric)\r\n        : base(lyric)\r\n    {\r\n    }\r\n\r\n    protected override bool OnMouseMove(MouseMoveEvent e)\r\n    {\r\n        if (!lyricCaretState.CaretEnabled)\r\n            return false;\r\n\r\n        if (IsDragged)\r\n            return false;\r\n\r\n        lyricCaretState.MoveHoverCaretToTargetPosition(Lyric);\r\n\r\n        return base.OnMouseMove(e);\r\n    }\r\n\r\n    protected override void OnHoverLost(HoverLostEvent e)\r\n    {\r\n        base.OnHoverLost(e);\r\n\r\n        // lost hover caret and time-tag caret\r\n        lyricCaretState.ClearHoverCaretPosition();\r\n    }\r\n\r\n    protected override bool OnClick(ClickEvent e)\r\n    {\r\n        return lyricCaretState.MoveCaretToTargetPosition(Lyric);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Components/Lyrics/InteractableLyric.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Extensions.IEnumerableExtensions;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Cursor;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Components.Lyrics;\r\n\r\n[Cached(typeof(IInteractableLyricState))]\r\npublic sealed partial class InteractableLyric : CompositeDrawable, IHasTooltip, IInteractableLyricState\r\n{\r\n    private readonly IBindable<LyricEditorMode> bindableMode = new Bindable<LyricEditorMode>();\r\n    private readonly IBindable<int> bindableLyricPropertyWritableVersion;\r\n\r\n    private readonly Lyric lyric;\r\n    private LocalisableString? lockReason;\r\n\r\n    public Action<InteractableLyric, Vector2>? TextSizeChanged;\r\n\r\n    public InteractableLyric(Lyric lyric)\r\n    {\r\n        this.lyric = lyric;\r\n\r\n        bindableLyricPropertyWritableVersion = lyric.LyricPropertyWritableVersion.GetBoundCopy();\r\n\r\n        bindableMode.BindValueChanged(x =>\r\n        {\r\n            triggerWritableVersionChanged();\r\n        });\r\n\r\n        bindableLyricPropertyWritableVersion.BindValueChanged(_ =>\r\n        {\r\n            triggerWritableVersionChanged();\r\n        });\r\n    }\r\n\r\n    public IEnumerable<LayerLoader> Loaders\r\n    {\r\n        init\r\n        {\r\n            foreach (var loader in value)\r\n            {\r\n                var layer = loader.CreateLayer(lyric);\r\n                AddInternal(layer);\r\n            }\r\n\r\n            var lyricLayers = layers.OfType<LyricLayer>().Single();\r\n            lyricLayers.SizeChanged = size =>\r\n            {\r\n                TextSizeChanged?.Invoke(this, size);\r\n            };\r\n        }\r\n    }\r\n\r\n    private IEnumerable<Layer> layers => InternalChildren.OfType<Layer>();\r\n\r\n    protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)\r\n    {\r\n        var baseDependencies = new DependencyContainer(base.CreateChildDependencies(parent));\r\n\r\n        var lyricLayer = layers.OfType<LyricLayer>().Single();\r\n        baseDependencies.CacheAs<IPreviewLyricPositionProvider>(lyricLayer);\r\n        return baseDependencies;\r\n    }\r\n\r\n    public void TriggerDisallowEditEffect()\r\n    {\r\n        layers.ForEach(x => x.TriggerDisallowEditEffect(bindableMode.Value));\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(ILyricEditorState state)\r\n    {\r\n        bindableMode.BindTo(state.BindableMode);\r\n    }\r\n\r\n    private void triggerWritableVersionChanged()\r\n    {\r\n        var loadReason = GetLyricPropertyLockedReason(lyric, bindableMode.Value);\r\n        lockReason = loadReason;\r\n\r\n        // adjust the style.\r\n        bool editable = lockReason == null;\r\n        layers.ForEach(x => x.UpdateDisableEditState(editable));\r\n    }\r\n\r\n    public LocalisableString TooltipText => lockReason ?? string.Empty;\r\n\r\n    public static LocalisableString? GetLyricPropertyLockedReason(Lyric lyric, LyricEditorMode mode)\r\n    {\r\n        var reasons = getLyricPropertyLockedReasons(lyric, mode);\r\n\r\n        return reasons switch\r\n        {\r\n            LockLyricPropertyBy.ReferenceLyricConfig => \"Cannot modify this property due to this lyric is property is sync from another lyric.\",\r\n            LockLyricPropertyBy.LockState => \"This property is locked and not editable\",\r\n            null => default(LocalisableString?),\r\n            _ => throw new ArgumentOutOfRangeException(),\r\n        };\r\n    }\r\n\r\n    private static LockLyricPropertyBy? getLyricPropertyLockedReasons(Lyric lyric, LyricEditorMode mode)\r\n    {\r\n        return mode switch\r\n        {\r\n            LyricEditorMode.View => null,\r\n            LyricEditorMode.EditText => HitObjectWritableUtils.GetLyricPropertyLockedBy(lyric, nameof(Lyric.Text), nameof(Lyric.RubyTags), nameof(Lyric.TimeTags)),\r\n            LyricEditorMode.EditReferenceLyric => HitObjectWritableUtils.GetLyricPropertyLockedBy(lyric, nameof(Lyric.ReferenceLyric), nameof(Lyric.ReferenceLyricConfig)),\r\n            LyricEditorMode.EditLanguage => HitObjectWritableUtils.GetLyricPropertyLockedBy(lyric, nameof(Lyric.Language)),\r\n            LyricEditorMode.EditRuby => HitObjectWritableUtils.GetLyricPropertyLockedBy(lyric, nameof(Lyric.RubyTags)),\r\n            LyricEditorMode.EditTimeTag => HitObjectWritableUtils.GetLyricPropertyLockedBy(lyric, nameof(Lyric.TimeTags)),\r\n            LyricEditorMode.EditRomanisation => HitObjectWritableUtils.GetLyricPropertyLockedBy(lyric, nameof(Lyric.TimeTags)),\r\n            LyricEditorMode.EditNote => HitObjectWritableUtils.GetCreateOrRemoveNoteLockedBy(lyric),\r\n            LyricEditorMode.EditSinger => HitObjectWritableUtils.GetLyricPropertyLockedBy(lyric, nameof(Lyric.SingerIds)),\r\n            _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null),\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Components/Lyrics/Layer.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Components.Lyrics;\r\n\r\npublic abstract partial class Layer : CompositeDrawable\r\n{\r\n    protected readonly Lyric Lyric;\r\n\r\n    protected Layer(Lyric lyric)\r\n    {\r\n        Lyric = lyric;\r\n\r\n        RelativeSizeAxes = Axes.Both;\r\n    }\r\n\r\n    public abstract void UpdateDisableEditState(bool editable);\r\n\r\n    public virtual void TriggerDisallowEditEffect(LyricEditorMode editorMode)\r\n    {\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Components/Lyrics/LayerLoader.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Components.Lyrics;\r\n\r\npublic class LayerLoader<TLayer> : LayerLoader where TLayer : Layer\r\n{\r\n    public Action<TLayer>? OnLoad { get; init; }\r\n\r\n    public override Layer CreateLayer(Lyric lyric)\r\n    {\r\n        var layer = ActivatorUtils.CreateInstance<TLayer>(lyric);\r\n        OnLoad?.Invoke(layer);\r\n        return layer;\r\n    }\r\n}\r\n\r\npublic abstract class LayerLoader\r\n{\r\n    public abstract Layer CreateLayer(Lyric lyric);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Components/Lyrics/LyricLayer.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Primitives;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Screens.Edit;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Components.Lyrics;\r\n\r\npublic partial class LyricLayer : Layer, IPreviewLyricPositionProvider\r\n{\r\n    [Resolved]\r\n    private OsuColour colours { get; set; } = null!;\r\n\r\n    private readonly PreviewKaraokeSpriteText previewKaraokeSpriteText;\r\n\r\n    public Action<Vector2>? SizeChanged;\r\n\r\n    public LyricLayer(Lyric lyric)\r\n        : base(lyric)\r\n    {\r\n        InternalChild = previewKaraokeSpriteText = new PreviewKaraokeSpriteText(lyric);\r\n\r\n        previewKaraokeSpriteText.SizeChanged = size =>\r\n        {\r\n            SizeChanged?.Invoke(size);\r\n        };\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(EditorClock clock)\r\n    {\r\n        previewKaraokeSpriteText.Clock = clock;\r\n    }\r\n\r\n    public Vector2 LyricPosition\r\n    {\r\n        get => previewKaraokeSpriteText.Position;\r\n        set => previewKaraokeSpriteText.Position = value;\r\n    }\r\n\r\n    public override void UpdateDisableEditState(bool editable)\r\n    {\r\n        this.FadeTo(editable ? 1 : 0.5f, 100);\r\n    }\r\n\r\n    public override void TriggerDisallowEditEffect(LyricEditorMode editorMode)\r\n    {\r\n        this.FlashColour(colours.Red, 200);\r\n    }\r\n\r\n    #region Text char index\r\n\r\n    public int? GetCharIndexByPosition(Vector2 position)\r\n        => previewKaraokeSpriteText.GetCharIndexByPosition(position - LyricPosition);\r\n\r\n    public RectangleF GetRectByCharIndex(int charIndex)\r\n        => previewKaraokeSpriteText.GetRectByCharIndex(charIndex).Offset(LyricPosition);\r\n\r\n    #endregion\r\n\r\n    #region Text indicator\r\n\r\n    public int? GetCharIndicatorByPosition(Vector2 position)\r\n        => previewKaraokeSpriteText.GetCharIndicatorByPosition(position - LyricPosition);\r\n\r\n    public RectangleF GetRectByCharIndicator(int gapIndex)\r\n        => previewKaraokeSpriteText.GetRectByCharIndicator(gapIndex).Offset(LyricPosition);\r\n\r\n    #endregion\r\n\r\n    #region Ruby tag\r\n\r\n    public RectangleF? GetRubyTagByPosition(RubyTag rubyTag)\r\n        => previewKaraokeSpriteText.GetRubyTagByPosition(rubyTag)?.Offset(LyricPosition);\r\n\r\n    #endregion\r\n\r\n    #region Time tag\r\n\r\n    public TimeTag? GetTimeTagByPosition(Vector2 position)\r\n        => previewKaraokeSpriteText.GetTimeTagByPosition(position - LyricPosition);\r\n\r\n    public Vector2 GetPositionByTimeTag(TimeTag timeTag)\r\n        => previewKaraokeSpriteText.GetPositionByTimeTag(timeTag) + LyricPosition;\r\n\r\n    #endregion\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Components/Lyrics/PreviewKaraokeSpriteText.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Extensions.Color4Extensions;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Primitives;\r\nusing osu.Framework.Graphics.Shaders;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Skinning.Elements;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\nusing osu.Game.Skinning;\r\nusing osuTK;\r\nusing LyricStyle = osu.Game.Rulesets.Karaoke.Graphics.Sprites.LyricStyle;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Components.Lyrics;\r\n\r\npublic partial class PreviewKaraokeSpriteText : DrawableKaraokeSpriteText<PreviewKaraokeSpriteText.EditorLyricSpriteText>, IPreviewLyricPositionProvider\r\n{\r\n    private const int time_tag_spacing = 8;\r\n\r\n    public Lyric HitObject;\r\n\r\n    public Action<Vector2>? SizeChanged;\r\n\r\n    private readonly EditorLyricSpriteText spriteText;\r\n\r\n    public PreviewKaraokeSpriteText(Lyric lyric)\r\n        : base(lyric)\r\n    {\r\n        HitObject = lyric;\r\n\r\n        // should display ruby and romanisation by default.\r\n        DisplayType = LyricDisplayType.Lyric;\r\n        DisplayProperty = LyricDisplayProperty.Both;\r\n\r\n        // give it a default style.\r\n        UpdateStyle(new LyricStyle\r\n        {\r\n            LeftLyricTextShaders = new List<ICustomizedShader>\r\n            {\r\n                new OutlineShader\r\n                {\r\n                    Radius = 2,\r\n                    Colour = Color4Extensions.FromHex(\"#3D2D6B\"),\r\n                    OutlineColour = Color4Extensions.FromHex(\"#CCA532\"),\r\n                },\r\n            },\r\n            RightLyricTextShaders = new List<ICustomizedShader>\r\n            {\r\n                new OutlineShader\r\n                {\r\n                    Radius = 2,\r\n                    OutlineColour = Color4Extensions.FromHex(\"#5932CC\"),\r\n                },\r\n            },\r\n        });\r\n\r\n        spriteText = getSpriteText();\r\n\r\n        EditorLyricSpriteText getSpriteText()\r\n        {\r\n            if (InternalChildren.First() is not Container<EditorLyricSpriteText> lyricSpriteTexts)\r\n                throw new ArgumentNullException();\r\n\r\n            return lyricSpriteTexts.Child;\r\n        }\r\n    }\r\n\r\n    protected override void OnPropertyChanged()\r\n    {\r\n        triggerSizeChangedEvent();\r\n    }\r\n\r\n    private void triggerSizeChangedEvent()\r\n    {\r\n        ScheduleAfterChildren(() =>\r\n        {\r\n            SizeChanged?.Invoke(DrawSize);\r\n        });\r\n    }\r\n\r\n    #region Text char index\r\n\r\n    public int? GetCharIndexByPosition(Vector2 position)\r\n    {\r\n        for (int i = 0; i < Text.Length; i++)\r\n        {\r\n            var rectangle = spriteText.GetCharacterDrawRectangle(i);\r\n            if (rectangle.Contains(position))\r\n                return i;\r\n        }\r\n\r\n        return null;\r\n    }\r\n\r\n    public RectangleF GetRectByCharIndex(int charIndex)\r\n    {\r\n        if (charIndex < 0 || charIndex >= Text.Length)\r\n            throw new ArgumentOutOfRangeException(nameof(charIndex));\r\n\r\n        return spriteText.GetCharacterDrawRectangle(charIndex);\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Text indicator\r\n\r\n    public int? GetCharIndicatorByPosition(Vector2 position)\r\n    {\r\n        for (int i = 0; i < Text.Length + 1; i++)\r\n        {\r\n            var rect = getTriggerPositionByTimeIndex(i);\r\n            if (rect.Contains(position))\r\n                return i;\r\n        }\r\n\r\n        return null;\r\n\r\n        RectangleF getTriggerPositionByTimeIndex(int gapIndex)\r\n        {\r\n            if (gapIndex == 0)\r\n            {\r\n                var rectangle = spriteText.GetCharacterDrawRectangle(gapIndex);\r\n                return new RectangleF(rectangle.Left, rectangle.Top, rectangle.Width / 2, rectangle.Height);\r\n            }\r\n\r\n            if (gapIndex == Text.Length)\r\n            {\r\n                var rectangle = spriteText.GetCharacterDrawRectangle(gapIndex - 1);\r\n                return new RectangleF(rectangle.Centre.X, rectangle.Top, rectangle.Width / 2, rectangle.Height);\r\n            }\r\n\r\n            var leftRectangle = spriteText.GetCharacterDrawRectangle(gapIndex - 1);\r\n            var rightRectangle = spriteText.GetCharacterDrawRectangle(gapIndex);\r\n\r\n            float x = leftRectangle.Centre.X;\r\n            float y = Math.Min(leftRectangle.Y, rightRectangle.Y);\r\n            float width = rightRectangle.Centre.X - leftRectangle.Centre.X;\r\n            float height = Math.Max(leftRectangle.Height, rightRectangle.Height);\r\n\r\n            return new RectangleF(x, y, width, height);\r\n        }\r\n    }\r\n\r\n    public RectangleF GetRectByCharIndicator(int gapIndex)\r\n    {\r\n        if (gapIndex < 0 || gapIndex > Text.Length)\r\n            throw new ArgumentOutOfRangeException(nameof(gapIndex));\r\n\r\n        const float min_spacing_width = 1;\r\n\r\n        if (gapIndex == 0)\r\n        {\r\n            var referenceRectangle = spriteText.GetCharacterDrawRectangle(gapIndex);\r\n            return new RectangleF(referenceRectangle.X - min_spacing_width, referenceRectangle.Y, min_spacing_width, referenceRectangle.Height);\r\n        }\r\n\r\n        if (gapIndex == Text.Length)\r\n        {\r\n            var referenceRectangle = spriteText.GetCharacterDrawRectangle(gapIndex - 1);\r\n            return new RectangleF(referenceRectangle.Right, referenceRectangle.Top, min_spacing_width, referenceRectangle.Height);\r\n        }\r\n\r\n        var leftRectangle = spriteText.GetCharacterDrawRectangle(gapIndex - 1);\r\n        var rightRectangle = spriteText.GetCharacterDrawRectangle(gapIndex);\r\n        return new RectangleF(leftRectangle.Right, leftRectangle.Top, rightRectangle.X - leftRectangle.Right, leftRectangle.Y);\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Ruby tag\r\n\r\n    public RectangleF? GetRubyTagByPosition(RubyTag rubyTag) =>\r\n        spriteText.GetRubyTagPosition(rubyTag);\r\n\r\n    #endregion\r\n\r\n    #region Time tag\r\n\r\n    public TimeTag? GetTimeTagByPosition(Vector2 position)\r\n    {\r\n        var hoverIndex = getHoverIndex();\r\n        if (hoverIndex == null)\r\n            return null;\r\n\r\n        // todo: will use better way to get the time-tag\r\n        return HitObject.TimeTags.FirstOrDefault(x => x.Index == hoverIndex);\r\n\r\n        TextIndex? getHoverIndex()\r\n        {\r\n            for (int i = 0; i < Text.Length; i++)\r\n            {\r\n                foreach (var indexState in Enum.GetValues<TextIndex.IndexState>())\r\n                {\r\n                    var textIndex = new TextIndex(i, indexState);\r\n                    var triggerRange = getTriggerRange(textIndex);\r\n                    if (triggerRange.Contains(position))\r\n                        return textIndex;\r\n                }\r\n            }\r\n\r\n            // hover the last time-tag if exceed the range.\r\n            return null;\r\n\r\n            RectangleF getTriggerRange(TextIndex textIndex)\r\n            {\r\n                var rect = spriteText.GetCharacterDrawRectangle(textIndex.Index);\r\n                return TextIndexUtils.GetValueByState(textIndex,\r\n                    () => new RectangleF(rect.Left, rect.Top, rect.Width / 2, rect.Height),\r\n                    () => new RectangleF(rect.Centre.X, rect.Top, rect.Width / 2, rect.Height));\r\n            }\r\n        }\r\n    }\r\n\r\n    public Vector2 GetPositionByTimeTag(TimeTag timeTag)\r\n    {\r\n        var basePosition = spriteText.GetTimeTagPosition(timeTag.Index);\r\n        float extraPosition = extraSpacing(HitObject.TimeTags, timeTag);\r\n        return basePosition + new Vector2(extraPosition, 0);\r\n\r\n        static float extraSpacing(IList<TimeTag> timeTagsInLyric, TimeTag timeTag)\r\n        {\r\n            var textIndex = timeTag.Index;\r\n            var timeTags = TextIndexUtils.GetValueByState(textIndex, timeTagsInLyric.Reverse, () => timeTagsInLyric);\r\n            int duplicatedTagAmount = timeTags.SkipWhile(t => t != timeTag).Count(x => x.Index == textIndex) - 1;\r\n            int spacing = duplicatedTagAmount * time_tag_spacing * TextIndexUtils.GetValueByState(textIndex, 1, -1);\r\n            return spacing;\r\n        }\r\n    }\r\n\r\n    #endregion\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(ISkinSource skin, ShaderManager? shaderManager)\r\n    {\r\n        skin.GetConfig<Lyric, LyricFontInfo>(HitObject)?.BindValueChanged(e =>\r\n        {\r\n            var newConfig = e.NewValue;\r\n            if (newConfig == null)\r\n                return;\r\n\r\n            // Apply text font info\r\n            var lyricFont = newConfig.MainTextFont;\r\n            var rubyFont = newConfig.RubyTextFont;\r\n            var romanisationTextFont = newConfig.RomanisationTextFont;\r\n\r\n            Font = getFont(lyricFont.Size);\r\n            TopTextFont = getFont(rubyFont.Size);\r\n            BottomTextFont = getFont(romanisationTextFont.Size);\r\n\r\n            triggerSizeChangedEvent();\r\n\r\n            static FontUsage getFont(float? charSize = null)\r\n                => FontUsage.Default.With(size: charSize * 2);\r\n        }, true);\r\n    }\r\n\r\n    public override bool RemoveCompletedTransforms => false;\r\n\r\n    public partial class EditorLyricSpriteText : LyricSpriteText\r\n    {\r\n        public RectangleF? GetRubyTagPosition(RubyTag rubyTag)\r\n            => GetTopPositionTextDrawRectangle(RubyTagUtils.ToPositionText(rubyTag));\r\n\r\n        public Vector2 GetTimeTagPosition(TextIndex index)\r\n        {\r\n            var drawRectangle = GetCharacterDrawRectangle(index.Index);\r\n            return TextIndexUtils.GetValueByState(index, drawRectangle.BottomLeft, drawRectangle.BottomRight);\r\n        }\r\n\r\n        // todo: get romanisation position.\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Components/Lyrics/TimeTagLayer.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Components.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Components.Lyrics;\r\n\r\npublic partial class TimeTagLayer : Layer\r\n{\r\n    [Resolved]\r\n    private IPreviewLyricPositionProvider previewLyricPositionProvider { get; set; } = null!;\r\n\r\n    private readonly IBindableList<TimeTag> timeTagsBindable = new BindableList<TimeTag>();\r\n\r\n    public TimeTagLayer(Lyric lyric)\r\n        : base(lyric)\r\n    {\r\n        timeTagsBindable.BindCollectionChanged((_, _) =>\r\n        {\r\n            ScheduleAfterChildren(updateTimeTags);\r\n        });\r\n\r\n        timeTagsBindable.BindTo(lyric.TimeTagsBindable);\r\n    }\r\n\r\n    private void updateTimeTags()\r\n    {\r\n        ClearInternal();\r\n\r\n        foreach (var timeTag in timeTagsBindable)\r\n        {\r\n            var position = previewLyricPositionProvider.GetPositionByTimeTag(timeTag);\r\n            AddInternal(new DrawableTimeTag\r\n            {\r\n                Size = new Vector2(6),\r\n                TimeTag = timeTag,\r\n                Position = position,\r\n            });\r\n        }\r\n    }\r\n\r\n    public override void UpdateDisableEditState(bool editable)\r\n    {\r\n        this.FadeTo(editable ? 1 : 0.5f, 100);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Components/Lyrics/UIEventLayer.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Components.Lyrics;\r\n\r\npublic abstract partial class UIEventLayer : Layer\r\n{\r\n    protected UIEventLayer(Lyric lyric)\r\n        : base(lyric)\r\n    {\r\n    }\r\n\r\n    public sealed override void UpdateDisableEditState(bool editable)\r\n    {\r\n        // todo: should have some effect\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/BottomEditor/AdjustTimeTagBottomEditor.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose.BottomEditor.AdjustTimeTags;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose.BottomEditor;\r\n\r\npublic partial class AdjustTimeTagBottomEditor : BaseBottomEditor\r\n{\r\n    public override float ContentHeight => 100;\r\n\r\n    protected override Drawable CreateInfo()\r\n    {\r\n        // todo : waiting for implementation.\r\n        return new Container();\r\n    }\r\n\r\n    protected override Drawable CreateContent() => new AdjustTimeTagScrollContainer();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/BottomEditor/AdjustTimeTags/AdjustTimeTagBlueprintContainer.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Extensions.IEnumerableExtensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Rulesets.Edit;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\nusing osu.Game.Screens.Edit;\r\nusing osu.Game.Screens.Edit.Components.Timelines.Summary.Parts;\r\nusing osu.Game.Screens.Edit.Compose.Components;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose.BottomEditor.AdjustTimeTags;\r\n\r\npublic partial class AdjustTimeTagBlueprintContainer : BindableBlueprintContainer<TimeTag>\r\n{\r\n    [Resolved]\r\n    private AdjustTimeTagScrollContainer timeline { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private EditorClock editorClock { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private ILyricTimeTagsChangeHandler lyricTimeTagsChangeHandler { get; set; } = null!;\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(BindableList<TimeTag> timeTags)\r\n    {\r\n        // Add time-tag into blueprint container\r\n        RegisterBindable(timeTags);\r\n    }\r\n\r\n    protected override IEnumerable<SelectionBlueprint<TimeTag>> SortForMovement(IReadOnlyList<SelectionBlueprint<TimeTag>> blueprints)\r\n        => blueprints.OrderBy(b => b.Item.Index);\r\n\r\n    protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint<TimeTag> blueprint, Vector2[] originalSnapPositions)> blueprints)\r\n    {\r\n        var result = timeline.FindSnappedPositionAndTime(e.ScreenSpaceMousePosition);\r\n\r\n        if (result.Time == null)\r\n            return false;\r\n\r\n        var timeTags = blueprints.Select(x => x.blueprint).OfType<AdjustTimeTagSelectionBlueprint>().Select(x => x.Item).ToArray();\r\n        var firstTimeTag = timeTags.FirstOrDefault();\r\n        if (firstTimeTag == null)\r\n            return false;\r\n\r\n        double offset = result.Time.Value - timeline.GetPreviewTime(firstTimeTag);\r\n        if (offset == 0)\r\n            return false;\r\n\r\n        lyricTimeTagsChangeHandler.ShiftingTimeTagTime(timeTags, offset);\r\n\r\n        return true;\r\n    }\r\n\r\n    protected override SelectionBlueprintContainer CreateSelectionBlueprintContainer()\r\n        => new TimeTagEditorSelectionBlueprintContainer { RelativeSizeAxes = Axes.Both };\r\n\r\n    protected override SelectionHandler<TimeTag> CreateSelectionHandler()\r\n        => new TimeTagEditorSelectionHandler();\r\n\r\n    protected override SelectionBlueprint<TimeTag> CreateBlueprintFor(TimeTag item)\r\n        => new AdjustTimeTagSelectionBlueprint(item);\r\n\r\n    protected override DragBox CreateDragBox() => new TimelineDragBox();\r\n\r\n    protected override bool OnClick(ClickEvent e)\r\n    {\r\n        base.OnClick(e);\r\n\r\n        // skip if already have selected blueprint.\r\n        if (ClickedBlueprint != null)\r\n            return true;\r\n\r\n        // navigation to target time.\r\n        var navigationTime = timeline.FindSnappedPositionAndTime(e.ScreenSpaceMousePosition);\r\n        if (navigationTime.Time == null)\r\n            return false;\r\n\r\n        editorClock.SeekSmoothlyTo(navigationTime.Time.Value);\r\n        return true;\r\n    }\r\n\r\n    protected partial class TimeTagEditorSelectionHandler : BindableSelectionHandler\r\n    {\r\n        [Resolved]\r\n        private ILyricTimeTagsChangeHandler lyricTimeTagsChangeHandler { get; set; } = null!;\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(IEditTimeTagModeState editTimeTagModeState)\r\n        {\r\n            SelectedItems.BindTo(editTimeTagModeState.SelectedItems);\r\n        }\r\n\r\n        // for now we always allow movement. snapping is provided by the Timeline's \"distance\" snap implementation\r\n        public override bool HandleMovement(MoveSelectionEvent<TimeTag> moveEvent) => true;\r\n\r\n        protected override void DeleteItems(IEnumerable<TimeTag> items)\r\n        {\r\n            lyricTimeTagsChangeHandler.RemoveRange(items);\r\n        }\r\n\r\n        protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint<TimeTag>> selection)\r\n        {\r\n            var timeTags = selection.Select(x => x.Item).ToArray();\r\n\r\n            if (timeTags.Any(x => x.Time != null))\r\n            {\r\n                return new[]\r\n                {\r\n                    new OsuMenuItem(\"Clear time\", MenuItemType.Standard, () =>\r\n                    {\r\n                        timeTags.ForEach(x => x.Time = null);\r\n\r\n                        // todo : should re-calculate all preview position because some time-tag without position might be affected.\r\n                    }),\r\n                };\r\n            }\r\n\r\n            return base.GetContextMenuItemsForSelection(selection);\r\n        }\r\n    }\r\n\r\n    private partial class TimelineDragBox : DragBox\r\n    {\r\n        public double MinTime { get; private set; }\r\n\r\n        public double MaxTime { get; private set; }\r\n\r\n        private double? startTime;\r\n\r\n        [Resolved]\r\n        private AdjustTimeTagScrollContainer timeline { get; set; } = null!;\r\n\r\n        protected override Drawable CreateBox() => new Box\r\n        {\r\n            RelativeSizeAxes = Axes.Y,\r\n            Alpha = 0.3f,\r\n        };\r\n\r\n        public override void HandleDrag(MouseButtonEvent e)\r\n        {\r\n            startTime ??= timeline.TimeAtPosition(e.MouseDownPosition.X);\r\n            double endTime = timeline.TimeAtPosition(e.MousePosition.X);\r\n\r\n            MinTime = Math.Min(startTime.Value, endTime);\r\n            MaxTime = Math.Max(startTime.Value, endTime);\r\n\r\n            Box.X = timeline.PositionAtTime(MinTime);\r\n            Box.Width = timeline.PositionAtTime(MaxTime) - Box.X;\r\n        }\r\n\r\n        public override void Hide()\r\n        {\r\n            base.Hide();\r\n            startTime = null;\r\n        }\r\n    }\r\n\r\n    protected partial class TimeTagEditorSelectionBlueprintContainer : SelectionBlueprintContainer\r\n    {\r\n        protected override Container<SelectionBlueprint<TimeTag>> Content { get; }\r\n\r\n        public TimeTagEditorSelectionBlueprintContainer()\r\n        {\r\n            AddInternal(new TimelinePart<SelectionBlueprint<TimeTag>>(Content = new TimeTagOrderedSelectionContainer { RelativeSizeAxes = Axes.Both }) { RelativeSizeAxes = Axes.Both });\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/BottomEditor/AdjustTimeTags/AdjustTimeTagScrollContainer.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Rulesets.Edit;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\nusing osu.Game.Screens.Edit;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose.BottomEditor.AdjustTimeTags;\r\n\r\n[Cached]\r\npublic partial class AdjustTimeTagScrollContainer : TimeTagScrollContainer\r\n{\r\n    public const float TIMELINE_HEIGHT = 38;\r\n\r\n    [Resolved]\r\n    private EditorClock editorClock { get; set; } = null!;\r\n\r\n    private CurrentTimeMarker? currentTimeMarker;\r\n\r\n    public AdjustTimeTagScrollContainer()\r\n    {\r\n        Padding = new MarginPadding { Top = 10 };\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OsuColour colours, IEditTimeTagModeState editTimeTagModeState, KaraokeRulesetLyricEditorConfigManager lyricEditorConfigManager)\r\n    {\r\n        BindableZoom.BindTo(editTimeTagModeState.BindableAdjustZoom);\r\n\r\n        lyricEditorConfigManager.BindWith(KaraokeRulesetLyricEditorSetting.AdjustTimeTagShowWaveform, ShowWaveformGraph);\r\n        lyricEditorConfigManager.BindWith(KaraokeRulesetLyricEditorSetting.AdjustTimeTagWaveformOpacity, WaveformOpacity);\r\n        lyricEditorConfigManager.BindWith(KaraokeRulesetLyricEditorSetting.AdjustTimeTagShowTick, ShowTick);\r\n        lyricEditorConfigManager.BindWith(KaraokeRulesetLyricEditorSetting.AdjustTimeTagTickOpacity, TickOpacity);\r\n\r\n        AddInternal(new Box\r\n        {\r\n            Name = \"Background\",\r\n            Depth = 1,\r\n            RelativeSizeAxes = Axes.X,\r\n            Height = TIMELINE_HEIGHT,\r\n            Colour = colours.Gray3,\r\n        });\r\n    }\r\n\r\n    protected override void PostProcessContent(Container content)\r\n    {\r\n        content.Height = TIMELINE_HEIGHT;\r\n        content.AddRange(new Drawable[]\r\n        {\r\n            new AdjustTimeTagBlueprintContainer(),\r\n            currentTimeMarker = new CurrentTimeMarker(),\r\n        });\r\n    }\r\n\r\n    protected override void OnLyricChanged(Lyric newLyric)\r\n    {\r\n        // add the little bit delay to make sure that content width is not zero.\r\n        this.FadeOut(1).OnComplete(x =>\r\n        {\r\n            if (newLyric.TimeValid)\r\n            {\r\n                const float preempt_time = 200;\r\n\r\n                float position = PositionAtTime(newLyric.StartTime - preempt_time);\r\n                ScrollTo(position, false);\r\n            }\r\n\r\n            this.FadeIn(100);\r\n        });\r\n    }\r\n\r\n    protected override void UpdateAfterChildren()\r\n    {\r\n        base.UpdateAfterChildren();\r\n\r\n        float position = PositionAtTime(editorClock.CurrentTime);\r\n        currentTimeMarker?.MoveToX(position);\r\n    }\r\n\r\n    public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition)\r\n    {\r\n        double time = TimeAtPosition(Content.ToLocalSpace(screenSpacePosition).X);\r\n        return new SnapResult(screenSpacePosition, time);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/BottomEditor/AdjustTimeTags/AdjustTimeTagSelectionBlueprint.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing JetBrains.Annotations;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Cursor;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Components.Cursor;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Components.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Timeline;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\nusing osu.Game.Screens.Edit;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose.BottomEditor.AdjustTimeTags;\r\n\r\npublic partial class AdjustTimeTagSelectionBlueprint : EditableTimelineSelectionBlueprint<TimeTag>, IHasCustomTooltip<TimeTag>\r\n{\r\n    private const float time_tag_triangle_size = 10;\r\n\r\n    [UsedImplicitly]\r\n    private readonly Bindable<double?> startTime;\r\n\r\n    private readonly TimeTagPiece timeTagPiece;\r\n    private readonly TimeTagWithNoTimePiece timeTagWithNoTimePiece;\r\n    private readonly OsuSpriteText timeTagText;\r\n\r\n    public AdjustTimeTagSelectionBlueprint(TimeTag item)\r\n        : base(item)\r\n    {\r\n        startTime = item.TimeBindable.GetBoundCopy();\r\n        RelativeSizeAxes = Axes.None;\r\n        AutoSizeAxes = Axes.X;\r\n\r\n        // todo: not really sure why it fix the issue. should have more checks about this.\r\n        Height = AdjustTimeTagScrollContainer.TIMELINE_HEIGHT - 1;\r\n\r\n        AddRangeInternal(new Drawable[]\r\n        {\r\n            timeTagPiece = new TimeTagPiece(item)\r\n            {\r\n                Anchor = Anchor.CentreLeft,\r\n                Origin = TextIndexUtils.GetValueByState(item.Index, Anchor.CentreLeft, Anchor.CentreRight),\r\n            },\r\n            timeTagWithNoTimePiece = new TimeTagWithNoTimePiece(item)\r\n            {\r\n                Anchor = Anchor.BottomLeft,\r\n                Origin = TextIndexUtils.GetValueByState(item.Index, Anchor.BottomLeft, Anchor.BottomRight),\r\n            },\r\n            timeTagText = new OsuSpriteText\r\n            {\r\n                Text = \"Text\",\r\n                Anchor = Anchor.BottomLeft,\r\n                Origin = TextIndexUtils.GetValueByState(item.Index, Anchor.TopLeft, Anchor.TopRight),\r\n                Y = 10,\r\n            },\r\n        });\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(EditorClock clock, AdjustTimeTagScrollContainer timeline, ILyricCaretState lyricCaretState, OsuColour colours)\r\n    {\r\n        // todo : should be able to let user able to select show from ruby or main text.\r\n        timeTagText.Text = LyricUtils.GetTimeTagDisplayRubyText(lyricCaretState.BindableFocusedLyric.Value!, Item);\r\n\r\n        timeTagPiece.Clock = clock;\r\n        timeTagPiece.Colour = colours.BlueLight;\r\n\r\n        timeTagWithNoTimePiece.Colour = colours.Red;\r\n        startTime.BindValueChanged(_ =>\r\n        {\r\n            bool hasValue = hasTime();\r\n\r\n            // update show time-tag style.\r\n            switch (hasValue)\r\n            {\r\n                case true:\r\n                    timeTagPiece.Show();\r\n                    timeTagWithNoTimePiece.Hide();\r\n                    break;\r\n\r\n                case false:\r\n                    timeTagPiece.Hide();\r\n                    timeTagWithNoTimePiece.Show();\r\n                    break;\r\n            }\r\n\r\n            // should wait until all time-tag time has been modified.\r\n            Schedule(() =>\r\n            {\r\n                double previewTime = timeline.GetPreviewTime(Item);\r\n\r\n                // adjust position.\r\n                X = (float)previewTime;\r\n\r\n                // make tickle effect.\r\n                timeTagPiece.ClearTransforms();\r\n\r\n                using (timeTagPiece.BeginAbsoluteSequence(previewTime))\r\n                {\r\n                    timeTagPiece.Colour = colours.BlueLight;\r\n                    timeTagPiece.FlashColour(colours.PurpleDark, 750, Easing.OutQuint);\r\n                }\r\n            });\r\n        }, true);\r\n    }\r\n\r\n    protected override Drawable GetInteractDrawable() => hasTime() ? timeTagPiece : timeTagWithNoTimePiece;\r\n\r\n    public ITooltip<TimeTag> GetCustomTooltip() => new TimeTagTooltip();\r\n\r\n    public TimeTag TooltipContent => Item;\r\n\r\n    private bool hasTime() => startTime.Value.HasValue;\r\n\r\n    public partial class TimeTagPiece : CompositeDrawable\r\n    {\r\n        public TimeTagPiece(TimeTag timeTag)\r\n        {\r\n            RelativeSizeAxes = Axes.Y;\r\n            Width = time_tag_triangle_size;\r\n\r\n            var textIndex = timeTag.Index;\r\n            InternalChildren = new Drawable[]\r\n            {\r\n                new Box\r\n                {\r\n                    RelativeSizeAxes = Axes.Y,\r\n                    Width = 1.5f,\r\n                    Anchor = TextIndexUtils.GetValueByState(textIndex, Anchor.CentreLeft, Anchor.CentreRight),\r\n                    Origin = TextIndexUtils.GetValueByState(textIndex, Anchor.CentreLeft, Anchor.CentreRight),\r\n                },\r\n                new DrawableTextIndex\r\n                {\r\n                    Size = new Vector2(time_tag_triangle_size),\r\n                    Anchor = Anchor.BottomCentre,\r\n                    Origin = Anchor.BottomCentre,\r\n                    State = textIndex.State,\r\n                },\r\n            };\r\n        }\r\n\r\n        public override bool RemoveCompletedTransforms => false;\r\n    }\r\n\r\n    public partial class TimeTagWithNoTimePiece : CompositeDrawable\r\n    {\r\n        public TimeTagWithNoTimePiece(TimeTag timeTag)\r\n        {\r\n            AutoSizeAxes = Axes.Y;\r\n            Width = time_tag_triangle_size;\r\n\r\n            var state = timeTag.Index.State;\r\n            InternalChildren = new Drawable[]\r\n            {\r\n                new DrawableTextIndex\r\n                {\r\n                    Size = new Vector2(time_tag_triangle_size),\r\n                    Anchor = Anchor.BottomCentre,\r\n                    Origin = Anchor.BottomCentre,\r\n                    State = state,\r\n                },\r\n            };\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/BottomEditor/AdjustTimeTags/CurrentTimeMarker.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Game.Graphics;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose.BottomEditor.AdjustTimeTags;\r\n\r\npublic partial class CurrentTimeMarker : CompositeDrawable\r\n{\r\n    private const float triangle_width = 15;\r\n    private const float triangle_height = 10;\r\n    private const float bar_width = 2;\r\n\r\n    public CurrentTimeMarker()\r\n    {\r\n        RelativeSizeAxes = Axes.Y;\r\n        Size = new Vector2(triangle_width, 1);\r\n\r\n        InternalChildren = new Drawable[]\r\n        {\r\n            new Box\r\n            {\r\n                Anchor = Anchor.Centre,\r\n                Origin = Anchor.Centre,\r\n                RelativeSizeAxes = Axes.Y,\r\n                Width = bar_width,\r\n            },\r\n            new Triangle\r\n            {\r\n                Anchor = Anchor.TopCentre,\r\n                Origin = Anchor.BottomCentre,\r\n                Size = new Vector2(triangle_width, triangle_height),\r\n                Scale = new Vector2(1, -1),\r\n            },\r\n            new Triangle\r\n            {\r\n                Anchor = Anchor.BottomCentre,\r\n                Origin = Anchor.BottomCentre,\r\n                Size = new Vector2(triangle_width, triangle_height),\r\n            },\r\n        };\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OsuColour colours)\r\n    {\r\n        Colour = colours.RedDark;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/BottomEditor/AdjustTimeTags/TimeTagOrderedSelectionContainer.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Rulesets.Edit;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose.BottomEditor.AdjustTimeTags;\r\n\r\n/// <summary>\r\n/// A container for <see cref=\"SelectionBlueprint{T}\"/> ordered by their <see cref=\"TimeTag\"/> start times.\r\n/// </summary>\r\npublic partial class TimeTagOrderedSelectionContainer : Container<SelectionBlueprint<TimeTag>>\r\n{\r\n    public override void Add(SelectionBlueprint<TimeTag> drawable)\r\n    {\r\n        SortInternal();\r\n        base.Add(drawable);\r\n    }\r\n\r\n    public override bool Remove(SelectionBlueprint<TimeTag> drawable, bool disposeImmediately)\r\n    {\r\n        SortInternal();\r\n        return base.Remove(drawable, disposeImmediately);\r\n    }\r\n\r\n    protected override int Compare(Drawable x, Drawable y)\r\n    {\r\n        var xObj = ((SelectionBlueprint<TimeTag>)x).Item;\r\n        var yObj = ((SelectionBlueprint<TimeTag>)y).Item;\r\n\r\n        double xTime = xObj.Time ?? 0;\r\n        double yTime = yObj.Time ?? 0;\r\n\r\n        // Put earlier blueprints towards the end of the list, so they handle input first\r\n        int result = yTime.CompareTo(xTime);\r\n        if (result != 0) return result;\r\n\r\n        return CompareReverseChildID(x, y);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/BottomEditor/BaseBottomEditor.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Input.Events;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose.BottomEditor;\r\n\r\npublic abstract partial class BaseBottomEditor : CompositeDrawable\r\n{\r\n    private const int info_part_spacing = 210;\r\n\r\n    public abstract float ContentHeight { get; }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(ILyricEditorState state, LyricEditorColourProvider colourProvider)\r\n    {\r\n        Height = ContentHeight;\r\n        RelativeSizeAxes = Axes.X;\r\n\r\n        InternalChildren = new Drawable[]\r\n        {\r\n            new Box\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n                Colour = colourProvider.Background5(state.Mode),\r\n            },\r\n            new GridContainer\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n                ColumnDimensions = new[]\r\n                {\r\n                    new Dimension(GridSizeMode.Absolute, info_part_spacing),\r\n                    new Dimension(),\r\n                },\r\n                Content = new[]\r\n                {\r\n                    new[]\r\n                    {\r\n                        CreateInfo().With(x =>\r\n                        {\r\n                            x.RelativeSizeAxes = Axes.Both;\r\n                        }),\r\n                        new Container\r\n                        {\r\n                            Masking = true,\r\n                            RelativeSizeAxes = Axes.Both,\r\n                            Child = CreateContent().With(x =>\r\n                            {\r\n                                x.RelativeSizeAxes = Axes.Both;\r\n                            }),\r\n                        },\r\n                    },\r\n                },\r\n            },\r\n        };\r\n    }\r\n\r\n    protected abstract Drawable CreateInfo();\r\n\r\n    protected abstract Drawable CreateContent();\r\n\r\n    protected override bool OnDragStart(DragStartEvent e)\r\n    {\r\n        // prevent scroll container drag event.\r\n        return true;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/BottomEditor/NoteBottomEditor.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose.BottomEditor.Notes;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose.BottomEditor;\r\n\r\npublic partial class NoteBottomEditor : BaseBottomEditor\r\n{\r\n    public override float ContentHeight => 180;\r\n\r\n    protected override Drawable CreateInfo()\r\n    {\r\n        // todo : waiting for implementation.\r\n        return new Container();\r\n    }\r\n\r\n    protected override Drawable CreateContent() => new NoteEditor();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/BottomEditor/Notes/NoteEditPopover.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Graphics.Containers;\r\nusing osu.Game.Graphics.UserInterfaceV2;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Notes;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose.BottomEditor.Notes;\r\n\r\npublic partial class NoteEditPopover : OsuPopover\r\n{\r\n    public NoteEditPopover(Note note)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(note);\r\n\r\n        Child = new OsuScrollContainer\r\n        {\r\n            Height = 320,\r\n            Width = 300,\r\n            Child = new FillFlowContainer<Section>\r\n            {\r\n                Direction = FillDirection.Vertical,\r\n                RelativeSizeAxes = Axes.X,\r\n                AutoSizeAxes = Axes.Y,\r\n                Children = new Section[]\r\n                {\r\n                    new NoteSection(note),\r\n                },\r\n            },\r\n        };\r\n    }\r\n\r\n    private partial class NoteSection : Section\r\n    {\r\n        [Resolved]\r\n        private INotePropertyChangeHandler notePropertyChangeHandler { get; set; } = null!;\r\n\r\n        protected override LocalisableString Title => \"Note property\";\r\n\r\n        public NoteSection(Note note)\r\n        {\r\n            LabelledTextBox text;\r\n            LabelledTextBox rubyText;\r\n            LabelledSwitchButton display;\r\n\r\n            Children = new Drawable[]\r\n            {\r\n                text = new LabelledTextBox\r\n                {\r\n                    Label = \"Text\",\r\n                    Description = \"The text display on the note.\",\r\n                    Current = note.TextBindable,\r\n                    TabbableContentContainer = this,\r\n                },\r\n                rubyText = new LabelledTextBox\r\n                {\r\n                    Label = \"Ruby text\",\r\n                    Description = \"Should place something like ruby, 拼音 or ふりがな.\",\r\n                    Current = note.RubyTextBindable,\r\n                    TabbableContentContainer = this,\r\n                },\r\n                display = new LabelledSwitchButton\r\n                {\r\n                    Label = \"Display\",\r\n                    Description = \"This note will be hidden and not scorable if not display.\",\r\n                    Current = note.DisplayBindable,\r\n                },\r\n            };\r\n\r\n            ScheduleAfterChildren(() =>\r\n            {\r\n                GetContainingFocusManager().ChangeFocus(text);\r\n            });\r\n\r\n            text.OnCommit += (sender, newText) =>\r\n            {\r\n                if (!newText)\r\n                    return;\r\n\r\n                string text = sender.Text.Trim();\r\n                notePropertyChangeHandler.ChangeText(text);\r\n            };\r\n\r\n            rubyText.OnCommit += (sender, newText) =>\r\n            {\r\n                if (!newText)\r\n                    return;\r\n\r\n                string text = sender.Text.Trim();\r\n                notePropertyChangeHandler.ChangeRubyText(text);\r\n            };\r\n\r\n            display.Current.BindValueChanged(v =>\r\n            {\r\n                notePropertyChangeHandler.ChangeDisplayState(v.NewValue);\r\n            });\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/BottomEditor/Notes/NoteEditor.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Specialized;\r\nusing System.Diagnostics;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Rulesets.Karaoke.Edit;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\nusing osu.Game.Rulesets.Karaoke.Timing;\r\nusing osu.Game.Rulesets.Karaoke.UI.Position;\r\nusing osu.Game.Rulesets.Karaoke.UI.Scrolling;\r\nusing osu.Game.Rulesets.Objects;\r\nusing osu.Game.Rulesets.UI;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose.BottomEditor.Notes;\r\n\r\n[Cached]\r\npublic partial class NoteEditor : CompositeDrawable\r\n{\r\n    private const int columns = 9;\r\n\r\n    [Cached(typeof(INotePositionInfo))]\r\n    private readonly PreviewNotePositionInfo notePositionInfo = new();\r\n\r\n    [Resolved]\r\n    private EditorBeatmap beatmap { get; set; } = null!;\r\n\r\n    private readonly IBindable<Lyric?> bindableFocusedLyric = new Bindable<Lyric?>();\r\n\r\n    [Cached]\r\n    private readonly BindableList<Note> bindableNotes = new();\r\n\r\n    [Cached(typeof(Playfield))]\r\n    public EditorNotePlayfield Playfield { get; }\r\n\r\n    public NoteEditor()\r\n    {\r\n        InternalChild = new Container\r\n        {\r\n            Name = \"Content\",\r\n            RelativeSizeAxes = Axes.Both,\r\n            Children = new Drawable[]\r\n            {\r\n                // layers below playfield\r\n                Playfield = new EditorNotePlayfield(columns)\r\n                {\r\n                    Anchor = Anchor.Centre,\r\n                    Origin = Anchor.Centre,\r\n                },\r\n                // layers above playfield\r\n                new EditNoteBlueprintContainer\r\n                {\r\n                    Anchor = Anchor.Centre,\r\n                    Origin = Anchor.Centre,\r\n                },\r\n            },\r\n        };\r\n\r\n        bindableFocusedLyric.BindValueChanged(e =>\r\n        {\r\n            bindableNotes.Clear();\r\n\r\n            var lyric = e.NewValue;\r\n            if (lyric == null)\r\n                return;\r\n\r\n            if (lyric.TimeValid)\r\n            {\r\n                // set the clock to the lyric start time\r\n                Playfield.Clock = new StopClock(lyric.StartTime);\r\n            }\r\n\r\n            // add all matched notes into playfield\r\n            var notes = EditorBeatmapUtils.GetNotesByLyric(beatmap, lyric);\r\n            bindableNotes.AddRange(notes);\r\n        });\r\n\r\n        bindableNotes.BindCollectionChanged((_, args) =>\r\n        {\r\n            switch (args.Action)\r\n            {\r\n                case NotifyCollectionChangedAction.Add:\r\n                    Debug.Assert(args.NewItems != null);\r\n\r\n                    foreach (var obj in args.NewItems.OfType<Note>())\r\n                        Playfield.Add(obj);\r\n\r\n                    break;\r\n\r\n                case NotifyCollectionChangedAction.Remove:\r\n                    Debug.Assert(args.OldItems != null);\r\n\r\n                    foreach (var obj in args.OldItems.OfType<Note>())\r\n                        Playfield.Remove(obj);\r\n\r\n                    break;\r\n            }\r\n        });\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(ILyricCaretState lyricCaretState)\r\n    {\r\n        bindableFocusedLyric.BindTo(lyricCaretState.BindableFocusedLyric);\r\n\r\n        beatmap.HitObjectAdded += addHitObject;\r\n        beatmap.HitObjectRemoved += removeHitObject;\r\n    }\r\n\r\n    private void addHitObject(HitObject hitObject)\r\n    {\r\n        if (hitObject is not Note note)\r\n            return;\r\n\r\n        if (note.ReferenceLyric != bindableFocusedLyric.Value)\r\n            return;\r\n\r\n        bindableNotes.Add(note);\r\n    }\r\n\r\n    private void removeHitObject(HitObject hitObject)\r\n    {\r\n        if (hitObject is not Note note)\r\n            return;\r\n\r\n        if (note.ReferenceLyric != bindableFocusedLyric.Value)\r\n            return;\r\n\r\n        bindableNotes.Remove(note);\r\n    }\r\n\r\n    protected override void Dispose(bool isDisposing)\r\n    {\r\n        base.Dispose(isDisposing);\r\n\r\n        beatmap.HitObjectAdded -= addHitObject;\r\n        beatmap.HitObjectRemoved -= removeHitObject;\r\n    }\r\n\r\n    private class PreviewNotePositionInfo : INotePositionInfo\r\n    {\r\n        public IBindable<NotePositionCalculator> Position { get; } = new Bindable<NotePositionCalculator>(new NotePositionCalculator(columns, 12, ScrollingNotePlayfield.COLUMN_SPACING));\r\n\r\n        public NotePositionCalculator Calculator => Position.Value;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/BottomEditor/Notes/NoteEditorBlueprintContainer.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Game.Rulesets.Edit;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\nusing osu.Game.Screens.Edit.Compose.Components;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose.BottomEditor.Notes;\r\n\r\ninternal partial class EditNoteBlueprintContainer : BindableBlueprintContainer<Note>\r\n{\r\n    protected override SelectionBlueprint<Note> CreateBlueprintFor(Note hitObject)\r\n        => new NoteEditorSelectionBlueprint(hitObject);\r\n\r\n    protected override SelectionHandler<Note> CreateSelectionHandler() => new NoteEditorSelectionHandler();\r\n\r\n    protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint<Note> blueprint, Vector2[] originalSnapPositions)> blueprints)\r\n    {\r\n        // todo: implement able to drag to change the tone.\r\n        return false;\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(BindableList<Note> notes)\r\n    {\r\n        // Add time-tag into blueprint container\r\n        RegisterBindable(notes);\r\n    }\r\n\r\n    protected partial class NoteEditorSelectionHandler : BindableSelectionHandler\r\n    {\r\n        [BackgroundDependencyLoader]\r\n        private void load(IEditNoteModeState editNoteModeState)\r\n        {\r\n            SelectedItems.BindTo(editNoteModeState.SelectedItems);\r\n        }\r\n\r\n        protected override void DeleteItems(IEnumerable<Note> items)\r\n        {\r\n            // todo : delete notes\r\n            foreach (var item in items)\r\n            {\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/BottomEditor/Notes/NoteEditorSelectionBlueprint.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Extensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Cursor;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Rulesets.Edit;\r\nusing osu.Game.Rulesets.Karaoke.Edit;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Blueprints.Notes;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Blueprints.Notes.Components;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Notes;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\nusing osu.Game.Rulesets.Karaoke.UI.Position;\r\nusing osu.Game.Rulesets.UI;\r\nusing osu.Game.Rulesets.UI.Scrolling;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose.BottomEditor.Notes;\r\n\r\n/// <summary>\r\n/// Copy from <see cref=\"NoteSelectionBlueprint\"/>\r\n/// </summary>\r\npublic partial class NoteEditorSelectionBlueprint : SelectionBlueprint<Note>, IHasPopover\r\n{\r\n    private readonly IBindable<double> timeRange = new BindableDouble();\r\n    private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();\r\n\r\n    private float scrollLength => Parent.DrawWidth;\r\n\r\n    private bool axisInverted => direction.Value == ScrollingDirection.Right;\r\n\r\n    [Resolved]\r\n    private INotesChangeHandler notesChangeHandler { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private INotePropertyChangeHandler notePropertyChangeHandler { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private IScrollingInfo scrollingInfo { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private INotePositionInfo notePositionInfo { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private IEditNoteModeState editNoteModeState { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private Playfield playfield { get; set; } = null!;\r\n\r\n    protected ScrollingHitObjectContainer HitObjectContainer => ((EditorNotePlayfield)playfield).HitObjectContainer;\r\n\r\n    public NoteEditorSelectionBlueprint(Note note)\r\n        : base(note)\r\n    {\r\n        RelativeSizeAxes = Axes.None;\r\n        AddInternal(new EditBodyPiece\r\n        {\r\n            RelativeSizeAxes = Axes.Both,\r\n        });\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        direction.BindTo(scrollingInfo.Direction);\r\n        timeRange.BindTo(scrollingInfo.TimeRange);\r\n    }\r\n\r\n    protected override void Update()\r\n    {\r\n        base.Update();\r\n\r\n        var anchor = scrollingInfo.Direction.Value == ScrollingDirection.Left ? Anchor.CentreLeft : Anchor.CentreRight;\r\n        Anchor = Origin = anchor;\r\n\r\n        Position = Parent.ToLocalSpace(HitObjectContainer.ScreenSpacePositionAtTime(Item.StartTime)) - AnchorPosition;\r\n        Y += notePositionInfo.Calculator.YPositionAt(Item.Tone);\r\n\r\n        Width = HitObjectContainer.LengthAtTime(Item.StartTime, Item.EndTime);\r\n        Height = notePositionInfo.Calculator.ColumnHeight;\r\n    }\r\n\r\n    public override MenuItem[] ContextMenuItems => new MenuItem[]\r\n    {\r\n        new OsuMenuItem(Item.Display ? \"Hide\" : \"Show\", Item.Display ? MenuItemType.Destructive : MenuItemType.Standard, () => notePropertyChangeHandler.ChangeDisplayState(!Item.Display)),\r\n        new OsuMenuItem(\"Split\", MenuItemType.Destructive, () => notesChangeHandler.Split()),\r\n    };\r\n\r\n    public Popover GetPopover() => new NoteEditPopover(Item);\r\n\r\n    protected override bool OnClick(ClickEvent e)\r\n    {\r\n        // should only select current note before open the popover because note change handler will change property in all selected notes.\r\n        editNoteModeState.SelectedItems.Clear();\r\n        editNoteModeState.SelectedItems.Add(Item);\r\n\r\n        this.ShowPopover();\r\n        return base.OnClick(e);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/BottomEditor/RecordingTimeTagBottomEditor.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose.BottomEditor.RecordingTimeTags;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose.BottomEditor;\r\n\r\npublic partial class RecordingTimeTagBottomEditor : BaseBottomEditor\r\n{\r\n    public override float ContentHeight => 100;\r\n\r\n    protected override Drawable CreateInfo()\r\n    {\r\n        // todo : waiting for implementation.\r\n        return new Container();\r\n    }\r\n\r\n    protected override Drawable CreateContent() => new RecordingTimeTagScrollContainer();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/BottomEditor/RecordingTimeTags/CentreMarker.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Game.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose.BottomEditor.RecordingTimeTags;\r\n\r\npublic partial class CentreMarker : CompositeDrawable\r\n{\r\n    private const float bar_width = 2;\r\n\r\n    public CentreMarker()\r\n    {\r\n        Anchor = Anchor.Centre;\r\n        Origin = Anchor.Centre;\r\n        RelativeSizeAxes = Axes.Y;\r\n        InternalChild = new Box\r\n        {\r\n            Anchor = Anchor.Centre,\r\n            Origin = Anchor.Centre,\r\n            RelativeSizeAxes = Axes.Y,\r\n            Width = bar_width,\r\n        };\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OsuColour colours)\r\n    {\r\n        Colour = colours.RedDark;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/BottomEditor/RecordingTimeTags/RecordingTimeTagPart.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Cursor;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Components.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\nusing osu.Game.Screens.Edit;\r\nusing osu.Game.Screens.Edit.Components.Timelines.Summary.Parts;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose.BottomEditor.RecordingTimeTags;\r\n\r\npublic partial class RecordingTimeTagPart : TimelinePart\r\n{\r\n    private readonly IBindable<Lyric?> bindableFocusedLyric = new Bindable<Lyric?>();\r\n\r\n    public RecordingTimeTagPart()\r\n    {\r\n        RelativeSizeAxes = Axes.Both;\r\n    }\r\n\r\n    protected override void LoadBeatmap(EditorBeatmap beatmap)\r\n    {\r\n        base.LoadBeatmap(beatmap);\r\n\r\n        bindableFocusedLyric.BindValueChanged(e =>\r\n        {\r\n            Clear();\r\n\r\n            var lyric = e.NewValue;\r\n            if (lyric == null)\r\n                return;\r\n\r\n            foreach (var timeTag in lyric.TimeTags)\r\n            {\r\n                Add(new RecordingTimeTagVisualization(lyric, timeTag));\r\n            }\r\n\r\n            Add(new CurrentRecordingTimeTagVisualization(lyric));\r\n        });\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(ILyricCaretState lyricCaretState)\r\n    {\r\n        bindableFocusedLyric.BindTo(lyricCaretState.BindableFocusedLyric);\r\n    }\r\n\r\n    private partial class CurrentRecordingTimeTagVisualization : CompositeDrawable\r\n    {\r\n        private IBindable<ICaretPosition?> position = null!;\r\n\r\n        private readonly Lyric lyric;\r\n\r\n        private readonly DrawableTimeTag drawableTimeTag;\r\n\r\n        public CurrentRecordingTimeTagVisualization(Lyric lyric)\r\n        {\r\n            this.lyric = lyric;\r\n\r\n            Anchor = Anchor.BottomLeft;\r\n            RelativePositionAxes = Axes.X;\r\n            Size = new Vector2(RecordingTimeTagScrollContainer.TIMELINE_HEIGHT / 2);\r\n\r\n            InternalChild = drawableTimeTag = new DrawableTimeTag\r\n            {\r\n                Name = \"Time tag triangle\",\r\n                Anchor = Anchor.Centre,\r\n                Origin = Anchor.Centre,\r\n                RelativeSizeAxes = Axes.Both,\r\n                TimeTagColourFunc = (timeTag, colours) => colours.GetRecordingTimeTagCaretColour(timeTag),\r\n            };\r\n        }\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(RecordingTimeTagScrollContainer timeline, ILyricCaretState lyricCaretState)\r\n        {\r\n            position = lyricCaretState.BindableCaretPosition.GetBoundCopy();\r\n            position.BindValueChanged(e =>\r\n            {\r\n                if (e.NewValue is not RecordingTimeTagCaretPosition recordingTimeTagCaretPosition)\r\n                    return;\r\n\r\n                if (recordingTimeTagCaretPosition.Lyric != lyric)\r\n                {\r\n                    Hide();\r\n                    return;\r\n                }\r\n\r\n                var timeTag = recordingTimeTagCaretPosition.TimeTag;\r\n                var textIndex = timeTag.Index;\r\n\r\n                Origin = TextIndexUtils.GetValueByState(textIndex, Anchor.BottomLeft, Anchor.BottomRight);\r\n                drawableTimeTag.TimeTag = timeTag;\r\n\r\n                if (timeTag.Time.HasValue)\r\n                {\r\n                    Show();\r\n                    this.MoveToX((float)timeline.GetPreviewTime(timeTag), 100, Easing.OutCubic);\r\n                }\r\n                else\r\n                {\r\n                    Hide();\r\n                }\r\n            });\r\n        }\r\n    }\r\n\r\n    private partial class RecordingTimeTagVisualization : CompositeDrawable, IHasContextMenu\r\n    {\r\n        [Resolved]\r\n        private ILyricCaretState lyricCaretState { get; set; } = null!;\r\n\r\n        [Resolved]\r\n        private ILyricTimeTagsChangeHandler lyricTimeTagsChangeHandler { get; set; } = null!;\r\n\r\n        private readonly Bindable<double?> bindableTime;\r\n\r\n        private readonly Lyric lyric;\r\n        private readonly TimeTag timeTag;\r\n\r\n        public RecordingTimeTagVisualization(Lyric lyric, TimeTag timeTag)\r\n        {\r\n            this.lyric = lyric;\r\n            this.timeTag = timeTag;\r\n\r\n            var textIndex = timeTag.Index;\r\n\r\n            Anchor = Anchor.CentreLeft;\r\n            Origin = TextIndexUtils.GetValueByState(textIndex, Anchor.CentreLeft, Anchor.CentreRight);\r\n\r\n            RelativePositionAxes = Axes.X;\r\n            Size = new Vector2(RecordingTimeTagScrollContainer.TIMELINE_HEIGHT);\r\n\r\n            bindableTime = timeTag.TimeBindable.GetBoundCopy();\r\n            InternalChildren = new Drawable[]\r\n            {\r\n                new DrawableTimeTag\r\n                {\r\n                    Name = \"Time tag triangle\",\r\n                    Anchor = Anchor.Centre,\r\n                    Origin = Anchor.Centre,\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    TimeTag = timeTag,\r\n                },\r\n                new OsuSpriteText\r\n                {\r\n                    Text = LyricUtils.GetTimeTagDisplayRubyText(lyric, timeTag),\r\n                    Anchor = TextIndexUtils.GetValueByState(textIndex, Anchor.BottomLeft, Anchor.BottomRight),\r\n                    Origin = TextIndexUtils.GetValueByState(textIndex, Anchor.TopLeft, Anchor.TopRight),\r\n                },\r\n            };\r\n        }\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(RecordingTimeTagScrollContainer timeline)\r\n        {\r\n            bindableTime.BindValueChanged(e =>\r\n            {\r\n                bool hasValue = e.NewValue.HasValue;\r\n                Alpha = hasValue ? 1 : 0;\r\n\r\n                if (!hasValue)\r\n                    return;\r\n\r\n                // should wait until all time-tag time has been modified.\r\n                Schedule(() =>\r\n                {\r\n                    double previewTime = timeline.GetPreviewTime(timeTag);\r\n\r\n                    // adjust position.\r\n                    X = (float)previewTime;\r\n                });\r\n            }, true);\r\n        }\r\n\r\n        protected override bool OnClick(ClickEvent e)\r\n        {\r\n            lyricCaretState.MoveCaretToTargetPosition(lyric, timeTag);\r\n\r\n            return base.OnClick(e);\r\n        }\r\n\r\n        public MenuItem[] ContextMenuItems =>\r\n            new MenuItem[]\r\n            {\r\n                new OsuMenuItem(\"Clear time\", MenuItemType.Destructive, () =>\r\n                {\r\n                    lyricTimeTagsChangeHandler.ClearTimeTagTime(timeTag);\r\n                }),\r\n            };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/BottomEditor/RecordingTimeTags/RecordingTimeTagScrollContainer.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Game.Extensions;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose.BottomEditor.RecordingTimeTags;\r\n\r\n[Cached]\r\npublic partial class RecordingTimeTagScrollContainer : TimeTagScrollContainer\r\n{\r\n    private const float time_tag_visualisation_spacing = 60;\r\n    public const float TIMELINE_HEIGHT = 20;\r\n\r\n    [Resolved]\r\n    private EditorClock editorClock { get; set; } = null!;\r\n\r\n    /// <summary>\r\n    /// The timeline's scroll position in the last frame.\r\n    /// </summary>\r\n    private double lastScrollPosition;\r\n\r\n    /// <summary>\r\n    /// The track time in the last frame.\r\n    /// </summary>\r\n    private double lastTrackTime;\r\n\r\n    /// <summary>\r\n    /// Whether the user is currently dragging the timeline.\r\n    /// </summary>\r\n    private bool handlingDragInput;\r\n\r\n    /// <summary>\r\n    /// Whether the track was playing before a user drag event.\r\n    /// </summary>\r\n    private bool trackWasPlaying;\r\n\r\n    private readonly CentreMarker centreMarker;\r\n\r\n    private OsuSpriteText trackTimer = null!;\r\n\r\n    public RecordingTimeTagScrollContainer()\r\n    {\r\n        // We don't want the centre marker to scroll\r\n        AddInternal(centreMarker = new CentreMarker());\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OsuColour colours, IEditTimeTagModeState editTimeTagModeState, KaraokeRulesetLyricEditorConfigManager lyricEditorConfigManager)\r\n    {\r\n        BindableZoom.BindTo(editTimeTagModeState.BindableRecordZoom);\r\n\r\n        lyricEditorConfigManager.BindWith(KaraokeRulesetLyricEditorSetting.RecordingTimeTagShowWaveform, ShowWaveformGraph);\r\n        lyricEditorConfigManager.BindWith(KaraokeRulesetLyricEditorSetting.RecordingTimeTagWaveformOpacity, WaveformOpacity);\r\n        lyricEditorConfigManager.BindWith(KaraokeRulesetLyricEditorSetting.RecordingTimeTagShowTick, ShowTick);\r\n        lyricEditorConfigManager.BindWith(KaraokeRulesetLyricEditorSetting.RecordingTimeTagTickOpacity, TickOpacity);\r\n\r\n        AddRangeInternal(new Drawable[]\r\n        {\r\n            new TimeTagsVisualisation\r\n            {\r\n                Anchor = Anchor.TopCentre,\r\n                Origin = Anchor.TopLeft,\r\n                Y = 5,\r\n            },\r\n            new Box\r\n            {\r\n                Name = \"Background\",\r\n                Depth = 1,\r\n                Y = time_tag_visualisation_spacing,\r\n                RelativeSizeAxes = Axes.X,\r\n                Height = TIMELINE_HEIGHT,\r\n                Colour = colours.Gray3,\r\n            },\r\n            trackTimer = new OsuSpriteText\r\n            {\r\n                Anchor = Anchor.BottomCentre,\r\n                Origin = Anchor.BottomLeft,\r\n                Colour = colours.Red,\r\n                X = 5,\r\n                Font = OsuFont.GetFont(size: 16, fixedWidth: true),\r\n            },\r\n        });\r\n    }\r\n\r\n    protected override void PostProcessContent(Container content)\r\n    {\r\n        content.Height = TIMELINE_HEIGHT;\r\n        content.Y = time_tag_visualisation_spacing;\r\n        content.AddRange(new[]\r\n        {\r\n            centreMarker.CreateProxy(),\r\n            new RecordingTimeTagPart(),\r\n        });\r\n    }\r\n\r\n    protected override void OnLyricChanged(Lyric newLyric)\r\n    {\r\n    }\r\n\r\n    protected override void Update()\r\n    {\r\n        base.Update();\r\n\r\n        // The extrema of track time should be positioned at the centre of the container when scrolled to the start or end\r\n        Content.Margin = new MarginPadding { Horizontal = DrawWidth / 2 };\r\n\r\n        trackTimer.Text = editorClock.CurrentTime.ToEditorFormattedString();\r\n\r\n        // This needs to happen after transforms are updated, but before the scroll position is updated in base.UpdateAfterChildren\r\n        if (editorClock.IsRunning)\r\n            scrollToTrackTime();\r\n    }\r\n\r\n    protected override void UpdateAfterChildren()\r\n    {\r\n        base.UpdateAfterChildren();\r\n\r\n        if (handlingDragInput)\r\n            seekTrackToCurrent();\r\n        else if (!editorClock.IsRunning)\r\n        {\r\n            // The track isn't running. There are three cases we have to be wary of:\r\n            // 1) The user flick-drags on this timeline and we are applying an interpolated seek on the clock, until interrupted by 2 or 3.\r\n            // 2) The user changes the track time through some other means (scrolling in the editor or overview timeline; clicking a hitobject etc.). We want the timeline to track the clock's time.\r\n            // 3) An ongoing seek transform is running from an external seek. We want the timeline to track the clock's time.\r\n\r\n            // The simplest way to cover the first two cases is by checking whether the scroll position has changed and the audio hasn't been changed externally\r\n            // Checking IsSeeking covers the third case, where the transform may not have been applied yet.\r\n            if (Current != lastScrollPosition && editorClock.CurrentTime == lastTrackTime && !editorClock.IsSeeking)\r\n                seekTrackToCurrent();\r\n            else\r\n                scrollToTrackTime();\r\n        }\r\n\r\n        lastScrollPosition = Current;\r\n        lastTrackTime = editorClock.CurrentTime;\r\n    }\r\n\r\n    private void seekTrackToCurrent()\r\n    {\r\n        double target = TimeAtPosition(Current);\r\n        editorClock.Seek(Math.Min(editorClock.TrackLength, target));\r\n    }\r\n\r\n    private void scrollToTrackTime()\r\n    {\r\n        if (editorClock.TrackLength == 0)\r\n            return;\r\n\r\n        // covers the case where the user starts playback after a drag is in progress.\r\n        // we want to ensure the clock is always stopped during drags to avoid weird audio playback.\r\n        if (handlingDragInput)\r\n            editorClock.Stop();\r\n\r\n        float position = PositionAtTime(editorClock.CurrentTime);\r\n        ScrollTo(position, false);\r\n    }\r\n\r\n    protected override bool OnMouseDown(MouseDownEvent e)\r\n    {\r\n        if (!base.OnMouseDown(e))\r\n            return false;\r\n\r\n        beginUserDrag();\r\n        return true;\r\n    }\r\n\r\n    protected override void OnMouseUp(MouseUpEvent e)\r\n    {\r\n        endUserDrag();\r\n        base.OnMouseUp(e);\r\n    }\r\n\r\n    private void beginUserDrag()\r\n    {\r\n        handlingDragInput = true;\r\n        trackWasPlaying = editorClock.IsRunning;\r\n        editorClock.Stop();\r\n    }\r\n\r\n    private void endUserDrag()\r\n    {\r\n        handlingDragInput = false;\r\n        if (trackWasPlaying)\r\n            editorClock.Start();\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/BottomEditor/RecordingTimeTags/TimeTagsVisualisation.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Components.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose.BottomEditor.RecordingTimeTags;\r\n\r\n/// <summary>\r\n/// Display all time-tags in the lyric, and current time-tag.\r\n/// </summary>\r\npublic partial class TimeTagsVisualisation : CompositeDrawable\r\n{\r\n    private readonly IBindable<ICaretPosition?> bindableCaret = new Bindable<ICaretPosition?>();\r\n\r\n    public TimeTagsVisualisation()\r\n    {\r\n        AutoSizeAxes = Axes.Both;\r\n\r\n        FocusedTimeTagArea focusedTimeTagArea;\r\n        PendingTimeTagsArea leftPendingTimeTagsArea;\r\n        PendingTimeTagsArea rightPendingTimeTagsArea;\r\n\r\n        InternalChildren = new Drawable[]\r\n        {\r\n            focusedTimeTagArea = new FocusedTimeTagArea\r\n            {\r\n                Anchor = Anchor.Centre,\r\n                Origin = Anchor.Centre,\r\n            },\r\n            leftPendingTimeTagsArea = new PendingTimeTagsArea\r\n            {\r\n                X = -5,\r\n                Anchor = Anchor.CentreLeft,\r\n                Origin = Anchor.CentreRight,\r\n            },\r\n            rightPendingTimeTagsArea = new PendingTimeTagsArea\r\n            {\r\n                X = 5,\r\n                Anchor = Anchor.CentreRight,\r\n                Origin = Anchor.CentreLeft,\r\n            },\r\n        };\r\n\r\n        bindableCaret.BindValueChanged(x =>\r\n        {\r\n            if (x.NewValue is not RecordingTimeTagCaretPosition newCaret)\r\n                return;\r\n\r\n            focusedTimeTagArea.UpdateDisplayTimeTag(newCaret.Lyric, newCaret.TimeTag);\r\n            leftPendingTimeTagsArea.UpdateDisplayTimeTags(newCaret.Lyric, newCaret.GetRecordedTimeTags());\r\n            rightPendingTimeTagsArea.UpdateDisplayTimeTags(newCaret.Lyric, newCaret.GetPendingTimeTags());\r\n        });\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(ILyricCaretState lyricCaretState)\r\n    {\r\n        bindableCaret.BindTo(lyricCaretState.BindableCaretPosition);\r\n    }\r\n\r\n    private partial class FocusedTimeTagArea : CompositeDrawable\r\n    {\r\n        private readonly Box background;\r\n        private readonly DrawableTimeTag drawableTimeTag;\r\n        private readonly OsuSpriteText timeTagText;\r\n\r\n        public FocusedTimeTagArea()\r\n        {\r\n            Size = new Vector2(30);\r\n\r\n            InternalChildren = new Drawable[]\r\n            {\r\n                new Container\r\n                {\r\n                    Masking = true,\r\n                    CornerRadius = 5,\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    Child = background = new Box\r\n                    {\r\n                        RelativeSizeAxes = Axes.Both,\r\n                    },\r\n                },\r\n                drawableTimeTag = new DrawableTimeTag\r\n                {\r\n                    Anchor = Anchor.Centre,\r\n                    Origin = Anchor.Centre,\r\n                    Size = new Vector2(20),\r\n                },\r\n                timeTagText = new OsuSpriteText\r\n                {\r\n                    Anchor = Anchor.BottomLeft,\r\n                    Origin = Anchor.TopLeft,\r\n                    X = 5,\r\n                },\r\n            };\r\n        }\r\n\r\n        private TimeTag? timeTag;\r\n\r\n        public void UpdateDisplayTimeTag(Lyric lyric, TimeTag timeTag)\r\n        {\r\n            this.timeTag = timeTag;\r\n\r\n            drawableTimeTag.TimeTag = timeTag;\r\n            timeTagText.Text = LyricUtils.GetTimeTagDisplayRubyText(lyric, timeTag);\r\n        }\r\n    }\r\n\r\n    public partial class PendingTimeTagsArea : CompositeDrawable\r\n    {\r\n        private readonly Box background;\r\n\r\n        private readonly FillFlowContainer<PendingTimeTag> drawableTimeTags;\r\n\r\n        [Resolved]\r\n        private OsuColour colours { get; set; } = null!;\r\n\r\n        public PendingTimeTagsArea()\r\n        {\r\n            AutoSizeAxes = Axes.Both;\r\n\r\n            InternalChildren = new Drawable[]\r\n            {\r\n                new Container\r\n                {\r\n                    Masking = true,\r\n                    CornerRadius = 5,\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    Child = background = new Box\r\n                    {\r\n                        RelativeSizeAxes = Axes.Both,\r\n                    },\r\n                },\r\n                drawableTimeTags = new FillFlowContainer<PendingTimeTag>\r\n                {\r\n                    Margin = new MarginPadding(5),\r\n                    AutoSizeAxes = Axes.Both,\r\n                    Direction = FillDirection.Horizontal,\r\n                    Spacing = new Vector2(25),\r\n                    Anchor = Anchor.Centre,\r\n                    Origin = Anchor.Centre,\r\n                },\r\n            };\r\n        }\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(OsuColour colours)\r\n        {\r\n            background.Colour = colours.Gray4;\r\n        }\r\n\r\n        public void UpdateDisplayTimeTags(Lyric lyric, TimeTag[] timeTags)\r\n        {\r\n            drawableTimeTags.Clear();\r\n\r\n            Alpha = timeTags.Length > 0 ? 1 : 0;\r\n\r\n            foreach (var timeTag in timeTags)\r\n            {\r\n                drawableTimeTags.Add(new PendingTimeTag(lyric, timeTag)\r\n                {\r\n                    Size = new Vector2(12),\r\n                });\r\n            }\r\n        }\r\n\r\n        private partial class PendingTimeTag : CompositeDrawable\r\n        {\r\n            public PendingTimeTag(Lyric lyric, TimeTag timeTag)\r\n            {\r\n                InternalChildren = new Drawable[]\r\n                {\r\n                    new DrawableTimeTag\r\n                    {\r\n                        TimeTag = timeTag,\r\n                        Size = new Vector2(12),\r\n                    },\r\n                    new OsuSpriteText\r\n                    {\r\n                        Font = OsuFont.Default.With(size: 12),\r\n                        Text = LyricUtils.GetTimeTagDisplayRubyText(lyric, timeTag),\r\n                        Anchor = Anchor.BottomLeft,\r\n                        Origin = Anchor.TopLeft,\r\n                        Y = 10,\r\n                    },\r\n                };\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/BottomEditor/TimeTagScrollContainer.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Extensions.Color4Extensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Audio;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Extensions;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Containers;\r\nusing osu.Game.Screens.Edit;\r\nusing osu.Game.Screens.Edit.Compose.Components.Timeline;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose.BottomEditor;\r\n\r\npublic abstract partial class TimeTagScrollContainer : BindableScrollContainer\r\n{\r\n    private readonly IBindable<Lyric?> bindableFocusedLyric = new Bindable<Lyric?>();\r\n\r\n    private readonly IBindable<int> timeTagsTimingVersion = new Bindable<int>();\r\n\r\n    [Cached]\r\n    private readonly BindableList<TimeTag> timeTagsBindable = new();\r\n\r\n    private readonly IBindable<WorkingBeatmap> beatmap = new Bindable<WorkingBeatmap>();\r\n\r\n    protected readonly IBindable<bool> ShowWaveformGraph = new BindableBool();\r\n    protected readonly IBindable<float> WaveformOpacity = new BindableFloat();\r\n    protected readonly IBindable<bool> ShowTick = new BindableBool();\r\n    protected readonly IBindable<float> TickOpacity = new BindableFloat();\r\n\r\n    [Resolved]\r\n    private EditorClock editorClock { get; set; } = null!;\r\n\r\n    protected TimeTagScrollContainer()\r\n    {\r\n        RelativeSizeAxes = Axes.X;\r\n\r\n        timeTagsTimingVersion.BindValueChanged(_ => updateTimeRange());\r\n        timeTagsBindable.BindCollectionChanged((_, _) => updateTimeRange());\r\n\r\n        bindableFocusedLyric.BindValueChanged(e =>\r\n        {\r\n            timeTagsTimingVersion.UnbindBindings();\r\n            timeTagsBindable.UnbindBindings();\r\n\r\n            var lyric = e.NewValue;\r\n            if (lyric == null)\r\n                return;\r\n\r\n            timeTagsTimingVersion.BindTo(lyric.TimeTagsTimingVersion);\r\n            timeTagsBindable.BindTo(lyric.TimeTagsBindable);\r\n\r\n            Schedule(() =>\r\n            {\r\n                OnLyricChanged(lyric);\r\n            });\r\n        });\r\n\r\n        updateTimeRange();\r\n    }\r\n\r\n    private void updateTimeRange()\r\n    {\r\n        var fistTimeTag = timeTagsBindable.FirstOrDefault();\r\n        var lastTimeTag = timeTagsBindable.LastOrDefault();\r\n\r\n        double startTime = fistTimeTag != null ? GetPreviewTime(fistTimeTag) : 0;\r\n        double endTime = lastTimeTag != null ? GetPreviewTime(lastTimeTag) : 0;\r\n\r\n        OnTimeRangeChanged(startTime, endTime);\r\n    }\r\n\r\n    protected abstract void OnLyricChanged(Lyric newLyric);\r\n\r\n    protected virtual void OnTimeRangeChanged(double startTime, double endTime) { }\r\n\r\n    private WaveformGraph waveform = null!;\r\n\r\n    private TimelineTickDisplay ticks = null!;\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(ILyricCaretState lyricCaretState, OsuColour colours, IBindable<WorkingBeatmap> beatmap)\r\n    {\r\n        bindableFocusedLyric.BindTo(lyricCaretState.BindableFocusedLyric);\r\n\r\n        this.beatmap.BindTo(beatmap);\r\n\r\n        Container container;\r\n\r\n        Add(container = new Container\r\n        {\r\n            RelativeSizeAxes = Axes.X,\r\n            Depth = float.MaxValue,\r\n            Children = new Drawable[]\r\n            {\r\n                waveform = new WaveformGraph\r\n                {\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    BaseColour = colours.Blue.Opacity(0.2f),\r\n                    LowColour = colours.BlueLighter,\r\n                    MidColour = colours.BlueDark,\r\n                    HighColour = colours.BlueDarker,\r\n                },\r\n                ticks = new TimelineTickDisplay(),\r\n            },\r\n        });\r\n\r\n        PostProcessContent(container);\r\n\r\n        this.beatmap.BindValueChanged(b =>\r\n        {\r\n            waveform.Waveform = b.NewValue.Waveform;\r\n        }, true);\r\n\r\n        ShowWaveformGraph.BindValueChanged(e => updateWaveformOpacity());\r\n        WaveformOpacity.BindValueChanged(e => updateWaveformOpacity());\r\n        ShowTick.BindValueChanged(e => updateTickOpacity());\r\n        TickOpacity.BindValueChanged(e => updateTickOpacity());\r\n    }\r\n\r\n    private void updateWaveformOpacity() =>\r\n        waveform.FadeTo(ShowWaveformGraph.Value ? WaveformOpacity.Value : 0, 200, Easing.OutQuint);\r\n\r\n    private void updateTickOpacity() =>\r\n        ticks.FadeTo(ShowTick.Value ? TickOpacity.Value : 0, 200, Easing.OutQuint);\r\n\r\n    protected abstract void PostProcessContent(Container content);\r\n\r\n    public double GetPreviewTime(TimeTag timeTag)\r\n    {\r\n        double? time = timeTag.Time;\r\n\r\n        if (time != null)\r\n            return time.Value;\r\n\r\n        var timeTags = timeTagsBindable.ToArray();\r\n        int index = timeTags.IndexOf(timeTag);\r\n\r\n        const float preempt_time = 200;\r\n        var previousTimeTagWithTime = timeTags.GetPreviousMatch(timeTag, x => x.Time.HasValue);\r\n        var nextTimeTagWithTime = timeTags.GetNextMatch(timeTag, x => x.Time.HasValue);\r\n\r\n        if (previousTimeTagWithTime?.Time != null)\r\n        {\r\n            int diffIndex = timeTags.IndexOf(previousTimeTagWithTime) - index;\r\n            return previousTimeTagWithTime.Time.Value - preempt_time * diffIndex;\r\n        }\r\n\r\n        if (nextTimeTagWithTime?.Time != null)\r\n        {\r\n            int diffIndex = timeTags.IndexOf(nextTimeTagWithTime) - index;\r\n            return nextTimeTagWithTime.Time.Value - preempt_time * diffIndex;\r\n        }\r\n\r\n        // will goes in here if all time-tag are no time.\r\n        return index * preempt_time;\r\n    }\r\n\r\n    public double TimeAtPosition(double x)\r\n    {\r\n        return x / Content.DrawWidth * editorClock.TrackLength;\r\n    }\r\n\r\n    public float PositionAtTime(double time)\r\n    {\r\n        return (float)(time / editorClock.TrackLength * Content.DrawWidth);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/ComposeContent.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose;\r\n\r\npublic partial class ComposeContent : MainContent\r\n{\r\n    public ComposeContent()\r\n    {\r\n        InternalChildren = new Drawable[]\r\n        {\r\n            new LyricComposer\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n                Size = new Vector2(1, 0.6f),\r\n            },\r\n            new DetailLyricList\r\n            {\r\n                RelativePositionAxes = Axes.Y,\r\n                Position = new Vector2(0, 0.6f),\r\n                Size = new Vector2(1, 0.4f),\r\n                RelativeSizeAxes = Axes.Both,\r\n            },\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/CreateNewLyricDetailRow.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose;\r\n\r\npublic partial class CreateNewLyricDetailRow : DetailRow\r\n{\r\n    [Resolved]\r\n    private ILyricsChangeHandler lyricsChangeHandler { get; set; } = null!;\r\n\r\n    public CreateNewLyricDetailRow()\r\n        : base(new Lyric { Text = \"New lyric\" })\r\n    {\r\n    }\r\n\r\n    protected override IEnumerable<Dimension> GetColumnDimensions() =>\r\n        new[]\r\n        {\r\n            new Dimension(GridSizeMode.Absolute, TIMING_WIDTH),\r\n            new Dimension(),\r\n        };\r\n\r\n    protected override Drawable CreateTimingInfo(Lyric lyric)\r\n        => Empty();\r\n\r\n    protected override Drawable CreateContent(Lyric lyric)\r\n    {\r\n        return new IconButton\r\n        {\r\n            Anchor = Anchor.Centre,\r\n            Origin = Anchor.Centre,\r\n            Icon = FontAwesome.Solid.PlusCircle,\r\n            Size = new Vector2(32),\r\n            TooltipText = \"Click to add new lyric\",\r\n            Action = () =>\r\n            {\r\n                lyricsChangeHandler.CreateAtLast();\r\n            },\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/DetailLyricList.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose;\r\n\r\npublic partial class DetailLyricList : LyricList\r\n{\r\n    public DetailLyricList()\r\n    {\r\n        AdjustSkin(skin =>\r\n        {\r\n            skin.FontSize = 15;\r\n        });\r\n\r\n        AddInternal(new DetailLyricListBackground\r\n        {\r\n            RelativeSizeAxes = Axes.Both,\r\n            Depth = int.MaxValue,\r\n        });\r\n    }\r\n\r\n    protected override DrawableLyricList CreateDrawableLyricList()\r\n        => new DrawableDetailLyricList();\r\n\r\n    public partial class DrawableDetailLyricList : DrawableLyricList\r\n    {\r\n        protected override Vector2 Spacing => new();\r\n\r\n        protected override bool ScrollToPosition(ICaretPosition caret)\r\n        {\r\n            // should scroll to the target position on every case.\r\n            return true;\r\n        }\r\n\r\n        protected override int SkipRows()\r\n        {\r\n            // it's a fixed number for now.\r\n            return 3;\r\n        }\r\n\r\n        protected override Row CreateEditRow(Lyric lyric)\r\n            => new EditLyricDetailRow(lyric);\r\n\r\n        protected override Row GetCreateNewLyricRow()\r\n            => new CreateNewLyricDetailRow();\r\n    }\r\n\r\n    public partial class DetailLyricListBackground : CompositeDrawable\r\n    {\r\n        private readonly Box infoBackground;\r\n        private readonly Box lyricBackground;\r\n\r\n        private readonly IBindable<LyricEditorMode> bindableMode = new Bindable<LyricEditorMode>();\r\n        private readonly IBindable<bool> bindableSelecting = new Bindable<bool>();\r\n\r\n        public DetailLyricListBackground()\r\n        {\r\n            InternalChildren = new Drawable[]\r\n            {\r\n                infoBackground = new Box\r\n                {\r\n                    Anchor = Anchor.TopLeft,\r\n                    Origin = Anchor.TopLeft,\r\n                    RelativeSizeAxes = Axes.Y,\r\n                },\r\n                lyricBackground = new Box\r\n                {\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    Depth = int.MaxValue,\r\n                },\r\n            };\r\n        }\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(ILyricEditorState state, ILyricSelectionState lyricSelectionState, LyricEditorColourProvider colourProvider)\r\n        {\r\n            bindableMode.BindTo(state.BindableMode);\r\n            bindableSelecting.BindTo(lyricSelectionState.Selecting);\r\n\r\n            bindableMode.BindValueChanged(e =>\r\n            {\r\n                resizeBackground();\r\n                updateColour(colourProvider, e.NewValue);\r\n            }, true);\r\n\r\n            bindableSelecting.BindValueChanged(_ =>\r\n            {\r\n                resizeBackground();\r\n            }, true);\r\n        }\r\n\r\n        private void updateColour(LyricEditorColourProvider colourProvider, LyricEditorMode mode)\r\n        {\r\n            infoBackground.Colour = colourProvider.Background3(mode);\r\n            lyricBackground.Colour = colourProvider.Background4(mode);\r\n        }\r\n\r\n        private void resizeBackground()\r\n        {\r\n            bool showDragHandler = ShowDragHandler(bindableMode.Value, bindableSelecting.Value);\r\n            bool selecting = bindableSelecting.Value;\r\n\r\n            float handlerWidth = showDragHandler ? HANDLER_WIDTH : 0;\r\n            float selectingAreaWidth = selecting ? Row.SELECT_AREA_WIDTH : 0;\r\n\r\n            infoBackground.Width = LYRIC_LIST_PADDING + handlerWidth + selectingAreaWidth + DetailRow.TIMING_WIDTH;\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/DetailRow.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose;\r\n\r\npublic abstract partial class DetailRow : Row\r\n{\r\n    public const int TIMING_WIDTH = 210;\r\n\r\n    protected DetailRow(Lyric lyric)\r\n        : base(lyric)\r\n    {\r\n    }\r\n\r\n    protected override IEnumerable<Dimension> GetColumnDimensions() =>\r\n        new[]\r\n        {\r\n            new Dimension(GridSizeMode.Absolute, TIMING_WIDTH),\r\n            new Dimension(),\r\n        };\r\n\r\n    protected override Dimension GetRowDimensions()\r\n        => new(GridSizeMode.Absolute, 40);\r\n\r\n    protected override IEnumerable<Drawable> GetDrawables(Lyric lyric) =>\r\n        new[]\r\n        {\r\n            CreateTimingInfo(lyric),\r\n            CreateContent(lyric),\r\n        };\r\n\r\n    protected override bool HighlightBackgroundWhenSelected(ICaretPosition? caretPosition) => true;\r\n\r\n    protected override Func<LyricEditorMode, Color4> GetBackgroundColour(BackgroundStyle style, LyricEditorColourProvider colourProvider) =>\r\n        style switch\r\n        {\r\n            BackgroundStyle.Idle => _ => new Color4(), // should not have background if not hover.\r\n            BackgroundStyle.Hover => colourProvider.Background2, // follow the colour in the editor table.\r\n            BackgroundStyle.Focus => colourProvider.Background1, // follow the colour in the editor table.\r\n            _ => throw new ArgumentOutOfRangeException(nameof(style), style, null),\r\n        };\r\n\r\n    protected abstract Drawable CreateTimingInfo(Lyric lyric);\r\n\r\n    protected abstract Drawable CreateContent(Lyric lyric);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/EditLyricDetailRow.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Components.Badges;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Components.Lyrics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose;\r\n\r\npublic partial class EditLyricDetailRow : DetailRow\r\n{\r\n    public EditLyricDetailRow(Lyric lyric)\r\n        : base(lyric)\r\n    {\r\n    }\r\n\r\n    protected override Drawable CreateTimingInfo(Lyric lyric)\r\n    {\r\n        return new TimeTagBadge(lyric)\r\n        {\r\n            Anchor = Anchor.CentreRight,\r\n            Origin = Anchor.CentreRight,\r\n            Margin = new MarginPadding { Right = 10 },\r\n        };\r\n    }\r\n\r\n    protected override Drawable CreateContent(Lyric lyric)\r\n    {\r\n        return new InteractableLyric(lyric)\r\n        {\r\n            Anchor = Anchor.BottomLeft,\r\n            Origin = Anchor.BottomLeft,\r\n            Margin = new MarginPadding { Left = 10 },\r\n            RelativeSizeAxes = Axes.X,\r\n            TextSizeChanged = (self, size) =>\r\n            {\r\n                self.Height = size.Y;\r\n            },\r\n            Loaders = new LayerLoader[]\r\n            {\r\n                new LayerLoader<LyricLayer>(),\r\n                new LayerLoader<InteractLyricLayer>(),\r\n                new LayerLoader<TimeTagLayer>(),\r\n            },\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/LyricComposer.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Extensions.IEnumerableExtensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Layout;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose.BottomEditor;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose.Panels;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose;\r\n\r\npublic partial class LyricComposer : CompositeDrawable\r\n{\r\n    private readonly Bindable<PanelLayout> bindablePanelLayout = new();\r\n    private readonly Bindable<BottomEditorType?> bindableBottomEditorType = new();\r\n\r\n    private readonly IBindable<EditorModeWithEditStep> bindableModeWithEditStep = new Bindable<EditorModeWithEditStep>();\r\n\r\n    private readonly IDictionary<PanelType, Bindable<bool>> panelStatus = new Dictionary<PanelType, Bindable<bool>>();\r\n    private readonly IDictionary<PanelType, Panel> panelInstance = new Dictionary<PanelType, Panel>();\r\n\r\n    private readonly IDictionary<PanelDirection, List<PanelType>> panelDirections = new Dictionary<PanelDirection, List<PanelType>>\r\n    {\r\n        { PanelDirection.Left, new List<PanelType>() },\r\n        { PanelDirection.Right, new List<PanelType>() },\r\n    };\r\n\r\n    [Resolved]\r\n    private LyricEditorColourProvider colourProvider { get; set; } = null!;\r\n\r\n    private readonly GridContainer gridContainer;\r\n\r\n    private readonly Container centerEditArea;\r\n    private readonly Container mainEditorArea;\r\n\r\n    private readonly Container<BaseBottomEditor> bottomEditorContainer;\r\n\r\n    public LyricComposer()\r\n    {\r\n        Box centerEditorBackground;\r\n        Box bottomEditorBackground;\r\n\r\n        InternalChild = gridContainer = new GridContainer\r\n        {\r\n            RelativeSizeAxes = Axes.Both,\r\n            Content = new[]\r\n            {\r\n                new Drawable[]\r\n                {\r\n                    centerEditArea = new Container\r\n                    {\r\n                        Name = \"Edit area and action buttons\",\r\n                        RelativeSizeAxes = Axes.Both,\r\n                        Children = new Drawable[]\r\n                        {\r\n                            centerEditorBackground = new Box\r\n                            {\r\n                                Name = \"Background\",\r\n                                RelativeSizeAxes = Axes.Both,\r\n                            },\r\n                            mainEditorArea = new Container\r\n                            {\r\n                                RelativeSizeAxes = Axes.Both,\r\n                                Children = new Drawable[]\r\n                                {\r\n                                    new LyricEditor(),\r\n                                    new SpecialActionToolbar\r\n                                    {\r\n                                        Name = \"Toolbar\",\r\n                                        Anchor = Anchor.BottomCentre,\r\n                                        Origin = Anchor.BottomCentre,\r\n                                    },\r\n                                },\r\n                            },\r\n                        },\r\n                    },\r\n                },\r\n                new Drawable[]\r\n                {\r\n                    new Container\r\n                    {\r\n                        Name = \"Edit area and action buttons\",\r\n                        RelativeSizeAxes = Axes.Both,\r\n                        Masking = true,\r\n                        Children = new Drawable[]\r\n                        {\r\n                            bottomEditorBackground = new Box\r\n                            {\r\n                                Name = \"Background\",\r\n                                RelativeSizeAxes = Axes.Both,\r\n                            },\r\n                            bottomEditorContainer = new Container<BaseBottomEditor>\r\n                            {\r\n                                RelativeSizeAxes = Axes.Both,\r\n                            },\r\n                        },\r\n                    },\r\n                },\r\n            },\r\n        };\r\n\r\n        bindableModeWithEditStep.BindValueChanged(e =>\r\n        {\r\n            toggleChangeBottomEditor();\r\n\r\n            Schedule(() =>\r\n            {\r\n                if (!ValueChangedEventUtils.EditModeChanged(e) && IsLoaded)\r\n                    return;\r\n\r\n                centerEditorBackground.Colour = colourProvider.Background1(e.NewValue.Mode);\r\n                bottomEditorBackground.Colour = colourProvider.Background4(e.NewValue.Mode);\r\n            });\r\n        }, true);\r\n\r\n        initializePanel();\r\n\r\n        bindablePanelLayout.BindValueChanged(e =>\r\n        {\r\n            assignPanelPosition(e.NewValue);\r\n        }, true);\r\n\r\n        bindableBottomEditorType.BindValueChanged(e =>\r\n        {\r\n            assignBottomEditor(e.NewValue);\r\n        }, true);\r\n\r\n        foreach (var (type, bindable) in panelStatus)\r\n        {\r\n            bindable.BindValueChanged(e =>\r\n            {\r\n                bool show = e.NewValue;\r\n\r\n                if (show)\r\n                {\r\n                    closeOtherPanelsInTheSameDirection(type);\r\n                }\r\n\r\n                panelInstance[type].State.Value = show ? Visibility.Visible : Visibility.Hidden;\r\n            }, true);\r\n        }\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(KaraokeRulesetLyricEditorConfigManager lyricEditorConfigManager, ILyricEditorState state)\r\n    {\r\n        lyricEditorConfigManager.BindWith(KaraokeRulesetLyricEditorSetting.ShowPropertyPanelInComposer, panelStatus[PanelType.Property]);\r\n        lyricEditorConfigManager.BindWith(KaraokeRulesetLyricEditorSetting.ShowInvalidInfoInComposer, panelStatus[PanelType.InvalidInfo]);\r\n\r\n        bindableModeWithEditStep.BindTo(state.BindableModeWithEditStep);\r\n    }\r\n\r\n    protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source)\r\n    {\r\n        if (invalidation.HasFlag(Invalidation.DrawSize) && source == InvalidationSource.Parent)\r\n            calculatePanelPosition();\r\n\r\n        return base.OnInvalidate(invalidation, source);\r\n    }\r\n\r\n    #region Panel\r\n\r\n    private void initializePanel()\r\n    {\r\n        foreach (var panelType in Enum.GetValues<PanelType>())\r\n        {\r\n            var instance = getInstance(panelType);\r\n\r\n            panelStatus.Add(panelType, new Bindable<bool>(true));\r\n            panelInstance.Add(panelType, instance);\r\n\r\n            centerEditArea.Add(instance);\r\n        }\r\n\r\n        static Panel getInstance(PanelType panelType) =>\r\n            panelType switch\r\n            {\r\n                PanelType.Property => new PropertyPanel(),\r\n                PanelType.InvalidInfo => new InvalidPanel(),\r\n                _ => throw new ArgumentOutOfRangeException(nameof(panelType), panelType, null),\r\n            };\r\n    }\r\n\r\n    private void calculatePanelPosition()\r\n    {\r\n        float radio = DrawWidth / DrawHeight;\r\n        bindablePanelLayout.Value = radio < 2 ? PanelLayout.LeftOnly : PanelLayout.LeftAndRight;\r\n    }\r\n\r\n    private void assignPanelPosition(PanelLayout panelLayout)\r\n    {\r\n        panelDirections[PanelDirection.Left].Clear();\r\n        panelDirections[PanelDirection.Right].Clear();\r\n\r\n        switch (panelLayout)\r\n        {\r\n            case PanelLayout.LeftAndRight:\r\n                panelDirections[PanelDirection.Left].Add(PanelType.Property);\r\n                panelDirections[PanelDirection.Right].Add(PanelType.InvalidInfo);\r\n                break;\r\n\r\n            case PanelLayout.LeftOnly:\r\n                panelDirections[PanelDirection.Left].Add(PanelType.Property);\r\n                panelDirections[PanelDirection.Left].Add(PanelType.InvalidInfo);\r\n                break;\r\n\r\n            default:\r\n                throw new ArgumentOutOfRangeException(nameof(panelLayout), panelLayout, null);\r\n        }\r\n\r\n        foreach (var (direction, panelTypes) in panelDirections)\r\n        {\r\n            foreach (var instance in panelTypes.Select(panelType => panelInstance[panelType]))\r\n            {\r\n                instance.Direction = direction;\r\n            }\r\n        }\r\n\r\n        closeOtherPanelsInTheSameDirection(PanelType.Property);\r\n    }\r\n\r\n    private void closeOtherPanelsInTheSameDirection(PanelType exceptPanel)\r\n    {\r\n        var closePanelList = panelDirections.First(x => x.Value.Contains(exceptPanel)).Value;\r\n\r\n        foreach (var panel in closePanelList.Where(x => x != exceptPanel))\r\n        {\r\n            var status = panelStatus[panel];\r\n            status.Value = false;\r\n        }\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Bottom editor\r\n\r\n    private void toggleChangeBottomEditor()\r\n    {\r\n        var modeWithEditStep = bindableModeWithEditStep.Value;\r\n        bindableBottomEditorType.Value = getBottomEditorType(modeWithEditStep);\r\n\r\n        static BottomEditorType? getBottomEditorType(EditorModeWithEditStep modeWithEditStep) =>\r\n            modeWithEditStep.Mode switch\r\n            {\r\n                LyricEditorMode.EditTimeTag when modeWithEditStep.EditStep is TimeTagEditStep.Recording => BottomEditorType.RecordingTimeTag,\r\n                LyricEditorMode.EditTimeTag when modeWithEditStep.EditStep is TimeTagEditStep.Adjust => BottomEditorType.AdjustTimeTags,\r\n                LyricEditorMode.EditNote => BottomEditorType.Note,\r\n                _ => null,\r\n            };\r\n    }\r\n\r\n    private void assignBottomEditor(BottomEditorType? bottomEditorType)\r\n    {\r\n        const double remove_old_editor_time = 200;\r\n        const double new_animation_time = 200;\r\n\r\n        bool hasOldButtonEditor = bottomEditorContainer.Children.Any();\r\n        var newButtonEditor = createBottomEditor(bottomEditorType)?.With(x =>\r\n        {\r\n            x.RelativePositionAxes = Axes.Y;\r\n            x.Y = -1;\r\n            x.Alpha = 0;\r\n        });\r\n\r\n        if (hasOldButtonEditor)\r\n        {\r\n            bottomEditorContainer.Children.ForEach(editor =>\r\n            {\r\n                editor.MoveToY(-1, remove_old_editor_time).FadeOut(remove_old_editor_time).OnComplete(x =>\r\n                {\r\n                    x.Expire();\r\n\r\n                    updateBottomEditAreaSize(newButtonEditor);\r\n                });\r\n            });\r\n        }\r\n        else\r\n        {\r\n            updateBottomEditAreaSize(newButtonEditor);\r\n        }\r\n\r\n        if (newButtonEditor == null)\r\n            return;\r\n\r\n        bottomEditorContainer.Add(newButtonEditor);\r\n        newButtonEditor.Delay(hasOldButtonEditor ? remove_old_editor_time : 0).FadeIn().MoveToY(0, new_animation_time);\r\n\r\n        static BaseBottomEditor? createBottomEditor(BottomEditorType? bottomEditorType) =>\r\n            bottomEditorType switch\r\n            {\r\n                BottomEditorType.RecordingTimeTag => new RecordingTimeTagBottomEditor(),\r\n                BottomEditorType.AdjustTimeTags => new AdjustTimeTagBottomEditor(),\r\n                BottomEditorType.Note => new NoteBottomEditor(),\r\n                _ => null,\r\n            };\r\n\r\n        void updateBottomEditAreaSize(BaseBottomEditor? bottomEditor)\r\n        {\r\n            float bottomEditorHeight = bottomEditor?.ContentHeight ?? 0;\r\n            gridContainer.RowDimensions = new[]\r\n            {\r\n                new Dimension(),\r\n                new Dimension(GridSizeMode.Absolute, bottomEditorHeight),\r\n            };\r\n        }\r\n    }\r\n\r\n    #endregion\r\n\r\n    private enum PanelType\r\n    {\r\n        Property,\r\n\r\n        InvalidInfo,\r\n    }\r\n\r\n    private enum PanelLayout\r\n    {\r\n        LeftAndRight,\r\n\r\n        LeftOnly,\r\n    }\r\n\r\n    private enum BottomEditorType\r\n    {\r\n        RecordingTimeTag,\r\n\r\n        AdjustTimeTags,\r\n\r\n        Note,\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/LyricEditor.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Audio;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Extensions;\r\nusing osu.Framework.Extensions.Color4Extensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Cursor;\r\nusing osu.Framework.Graphics.Effects;\r\nusing osu.Framework.Graphics.Primitives;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Framework.Input.Bindings;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Game.Graphics.Containers;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Graphics.UserInterfaceV2;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Components.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Markdown;\r\nusing osu.Game.Skinning;\r\nusing osuTK;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose;\r\n\r\npublic partial class LyricEditor : CompositeDrawable\r\n{\r\n    private readonly IBindable<Lyric?> bindableFocusedLyric = new Bindable<Lyric?>();\r\n    private readonly IBindable<float> bindableFontSize = new Bindable<float>();\r\n\r\n    private readonly LyricEditorSkin skin;\r\n    private readonly DragContainer dragContainer;\r\n\r\n    public LyricEditor()\r\n    {\r\n        RelativeSizeAxes = Axes.Both;\r\n\r\n        InternalChild = new SkinProvidingContainer(skin = new LyricEditorSkin(null))\r\n        {\r\n            RelativeSizeAxes = Axes.Both,\r\n            Children = new Drawable[]\r\n            {\r\n                dragContainer = new DragContainer\r\n                {\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    Anchor = Anchor.Centre,\r\n                    Origin = Anchor.Centre,\r\n                },\r\n                new ScrollBackButton\r\n                {\r\n                    Anchor = Anchor.BottomRight,\r\n                    Origin = Anchor.BottomRight,\r\n                    Size = new Vector2(40),\r\n                    Margin = new MarginPadding(20),\r\n                },\r\n            },\r\n        };\r\n\r\n        bindableFocusedLyric.BindValueChanged(e =>\r\n        {\r\n            refreshPreviewLyric(e.NewValue);\r\n        });\r\n\r\n        bindableFontSize.BindValueChanged(e =>\r\n        {\r\n            skin.FontSize = e.NewValue;\r\n            refreshPreviewLyric(bindableFocusedLyric.Value);\r\n        });\r\n    }\r\n\r\n    private void refreshPreviewLyric(Lyric? lyric)\r\n    {\r\n        dragContainer.Clear();\r\n\r\n        if (lyric == null)\r\n            return;\r\n\r\n        const int border = 36;\r\n\r\n        dragContainer.Add(new InteractableLyric(lyric)\r\n        {\r\n            TextSizeChanged = (self, size) =>\r\n            {\r\n                self.Width = size.X + border * 2;\r\n                self.Height = size.Y + border * 2;\r\n            },\r\n            Loaders = new LayerLoader[]\r\n            {\r\n                new LayerLoader<GridLayer>\r\n                {\r\n                    OnLoad = layer =>\r\n                    {\r\n                        layer.Spacing = 10;\r\n                    },\r\n                },\r\n                new LayerLoader<LyricLayer>\r\n                {\r\n                    OnLoad = layer =>\r\n                    {\r\n                        layer.LyricPosition = new Vector2(border);\r\n                    },\r\n                },\r\n                new LayerLoader<EditLyricLayer>(),\r\n                new LayerLoader<TimeTagLayer>(),\r\n                new LayerLoader<CaretLayer>(),\r\n                new LayerLoader<BlueprintLayer>(),\r\n            },\r\n        });\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(ILyricCaretState lyricCaretState, KaraokeRulesetLyricEditorConfigManager lyricEditorConfigManager)\r\n    {\r\n        bindableFocusedLyric.BindTo(lyricCaretState.BindableFocusedLyric);\r\n\r\n        lyricEditorConfigManager.BindWith(KaraokeRulesetLyricEditorSetting.FontSizeInComposer, bindableFontSize);\r\n    }\r\n\r\n    private partial class DragContainer : Container\r\n    {\r\n        protected override bool OnDragStart(DragStartEvent e) => true;\r\n\r\n        public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;\r\n\r\n        protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;\r\n\r\n        protected override void OnDrag(DragEvent e)\r\n        {\r\n            if (!e.AltPressed)\r\n                return;\r\n\r\n            Position += e.Delta;\r\n        }\r\n\r\n        protected override bool OnScroll(ScrollEvent e)\r\n        {\r\n            if (!e.AltPressed)\r\n                return false;\r\n\r\n            var triggerKey = e.ScrollDelta.Y > 0 ? KaraokeEditAction.DecreasePreviewFontSize : KaraokeEditAction.IncreasePreviewFontSize;\r\n            return trigger(triggerKey);\r\n\r\n            bool trigger(KaraokeEditAction action)\r\n            {\r\n                var inputManager = this.FindClosestParent<KeyBindingContainer<KaraokeEditAction>>();\r\n                if (inputManager == null)\r\n                    return false;\r\n\r\n                inputManager.TriggerPressed(action);\r\n                inputManager.TriggerReleased(action);\r\n                return true;\r\n            }\r\n        }\r\n    }\r\n\r\n    public partial class ScrollBackButton : OsuHoverContainer, IHasPopover\r\n    {\r\n        private const int fade_duration = 500;\r\n\r\n        private Visibility state;\r\n\r\n        public Visibility State\r\n        {\r\n            get => state;\r\n            set\r\n            {\r\n                if (value == state)\r\n                    return;\r\n\r\n                state = value;\r\n                Enabled.Value = state == Visibility.Visible;\r\n                this.FadeTo(state == Visibility.Visible ? 1 : 0, fade_duration, Easing.OutQuint);\r\n            }\r\n        }\r\n\r\n        protected override IEnumerable<Drawable> EffectTargets => new[] { background };\r\n\r\n        private Color4 flashColour;\r\n\r\n        private readonly Container content;\r\n        private readonly Box background;\r\n        private readonly SpriteIcon spriteIcon;\r\n\r\n        protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new();\r\n\r\n        public ScrollBackButton()\r\n        {\r\n            Add(content = new CircularContainer\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n                Anchor = Anchor.Centre,\r\n                Origin = Anchor.Centre,\r\n                Masking = true,\r\n                EdgeEffect = new EdgeEffectParameters\r\n                {\r\n                    Type = EdgeEffectType.Shadow,\r\n                    Offset = new Vector2(0f, 1f),\r\n                    Radius = 3f,\r\n                    Colour = Color4.Black.Opacity(0.25f),\r\n                },\r\n                Children = new Drawable[]\r\n                {\r\n                    background = new Box\r\n                    {\r\n                        RelativeSizeAxes = Axes.Both,\r\n                    },\r\n                    spriteIcon = new SpriteIcon\r\n                    {\r\n                        Anchor = Anchor.Centre,\r\n                        Origin = Anchor.Centre,\r\n                        Size = new Vector2(15),\r\n                        Icon = FontAwesome.Solid.Lightbulb,\r\n                    },\r\n                },\r\n            });\r\n\r\n            TooltipText = \"Hover to see the tutorial\";\r\n        }\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(OverlayColourProvider colourProvider, AudioManager audio)\r\n        {\r\n            IdleColour = colourProvider.Background6;\r\n            HoverColour = colourProvider.Background5;\r\n            flashColour = colourProvider.Light1;\r\n        }\r\n\r\n        protected override bool OnClick(ClickEvent e)\r\n        {\r\n            background.FlashColour(flashColour, 800, Easing.OutQuint);\r\n\r\n            this.ShowPopover();\r\n            return base.OnClick(e);\r\n        }\r\n\r\n        protected override bool OnHover(HoverEvent e)\r\n        {\r\n            content.ScaleTo(1.1f, 2000, Easing.OutQuint);\r\n            return base.OnHover(e);\r\n        }\r\n\r\n        protected override void OnHoverLost(HoverLostEvent e)\r\n        {\r\n            content.ScaleTo(1, 1000, Easing.OutElastic);\r\n            base.OnHoverLost(e);\r\n        }\r\n\r\n        public Popover GetPopover() => new DescriptionPopover();\r\n\r\n        private partial class DescriptionPopover : OsuPopover\r\n        {\r\n            public DescriptionPopover()\r\n            {\r\n                Child = new DescriptionTextFlowContainer\r\n                {\r\n                    Size = new Vector2(200, 100),\r\n                    Description = \"Press `alt` and `drag the compose area` or `scroll the mouse wheel` can move the lyric position or change the font size.\",\r\n                };\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/Panel.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Audio;\r\nusing osu.Framework.Audio.Sample;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Game.Graphics.Containers;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose;\r\n\r\npublic abstract partial class Panel : FocusedOverlayContainer\r\n{\r\n    private Sample? samplePopIn;\r\n    private Sample? samplePopOut;\r\n\r\n    private const float transition_length = 600;\r\n\r\n    protected virtual string PopInSampleName => \"UI/overlay-pop-in\";\r\n    protected virtual string PopOutSampleName => \"UI/overlay-pop-out\";\r\n\r\n    private readonly IBindable<LyricEditorMode> bindableMode = new Bindable<LyricEditorMode>();\r\n    private readonly Box background;\r\n    private readonly FillFlowContainer fillFlowContainer;\r\n\r\n    protected override bool BlockPositionalInput => false;\r\n\r\n    protected Panel()\r\n    {\r\n        Padding = new MarginPadding(10);\r\n\r\n        InternalChild = new Container\r\n        {\r\n            Masking = true,\r\n            CornerRadius = 10,\r\n            RelativeSizeAxes = Axes.Both,\r\n            Children = new Drawable[]\r\n            {\r\n                background = new Box\r\n                {\r\n                    Name = \"Background\",\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    Alpha = 0.6f,\r\n                },\r\n                new OsuScrollContainer\r\n                {\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    Child = fillFlowContainer = new FillFlowContainer\r\n                    {\r\n                        RelativeSizeAxes = Axes.X,\r\n                        AutoSizeAxes = Axes.Y,\r\n                        Direction = FillDirection.Vertical,\r\n                        Spacing = new Vector2(10),\r\n                    },\r\n                },\r\n            },\r\n        };\r\n    }\r\n\r\n    protected abstract IReadOnlyList<Drawable> CreateSections();\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(ILyricEditorState state, LyricEditorColourProvider colourProvider, AudioManager audio)\r\n    {\r\n        bindableMode.BindTo(state.BindableMode);\r\n        bindableMode.BindValueChanged(x =>\r\n        {\r\n            background.Colour = colourProvider.Background2(state.Mode);\r\n        }, true);\r\n\r\n        samplePopIn = audio.Samples.Get(PopInSampleName);\r\n        samplePopOut = audio.Samples.Get(PopOutSampleName);\r\n    }\r\n\r\n    private PanelDirection direction;\r\n\r\n    public PanelDirection Direction\r\n    {\r\n        get => direction;\r\n        set\r\n        {\r\n            if (direction == value)\r\n                return;\r\n\r\n            direction = value;\r\n\r\n            switch (direction)\r\n            {\r\n                case PanelDirection.Left:\r\n                    Anchor = Anchor.TopLeft;\r\n                    Origin = Anchor.TopLeft;\r\n                    break;\r\n\r\n                case PanelDirection.Right:\r\n                    Anchor = Anchor.TopRight;\r\n                    Origin = Anchor.TopRight;\r\n                    break;\r\n\r\n                default:\r\n                    throw new ArgumentOutOfRangeException(nameof(direction));\r\n            }\r\n        }\r\n    }\r\n\r\n    protected override void PopIn()\r\n    {\r\n        samplePopIn?.Play();\r\n\r\n        this.FadeTo(1, transition_length, Easing.OutQuint);\r\n\r\n        // should load the content after opened.\r\n        fillFlowContainer.Children = CreateSections();\r\n    }\r\n\r\n    protected override void PopOut()\r\n    {\r\n        samplePopOut?.Play();\r\n\r\n        this.FadeTo(0, transition_length, Easing.OutQuint).OnComplete(_ =>\r\n        {\r\n            // should clear the content if close.\r\n            fillFlowContainer.Clear();\r\n        });\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/PanelDirection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose;\r\n\r\npublic enum PanelDirection\r\n{\r\n    Left,\r\n\r\n    Right,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/Panels/InvalidPanel.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose.Panels;\r\n\r\npublic partial class InvalidPanel : Panel\r\n{\r\n    public InvalidPanel()\r\n    {\r\n        Width = 200;\r\n        Height = 300;\r\n    }\r\n\r\n    protected override IReadOnlyList<Drawable> CreateSections() =>\r\n        new Drawable[]\r\n        {\r\n            new IssueSection(),\r\n        };\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/Panels/IssueSection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Issues;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose.Panels;\r\n\r\npublic partial class IssueSection : PanelSection\r\n{\r\n    protected override LocalisableString Title => \"Issues\";\r\n\r\n    private readonly IBindableList<Issue> bindableIssues = new BindableList<Issue>();\r\n\r\n    [Resolved]\r\n    private ILyricEditorVerifier verifier { get; set; } = null!;\r\n\r\n    public IssueSection()\r\n    {\r\n        EmptyIssue emptyIssue;\r\n\r\n        IconButton reloadButton;\r\n        LyricEditorIssueTable issueTable;\r\n\r\n        Children = new Drawable[]\r\n        {\r\n            emptyIssue = new EmptyIssue\r\n            {\r\n                Anchor = Anchor.Centre,\r\n                Origin = Anchor.Centre,\r\n                RelativeSizeAxes = Axes.X,\r\n                AutoSizeAxes = Axes.Y,\r\n                Padding = new MarginPadding(10),\r\n            },\r\n            issueTable = new SingleLyricIssueTable(),\r\n        };\r\n\r\n        AddInternal(reloadButton = new IconButton\r\n        {\r\n            Anchor = Anchor.TopRight,\r\n            Origin = Anchor.TopRight,\r\n            Icon = FontAwesome.Solid.Redo,\r\n            Scale = new Vector2(0.7f),\r\n            Action = () =>\r\n            {\r\n                if (Lyric == null)\r\n                    throw new ArgumentNullException(nameof(Lyric));\r\n\r\n                verifier.RefreshByHitObject(Lyric);\r\n            },\r\n        });\r\n\r\n        bindableIssues.BindCollectionChanged((_, _) =>\r\n        {\r\n            bool hasIssue = bindableIssues.Any();\r\n\r\n            emptyIssue.Alpha = hasIssue ? 0 : 1;\r\n\r\n            reloadButton.Alpha = hasIssue ? 1 : 0;\r\n            issueTable.Alpha = hasIssue ? 1 : 0;\r\n            issueTable.Issues = bindableIssues.Take(100);\r\n        }, true);\r\n    }\r\n\r\n    protected override void OnLyricChanged(Lyric? lyric)\r\n    {\r\n        bindableIssues.UnbindBindings();\r\n        if (lyric == null)\r\n            return;\r\n\r\n        bindableIssues.BindTo(verifier.GetBindable(lyric));\r\n    }\r\n\r\n    // todo: change the style.\r\n    private partial class EmptyIssue : ClickableContainer\r\n    {\r\n        [BackgroundDependencyLoader]\r\n        private void load(LyricEditorColourProvider colourProvider, ILyricEditorState state, ILyricEditorVerifier verifier, OsuColour colours)\r\n        {\r\n            Action = verifier.Refresh;\r\n\r\n            InternalChild = new FillFlowContainer\r\n            {\r\n                RelativeSizeAxes = Axes.X,\r\n                AutoSizeAxes = Axes.Y,\r\n                Anchor = Anchor.Centre,\r\n                Origin = Anchor.Centre,\r\n                Padding = new MarginPadding(20),\r\n                Direction = FillDirection.Vertical,\r\n                Children = new Drawable[]\r\n                {\r\n                    new SpriteIcon\r\n                    {\r\n                        Icon = FontAwesome.Solid.CheckCircle,\r\n                        Colour = colours.Green,\r\n                        Anchor = Anchor.TopCentre,\r\n                        Origin = Anchor.TopCentre,\r\n                        Size = new Vector2(36),\r\n                    },\r\n                    new OsuSpriteText\r\n                    {\r\n                        Anchor = Anchor.TopCentre,\r\n                        Origin = Anchor.TopCentre,\r\n                        Text = \"No issue here!\",\r\n                        Colour = colourProvider.Colour1(state.Mode),\r\n                        Font = OsuFont.GetFont(size: 24),\r\n                    },\r\n                    new OsuSpriteText\r\n                    {\r\n                        Anchor = Anchor.TopCentre,\r\n                        Origin = Anchor.TopCentre,\r\n                        Text = \"Click this area to re-check again.\",\r\n                        Font = OsuFont.GetFont(size: 14),\r\n                    },\r\n                },\r\n            };\r\n\r\n            AddInternal(new HoverClickSounds(HoverSampleSet.Button));\r\n        }\r\n\r\n        protected override bool OnMouseDown(MouseDownEvent e)\r\n        {\r\n            Content.ScaleTo(0.9f, 4000, Easing.OutQuint);\r\n            return base.OnMouseDown(e);\r\n        }\r\n\r\n        protected override void OnMouseUp(MouseUpEvent e)\r\n        {\r\n            Content.ScaleTo(1, 1000, Easing.OutElastic);\r\n            base.OnMouseUp(e);\r\n        }\r\n    }\r\n\r\n    private partial class SingleLyricIssueTable : LyricEditorIssueTable\r\n    {\r\n        public SingleLyricIssueTable()\r\n        {\r\n            ShowHeaders = false;\r\n        }\r\n\r\n        protected override TableColumn[] CreateHeaders() => new[]\r\n        {\r\n            new TableColumn(string.Empty, Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize, minSize: 30)),\r\n            new TableColumn(\"Message\", Anchor.CentreLeft),\r\n        };\r\n\r\n        protected override Drawable[] CreateContent(Issue issue) =>\r\n            new Drawable[]\r\n            {\r\n                new IssueIcon\r\n                {\r\n                    Origin = Anchor.Centre,\r\n                    Size = new Vector2(10),\r\n                    Margin = new MarginPadding { Left = 10 },\r\n                    Issue = issue,\r\n                },\r\n                new TruncatingSpriteText\r\n                {\r\n                    Text = issue.ToString(),\r\n                    RelativeSizeAxes = Axes.X,\r\n                    Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Medium),\r\n                },\r\n            };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/Panels/PanelSection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose.Panels;\r\n\r\npublic abstract partial class PanelSection : Section\r\n{\r\n    private readonly IBindable<Lyric?> bindableFocusedLyric = new Bindable<Lyric?>();\r\n\r\n    protected Lyric? Lyric;\r\n\r\n    protected override void LoadComplete()\r\n    {\r\n        base.LoadComplete();\r\n\r\n        bindableFocusedLyric.BindValueChanged(x =>\r\n        {\r\n            Lyric = x.NewValue;\r\n\r\n            OnLyricChanged(Lyric);\r\n        }, true);\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(ILyricCaretState lyricCaretState)\r\n    {\r\n        bindableFocusedLyric.BindTo(lyricCaretState.BindableFocusedLyric);\r\n    }\r\n\r\n    protected abstract void OnLyricChanged(Lyric? lyric);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/Panels/PropertyPanel.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose.Panels;\r\n\r\npublic partial class PropertyPanel : Panel\r\n{\r\n    public PropertyPanel()\r\n    {\r\n        Width = 200;\r\n        RelativeSizeAxes = Axes.Y;\r\n    }\r\n\r\n    protected override IReadOnlyList<Drawable> CreateSections()\r\n    {\r\n        return Array.Empty<Drawable>();\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/SpecialActionToolbar.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose.Toolbar;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose.Toolbar.Carets;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose.Toolbar.Panels;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose.Toolbar.Playback;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose.Toolbar.View;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose;\r\n\r\npublic partial class SpecialActionToolbar : CompositeDrawable\r\n{\r\n    public const int HEIGHT = 26;\r\n    public const int PADDING = 2;\r\n    public const int ICON_SIZE = HEIGHT - PADDING * 2;\r\n\r\n    public const int SPACING = 5;\r\n\r\n    private readonly IBindable<EditorModeWithEditStep> bindableModeWithEditStep = new Bindable<EditorModeWithEditStep>();\r\n\r\n    private readonly Box background;\r\n\r\n    private readonly FillFlowContainer buttonContainer;\r\n\r\n    public SpecialActionToolbar()\r\n    {\r\n        AutoSizeAxes = Axes.Both;\r\n\r\n        InternalChildren = new Drawable[]\r\n        {\r\n            background = new Box\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n            },\r\n            buttonContainer = new FillFlowContainer\r\n            {\r\n                AutoSizeAxes = Axes.Both,\r\n                Padding = new MarginPadding(5),\r\n                Spacing = new Vector2(SPACING),\r\n            },\r\n        };\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(ILyricEditorState state, LyricEditorColourProvider colourProvider)\r\n    {\r\n        bindableModeWithEditStep.BindTo(state.BindableModeWithEditStep);\r\n        bindableModeWithEditStep.BindValueChanged(e =>\r\n        {\r\n            // Note: add the schedule because will have the \"The collection's state is no longer correct.\" error if not add this.\r\n            Schedule(reGenerateButtons);\r\n\r\n            if (ValueChangedEventUtils.EditModeChanged(e) || !IsLoaded)\r\n                background.Colour = colourProvider.Background2(state.Mode);\r\n        }, true);\r\n    }\r\n\r\n    private void reGenerateButtons()\r\n    {\r\n        buttonContainer.Clear();\r\n\r\n        buttonContainer.AddRange(createAdjustLyricSizeItem());\r\n\r\n        buttonContainer.Add(new Separator());\r\n\r\n        buttonContainer.AddRange(createPanelItems());\r\n\r\n        buttonContainer.Add(new Separator());\r\n\r\n        buttonContainer.AddRange(createSwitchLyricItem());\r\n\r\n        buttonContainer.Add(new Separator());\r\n\r\n        var modeWithEditStep = bindableModeWithEditStep.Value;\r\n        buttonContainer.AddRange(createItemForEditMode(modeWithEditStep));\r\n    }\r\n\r\n    private static IEnumerable<Drawable> createAdjustLyricSizeItem() => new Drawable[]\r\n    {\r\n        new AdjustFontSizeButton(),\r\n    };\r\n\r\n    private static IEnumerable<Drawable> createPanelItems() => new Drawable[]\r\n    {\r\n        new TogglePropertyPanelButton(),\r\n        new ToggleInvalidInfoPanelButton(),\r\n    };\r\n\r\n    private static IEnumerable<Drawable> createSwitchLyricItem() => new Drawable[]\r\n    {\r\n        new MoveToPreviousLyricButton(),\r\n        new MoveToNextLyricButton(),\r\n    };\r\n\r\n    private static IEnumerable<Drawable> createItemForEditMode(EditorModeWithEditStep editorModeWithEditStep)\r\n    {\r\n        return editorModeWithEditStep.Mode switch\r\n        {\r\n            LyricEditorMode.View => Array.Empty<Drawable>(),\r\n            LyricEditorMode.EditText => createItemsForTextEditStep(editorModeWithEditStep.GetEditStep<TextEditStep>()),\r\n            LyricEditorMode.EditReferenceLyric => Array.Empty<Drawable>(),\r\n            LyricEditorMode.EditLanguage => Array.Empty<Drawable>(),\r\n            LyricEditorMode.EditRuby => Array.Empty<Drawable>(),\r\n            LyricEditorMode.EditTimeTag => createItemsForTimeTagEditStep(editorModeWithEditStep.GetEditStep<TimeTagEditStep>()),\r\n            LyricEditorMode.EditRomanisation => Array.Empty<Drawable>(),\r\n            LyricEditorMode.EditNote => createItemsForNoteEditStep(editorModeWithEditStep.GetEditStep<NoteEditStep>()),\r\n            LyricEditorMode.EditSinger => Array.Empty<Drawable>(),\r\n            _ => throw new ArgumentOutOfRangeException(),\r\n        };\r\n\r\n        static IEnumerable<Drawable> createItemsForTextEditStep(TextEditStep textEditMode)\r\n        {\r\n            switch (textEditMode)\r\n            {\r\n                case TextEditStep.Typing:\r\n                case TextEditStep.Split:\r\n                    return new Drawable[]\r\n                    {\r\n                        new MoveToFirstIndexButton(),\r\n                        new MoveToPreviousIndexButton(),\r\n                        new MoveToNextIndexButton(),\r\n                        new MoveToLastIndexButton(),\r\n                    };\r\n\r\n                case TextEditStep.Verify:\r\n                    return Array.Empty<Drawable>();\r\n\r\n                default:\r\n                    throw new ArgumentOutOfRangeException(nameof(textEditMode));\r\n            }\r\n        }\r\n\r\n        static IEnumerable<Drawable> createItemsForTimeTagEditStep(TimeTagEditStep timeTagEditMode) =>\r\n            timeTagEditMode switch\r\n            {\r\n                TimeTagEditStep.Create => new Drawable[]\r\n                {\r\n                    new MoveToFirstIndexButton(),\r\n                    new MoveToPreviousIndexButton(),\r\n                    new MoveToNextIndexButton(),\r\n                    new MoveToLastIndexButton(),\r\n                },\r\n                TimeTagEditStep.Recording => new Drawable[]\r\n                {\r\n                    new PlaybackSwitchButton(),\r\n                    new Separator(),\r\n                    new MoveToFirstIndexButton(),\r\n                    new MoveToPreviousIndexButton(),\r\n                    new MoveToNextIndexButton(),\r\n                    new MoveToLastIndexButton(),\r\n                },\r\n                TimeTagEditStep.Adjust => new Drawable[]\r\n                {\r\n                    new PlaybackSwitchButton(),\r\n                },\r\n                _ => throw new ArgumentOutOfRangeException(nameof(timeTagEditMode), timeTagEditMode, null),\r\n            };\r\n\r\n        static IEnumerable<Drawable> createItemsForNoteEditStep(NoteEditStep noteEditMode) =>\r\n            noteEditMode switch\r\n            {\r\n                NoteEditStep.Generate => Array.Empty<Drawable>(),\r\n                NoteEditStep.Edit => Array.Empty<Drawable>(),\r\n                NoteEditStep.Verify => Array.Empty<Drawable>(),\r\n                _ => throw new ArgumentOutOfRangeException(nameof(noteEditMode), noteEditMode, null),\r\n            };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/Toolbar/Carets/MoveToCaretPositionButton.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose.Toolbar.Carets;\r\n\r\npublic abstract partial class MoveToCaretPositionButton : ToolbarEditActionButton\r\n{\r\n    protected abstract MovingCaretAction AcceptAction { get; }\r\n\r\n    [Resolved]\r\n    private ILyricCaretState lyricCaretState { get; set; } = null!;\r\n\r\n    private readonly IBindable<ICaretPosition?> bindableCaretPosition = new Bindable<ICaretPosition?>();\r\n\r\n    protected MoveToCaretPositionButton()\r\n    {\r\n        Action = () =>\r\n        {\r\n            lyricCaretState.MoveCaret(AcceptAction);\r\n        };\r\n\r\n        bindableCaretPosition.BindValueChanged(e =>\r\n        {\r\n            bool movable = lyricCaretState.GetCaretPositionByAction(AcceptAction) != null;\r\n            SetState(movable);\r\n        });\r\n    }\r\n\r\n    protected override void LoadComplete()\r\n    {\r\n        base.LoadComplete();\r\n\r\n        bindableCaretPosition.BindTo(lyricCaretState.BindableCaretPosition);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/Toolbar/Carets/MoveToFirstIndexButton.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose.Toolbar.Carets;\r\n\r\npublic partial class MoveToFirstIndexButton : MoveToCaretPositionButton\r\n{\r\n    protected override KaraokeEditAction EditAction => KaraokeEditAction.MoveToFirstIndex;\r\n\r\n    protected override MovingCaretAction AcceptAction => MovingCaretAction.FirstIndex;\r\n\r\n    public MoveToFirstIndexButton()\r\n    {\r\n        SetIcon(FontAwesome.Solid.AngleDoubleLeft);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/Toolbar/Carets/MoveToLastIndexButton.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose.Toolbar.Carets;\r\n\r\npublic partial class MoveToLastIndexButton : MoveToCaretPositionButton\r\n{\r\n    protected override KaraokeEditAction EditAction => KaraokeEditAction.MoveToLastIndex;\r\n\r\n    protected override MovingCaretAction AcceptAction => MovingCaretAction.LastIndex;\r\n\r\n    public MoveToLastIndexButton()\r\n    {\r\n        SetIcon(FontAwesome.Solid.AngleDoubleRight);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/Toolbar/Carets/MoveToNextIndexButton.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose.Toolbar.Carets;\r\n\r\npublic partial class MoveToNextIndexButton : MoveToCaretPositionButton\r\n{\r\n    protected override KaraokeEditAction EditAction => KaraokeEditAction.MoveToNextIndex;\r\n\r\n    protected override MovingCaretAction AcceptAction => MovingCaretAction.NextIndex;\r\n\r\n    public MoveToNextIndexButton()\r\n    {\r\n        SetIcon(FontAwesome.Solid.AngleRight);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/Toolbar/Carets/MoveToNextLyricButton.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose.Toolbar.Carets;\r\n\r\npublic partial class MoveToNextLyricButton : MoveToCaretPositionButton\r\n{\r\n    protected override KaraokeEditAction EditAction => KaraokeEditAction.MoveToNextLyric;\r\n\r\n    protected override MovingCaretAction AcceptAction => MovingCaretAction.NextLyric;\r\n\r\n    public MoveToNextLyricButton()\r\n    {\r\n        SetIcon(FontAwesome.Solid.ArrowDown);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/Toolbar/Carets/MoveToPreviousIndexButton.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose.Toolbar.Carets;\r\n\r\npublic partial class MoveToPreviousIndexButton : MoveToCaretPositionButton\r\n{\r\n    protected override KaraokeEditAction EditAction => KaraokeEditAction.MoveToPreviousIndex;\r\n\r\n    protected override MovingCaretAction AcceptAction => MovingCaretAction.PreviousIndex;\r\n\r\n    public MoveToPreviousIndexButton()\r\n    {\r\n        SetIcon(FontAwesome.Solid.AngleLeft);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/Toolbar/Carets/MoveToPreviousLyricButton.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose.Toolbar.Carets;\r\n\r\npublic partial class MoveToPreviousLyricButton : MoveToCaretPositionButton\r\n{\r\n    protected override KaraokeEditAction EditAction => KaraokeEditAction.MoveToPreviousLyric;\r\n\r\n    protected override MovingCaretAction AcceptAction => MovingCaretAction.PreviousLyric;\r\n\r\n    public MoveToPreviousLyricButton()\r\n    {\r\n        SetIcon(FontAwesome.Solid.ArrowUp);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/Toolbar/Panels/ToggleInvalidInfoPanelButton.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose.Toolbar.Panels;\r\n\r\npublic partial class ToggleInvalidInfoPanelButton : ToolbarToggleButton\r\n{\r\n    public ToggleInvalidInfoPanelButton()\r\n    {\r\n        SetIcon(FontAwesome.Solid.ExclamationTriangle);\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(KaraokeRulesetLyricEditorConfigManager lyricEditorConfigManager)\r\n    {\r\n        lyricEditorConfigManager.BindWith(KaraokeRulesetLyricEditorSetting.ShowInvalidInfoInComposer, Active);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/Toolbar/Panels/TogglePropertyPanelButton.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose.Toolbar.Panels;\r\n\r\npublic partial class TogglePropertyPanelButton : ToolbarToggleButton\r\n{\r\n    public TogglePropertyPanelButton()\r\n    {\r\n        SetIcon(FontAwesome.Solid.FileImage);\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(KaraokeRulesetLyricEditorConfigManager lyricEditorConfigManager)\r\n    {\r\n        lyricEditorConfigManager.BindWith(KaraokeRulesetLyricEditorSetting.ShowPropertyPanelInComposer, Active);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/Toolbar/Playback/PlaybackSwitchButton.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Audio;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Extensions.IEnumerableExtensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Screens.Edit;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose.Toolbar.Playback;\r\n\r\npublic partial class PlaybackSwitchButton : CompositeDrawable\r\n{\r\n    private readonly BindableNumber<double> freqAdjust = new BindableDouble(1);\r\n\r\n    [Resolved]\r\n    private EditorClock editorClock { get; set; } = null!;\r\n\r\n    public PlaybackSwitchButton()\r\n    {\r\n        Height = SpecialActionToolbar.HEIGHT;\r\n        AutoSizeAxes = Axes.X;\r\n        InternalChild = new PlaybackTabControl\r\n        {\r\n            Anchor = Anchor.Centre,\r\n            Origin = Anchor.Centre,\r\n            Current = freqAdjust,\r\n        };\r\n    }\r\n\r\n    protected override void LoadComplete()\r\n    {\r\n        base.LoadComplete();\r\n\r\n        editorClock.AudioAdjustments.AddAdjustment(AdjustableProperty.Frequency, freqAdjust);\r\n    }\r\n\r\n    protected override void Dispose(bool isDisposing)\r\n    {\r\n        editorClock.AudioAdjustments.RemoveAdjustment(AdjustableProperty.Frequency, freqAdjust);\r\n\r\n        base.Dispose(isDisposing);\r\n    }\r\n\r\n    private partial class PlaybackTabControl : OsuTabControl<double>\r\n    {\r\n        private static readonly double[] tempo_values = { 0.25, 0.5, 0.75, 1 };\r\n\r\n        protected override Dropdown<double>? CreateDropdown() => null;\r\n\r\n        protected override TabItem<double> CreateTabItem(double value) => new PlaybackTabItem(value);\r\n\r\n        protected override TabFillFlowContainer CreateTabFlow() => new()\r\n        {\r\n            AutoSizeAxes = Axes.Both,\r\n            Spacing = new Vector2(SpecialActionToolbar.SPACING),\r\n            Direction = FillDirection.Horizontal,\r\n        };\r\n\r\n        public PlaybackTabControl()\r\n        {\r\n            AutoSizeAxes = Axes.Both;\r\n\r\n            tempo_values.ForEach(AddItem);\r\n        }\r\n\r\n        public partial class PlaybackTabItem : TabItem<double>\r\n        {\r\n            private const float fade_duration = 200;\r\n\r\n            private readonly OsuSpriteText text;\r\n\r\n            public PlaybackTabItem(double value)\r\n                : base(value)\r\n            {\r\n                Size = new Vector2(SpecialActionToolbar.ICON_SIZE);\r\n\r\n                Children = new Drawable[]\r\n                {\r\n                    text = new OsuSpriteText\r\n                    {\r\n                        Origin = Anchor.Centre,\r\n                        Anchor = Anchor.Centre,\r\n                        Text = $\"{value:0%}\",\r\n                    },\r\n                };\r\n\r\n                updateState();\r\n            }\r\n\r\n            protected override void OnActivated() => updateState();\r\n            protected override void OnDeactivated() => updateState();\r\n\r\n            private void updateState()\r\n            {\r\n                bool active = Active.Value;\r\n\r\n                text.Alpha = active ? 1 : 0.5f;\r\n                text.Font = OsuFont.GetFont(size: 14, weight: active ? FontWeight.Bold : FontWeight.Medium);\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/Toolbar/Separator.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose.Toolbar;\r\n\r\npublic partial class Separator : CompositeDrawable\r\n{\r\n    private readonly IBindable<LyricEditorMode> bindableMode = new Bindable<LyricEditorMode>();\r\n\r\n    private readonly Box barLine;\r\n\r\n    public Separator()\r\n    {\r\n        Size = new Vector2(3, SpecialActionToolbar.HEIGHT);\r\n        InternalChild = barLine = new Box\r\n        {\r\n            RelativeSizeAxes = Axes.Both,\r\n        };\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(ILyricEditorState state, LyricEditorColourProvider colourProvider)\r\n    {\r\n        bindableMode.BindValueChanged(x =>\r\n        {\r\n            barLine.Colour = colourProvider.Background1(state.Mode);\r\n        }, true);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/Toolbar/ToolbarButton.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Containers;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose.Toolbar;\r\n\r\n/// <summary>\r\n/// Base toolbar button.\r\n/// </summary>\r\npublic abstract partial class ToolbarButton : OsuClickableContainer\r\n{\r\n    [Resolved]\r\n    private OsuColour colours { get; set; } = null!;\r\n\r\n    protected ConstrainedIconContainer IconContainer;\r\n\r\n    protected ToolbarButton()\r\n    {\r\n        Children = new Drawable[]\r\n        {\r\n            IconContainer = new ConstrainedIconContainer\r\n            {\r\n                Anchor = Anchor.Centre,\r\n                Origin = Anchor.Centre,\r\n                Size = new Vector2(SpecialActionToolbar.ICON_SIZE),\r\n                Alpha = 0,\r\n            },\r\n        };\r\n    }\r\n\r\n    public void SetIcon(IconUsage iconUsage) =>\r\n        SetIcon(new SpriteIcon\r\n        {\r\n            Icon = iconUsage,\r\n        });\r\n\r\n    public void SetIcon(Drawable icon)\r\n    {\r\n        Size = new Vector2(SpecialActionToolbar.HEIGHT);\r\n        IconContainer.Icon = icon;\r\n        IconContainer.Show();\r\n    }\r\n\r\n    protected void SetState(bool enabled)\r\n    {\r\n        IconContainer.Icon.Alpha = enabled ? 1f : 0.5f;\r\n        Enabled.Value = enabled;\r\n    }\r\n\r\n    protected override bool OnClick(ClickEvent e)\r\n    {\r\n        ToggleClickEffect();\r\n\r\n        return base.OnClick(e);\r\n    }\r\n\r\n    public void ToggleClickEffect()\r\n    {\r\n        if (Enabled.Value)\r\n        {\r\n            IconContainer.FadeOut(100).Then().FadeIn();\r\n        }\r\n        else\r\n        {\r\n            IconContainer.FadeColour(colours.Red, 100).Then().FadeColour(Colour4.White);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/Toolbar/ToolbarEditActionButton.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Input.Bindings;\r\nusing osu.Framework.Input.Events;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose.Toolbar;\r\n\r\n/// <summary>\r\n/// Button that able to receive the <see cref=\"KaraokeEditAction\"/> event.\r\n/// </summary>\r\npublic abstract partial class ToolbarEditActionButton : ToolbarButton, IKeyBindingHandler<KaraokeEditAction>, IHasIKeyBindingHandlerOrder\r\n{\r\n    protected abstract KaraokeEditAction EditAction { get; }\r\n\r\n    public int KeyBindingHandlerOrder => int.MinValue;\r\n\r\n    public bool OnPressed(KeyBindingPressEvent<KaraokeEditAction> e)\r\n    {\r\n        if (e.Action != EditAction)\r\n            return false;\r\n\r\n        // press button should did the same things as click.\r\n        return TriggerClick();\r\n    }\r\n\r\n    public void OnReleased(KeyBindingReleaseEvent<KaraokeEditAction> e)\r\n    {\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/Toolbar/ToolbarToggleButton.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Bindables;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose.Toolbar;\r\n\r\n/// <summary>\r\n/// Button for toggle open and close.\r\n/// </summary>\r\npublic abstract partial class ToolbarToggleButton : ToolbarButton\r\n{\r\n    protected readonly Bindable<bool> Active = new();\r\n\r\n    protected ToolbarToggleButton()\r\n    {\r\n        Active.BindValueChanged(x =>\r\n        {\r\n            // should wait until set icon done.\r\n            Schedule(() =>\r\n            {\r\n                toggle(x.NewValue);\r\n            });\r\n        }, true);\r\n\r\n        Action = () =>\r\n        {\r\n            Active.Value = !Active.Value;\r\n        };\r\n    }\r\n\r\n    private void toggle(bool active)\r\n    {\r\n        IconContainer.Icon.Alpha = active ? 1 : 0.5f;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Compose/Toolbar/View/AdjustFontSizeButton.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Extensions.IEnumerableExtensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose.Toolbar.View;\r\n\r\npublic partial class AdjustFontSizeButton : CompositeDrawable\r\n{\r\n    private readonly Bindable<float> bindableFontSize = new();\r\n\r\n    public AdjustFontSizeButton()\r\n    {\r\n        OsuSpriteText fontSizeSpriteText;\r\n\r\n        Height = SpecialActionToolbar.HEIGHT;\r\n        AutoSizeAxes = Axes.X;\r\n        InternalChild = new FillFlowContainer\r\n        {\r\n            Anchor = Anchor.Centre,\r\n            Origin = Anchor.Centre,\r\n            AutoSizeAxes = Axes.Both,\r\n            Direction = FillDirection.Horizontal,\r\n            Children = new Drawable[]\r\n            {\r\n                new DecreasePreviewFontSizeActionButton\r\n                {\r\n                    Size = new Vector2(SpecialActionToolbar.ICON_SIZE),\r\n                },\r\n                new Container\r\n                {\r\n                    Width = 48,\r\n                    Height = SpecialActionToolbar.ICON_SIZE,\r\n                    Child = fontSizeSpriteText = new OsuSpriteText\r\n                    {\r\n                        Anchor = Anchor.Centre,\r\n                        Origin = Anchor.Centre,\r\n                    },\r\n                },\r\n                new IncreasePreviewFontSizeActionButton\r\n                {\r\n                    Size = new Vector2(SpecialActionToolbar.ICON_SIZE),\r\n                },\r\n            },\r\n        };\r\n\r\n        bindableFontSize.BindValueChanged(e =>\r\n        {\r\n            fontSizeSpriteText.Text = FontUtils.GetText(e.NewValue);\r\n        });\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(KaraokeRulesetLyricEditorConfigManager lyricEditorConfigManager)\r\n    {\r\n        lyricEditorConfigManager.BindWith(KaraokeRulesetLyricEditorSetting.FontSizeInComposer, bindableFontSize);\r\n    }\r\n\r\n    private partial class DecreasePreviewFontSizeActionButton : PreviewFontSizeActionButton\r\n    {\r\n        protected override KaraokeEditAction EditAction => KaraokeEditAction.DecreasePreviewFontSize;\r\n\r\n        protected override float GetTriggeredFontSize(float[] sizes, float currentFontSize) => sizes.GetPrevious(currentFontSize);\r\n\r\n        public DecreasePreviewFontSizeActionButton()\r\n        {\r\n            SetIcon(FontAwesome.Solid.Minus);\r\n        }\r\n    }\r\n\r\n    private partial class IncreasePreviewFontSizeActionButton : PreviewFontSizeActionButton\r\n    {\r\n        protected override KaraokeEditAction EditAction => KaraokeEditAction.IncreasePreviewFontSize;\r\n\r\n        protected override float GetTriggeredFontSize(float[] sizes, float currentFontSize) => sizes.GetNext(currentFontSize);\r\n\r\n        public IncreasePreviewFontSizeActionButton()\r\n        {\r\n            SetIcon(FontAwesome.Solid.Plus);\r\n        }\r\n    }\r\n\r\n    private abstract partial class PreviewFontSizeActionButton : ToolbarEditActionButton\r\n    {\r\n        private static readonly float[] sizes = FontUtils.ComposerFontSize();\r\n\r\n        private readonly Bindable<float> bindableFontSize = new();\r\n\r\n        protected PreviewFontSizeActionButton()\r\n        {\r\n            Action = () =>\r\n            {\r\n                float triggeredFontSize = GetTriggeredFontSize(sizes, bindableFontSize.Value);\r\n                bindableFontSize.Value = triggeredFontSize != default ? triggeredFontSize : bindableFontSize.Value;\r\n            };\r\n\r\n            bindableFontSize.BindValueChanged(e =>\r\n            {\r\n                float triggeredFontSize = GetTriggeredFontSize(sizes, e.NewValue);\r\n                SetState(triggeredFontSize != default);\r\n            });\r\n        }\r\n\r\n        protected abstract float GetTriggeredFontSize(float[] sizes, float currentFontSize);\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(KaraokeRulesetLyricEditorConfigManager lyricEditorConfigManager)\r\n        {\r\n            lyricEditorConfigManager.BindWith(KaraokeRulesetLyricEditorSetting.FontSizeInComposer, bindableFontSize);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/ContentWrapper.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Compose;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.List;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content;\r\n\r\n/// <summary>\r\n/// Wrapper for able to change the different type of <see cref=\"LyricEditorLayout\"/>\r\n/// </summary>\r\npublic partial class ContentWrapper : CompositeDrawable\r\n{\r\n    private readonly LoadingSpinner loading;\r\n\r\n    public ContentWrapper()\r\n    {\r\n        InternalChildren = new[]\r\n        {\r\n            loading = new LoadingSpinner(true)\r\n            {\r\n                Depth = int.MinValue,\r\n            },\r\n        };\r\n    }\r\n\r\n    public void SwitchLayout(LyricEditorLayout layout)\r\n    {\r\n        loading.Show();\r\n\r\n        // should switch the layout after loaded.\r\n        Schedule(() =>\r\n        {\r\n            var newContent = getContent(layout).With(x =>\r\n            {\r\n                x.RelativeSizeAxes = Axes.Both;\r\n            });\r\n\r\n            var wrapper = new DelayedLoadWrapper(newContent).With(x =>\r\n            {\r\n                x.RelativeSizeAxes = Axes.Both;\r\n                x.RelativePositionAxes = Axes.Y;\r\n                x.Y = -0.5f;\r\n                x.Alpha = 0;\r\n            });\r\n\r\n            LoadComponentAsync(wrapper, content =>\r\n            {\r\n                const double remove_old_editor_time = 300;\r\n                const double new_animation_time = 1000;\r\n\r\n                var oldComponent = InternalChildren.Where(x => x != loading).OfType<DelayedLoadWrapper>().FirstOrDefault();\r\n                oldComponent?.MoveToY(-0.5f, remove_old_editor_time).FadeOut(remove_old_editor_time).OnComplete(x =>\r\n                {\r\n                    x.Expire();\r\n                });\r\n\r\n                AddInternal(content);\r\n                content.Delay(oldComponent != null ? remove_old_editor_time : 0)\r\n                       .Then()\r\n                       .FadeIn(new_animation_time)\r\n                       .MoveToY(0, new_animation_time)\r\n                       .OnComplete(_ =>\r\n                       {\r\n                           loading.Hide();\r\n                       });\r\n            });\r\n        });\r\n    }\r\n\r\n    private static MainContent getContent(LyricEditorLayout layout) =>\r\n        layout switch\r\n        {\r\n            LyricEditorLayout.List => new ListContent(),\r\n            LyricEditorLayout.Compose => new ComposeContent(),\r\n            _ => throw new ArgumentOutOfRangeException(nameof(layout), layout, null),\r\n        };\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/List/CreateNewLyricPreviewRow.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.List;\r\n\r\npublic partial class CreateNewLyricPreviewRow : PreviewRow\r\n{\r\n    [Resolved]\r\n    private ILyricsChangeHandler lyricsChangeHandler { get; set; } = null!;\r\n\r\n    public CreateNewLyricPreviewRow()\r\n        : base(new Lyric { Text = \"New lyric\" })\r\n    {\r\n    }\r\n\r\n    protected override Dimension GetRowDimensions() => new(GridSizeMode.Absolute, DEFAULT_HEIGHT);\r\n\r\n    protected override Drawable CreateLyricInfo(Lyric lyric)\r\n    {\r\n        return new Container\r\n        {\r\n            RelativeSizeAxes = Axes.Both,\r\n            Child = new IconButton\r\n            {\r\n                Anchor = Anchor.Centre,\r\n                Origin = Anchor.Centre,\r\n                Icon = FontAwesome.Solid.PlusCircle,\r\n                Size = new Vector2(32),\r\n                TooltipText = \"Click to add new lyric\",\r\n                Action = () =>\r\n                {\r\n                    lyricsChangeHandler.CreateAtLast();\r\n                },\r\n            },\r\n        };\r\n    }\r\n\r\n    protected override Drawable CreateContent(Lyric lyric)\r\n        => Empty();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/List/EditLyricPreviewRow.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Components.Lyrics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.List;\r\n\r\npublic partial class EditLyricPreviewRow : PreviewRow\r\n{\r\n    private const int min_height = 75;\r\n\r\n    public EditLyricPreviewRow(Lyric lyric)\r\n        : base(lyric)\r\n    {\r\n    }\r\n\r\n    protected override Drawable CreateLyricInfo(Lyric lyric)\r\n    {\r\n        return new InfoControl(lyric)\r\n        {\r\n            // todo : cannot use relative size to both because it will cause size cannot roll-back if make lyric smaller.\r\n            RelativeSizeAxes = Axes.X,\r\n            Height = min_height,\r\n        };\r\n    }\r\n\r\n    protected override Drawable CreateContent(Lyric lyric)\r\n    {\r\n        return new InteractableLyric(lyric)\r\n        {\r\n            Margin = new MarginPadding { Left = 10 },\r\n            RelativeSizeAxes = Axes.X,\r\n            TextSizeChanged = (self, size) =>\r\n            {\r\n                self.Height = size.Y;\r\n            },\r\n            Loaders = new LayerLoader[]\r\n            {\r\n                new LayerLoader<LyricLayer>(),\r\n                new LayerLoader<EditLyricLayer>(),\r\n                new LayerLoader<TimeTagLayer>(),\r\n                new LayerLoader<CaretLayer>(),\r\n                new LayerLoader<BlueprintLayer>(),\r\n            },\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/List/InfoControl.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Cursor;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Components.Badges;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Components.FixedInfo;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.List;\r\n\r\npublic partial class InfoControl : CompositeDrawable, IHasContextMenu\r\n{\r\n    private const int max_height = 120;\r\n\r\n    private readonly Box background;\r\n    private readonly Box headerBackground;\r\n    private readonly OsuSpriteText timeRange;\r\n    private readonly Container subInfoContainer;\r\n\r\n    private readonly IBindable<EditorModeWithEditStep> bindableModeWithEditStep = new Bindable<EditorModeWithEditStep>();\r\n\r\n    [Resolved]\r\n    private IDialogOverlay? dialogOverlay { get; set; }\r\n\r\n    [Resolved]\r\n    private ILyricsChangeHandler lyricsChangeHandler { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private ILyricCaretState lyricCaretState { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private LyricEditorColourProvider colourProvider { get; set; } = null!;\r\n\r\n    public Lyric Lyric { get; }\r\n\r\n    public InfoControl(Lyric lyric)\r\n    {\r\n        Lyric = lyric;\r\n\r\n        InternalChildren = new Drawable[]\r\n        {\r\n            background = new Box\r\n            {\r\n                RelativeSizeAxes = Axes.X,\r\n                Height = max_height,\r\n            },\r\n            new FillFlowContainer\r\n            {\r\n                Direction = FillDirection.Vertical,\r\n                RelativeSizeAxes = Axes.X,\r\n                AutoSizeAxes = Axes.Y,\r\n                Anchor = Anchor.TopRight,\r\n                Origin = Anchor.TopRight,\r\n                Spacing = new Vector2(5),\r\n                Children = new Drawable[]\r\n                {\r\n                    new Container\r\n                    {\r\n                        RelativeSizeAxes = Axes.X,\r\n                        Height = 36,\r\n                        Children = new Drawable[]\r\n                        {\r\n                            headerBackground = new Box\r\n                            {\r\n                                RelativeSizeAxes = Axes.Both,\r\n                            },\r\n                            timeRange = new OsuSpriteText\r\n                            {\r\n                                Anchor = Anchor.CentreLeft,\r\n                                Origin = Anchor.CentreLeft,\r\n                                Font = OsuFont.GetFont(size: 16, fixedWidth: true),\r\n                                Margin = new MarginPadding(10),\r\n                            },\r\n                            new InvalidInfo(lyric)\r\n                            {\r\n                                Anchor = Anchor.CentreRight,\r\n                                Origin = Anchor.CentreRight,\r\n                                Margin = new MarginPadding(10),\r\n                                Scale = new Vector2(1.3f),\r\n                                Y = 1,\r\n                            },\r\n                        },\r\n                    },\r\n                    new GridContainer\r\n                    {\r\n                        RelativeSizeAxes = Axes.X,\r\n                        AutoSizeAxes = Axes.Y,\r\n                        ColumnDimensions = new[]\r\n                        {\r\n                            new Dimension(),\r\n                            new Dimension(GridSizeMode.Absolute, 28),\r\n                        },\r\n                        Content = new[]\r\n                        {\r\n                            new Drawable[]\r\n                            {\r\n                                subInfoContainer = new Container\r\n                                {\r\n                                    RelativeSizeAxes = Axes.X,\r\n                                },\r\n                                new FillFlowContainer\r\n                                {\r\n                                    RelativeSizeAxes = Axes.X,\r\n                                    AutoSizeAxes = Axes.Y,\r\n                                    Direction = FillDirection.Vertical,\r\n                                    Spacing = new Vector2(5),\r\n                                    Children = new Drawable[]\r\n                                    {\r\n                                        new OrderInfo(lyric),\r\n                                        new LockInfo(lyric),\r\n                                    },\r\n                                },\r\n                            },\r\n                        },\r\n                    },\r\n                },\r\n            },\r\n        };\r\n\r\n        timeRange.Text = LyricUtils.LyricTimeFormattedString(lyric);\r\n\r\n        bindableModeWithEditStep.BindValueChanged(e =>\r\n        {\r\n            initializeBadge(e.NewValue);\r\n\r\n            if (ValueChangedEventUtils.EditModeChanged(e) || !IsLoaded)\r\n                Schedule(() => updateColour(e.NewValue.Mode));\r\n        }, true);\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(ILyricEditorState state)\r\n    {\r\n        bindableModeWithEditStep.BindTo(state.BindableModeWithEditStep);\r\n    }\r\n\r\n    private void updateColour(LyricEditorMode mode)\r\n    {\r\n        background.Colour = colourProvider.Background2(mode);\r\n        headerBackground.Colour = colourProvider.Background5(mode);\r\n    }\r\n\r\n    private void initializeBadge(EditorModeWithEditStep editorMode)\r\n    {\r\n        subInfoContainer.Clear();\r\n        var subInfo = createSubInfo(editorMode, Lyric);\r\n        if (subInfo == null)\r\n            return;\r\n\r\n        subInfo.Margin = new MarginPadding { Right = 15 };\r\n        subInfo.Anchor = Anchor.TopRight;\r\n        subInfo.Origin = Anchor.TopRight;\r\n        subInfoContainer.Add(subInfo);\r\n        return;\r\n\r\n        static Drawable? createSubInfo(EditorModeWithEditStep editorMode, Lyric lyric)\r\n        {\r\n            switch (editorMode.Mode)\r\n            {\r\n                case LyricEditorMode.View:\r\n                case LyricEditorMode.EditText:\r\n                    return null;\r\n\r\n                case LyricEditorMode.EditReferenceLyric:\r\n                    return new ReferenceLyricBadge(lyric);\r\n\r\n                case LyricEditorMode.EditLanguage:\r\n                    return new LanguageBadge(lyric);\r\n\r\n                case LyricEditorMode.EditRuby:\r\n                    return new LanguageBadge(lyric);\r\n\r\n                case LyricEditorMode.EditTimeTag:\r\n                    return createTimeTagModeSubInfo(editorMode.GetEditStep<TimeTagEditStep>(), lyric);\r\n\r\n                case LyricEditorMode.EditRomanisation:\r\n                    return new LanguageBadge(lyric);\r\n\r\n                case LyricEditorMode.EditNote:\r\n                    return null;\r\n\r\n                case LyricEditorMode.EditSinger:\r\n                    return new SingerBadge(lyric);\r\n\r\n                default:\r\n                    throw new ArgumentOutOfRangeException(nameof(editorMode));\r\n            }\r\n\r\n            static Drawable createTimeTagModeSubInfo(TimeTagEditStep editMode, Lyric lyric)\r\n            {\r\n                switch (editMode)\r\n                {\r\n                    case TimeTagEditStep.Create:\r\n                        return new LanguageBadge(lyric);\r\n\r\n                    case TimeTagEditStep.Recording:\r\n                    case TimeTagEditStep.Adjust:\r\n                        return new TimeTagBadge(lyric);\r\n\r\n                    default:\r\n                        throw new ArgumentOutOfRangeException(nameof(editMode));\r\n                }\r\n            }\r\n        }\r\n    }\r\n\r\n    public MenuItem[] ContextMenuItems\r\n    {\r\n        get\r\n        {\r\n            var editMode = bindableModeWithEditStep.Value.Mode;\r\n            if (editMode != LyricEditorMode.EditText)\r\n                return Array.Empty<MenuItem>();\r\n\r\n            // should select lyric if trying to interact with context menu.\r\n            lyricCaretState.MoveCaretToTargetPosition(Lyric);\r\n\r\n            var menuItems = new List<MenuItem>\r\n            {\r\n                new OsuMenuItem(\"Create new lyric\", MenuItemType.Standard, () =>\r\n                {\r\n                    lyricsChangeHandler.CreateAtPosition();\r\n                }),\r\n            };\r\n\r\n            // use lazy way to check lyric is not in first\r\n            if (Lyric.Order > 1)\r\n            {\r\n                menuItems.Add(new OsuMenuItem(\"Combine with previous lyric\", MenuItemType.Standard, () =>\r\n                {\r\n                    lyricsChangeHandler.Combine();\r\n                }));\r\n            }\r\n\r\n            menuItems.Add(new OsuMenuItem(\"Delete\", MenuItemType.Destructive, () =>\r\n            {\r\n                if (dialogOverlay == null)\r\n                {\r\n                    // todo : remove lyric directly in test case because pop-up dialog is not registered.\r\n                    lyricsChangeHandler.Remove();\r\n                }\r\n                else\r\n                {\r\n                    dialogOverlay.Push(new DeleteLyricDialog(isOk =>\r\n                    {\r\n                        if (isOk)\r\n                            lyricsChangeHandler.Remove();\r\n                    }));\r\n                }\r\n            }));\r\n\r\n            return menuItems.ToArray();\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/List/ListContent.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.List;\r\n\r\npublic partial class ListContent : MainContent\r\n{\r\n    public ListContent()\r\n    {\r\n        InternalChildren = new[]\r\n        {\r\n            new PreviewLyricList\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n            },\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/List/PreviewLyricList.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.List;\r\n\r\npublic partial class PreviewLyricList : LyricList\r\n{\r\n    private readonly IBindable<float> bindableFontSize = new Bindable<float>();\r\n\r\n    public PreviewLyricList()\r\n    {\r\n        bindableFontSize.BindValueChanged(e =>\r\n        {\r\n            AdjustSkin(skin =>\r\n            {\r\n                skin.FontSize = e.NewValue;\r\n            });\r\n        });\r\n    }\r\n\r\n    protected override DrawableLyricList CreateDrawableLyricList()\r\n        => new DrawablePreviewLyricList();\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(KaraokeRulesetLyricEditorConfigManager lyricEditorConfigManager)\r\n    {\r\n        lyricEditorConfigManager.BindWith(KaraokeRulesetLyricEditorSetting.LyricEditorFontSize, bindableFontSize);\r\n    }\r\n\r\n    public partial class DrawablePreviewLyricList : DrawableLyricList\r\n    {\r\n        private readonly IBindable<LyricEditorMode> bindableMode = new Bindable<LyricEditorMode>();\r\n        private readonly IBindable<bool> bindableAutoFocusToEditLyric = new Bindable<bool>();\r\n        private readonly IBindable<int> bindableAutoFocusToEditLyricSkipRows = new Bindable<int>();\r\n\r\n        protected override bool ScrollToPosition(ICaretPosition caret)\r\n        {\r\n            // should not move the position if caret is only support clicking.\r\n            if (caret is ClickingCaretPosition)\r\n                return false;\r\n\r\n            // should not move the position in manage lyric mode.\r\n            if (bindableMode.Value is LyricEditorMode.EditText or LyricEditorMode.EditRuby)\r\n                return false;\r\n\r\n            // move to target position if auto focus.\r\n            return bindableAutoFocusToEditLyric.Value;\r\n        }\r\n\r\n        protected override int SkipRows()\r\n        {\r\n            return bindableAutoFocusToEditLyricSkipRows.Value;\r\n        }\r\n\r\n        protected override Row CreateEditRow(Lyric lyric)\r\n            => new EditLyricPreviewRow(lyric);\r\n\r\n        protected override Row GetCreateNewLyricRow()\r\n            => new CreateNewLyricPreviewRow();\r\n\r\n        protected override Vector2 Spacing => new(0, 2);\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(ILyricEditorState state, KaraokeRulesetLyricEditorConfigManager lyricEditorConfigManager)\r\n        {\r\n            bindableMode.BindTo(state.BindableMode);\r\n\r\n            lyricEditorConfigManager.BindWith(KaraokeRulesetLyricEditorSetting.AutoFocusToEditLyric, bindableAutoFocusToEditLyric);\r\n            lyricEditorConfigManager.BindWith(KaraokeRulesetLyricEditorSetting.AutoFocusToEditLyricSkipRows, bindableAutoFocusToEditLyricSkipRows);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/List/PreviewRow.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.List;\r\n\r\npublic abstract partial class PreviewRow : Row\r\n{\r\n    protected const int DEFAULT_HEIGHT = 75;\r\n\r\n    private const int info_part_spacing = 210;\r\n    private const int min_height = DEFAULT_HEIGHT;\r\n    private const int max_height = 120;\r\n\r\n    protected PreviewRow(Lyric lyric)\r\n        : base(lyric)\r\n    {\r\n    }\r\n\r\n    protected override IEnumerable<Dimension> GetColumnDimensions() =>\r\n        new[]\r\n        {\r\n            new Dimension(GridSizeMode.Absolute, info_part_spacing),\r\n            new Dimension(),\r\n        };\r\n\r\n    protected override Dimension GetRowDimensions() => new(GridSizeMode.AutoSize, minSize: min_height, maxSize: max_height);\r\n\r\n    protected override IEnumerable<Drawable> GetDrawables(Lyric lyric) =>\r\n        new[]\r\n        {\r\n            CreateLyricInfo(lyric),\r\n            CreateContent(lyric),\r\n        };\r\n\r\n    protected override bool HighlightBackgroundWhenSelected(ICaretPosition? caretPosition)\r\n    {\r\n        // should not show the background in the assign language mode.\r\n        if (caretPosition is ClickingCaretPosition)\r\n            return false;\r\n\r\n        return true;\r\n    }\r\n\r\n    protected override Func<LyricEditorMode, Color4> GetBackgroundColour(BackgroundStyle style, LyricEditorColourProvider colourProvider) =>\r\n        style switch\r\n        {\r\n            BackgroundStyle.Idle => colourProvider.Background5,\r\n            BackgroundStyle.Hover => colourProvider.Background4,\r\n            BackgroundStyle.Focus => colourProvider.Background3,\r\n            _ => throw new ArgumentOutOfRangeException(nameof(style), style, null),\r\n        };\r\n\r\n    protected abstract Drawable CreateLyricInfo(Lyric lyric);\r\n\r\n    protected abstract Drawable CreateContent(Lyric lyric);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/LyricList.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Graphics.Containers;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Extensions;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.Containers;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\nusing osu.Game.Skinning;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content;\r\n\r\npublic abstract partial class LyricList : CompositeDrawable\r\n{\r\n    public const float LYRIC_LIST_PADDING = 10;\r\n    public const float HANDLER_WIDTH = 22;\r\n\r\n    [Resolved]\r\n    private ILyricsChangeHandler? lyricsChangeHandler { get; set; }\r\n\r\n    private readonly IBindable<LyricEditorMode> bindableMode = new Bindable<LyricEditorMode>();\r\n    private readonly IBindable<bool> bindableSelecting = new Bindable<bool>();\r\n\r\n    private readonly LyricEditorSkin skin;\r\n    private readonly DrawableLyricList container;\r\n    private readonly ApplySelectingArea applySelectingArea;\r\n\r\n    protected LyricList()\r\n    {\r\n        InternalChild = new GridContainer\r\n        {\r\n            RelativeSizeAxes = Axes.Both,\r\n            RowDimensions = new[]\r\n            {\r\n                new Dimension(),\r\n                new Dimension(GridSizeMode.AutoSize),\r\n            },\r\n            Content = new[]\r\n            {\r\n                new Drawable[]\r\n                {\r\n                    new SkinProvidingContainer(skin = new LyricEditorSkin(null))\r\n                    {\r\n                        RelativeSizeAxes = Axes.Both,\r\n                        Padding = new MarginPadding(LYRIC_LIST_PADDING),\r\n                        Child = container = CreateDrawableLyricList().With(x =>\r\n                        {\r\n                            x.RelativeSizeAxes = Axes.Both;\r\n                        }),\r\n                    },\r\n                },\r\n                new Drawable[]\r\n                {\r\n                    applySelectingArea = new ApplySelectingArea(),\r\n                },\r\n            },\r\n        };\r\n\r\n        container.OnOrderChanged += (x, nowOrder) =>\r\n        {\r\n            lyricsChangeHandler?.ChangeOrder(nowOrder);\r\n        };\r\n\r\n        bindableMode.BindValueChanged(e =>\r\n        {\r\n            updateAddLyricState();\r\n        }, true);\r\n\r\n        bindableSelecting.BindValueChanged(e =>\r\n        {\r\n            updateAddLyricState();\r\n            updateApplySelectingArea();\r\n        }, true);\r\n    }\r\n\r\n    protected void AdjustSkin(Action<LyricEditorSkin> action)\r\n    {\r\n        action(skin);\r\n    }\r\n\r\n    protected abstract DrawableLyricList CreateDrawableLyricList();\r\n\r\n    private void updateApplySelectingArea()\r\n    {\r\n        if (bindableSelecting.Value)\r\n        {\r\n            applySelectingArea.Show();\r\n        }\r\n        else\r\n        {\r\n            applySelectingArea.Hide();\r\n        }\r\n    }\r\n\r\n    private void updateAddLyricState()\r\n    {\r\n        // display add new lyric only with edit mode.\r\n        bool disableBottomDrawable = bindableMode.Value == LyricEditorMode.EditText && !bindableSelecting.Value;\r\n        container.DisplayBottomDrawable = disableBottomDrawable;\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(ILyricEditorState state, ILyricSelectionState lyricSelectionState, ILyricsProvider lyricsProvider)\r\n    {\r\n        bindableMode.BindTo(state.BindableMode);\r\n        bindableSelecting.BindTo(lyricSelectionState.Selecting);\r\n\r\n        container.Items.BindTo(lyricsProvider.BindableLyrics);\r\n    }\r\n\r\n    /// <summary>\r\n    /// Visualises a list of <see cref=\"Lyric\"/>s.\r\n    /// </summary>\r\n    public abstract partial class DrawableLyricList : OrderRearrangeableListContainer<Lyric>\r\n    {\r\n        private readonly IBindable<ICaretPosition?> bindableCaretPosition = new Bindable<ICaretPosition?>();\r\n\r\n        protected DrawableLyricList()\r\n        {\r\n            // update selected style to child\r\n            bindableCaretPosition.BindValueChanged(e =>\r\n            {\r\n                var newLyric = e.NewValue?.Lyric;\r\n                if (newLyric == null || !ValueChangedEventUtils.LyricChanged(e))\r\n                    return;\r\n\r\n                if (!ScrollToPosition(e.NewValue!))\r\n                    return;\r\n\r\n                int skippingRows = SkipRows();\r\n                moveItemToTargetPosition(newLyric, skippingRows);\r\n            });\r\n        }\r\n\r\n        private void moveItemToTargetPosition(Lyric targetLyric, int skippingRows)\r\n        {\r\n            var drawable = getListItem(targetLyric);\r\n            if (drawable == null)\r\n                return;\r\n\r\n            float topSpacing = drawable.Height * skippingRows;\r\n            float bottomSpacing = DrawHeight - drawable.Height * (skippingRows + 1);\r\n            ScrollContainer.ScrollIntoViewWithSpacing(drawable, new MarginPadding\r\n            {\r\n                Top = topSpacing,\r\n                Bottom = bottomSpacing,\r\n            });\r\n            return;\r\n\r\n            DrawableLyricListItem? getListItem(Lyric? lyric)\r\n                => ListContainer.Children.OfType<DrawableLyricListItem>().FirstOrDefault(x => x.Model == lyric);\r\n        }\r\n\r\n        protected abstract bool ScrollToPosition(ICaretPosition caret);\r\n\r\n        protected abstract int SkipRows();\r\n\r\n        protected abstract Row CreateEditRow(Lyric lyric);\r\n\r\n        protected abstract Row GetCreateNewLyricRow();\r\n\r\n        protected sealed override OsuRearrangeableListItem<Lyric> CreateOsuDrawable(Lyric item)\r\n            => new DrawableLyricListItem(item, CreateEditRow);\r\n\r\n        protected sealed override Drawable CreateBottomDrawable()\r\n        {\r\n            return new Container\r\n            {\r\n                // todo: should based on the row's height.\r\n                RelativeSizeAxes = Axes.X,\r\n                Height = 75,\r\n                Padding = new MarginPadding { Left = HANDLER_WIDTH },\r\n                Child = GetCreateNewLyricRow(),\r\n            };\r\n        }\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(ILyricCaretState lyricCaretState)\r\n        {\r\n            bindableCaretPosition.BindTo(lyricCaretState.BindableCaretPosition);\r\n        }\r\n\r\n        /// <summary>\r\n        /// Visualises a <see cref=\"Lyric\"/> inside a <see cref=\"DrawableLyricList\"/>.\r\n        /// </summary>\r\n        public sealed partial class DrawableLyricListItem : OsuRearrangeableListItem<Lyric>\r\n        {\r\n            [Resolved]\r\n            private ILyricCaretState lyricCaretState { get; set; } = null!;\r\n\r\n            private readonly IBindable<LyricEditorMode> bindableMode = new Bindable<LyricEditorMode>();\r\n            private readonly IBindable<bool> bindableSelecting = new Bindable<bool>();\r\n\r\n            private readonly Func<Lyric, Row> createRowFunc;\r\n\r\n            public DrawableLyricListItem(Lyric item, Func<Lyric, Row> createRowFunc)\r\n                : base(item)\r\n            {\r\n                this.createRowFunc = createRowFunc;\r\n\r\n                bindableMode.BindValueChanged(_ =>\r\n                {\r\n                    updateDragHandler();\r\n                }, true);\r\n\r\n                bindableSelecting.BindValueChanged(_ =>\r\n                {\r\n                    updateDragHandler();\r\n                }, true);\r\n\r\n                DragActive.BindValueChanged(e =>\r\n                {\r\n                    // should mark object as selecting while dragging.\r\n                    lyricCaretState.MoveCaretToTargetPosition(Model);\r\n                });\r\n            }\r\n\r\n            private void updateDragHandler()\r\n            {\r\n                ShowDragHandle.Value = ShowDragHandler(bindableMode.Value, bindableSelecting.Value);\r\n            }\r\n\r\n            protected override Drawable CreateContent() => createRowFunc(Model);\r\n\r\n            [BackgroundDependencyLoader]\r\n            private void load(ILyricEditorState state, ILyricSelectionState lyricSelectionState)\r\n            {\r\n                bindableMode.BindTo(state.BindableMode);\r\n                bindableSelecting.BindTo(lyricSelectionState.Selecting);\r\n            }\r\n        }\r\n    }\r\n\r\n    protected static bool ShowDragHandler(LyricEditorMode editorMode, bool selecting)\r\n        => editorMode == LyricEditorMode.EditText && !selecting;\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/MainContent.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics.Containers;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content;\r\n\r\npublic partial class MainContent : CompositeDrawable;\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Content/Row.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content;\r\n\r\npublic abstract partial class Row : CompositeDrawable\r\n{\r\n    public const int SELECT_AREA_WIDTH = 48;\r\n\r\n    [Resolved]\r\n    private LyricEditorColourProvider colourProvider { get; set; } = null!;\r\n\r\n    private readonly IBindable<LyricEditorMode> bindableMode = new Bindable<LyricEditorMode>();\r\n    private readonly IBindable<ICaretPosition?> bindableHoverCaretPosition = new Bindable<ICaretPosition?>();\r\n    private readonly IBindable<ICaretPosition?> bindableCaretPosition = new Bindable<ICaretPosition?>();\r\n    private readonly IBindable<RangeCaretPosition?> bindableRangeCaretPosition = new Bindable<RangeCaretPosition?>();\r\n\r\n    protected readonly Lyric Lyric;\r\n\r\n    private readonly Box background;\r\n\r\n    protected Row(Lyric lyric)\r\n    {\r\n        Lyric = lyric;\r\n\r\n        RelativeSizeAxes = Axes.X;\r\n        AutoSizeAxes = Axes.Y;\r\n\r\n        var columnDimensions = new List<Dimension>\r\n        {\r\n            new(GridSizeMode.AutoSize),\r\n        };\r\n        columnDimensions.AddRange(GetColumnDimensions());\r\n\r\n        var rowDimensions = GetRowDimensions();\r\n\r\n        var columns = new List<Drawable>\r\n        {\r\n            new SelectArea(lyric),\r\n        };\r\n        columns.AddRange(GetDrawables(lyric));\r\n\r\n        InternalChild = new Container\r\n        {\r\n            Masking = true,\r\n            CornerRadius = 5,\r\n            AutoSizeAxes = Axes.Y,\r\n            RelativeSizeAxes = Axes.X,\r\n            Children = new Drawable[]\r\n            {\r\n                background = new Box\r\n                {\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    Alpha = 0.9f,\r\n                },\r\n                new GridContainer\r\n                {\r\n                    RelativeSizeAxes = Axes.X,\r\n                    AutoSizeAxes = Axes.Y,\r\n                    ColumnDimensions = columnDimensions.ToArray(),\r\n                    RowDimensions = new[] { rowDimensions },\r\n                    Content = new[]\r\n                    {\r\n                        columns.ToArray(),\r\n                    },\r\n                },\r\n            },\r\n        };\r\n\r\n        bindableMode.BindValueChanged(_ =>\r\n        {\r\n            updateBackgroundColour();\r\n        });\r\n\r\n        bindableHoverCaretPosition.BindValueChanged(e =>\r\n        {\r\n            if (!ValueChangedEventUtils.LyricChanged(e))\r\n                return;\r\n\r\n            updateBackgroundColour();\r\n        });\r\n\r\n        bindableCaretPosition.BindValueChanged(e =>\r\n        {\r\n            if (!ValueChangedEventUtils.LyricChanged(e))\r\n                return;\r\n\r\n            updateBackgroundColour();\r\n        });\r\n\r\n        bindableRangeCaretPosition.BindValueChanged(e =>\r\n        {\r\n            if (!ValueChangedEventUtils.LyricChanged(e))\r\n                return;\r\n\r\n            updateBackgroundColour();\r\n        });\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(ILyricEditorState state, ILyricCaretState lyricCaretState)\r\n    {\r\n        bindableMode.BindTo(state.BindableMode);\r\n\r\n        bindableHoverCaretPosition.BindTo(lyricCaretState.BindableHoverCaretPosition);\r\n        bindableCaretPosition.BindTo(lyricCaretState.BindableCaretPosition);\r\n        bindableRangeCaretPosition.BindTo(lyricCaretState.BindableRangeCaretPosition);\r\n\r\n        updateBackgroundColour();\r\n    }\r\n\r\n    protected abstract IEnumerable<Dimension> GetColumnDimensions();\r\n\r\n    protected abstract Dimension GetRowDimensions();\r\n\r\n    protected abstract IEnumerable<Drawable> GetDrawables(Lyric lyric);\r\n\r\n    protected abstract bool HighlightBackgroundWhenSelected(ICaretPosition? caretPosition);\r\n\r\n    protected abstract Func<LyricEditorMode, Color4> GetBackgroundColour(BackgroundStyle style, LyricEditorColourProvider colourProvider);\r\n\r\n    private void updateBackgroundColour()\r\n    {\r\n        var mode = bindableMode.Value;\r\n        var backgroundStyle = getBackgroundStyle();\r\n\r\n        var backgroundColourFunction = GetBackgroundColour(backgroundStyle, colourProvider);\r\n        var colour = backgroundColourFunction(mode);\r\n\r\n        background.Colour = colour;\r\n\r\n        BackgroundStyle getBackgroundStyle()\r\n        {\r\n            if (highlightBackgroundByCaretPosition(bindableCaretPosition.Value))\r\n                return BackgroundStyle.Focus;\r\n\r\n            if (highlightBackgroundByRangeCaretPosition(bindableRangeCaretPosition.Value))\r\n                return BackgroundStyle.Focus;\r\n\r\n            if (highlightBackgroundByCaretPosition(bindableHoverCaretPosition.Value))\r\n                return BackgroundStyle.Hover;\r\n\r\n            return BackgroundStyle.Idle;\r\n        }\r\n\r\n        bool highlightBackgroundByCaretPosition(ICaretPosition? caretPosition)\r\n        {\r\n            if (caretPosition?.Lyric != Lyric)\r\n                return false;\r\n\r\n            return HighlightBackgroundWhenSelected(caretPosition);\r\n        }\r\n\r\n        bool highlightBackgroundByRangeCaretPosition(RangeCaretPosition? rangeCaretPosition)\r\n        {\r\n            if (rangeCaretPosition == null || !rangeCaretPosition.IsInRange(Lyric))\r\n                return false;\r\n\r\n            return HighlightBackgroundWhenSelected(rangeCaretPosition.Start);\r\n        }\r\n    }\r\n\r\n    protected enum BackgroundStyle\r\n    {\r\n        Idle,\r\n        Hover,\r\n        Focus,\r\n    }\r\n\r\n    public partial class SelectArea : CompositeDrawable\r\n    {\r\n        private readonly IBindable<LyricEditorMode> bindableMode = new Bindable<LyricEditorMode>();\r\n        private readonly IBindable<bool> selecting = new Bindable<bool>();\r\n        private readonly IBindableDictionary<Lyric, LocalisableString> disableSelectingLyrics = new BindableDictionary<Lyric, LocalisableString>();\r\n        private readonly IBindableList<Lyric> selectedLyrics = new BindableList<Lyric>();\r\n\r\n        private readonly Box background;\r\n        private readonly CircleCheckbox selectedCheckbox;\r\n\r\n        private readonly Lyric lyric;\r\n\r\n        public SelectArea(Lyric lyric)\r\n        {\r\n            this.lyric = lyric;\r\n\r\n            Width = SELECT_AREA_WIDTH;\r\n            RelativeSizeAxes = Axes.Y;\r\n            InternalChildren = new Drawable[]\r\n            {\r\n                background = new Box\r\n                {\r\n                    RelativeSizeAxes = Axes.Both,\r\n                },\r\n                selectedCheckbox = new CircleCheckbox\r\n                {\r\n                    Anchor = Anchor.Centre,\r\n                    Origin = Anchor.Centre,\r\n                },\r\n            };\r\n        }\r\n\r\n        protected override bool OnClick(ClickEvent e)\r\n        {\r\n            // trigger checkbox click if click this area.\r\n            selectedCheckbox.TriggerEvent(e);\r\n            return base.OnClick(e);\r\n        }\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(ILyricEditorState state, ILyricSelectionState lyricSelectionState, LyricEditorColourProvider colourProvider)\r\n        {\r\n            bindableMode.BindTo(state.BindableMode);\r\n            selecting.BindTo(lyricSelectionState.Selecting);\r\n            disableSelectingLyrics.BindTo(lyricSelectionState.DisableSelectingLyric);\r\n            selectedLyrics.BindTo(lyricSelectionState.SelectedLyrics);\r\n\r\n            // should update background if mode changed.\r\n            bindableMode.BindValueChanged(e =>\r\n            {\r\n                background.Colour = colourProvider.Dark2(e.NewValue);\r\n                selectedCheckbox.AccentColour = colourProvider.Colour2(e.NewValue);\r\n            }, true);\r\n\r\n            // show this area only if in selecting.\r\n            selecting.BindValueChanged(e =>\r\n            {\r\n                if (e.NewValue)\r\n                {\r\n                    Show();\r\n                }\r\n                else\r\n                {\r\n                    Hide();\r\n                }\r\n            }, true);\r\n\r\n            // get bindable and update bindable if check / uncheck.\r\n            selectedLyrics.BindCollectionChanged((_, _) =>\r\n            {\r\n                if (selectedCheckbox.Current.Disabled)\r\n                    return;\r\n\r\n                bool selected = selectedLyrics.Contains(lyric);\r\n                selectedCheckbox.Current.Value = selected;\r\n            }, true);\r\n\r\n            // should disable selection if current lyric is disabled.\r\n            disableSelectingLyrics.BindCollectionChanged((_, _) =>\r\n            {\r\n                bool disabled = disableSelectingLyrics.Keys.Contains(lyric);\r\n                selectedCheckbox.Current.Disabled = disabled;\r\n                selectedCheckbox.TooltipText = disabled ? disableSelectingLyrics[lyric] : default;\r\n            });\r\n\r\n            selectedCheckbox.Current.BindValueChanged(e =>\r\n            {\r\n                if (e.NewValue)\r\n                {\r\n                    lyricSelectionState.Select(lyric);\r\n                }\r\n                else\r\n                {\r\n                    lyricSelectionState.UnSelect(lyric);\r\n                }\r\n            });\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/DeleteLyricDialog.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Overlays.Dialog;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics;\r\n\r\npublic partial class DeleteLyricDialog : PopupDialog\r\n{\r\n    public DeleteLyricDialog(Action<bool> okAction)\r\n    {\r\n        Icon = FontAwesome.Solid.Globe;\r\n        HeaderText = \"Confirm deletion of\";\r\n        BodyText = \"lyric\";\r\n        Buttons = new PopupDialogButton[]\r\n        {\r\n            new PopupDialogOkButton\r\n            {\r\n                Text = \"Yes. Go for it.\",\r\n                Action = () => okAction(true),\r\n            },\r\n            new PopupDialogCancelButton\r\n            {\r\n                Text = \"No! Abort mission!\",\r\n                Action = () => okAction(false),\r\n            },\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/IIssueNavigator.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics;\r\n\r\npublic interface IIssueNavigator\r\n{\r\n    void Navigate(Issue issue);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/ILyricEditorClipboard.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics;\r\n\r\npublic interface ILyricEditorClipboard\r\n{\r\n    bool Cut();\r\n\r\n    bool Copy();\r\n\r\n    bool Paste();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/ILyricEditorState.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Bindables;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics;\r\n\r\npublic interface ILyricEditorState\r\n{\r\n    IBindable<LyricEditorMode> BindableMode { get; }\r\n\r\n    IBindable<EditorModeWithEditStep> BindableModeWithEditStep { get; }\r\n\r\n    LyricEditorMode Mode { get; }\r\n\r\n    void SwitchMode(LyricEditorMode mode);\r\n\r\n    void SwitchEditStep<TEditStep>(TEditStep editStep) where TEditStep : Enum;\r\n\r\n    void NavigateToFix(LyricEditorMode mode);\r\n}\r\n\r\npublic struct EditorModeWithEditStep\r\n{\r\n    public LyricEditorMode Mode;\r\n\r\n    public Enum? EditStep;\r\n\r\n    public bool Default;\r\n\r\n    public EditorModeWithEditStep()\r\n    {\r\n        Mode = LyricEditorMode.View;\r\n        EditStep = null;\r\n        Default = true;\r\n    }\r\n\r\n    public TEditStep GetEditStep<TEditStep>() where TEditStep : Enum\r\n    {\r\n        if (EditStep is not TEditStep editStep)\r\n            throw new InvalidOperationException();\r\n\r\n        return editStep;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/ILyricEditorVerifier.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics;\r\n\r\npublic interface ILyricEditorVerifier : IEditorVerifier<LyricEditorMode>\r\n{\r\n    IBindableList<Issue> GetBindable(KaraokeHitObject hitObject);\r\n\r\n    void RefreshByHitObject(KaraokeHitObject hitObject);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/IssueNavigator.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks.Issues;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics;\r\n\r\npublic partial class IssueNavigator : Component, IIssueNavigator\r\n{\r\n    [Resolved]\r\n    private ILyricEditorState lyricEditorState { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private ILyricCaretState lyricCaretState { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private IEditRubyModeState editRubyModeState { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private IEditTimeTagModeState editTimeTagModeState { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private IEditRomanisationModeState editRomanisationModeState { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private IEditNoteModeState noteModeState { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private EditorClock clock { get; set; } = null!;\r\n\r\n    public void Navigate(Issue issue)\r\n    {\r\n        // seek the time if contains the time in the issue.\r\n        if (issue.Time.HasValue)\r\n            clock.Seek(issue.Time.Value);\r\n\r\n        // navigate to edit mode.\r\n        var targetEditMode = getNavigateEditMode(issue.Check);\r\n        if (targetEditMode != null)\r\n            lyricEditorState.SwitchMode(targetEditMode.Value);\r\n\r\n        // navigate to sub-mode if needed.\r\n        var targetEditStep = getNavigateEditStep(issue);\r\n        if (targetEditStep != null)\r\n            lyricEditorState.SwitchEditStep(targetEditStep);\r\n\r\n        // navigate to the target lyric.\r\n        (var lyric, object? lyricIndex) = getNavigateLyricAndIndex(issue);\r\n        if (lyric == null)\r\n            return;\r\n\r\n        lyricCaretState.MoveCaretToTargetPosition(lyric);\r\n\r\n        // navigate to the target index in the lyric.\r\n        if (lyricIndex == null)\r\n            return;\r\n\r\n        var blueprintSelection = getBlueprintSelection(lyricIndex);\r\n        blueprintSelection?.Select(lyricIndex);\r\n    }\r\n\r\n    private static LyricEditorMode? getNavigateEditMode(ICheck check)\r\n    {\r\n        switch (check)\r\n        {\r\n            case CheckLyricText:\r\n                return LyricEditorMode.EditText;\r\n\r\n            case CheckLyricReferenceLyric:\r\n                return LyricEditorMode.EditReferenceLyric;\r\n\r\n            case CheckLyricLanguage:\r\n                return LyricEditorMode.EditLanguage;\r\n\r\n            case CheckLyricRubyTag:\r\n                return LyricEditorMode.EditRuby;\r\n\r\n            case CheckLyricTimeTagOnly:\r\n                return LyricEditorMode.EditTimeTag;\r\n\r\n            case CheckLyricRomanisation:\r\n                return LyricEditorMode.EditRomanisation;\r\n\r\n            case CheckNoteReferenceLyric:\r\n            case CheckNoteText:\r\n                return LyricEditorMode.EditNote;\r\n\r\n            default:\r\n                return null;\r\n        }\r\n    }\r\n\r\n    private static Enum? getNavigateEditStep(Issue issue)\r\n    {\r\n        // todo: implement.\r\n        return null;\r\n    }\r\n\r\n    private static Tuple<Lyric?, object?> getNavigateLyricAndIndex(Issue issue) =>\r\n        issue switch\r\n        {\r\n            LyricRubyTagIssue rubyTagIssue => new Tuple<Lyric?, object?>(rubyTagIssue.Lyric, rubyTagIssue.RubyTag),\r\n            LyricTimeTagIssue timeTagIssue => new Tuple<Lyric?, object?>(timeTagIssue.Lyric, timeTagIssue.TimeTag),\r\n            LyricIssue lyricIssue => new Tuple<Lyric?, object?>(lyricIssue.Lyric, null),\r\n            NoteIssue noteIssue => new Tuple<Lyric?, object?>(noteIssue.Note.ReferenceLyric, null),\r\n            _ => new Tuple<Lyric?, object?>(null, null),\r\n        };\r\n\r\n    private IHasBlueprintSelection<TItem>? getBlueprintSelection<TItem>(TItem item) where TItem : class\r\n    {\r\n        object[] availableEditModes =\r\n        {\r\n            editRubyModeState,\r\n            editRomanisationModeState,\r\n            editTimeTagModeState,\r\n            noteModeState,\r\n        };\r\n\r\n        return availableEditModes.OfType<IHasBlueprintSelection<TItem>>().FirstOrDefault();\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/LyricEditor.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Input;\r\nusing osu.Framework.Input.Bindings;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Extensions;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\nusing osu.Game.Rulesets.Timing;\r\nusing osu.Game.Rulesets.UI;\r\nusing osu.Game.Rulesets.UI.Scrolling;\r\nusing osu.Game.Rulesets.UI.Scrolling.Algorithms;\r\nusing osu.Game.Screens;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics;\r\n\r\n[Cached(typeof(ILyricEditorState))]\r\npublic partial class LyricEditor : CompositeDrawable, ILyricEditorState, IKeyBindingHandler<KaraokeEditAction>, IKeyBindingHandler<PlatformAction>\r\n{\r\n    [Cached]\r\n    private readonly LyricEditorColourProvider colourProvider = new();\r\n\r\n    [Cached(typeof(ILyricSelectionState))]\r\n    private readonly LyricSelectionState lyricSelectionState;\r\n\r\n    [Cached(typeof(ILyricCaretState))]\r\n    private readonly LyricCaretState lyricCaretState;\r\n\r\n    [Cached(typeof(IEditTextModeState))]\r\n    private readonly EditTextModeState editTextModeState;\r\n\r\n    [Cached(typeof(IEditReferenceLyricModeState))]\r\n    private readonly EditReferenceLyricModeState editReferenceLyricModeState;\r\n\r\n    [Cached(typeof(IEditLanguageModeState))]\r\n    private readonly EditLanguageModeState editLanguageModeState;\r\n\r\n    [Cached(typeof(IEditRubyModeState))]\r\n    private readonly EditRubyModeState editRubyModeState;\r\n\r\n    [Cached(typeof(IEditTimeTagModeState))]\r\n    private readonly EditTimeTagModeState editTimeTagModeState;\r\n\r\n    [Cached(typeof(IEditRomanisationModeState))]\r\n    private readonly EditRomanisationModeState editRomanisationModeState;\r\n\r\n    [Cached(typeof(IEditNoteModeState))]\r\n    private readonly EditNoteModeState editNoteModeState;\r\n\r\n    [Cached(typeof(ILyricEditorClipboard))]\r\n    private readonly LyricEditorClipboard lyricEditorClipboard;\r\n\r\n    [Cached(typeof(ILyricEditorVerifier))]\r\n    private readonly LyricEditorVerifier lyricEditorVerifier;\r\n\r\n    [Cached(typeof(IIssueNavigator))]\r\n    private readonly IssueNavigator issueNavigator;\r\n\r\n    [Cached(typeof(IScrollingInfo))]\r\n    private readonly LocalScrollingInfo scrollingInfo = new();\r\n\r\n    [Cached]\r\n    private readonly BindableBeatDivisor beatDivisor = new();\r\n\r\n    private readonly Bindable<LyricEditorMode> bindableMode = new();\r\n    private readonly Bindable<EditorModeWithEditStep> bindableModeWithEditStep = new();\r\n    private readonly IBindable<LyricEditorLayout> bindablePreferLayout = new Bindable<LyricEditorLayout>(LyricEditorLayout.List);\r\n    private readonly Bindable<LyricEditorLayout> bindableCurrentLayout = new();\r\n\r\n    public IBindable<LyricEditorMode> BindableMode => bindableMode;\r\n\r\n    public IBindable<EditorModeWithEditStep> BindableModeWithEditStep => bindableModeWithEditStep;\r\n\r\n    private readonly GridContainer gridContainer;\r\n    private readonly ContentWrapper content;\r\n    private readonly Container leftSideSettings;\r\n    private readonly Container rightSideSettings;\r\n\r\n    public LyricEditor()\r\n    {\r\n        // global state\r\n        AddInternal(lyricSelectionState = new LyricSelectionState());\r\n        AddInternal(lyricCaretState = new LyricCaretState());\r\n\r\n        // state for target mode only.\r\n        AddInternal(editTextModeState = new EditTextModeState());\r\n        AddInternal(editReferenceLyricModeState = new EditReferenceLyricModeState());\r\n        AddInternal(editLanguageModeState = new EditLanguageModeState());\r\n        AddInternal(editRubyModeState = new EditRubyModeState());\r\n        AddInternal(editTimeTagModeState = new EditTimeTagModeState());\r\n        AddInternal(editRomanisationModeState = new EditRomanisationModeState());\r\n        AddInternal(editNoteModeState = new EditNoteModeState());\r\n\r\n        // Separated feature.\r\n        AddInternal(lyricEditorClipboard = new LyricEditorClipboard());\r\n        AddInternal(lyricEditorVerifier = new LyricEditorVerifier());\r\n        AddInternal(issueNavigator = new IssueNavigator());\r\n\r\n        AddInternal(gridContainer = new GridContainer\r\n        {\r\n            RelativeSizeAxes = Axes.Both,\r\n            Content = new[]\r\n            {\r\n                new Drawable[]\r\n                {\r\n                    leftSideSettings = new Container\r\n                    {\r\n                        RelativeSizeAxes = Axes.Both,\r\n                    },\r\n                    content = new ContentWrapper\r\n                    {\r\n                        RelativeSizeAxes = Axes.Both,\r\n                    },\r\n                    rightSideSettings = new Container\r\n                    {\r\n                        RelativeSizeAxes = Axes.Both,\r\n                    },\r\n                },\r\n            },\r\n        });\r\n\r\n        BindableMode.BindValueChanged(e =>\r\n        {\r\n            updateModeWithEditStep();\r\n\r\n            // should control grid container spacing and place some component.\r\n            initializeSettingsArea();\r\n\r\n            reCalculateLayout();\r\n\r\n            // cancel selecting if switch mode.\r\n            lyricSelectionState.EndSelecting(LyricEditorSelectingAction.Cancel);\r\n        }, true);\r\n\r\n        initialEditStepChanged<TextEditStep>();\r\n        initialEditStepChanged<ReferenceLyricEditStep>();\r\n        initialEditStepChanged<LanguageEditStep>();\r\n        initialEditStepChanged<RubyTagEditStep>();\r\n        initialEditStepChanged<RomanisationTagEditStep>();\r\n        initialEditStepChanged<TimeTagEditStep>();\r\n        initialEditStepChanged<NoteEditStep>();\r\n\r\n        bindablePreferLayout.BindValueChanged(e =>\r\n        {\r\n            reCalculateLayout();\r\n        });\r\n\r\n        bindableCurrentLayout.BindValueChanged(e =>\r\n        {\r\n            content.SwitchLayout(e.NewValue);\r\n        }, true);\r\n    }\r\n\r\n    private void initialEditStepChanged<TEditStep>() where TEditStep : Enum\r\n    {\r\n        var editModeState = getEditStepState<TEditStep>();\r\n        if (editModeState == null)\r\n            throw new ArgumentNullException();\r\n\r\n        editModeState.BindableEditStep.BindValueChanged(e =>\r\n        {\r\n            updateModeWithEditStep();\r\n        });\r\n    }\r\n\r\n    private void updateModeWithEditStep()\r\n    {\r\n        bindableModeWithEditStep.Value = new EditorModeWithEditStep\r\n        {\r\n            Mode = bindableMode.Value,\r\n            EditStep = getTheEditStep(bindableMode.Value),\r\n            Default = false,\r\n        };\r\n\r\n        Enum? getTheEditStep(LyricEditorMode mode) =>\r\n            mode switch\r\n            {\r\n                LyricEditorMode.View => null,\r\n                LyricEditorMode.EditText => editTextModeState.BindableEditStep.Value,\r\n                LyricEditorMode.EditReferenceLyric => editReferenceLyricModeState.BindableEditStep.Value,\r\n                LyricEditorMode.EditLanguage => editLanguageModeState.BindableEditStep.Value,\r\n                LyricEditorMode.EditRuby => editRubyModeState.BindableEditStep.Value,\r\n                LyricEditorMode.EditTimeTag => editTimeTagModeState.BindableEditStep.Value,\r\n                LyricEditorMode.EditRomanisation => editRomanisationModeState.BindableEditStep.Value,\r\n                LyricEditorMode.EditNote => editNoteModeState.BindableEditStep.Value,\r\n                LyricEditorMode.EditSinger => null,\r\n                _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null),\r\n            };\r\n    }\r\n\r\n    private void initializeSettingsArea()\r\n    {\r\n        var settings = getSettings();\r\n        if (settings != null && checkDuplicatedWithExistSettings(settings))\r\n            return;\r\n\r\n        leftSideSettings.Clear();\r\n        rightSideSettings.Clear();\r\n\r\n        var direction = settings?.Direction;\r\n        float width = settings?.SettingsWidth ?? 0;\r\n\r\n        gridContainer.ColumnDimensions = new[]\r\n        {\r\n            new Dimension(GridSizeMode.Absolute, direction == SettingsDirection.Left ? width : 0),\r\n            new Dimension(),\r\n            new Dimension(GridSizeMode.Absolute, direction == SettingsDirection.Right ? width : 0),\r\n        };\r\n\r\n        if (settings == null)\r\n            return;\r\n\r\n        switch (settings.Direction)\r\n        {\r\n            case SettingsDirection.Left:\r\n                leftSideSettings.Add(settings);\r\n                break;\r\n\r\n            case SettingsDirection.Right:\r\n                rightSideSettings.Add(settings);\r\n                break;\r\n\r\n            default:\r\n                throw new ArgumentOutOfRangeException(nameof(settings.Direction));\r\n        }\r\n\r\n        LyricEditorSettings? getSettings() =>\r\n            Mode switch\r\n            {\r\n                LyricEditorMode.EditText => new TextSettings(),\r\n                LyricEditorMode.EditReferenceLyric => new ReferenceSettings(),\r\n                LyricEditorMode.EditLanguage => new LanguageSettings(),\r\n                LyricEditorMode.EditRuby => new RubyTagSettings(),\r\n                LyricEditorMode.EditTimeTag => new TimeTagSettings(),\r\n                LyricEditorMode.EditRomanisation => new RomanisationSettings(),\r\n                LyricEditorMode.EditNote => new NoteSettings(),\r\n                LyricEditorMode.EditSinger => new SingerSettings(),\r\n                _ => null,\r\n            };\r\n\r\n        bool checkDuplicatedWithExistSettings(LyricEditorSettings lyricEditorSettings)\r\n        {\r\n            var type = lyricEditorSettings.GetType();\r\n            if (leftSideSettings.Children.FirstOrDefault()?.GetType() == type)\r\n                return true;\r\n\r\n            if (rightSideSettings.Children.FirstOrDefault()?.GetType() == type)\r\n                return true;\r\n\r\n            return false;\r\n        }\r\n    }\r\n\r\n    private void reCalculateLayout()\r\n    {\r\n        var supportedLayout = getSupportedLayout(Mode);\r\n        var preferLayout = bindablePreferLayout.Value;\r\n\r\n        bindableCurrentLayout.Value = GetSuitableLayout(supportedLayout, preferLayout);\r\n\r\n        static LyricEditorLayout getSupportedLayout(LyricEditorMode mode) =>\r\n            mode switch\r\n            {\r\n                LyricEditorMode.View => LyricEditorLayout.List,\r\n                LyricEditorMode.EditText => LyricEditorLayout.List | LyricEditorLayout.Compose,\r\n                LyricEditorMode.EditReferenceLyric => LyricEditorLayout.List | LyricEditorLayout.Compose,\r\n                LyricEditorMode.EditLanguage => LyricEditorLayout.List | LyricEditorLayout.Compose,\r\n                LyricEditorMode.EditRuby => LyricEditorLayout.List | LyricEditorLayout.Compose,\r\n                LyricEditorMode.EditTimeTag => LyricEditorLayout.Compose,\r\n                LyricEditorMode.EditRomanisation => LyricEditorLayout.List | LyricEditorLayout.Compose,\r\n                LyricEditorMode.EditNote => LyricEditorLayout.Compose,\r\n                LyricEditorMode.EditSinger => LyricEditorLayout.List,\r\n                _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null),\r\n            };\r\n    }\r\n\r\n    internal static LyricEditorLayout GetSuitableLayout(LyricEditorLayout supportedLayout, LyricEditorLayout preferLayout)\r\n    {\r\n        var union = supportedLayout & preferLayout;\r\n        return union != 0 ? union : supportedLayout;\r\n    }\r\n\r\n    protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)\r\n    {\r\n        var baseDependencies = new DependencyContainer(base.CreateChildDependencies(parent));\r\n\r\n        // Add shader manager as part of dependencies.\r\n        // it will call CreateResourceStore() in KaraokeRuleset and add the resource.\r\n        return new OsuScreenDependencies(false, new DrawableRulesetDependencies(baseDependencies.GetRuleset(), baseDependencies));\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(EditorBeatmap beatmap, KaraokeRulesetLyricEditorConfigManager lyricEditorConfigManager)\r\n    {\r\n        // set-up divisor.\r\n        beatDivisor.Value = beatmap.BeatmapInfo.BeatDivisor;\r\n        lyricEditorConfigManager.BindWith(KaraokeRulesetLyricEditorSetting.LyricEditorPreferLayout, bindablePreferLayout);\r\n    }\r\n\r\n    public virtual bool OnPressed(KeyBindingPressEvent<KaraokeEditAction> e)\r\n    {\r\n        var movingCaretAction = ToMovingCaretAction(e.Action);\r\n        return movingCaretAction != null && lyricCaretState.MoveCaret(movingCaretAction.Value);\r\n    }\r\n\r\n    public void OnReleased(KeyBindingReleaseEvent<KaraokeEditAction> e)\r\n    {\r\n    }\r\n\r\n    public bool OnPressed(KeyBindingPressEvent<PlatformAction> e)\r\n    {\r\n        switch (e.Action)\r\n        {\r\n            case PlatformAction.Cut:\r\n                lyricEditorClipboard.Cut();\r\n                return true;\r\n\r\n            case PlatformAction.Copy:\r\n                lyricEditorClipboard.Copy();\r\n                return true;\r\n\r\n            case PlatformAction.Paste:\r\n                lyricEditorClipboard.Paste();\r\n                return true;\r\n        }\r\n\r\n        return false;\r\n    }\r\n\r\n    public void OnReleased(KeyBindingReleaseEvent<PlatformAction> e)\r\n    {\r\n    }\r\n\r\n    public LyricEditorMode Mode\r\n        => bindableMode.Value;\r\n\r\n    public void SwitchMode(LyricEditorMode mode)\r\n        => bindableMode.Value = mode;\r\n\r\n    public void SwitchEditStep<TEditStep>(TEditStep editStep) where TEditStep : Enum\r\n    {\r\n        var editStepState = getEditStepState<TEditStep>();\r\n        if (editStepState == null)\r\n            throw new ArgumentNullException();\r\n\r\n        editStepState.BindableEditStep.Value = editStep;\r\n    }\r\n\r\n    private IHasEditStep<TEditStep>? getEditStepState<TEditStep>() where TEditStep : Enum\r\n        => InternalChildren.OfType<IHasEditStep<TEditStep>>().FirstOrDefault();\r\n\r\n    public virtual void NavigateToFix(LyricEditorMode mode)\r\n    {\r\n        switch (mode)\r\n        {\r\n            case LyricEditorMode.EditText:\r\n            case LyricEditorMode.EditLanguage:\r\n            case LyricEditorMode.EditTimeTag:\r\n                SwitchMode(mode);\r\n                break;\r\n\r\n            default:\r\n                throw new ArgumentOutOfRangeException(nameof(mode));\r\n        }\r\n    }\r\n\r\n    public static MovingCaretAction? ToMovingCaretAction(KaraokeEditAction action)\r\n    {\r\n        return action switch\r\n        {\r\n            KaraokeEditAction.MoveToPreviousLyric => MovingCaretAction.PreviousLyric,\r\n            KaraokeEditAction.MoveToNextLyric => MovingCaretAction.NextLyric,\r\n            KaraokeEditAction.MoveToFirstLyric => MovingCaretAction.FirstLyric,\r\n            KaraokeEditAction.MoveToLastLyric => MovingCaretAction.LastLyric,\r\n            KaraokeEditAction.MoveToPreviousIndex => MovingCaretAction.PreviousIndex,\r\n            KaraokeEditAction.MoveToNextIndex => MovingCaretAction.NextIndex,\r\n            KaraokeEditAction.MoveToFirstIndex => MovingCaretAction.FirstIndex,\r\n            KaraokeEditAction.MoveToLastIndex => MovingCaretAction.LastIndex,\r\n            _ => null,\r\n        };\r\n    }\r\n\r\n    private class LocalScrollingInfo : IScrollingInfo\r\n    {\r\n        public IBindable<ScrollingDirection> Direction { get; } = new Bindable<ScrollingDirection>(ScrollingDirection.Left);\r\n\r\n        public IBindable<double> TimeRange { get; } = new BindableDouble(5000)\r\n        {\r\n            MinValue = 1000,\r\n            MaxValue = 10000,\r\n        };\r\n\r\n        public IBindable<IScrollAlgorithm> Algorithm { get; } = new Bindable<IScrollAlgorithm>(new SequentialScrollAlgorithm(new List<MultiplierControlPoint>()));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/LyricEditorClipboard.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Globalization;\r\nusing System.Linq;\r\nusing Newtonsoft.Json;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Platform;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas.Types;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.IO.Serialization;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics;\r\n\r\npublic partial class LyricEditorClipboard : Component, ILyricEditorClipboard\r\n{\r\n    [Resolved]\r\n    private Clipboard clipboard { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private OnScreenDisplay? onScreenDisplay { get; set; }\r\n\r\n    [Resolved]\r\n    private ILyricCaretState lyricCaretState { get; set; } = null!;\r\n\r\n    private Lyric? getSelectedLyric() => lyricCaretState.BindableFocusedLyric.Value;\r\n\r\n    [Resolved]\r\n    private IEditTextModeState editTextModeState { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private IEditRubyModeState editRubyModeState { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private IEditTimeTagModeState editTimeTagModeState { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private ILyricsChangeHandler? lyricsChangeHandler { get; set; }\r\n\r\n    [Resolved]\r\n    private ILyricLanguageChangeHandler? languageChangeHandler { get; set; }\r\n\r\n    [Resolved]\r\n    private ILyricRubyTagsChangeHandler? lyricRubyTagsChangeHandler { get; set; }\r\n\r\n    [Resolved]\r\n    private ILyricTimeTagsChangeHandler? lyricTimeTagsChangeHandler { get; set; }\r\n\r\n    [Resolved]\r\n    private ILyricSingerChangeHandler? lyricSingerChangeHandler { get; set; }\r\n\r\n    [Resolved]\r\n    private IBeatmapSingersChangeHandler? singersChangeHandler { get; set; }\r\n\r\n    private readonly IBindable<LyricEditorMode> bindableMode = new Bindable<LyricEditorMode>();\r\n\r\n    // we should save the serialized lyric object into here instead of save into the clipboard for some reason:\r\n    // 1. It's hard to know which ruby/romanisation or time-tag being copied.\r\n    // 2. Maybe user did not want to copy the full json content?\r\n    private string clipboardContent = string.Empty;\r\n\r\n    public LyricEditorClipboard()\r\n    {\r\n        bindableMode.BindValueChanged(_ =>\r\n        {\r\n            clipboardContent = string.Empty;\r\n        });\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(ILyricEditorState state)\r\n    {\r\n        bindableMode.BindTo(state.BindableMode);\r\n    }\r\n\r\n    public bool Cut()\r\n    {\r\n        var selectedLyric = getSelectedLyric();\r\n        if (selectedLyric == null)\r\n            return false;\r\n\r\n        bool copied = performCopy(selectedLyric);\r\n        if (!copied)\r\n            return false;\r\n\r\n        bool cut = performCut();\r\n        if (!cut)\r\n            return false;\r\n\r\n        onScreenDisplay?.Display(new ClipboardToast(bindableMode.Value, ClipboardAction.Cut));\r\n        return true;\r\n    }\r\n\r\n    public bool Copy()\r\n    {\r\n        var selectedLyric = getSelectedLyric();\r\n        if (selectedLyric == null)\r\n            return false;\r\n\r\n        bool copy = performCopy(selectedLyric);\r\n        if (!copy)\r\n            return false;\r\n\r\n        onScreenDisplay?.Display(new ClipboardToast(bindableMode.Value, ClipboardAction.Copy));\r\n        return true;\r\n    }\r\n\r\n    public bool Paste()\r\n    {\r\n        if (string.IsNullOrEmpty(clipboardContent))\r\n            return false;\r\n\r\n        var selectedLyric = getSelectedLyric();\r\n        if (selectedLyric == null)\r\n            return false;\r\n\r\n        bool paste = performPaste(selectedLyric);\r\n        if (!paste)\r\n            return false;\r\n\r\n        onScreenDisplay?.Display(new ClipboardToast(bindableMode.Value, ClipboardAction.Paste));\r\n        return true;\r\n    }\r\n\r\n    #region logic\r\n\r\n    private bool performCut()\r\n    {\r\n        switch (bindableMode.Value)\r\n        {\r\n            case LyricEditorMode.View:\r\n                return false;\r\n\r\n            case LyricEditorMode.EditText:\r\n                switch (editTextModeState.EditStep)\r\n                {\r\n                    case TextEditStep.Typing:\r\n                        // cut, copy or paste event should be handled in the caret.\r\n                        return false;\r\n\r\n                    case TextEditStep.Split:\r\n                        if (lyricsChangeHandler == null)\r\n                            throw new NullDependencyException($\"Missing {nameof(lyricsChangeHandler)}\");\r\n\r\n                        lyricsChangeHandler.Remove();\r\n                        return true;\r\n\r\n                    case TextEditStep.Verify:\r\n                        // cut, copy or paste event should be handled in the caret.\r\n                        return false;\r\n\r\n                    default:\r\n                        throw new ArgumentOutOfRangeException();\r\n                }\r\n\r\n            case LyricEditorMode.EditReferenceLyric:\r\n                return false;\r\n\r\n            case LyricEditorMode.EditLanguage:\r\n                languageChangeHandler?.SetLanguage(null);\r\n                return true;\r\n\r\n            case LyricEditorMode.EditRuby:\r\n                var rubies = editRubyModeState.SelectedItems;\r\n                if (!rubies.Any())\r\n                    return false;\r\n\r\n                if (lyricRubyTagsChangeHandler == null)\r\n                    throw new NullDependencyException($\"Missing {nameof(lyricRubyTagsChangeHandler)}\");\r\n\r\n                lyricRubyTagsChangeHandler.RemoveRange(rubies);\r\n                return true;\r\n\r\n            case LyricEditorMode.EditTimeTag:\r\n                var timeTags = editTimeTagModeState.SelectedItems;\r\n                if (!timeTags.Any())\r\n                    return false;\r\n\r\n                if (lyricTimeTagsChangeHandler == null)\r\n                    throw new NullDependencyException($\"Missing {nameof(lyricTimeTagsChangeHandler)}\");\r\n\r\n                lyricTimeTagsChangeHandler.RemoveRange(timeTags);\r\n                return true;\r\n\r\n            case LyricEditorMode.EditRomanisation:\r\n                return false;\r\n\r\n            case LyricEditorMode.EditNote:\r\n                return false;\r\n\r\n            case LyricEditorMode.EditSinger:\r\n                if (lyricSingerChangeHandler == null)\r\n                    throw new NullDependencyException($\"Missing {nameof(lyricSingerChangeHandler)}\");\r\n\r\n                lyricSingerChangeHandler.Clear();\r\n                return true;\r\n\r\n            default:\r\n                throw new ArgumentOutOfRangeException();\r\n        }\r\n    }\r\n\r\n    private bool performCopy(Lyric lyric)\r\n    {\r\n        switch (bindableMode.Value)\r\n        {\r\n            case LyricEditorMode.View:\r\n                // for letting user to copy the lyric as plain text.\r\n                copyObjectToClipboard(lyric.Text);\r\n                return true;\r\n\r\n            case LyricEditorMode.EditText:\r\n                switch (editTextModeState.EditStep)\r\n                {\r\n                    case TextEditStep.Typing:\r\n                        // cut, copy or paste event should be handled in the caret.\r\n                        return false;\r\n\r\n                    case TextEditStep.Split:\r\n                        saveObjectToTheClipboardContent(lyric);\r\n                        copyObjectToClipboard(lyric.Text);\r\n                        return true;\r\n\r\n                    case TextEditStep.Verify:\r\n                        // cut, copy or paste event should be handled in the caret.\r\n                        return false;\r\n\r\n                    default:\r\n                        throw new ArgumentOutOfRangeException();\r\n                }\r\n\r\n            case LyricEditorMode.EditReferenceLyric:\r\n                return false;\r\n\r\n            case LyricEditorMode.EditLanguage:\r\n                saveObjectToTheClipboardContent(lyric.Language);\r\n                copyObjectToClipboard(lyric.Language);\r\n                return true;\r\n\r\n            case LyricEditorMode.EditRuby:\r\n                var rubies = editRubyModeState.SelectedItems;\r\n                if (!rubies.Any())\r\n                    return false;\r\n\r\n                saveObjectToTheClipboardContent(rubies);\r\n                copyObjectToClipboard(rubies);\r\n                return true;\r\n\r\n            case LyricEditorMode.EditTimeTag:\r\n                var timeTags = editTimeTagModeState.SelectedItems;\r\n                if (!timeTags.Any())\r\n                    return false;\r\n\r\n                saveObjectToTheClipboardContent(timeTags);\r\n                copyObjectToClipboard(timeTags);\r\n                return true;\r\n\r\n            case LyricEditorMode.EditRomanisation:\r\n                return false;\r\n\r\n            case LyricEditorMode.EditNote:\r\n                return false;\r\n\r\n            case LyricEditorMode.EditSinger:\r\n                saveObjectToTheClipboardContent(lyric.SingerIds);\r\n                var singers = getMatchedSinges(lyric.SingerIds);\r\n                copyObjectToClipboard(singers);\r\n                return true;\r\n\r\n            default:\r\n                throw new ArgumentOutOfRangeException();\r\n        }\r\n    }\r\n\r\n    private bool performPaste(Lyric lyric)\r\n    {\r\n        switch (bindableMode.Value)\r\n        {\r\n            case LyricEditorMode.View:\r\n                return false;\r\n\r\n            case LyricEditorMode.EditText:\r\n                switch (editTextModeState.EditStep)\r\n                {\r\n                    case TextEditStep.Typing:\r\n                        // cut, copy or paste event should be handled in the caret.\r\n                        return false;\r\n\r\n                    case TextEditStep.Split:\r\n                        var pasteLyric = getObjectFromClipboardContent<Lyric>();\r\n                        if (pasteLyric == null)\r\n                            return false;\r\n\r\n                        if (lyricsChangeHandler == null)\r\n                            throw new NullDependencyException($\"Missing {nameof(lyricsChangeHandler)}\");\r\n\r\n                        lyricsChangeHandler.AddBelowToSelection(pasteLyric);\r\n                        return true;\r\n\r\n                    case TextEditStep.Verify:\r\n                        // cut, copy or paste event should be handled in the caret.\r\n                        return false;\r\n\r\n                    default:\r\n                        throw new ArgumentOutOfRangeException();\r\n                }\r\n\r\n            case LyricEditorMode.EditReferenceLyric:\r\n                return false;\r\n\r\n            case LyricEditorMode.EditLanguage:\r\n                var pasteLanguage = getObjectFromClipboardContent<CultureInfo>();\r\n                if (pasteLanguage == null)\r\n                    return false;\r\n\r\n                if (languageChangeHandler == null)\r\n                    throw new NullDependencyException($\"Missing {nameof(languageChangeHandler)}\");\r\n\r\n                languageChangeHandler.SetLanguage(pasteLanguage);\r\n                return true;\r\n\r\n            case LyricEditorMode.EditRuby:\r\n                var pasteRubies = getObjectFromClipboardContent<RubyTag[]>();\r\n                if (pasteRubies == null)\r\n                    return false;\r\n\r\n                if (lyricRubyTagsChangeHandler == null)\r\n                    throw new NullDependencyException($\"Missing {nameof(lyricRubyTagsChangeHandler)}\");\r\n\r\n                lyricRubyTagsChangeHandler.AddRange(pasteRubies);\r\n                return true;\r\n\r\n            case LyricEditorMode.EditTimeTag:\r\n                var pasteTimeTags = getObjectFromClipboardContent<TimeTag[]>();\r\n                if (pasteTimeTags == null)\r\n                    return false;\r\n\r\n                if (lyricTimeTagsChangeHandler == null)\r\n                    throw new NullDependencyException($\"Missing {nameof(lyricTimeTagsChangeHandler)}\");\r\n\r\n                lyricTimeTagsChangeHandler.AddRange(pasteTimeTags);\r\n                return true;\r\n\r\n            case LyricEditorMode.EditRomanisation:\r\n                return false;\r\n\r\n            case LyricEditorMode.EditNote:\r\n                return false;\r\n\r\n            case LyricEditorMode.EditSinger:\r\n                ElementId[]? pasteSingerIds = getObjectFromClipboardContent<ElementId[]>();\r\n                if (pasteSingerIds == null)\r\n                    return false;\r\n\r\n                if (lyricSingerChangeHandler == null)\r\n                    throw new NullDependencyException($\"Missing {nameof(lyricSingerChangeHandler)}\");\r\n\r\n                var singers = getMatchedSinges(pasteSingerIds);\r\n                lyricSingerChangeHandler.AddRange(singers);\r\n                return true;\r\n\r\n            default:\r\n                throw new ArgumentOutOfRangeException();\r\n        }\r\n    }\r\n\r\n    private IEnumerable<ISinger> getMatchedSinges(IEnumerable<ElementId> singerIds)\r\n    {\r\n        return singersChangeHandler == null ? throw new NullDependencyException($\"Missing {nameof(singersChangeHandler)}\") : singersChangeHandler.Singers.Where(x => singerIds.Contains(x.ID));\r\n    }\r\n\r\n    private void copyObjectToClipboard<T>(T obj)\r\n    {\r\n        var settings = KaraokeJsonSerializableExtensions.CreateGlobalSettings();\r\n        string text = JsonConvert.SerializeObject(obj, settings);\r\n\r\n        clipboard.SetText(text);\r\n    }\r\n\r\n    private void saveObjectToTheClipboardContent<T>(T obj)\r\n    {\r\n        var settings = KaraokeJsonSerializableExtensions.CreateGlobalSettings();\r\n        clipboardContent = JsonConvert.SerializeObject(obj, settings);\r\n    }\r\n\r\n    private T? getObjectFromClipboardContent<T>()\r\n    {\r\n        if (string.IsNullOrEmpty(clipboardContent))\r\n            return default;\r\n\r\n        var settings = KaraokeJsonSerializableExtensions.CreateGlobalSettings();\r\n        return JsonConvert.DeserializeObject<T>(clipboardContent, settings);\r\n    }\r\n\r\n    #endregion\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/LyricEditorColourProvider.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.ComponentModel;\r\nusing osuTK;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics;\r\n\r\npublic class LyricEditorColourProvider\r\n{\r\n    public Color4 Colour1(LyricEditorMode mode) => getColour(mode, 1, 0.7f);\r\n    public Color4 Colour2(LyricEditorMode mode) => getColour(mode, 0.8f, 0.6f);\r\n    public Color4 Colour3(LyricEditorMode mode) => getColour(mode, 0.6f, 0.5f);\r\n    public Color4 Colour4(LyricEditorMode mode) => getColour(mode, 0.4f, 0.3f);\r\n\r\n    public Color4 Highlight1(LyricEditorMode mode) => getColour(mode, 1, 0.7f);\r\n    public Color4 Content1(LyricEditorMode mode) => getColour(mode, 0.4f, 1);\r\n    public Color4 Content2(LyricEditorMode mode) => getColour(mode, 0.4f, 0.9f);\r\n    public Color4 Light1(LyricEditorMode mode) => getColour(mode, 0.4f, 0.8f);\r\n    public Color4 Light2(LyricEditorMode mode) => getColour(mode, 0.4f, 0.75f);\r\n    public Color4 Light3(LyricEditorMode mode) => getColour(mode, 0.4f, 0.7f);\r\n    public Color4 Light4(LyricEditorMode mode) => getColour(mode, 0.4f, 0.5f);\r\n    public Color4 Dark1(LyricEditorMode mode) => getColour(mode, 0.2f, 0.35f);\r\n    public Color4 Dark2(LyricEditorMode mode) => getColour(mode, 0.2f, 0.3f);\r\n    public Color4 Dark3(LyricEditorMode mode) => getColour(mode, 0.2f, 0.25f);\r\n    public Color4 Dark4(LyricEditorMode mode) => getColour(mode, 0.2f, 0.2f);\r\n    public Color4 Dark5(LyricEditorMode mode) => getColour(mode, 0.2f, 0.15f);\r\n    public Color4 Dark6(LyricEditorMode mode) => getColour(mode, 0.2f, 0.1f);\r\n    public Color4 Foreground1(LyricEditorMode mode) => getColour(mode, 0.1f, 0.6f);\r\n    public Color4 Background1(LyricEditorMode mode) => getColour(mode, 0.1f, 0.4f);\r\n    public Color4 Background2(LyricEditorMode mode) => getColour(mode, 0.1f, 0.3f);\r\n    public Color4 Background3(LyricEditorMode mode) => getColour(mode, 0.1f, 0.25f);\r\n    public Color4 Background4(LyricEditorMode mode) => getColour(mode, 0.1f, 0.2f);\r\n    public Color4 Background5(LyricEditorMode mode) => getColour(mode, 0.1f, 0.15f);\r\n    public Color4 Background6(LyricEditorMode mode) => getColour(mode, 0.1f, 0.1f);\r\n\r\n    private Color4 getColour(LyricEditorMode mode, float saturation, float lightness) => Color4.FromHsl(new Vector4(getBaseHue(mode), saturation, lightness, 1));\r\n\r\n    private static float getBaseHue(LyricEditorMode mode)\r\n    {\r\n        switch (mode)\r\n        {\r\n            case LyricEditorMode.View:\r\n                return 200 / 360f; // blue\r\n\r\n            case LyricEditorMode.EditText:\r\n            case LyricEditorMode.EditReferenceLyric:\r\n                return 0 / 360f; // red\r\n\r\n            case LyricEditorMode.EditLanguage:\r\n            case LyricEditorMode.EditRuby:\r\n                return 333 / 360f; // pink\r\n\r\n            case LyricEditorMode.EditTimeTag:\r\n            case LyricEditorMode.EditRomanisation:\r\n                return 45 / 360f; // orange\r\n\r\n            case LyricEditorMode.EditNote:\r\n                return 200 / 360f; // blue\r\n\r\n            case LyricEditorMode.EditSinger:\r\n                return 255 / 360f; // purple\r\n\r\n            default:\r\n                throw new InvalidEnumArgumentException($\"{mode} colour scheme does not provide a hue value in {nameof(getBaseHue)}.\");\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/LyricEditorIssueTable.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics;\r\n\r\npublic abstract partial class LyricEditorIssueTable : IssueTable\r\n{\r\n    [Resolved]\r\n    private IIssueNavigator issueNavigator { get; set; } = null!;\r\n\r\n    public new bool ShowHeaders\r\n    {\r\n        get => base.ShowHeaders;\r\n        set\r\n        {\r\n            base.ShowHeaders = value;\r\n            BackgroundFlow.Margin = new MarginPadding { Top = value ? ROW_HEIGHT : 0 };\r\n        }\r\n    }\r\n\r\n    protected override void OnIssueClicked(Issue issue)\r\n    {\r\n        issueNavigator.Navigate(issue);\r\n    }\r\n\r\n    protected override RowBackground CreateRowBackground(Issue issue)\r\n        => new IssueTableRowBackground(issue);\r\n\r\n    /// <summary>\r\n    /// Inherit the class just for able to override the colour.\r\n    /// </summary>\r\n    protected partial class IssueTableRowBackground : RowBackground\r\n    {\r\n        private const int fade_duration = 100;\r\n\r\n        private readonly Box hoveredBackground;\r\n\r\n        public IssueTableRowBackground(object item)\r\n            : base(item)\r\n        {\r\n            hoveredBackground = Children.OfType<Box>().First();\r\n        }\r\n\r\n        private Color4 colourHover;\r\n        private Color4 colourSelected;\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(LyricEditorColourProvider colourProvider, ILyricEditorState state)\r\n        {\r\n            colourHover = colourProvider.Background1(state.Mode);\r\n            colourSelected = colourProvider.Colour3(state.Mode);\r\n\r\n            Schedule(() =>\r\n            {\r\n                hoveredBackground.Colour = colourHover;\r\n            });\r\n        }\r\n\r\n        protected override bool OnHover(HoverEvent e)\r\n        {\r\n            bool hover = base.OnHover(e);\r\n            updateState();\r\n            return hover;\r\n        }\r\n\r\n        protected override void OnHoverLost(HoverLostEvent e)\r\n        {\r\n            base.OnHoverLost(e);\r\n            updateState();\r\n        }\r\n\r\n        private void updateState()\r\n        {\r\n            hoveredBackground.FadeColour(Selected ? colourSelected : colourHover, 450, Easing.OutQuint);\r\n\r\n            if (Selected || IsHovered)\r\n                hoveredBackground.FadeIn(fade_duration, Easing.OutQuint);\r\n            else\r\n                hoveredBackground.FadeOut(fade_duration, Easing.OutQuint);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/LyricEditorLayout.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics;\r\n\r\n[Flags]\r\npublic enum LyricEditorLayout\r\n{\r\n    /// <summary>\r\n    /// Show the list of lyrics in the main content area.\r\n    /// </summary>\r\n    List = 1,\r\n\r\n    /// <summary>\r\n    /// Show the composer at the top of the main content area.\r\n    /// User can select the edit lyric at the bottom of the compose area.\r\n    /// </summary>\r\n    Compose = 1 << 1,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/LyricEditorMode.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.ComponentModel;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics;\r\n\r\npublic enum LyricEditorMode\r\n{\r\n    /// <summary>\r\n    /// Cannot edit anything except each lyric's left-side part.\r\n    /// </summary>\r\n    [Description(\"View\")]\r\n    View,\r\n\r\n    /// <summary>\r\n    /// Can create/delete/move/split/combine lyric.\r\n    /// And typing the lyric.\r\n    /// </summary>\r\n    [Description(\"Edit Text\")]\r\n    EditText,\r\n\r\n    /// <summary>\r\n    /// Mark the lyric is \"similar\" to another lyric.\r\n    /// </summary>\r\n    [Description(\"Edit Reference lyric\")]\r\n    EditReferenceLyric,\r\n\r\n    /// <summary>\r\n    /// Can edit each lyric's language.\r\n    /// </summary>\r\n    [Description(\"Select language\")]\r\n    EditLanguage,\r\n\r\n    /// <summary>\r\n    /// Able to create/delete ruby.\r\n    /// </summary>\r\n    [Description(\"Edit ruby\")]\r\n    EditRuby,\r\n\r\n    /// <summary>\r\n    /// Enable to create/delete and reset time tag.\r\n    /// </summary>\r\n    [Description(\"Edit time tag\")]\r\n    EditTimeTag,\r\n\r\n    /// <summary>\r\n    /// Able to edit the romanisation from the time-tag.\r\n    /// </summary>\r\n    [Description(\"Edit Romanisation\")]\r\n    EditRomanisation,\r\n\r\n    /// <summary>\r\n    /// Enable to create/delete notes.\r\n    /// </summary>\r\n    [Description(\"Edit note\")]\r\n    EditNote,\r\n\r\n    /// <summary>\r\n    /// Can edit each lyric's singer.\r\n    /// </summary>\r\n    [Description(\"Select singer\")]\r\n    EditSinger,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/LyricEditorScreen.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Extensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Overlays.OSD;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Notes;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\nusing osu.Game.Rulesets.Karaoke.UI.Position;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics;\r\n\r\npublic partial class LyricEditorScreen : BeatmapEditorScreen\r\n{\r\n    [Cached(typeof(ILyricPropertyAutoGenerateChangeHandler))]\r\n    private readonly LyricPropertyAutoGenerateChangeHandler lyricPropertyAutoGenerateChangeHandler;\r\n\r\n    [Cached(typeof(ILyricsChangeHandler))]\r\n    private readonly LyricsChangeHandler lyricsChangeHandler;\r\n\r\n    [Cached(typeof(ILyricTextChangeHandler))]\r\n    private readonly LyricTextChangeHandler lyricTextChangeHandler;\r\n\r\n    [Cached(typeof(ILyricReferenceChangeHandler))]\r\n    private readonly LyricReferenceChangeHandler lyricReferenceChangeHandler;\r\n\r\n    [Cached(typeof(ILyricLanguageChangeHandler))]\r\n    private readonly LyricLanguageChangeHandler lyricLanguageChangeHandler;\r\n\r\n    [Cached(typeof(ILyricRubyTagsChangeHandler))]\r\n    private readonly LyricRubyTagsChangeHandler lyricRubyTagsChangeHandler;\r\n\r\n    [Cached(typeof(ILyricTimeTagsChangeHandler))]\r\n    private readonly LyricTimeTagsChangeHandler lyricTimeTagsChangeHandler;\r\n\r\n    [Cached(typeof(INotePositionInfo))]\r\n    private readonly NotePositionInfo notePositionInfo;\r\n\r\n    [Cached(typeof(INotesChangeHandler))]\r\n    private readonly NotesChangeHandler notesChangeHandler;\r\n\r\n    [Cached(typeof(INotePropertyChangeHandler))]\r\n    private readonly NotePropertyChangeHandler notePropertyChangeHandler;\r\n\r\n    [Cached(typeof(ILyricSingerChangeHandler))]\r\n    private readonly LyricSingerChangeHandler lyricSingerChangeHandler;\r\n\r\n    [Cached(typeof(IBeatmapSingersChangeHandler))]\r\n    private readonly BeatmapSingersChangeHandler beatmapSingersChangeHandler;\r\n\r\n    [Cached(typeof(ILockChangeHandler))]\r\n    private readonly LockChangeHandler lockChangeHandler;\r\n\r\n    private readonly FullScreenLyricEditor lyricEditor;\r\n\r\n    public LyricEditorScreen()\r\n        : base(KaraokeBeatmapEditorScreenMode.Lyric)\r\n    {\r\n        AddInternal(lyricPropertyAutoGenerateChangeHandler = new LyricPropertyAutoGenerateChangeHandler());\r\n        AddInternal(lyricsChangeHandler = new LyricsChangeHandler());\r\n        AddInternal(lyricTextChangeHandler = new LyricTextChangeHandler());\r\n        AddInternal(lyricReferenceChangeHandler = new LyricReferenceChangeHandler());\r\n        AddInternal(lyricLanguageChangeHandler = new LyricLanguageChangeHandler());\r\n        AddInternal(lyricRubyTagsChangeHandler = new LyricRubyTagsChangeHandler());\r\n        AddInternal(lyricTimeTagsChangeHandler = new LyricTimeTagsChangeHandler());\r\n        AddInternal(notePositionInfo = new NotePositionInfo());\r\n        AddInternal(notesChangeHandler = new NotesChangeHandler());\r\n        AddInternal(notePropertyChangeHandler = new NotePropertyChangeHandler());\r\n        AddInternal(lyricSingerChangeHandler = new LyricSingerChangeHandler());\r\n        AddInternal(beatmapSingersChangeHandler = new BeatmapSingersChangeHandler());\r\n        AddInternal(lockChangeHandler = new LockChangeHandler());\r\n\r\n        Add(new KaraokeEditInputManager(new KaraokeRuleset().RulesetInfo)\r\n        {\r\n            RelativeSizeAxes = Axes.Both,\r\n            Child = lyricEditor = new FullScreenLyricEditor\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n            },\r\n        });\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(Bindable<LyricEditorMode> lyricEditorMode)\r\n    {\r\n        lyricEditor.BindableMode.BindTo(lyricEditorMode);\r\n    }\r\n\r\n    protected override void PopIn()\r\n    {\r\n        base.PopIn();\r\n\r\n        // should reset the selection because selected hitobject in the editor beatmap might not sync with the selection in lyric editor.\r\n        lyricEditor.ResetSelectedHitObject();\r\n    }\r\n\r\n    private partial class FullScreenLyricEditor : LyricEditor\r\n    {\r\n        private ILyricCaretState lyricCaretState { get; set; } = null!;\r\n\r\n        [Resolved]\r\n        private OnScreenDisplay? onScreenDisplay { get; set; }\r\n\r\n        protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)\r\n        {\r\n            var dependencies = base.CreateChildDependencies(parent);\r\n            lyricCaretState = dependencies.Get<ILyricCaretState>();\r\n            return dependencies;\r\n        }\r\n\r\n        public void ResetSelectedHitObject() => lyricCaretState.SyncSelectedHitObjectWithCaret();\r\n\r\n        public override bool OnPressed(KeyBindingPressEvent<KaraokeEditAction> e)\r\n        {\r\n            switch (e.Action)\r\n            {\r\n                case KaraokeEditAction.PreviousEditMode:\r\n                    SwitchMode(EnumUtils.GetPreviousValue(Mode));\r\n                    onScreenDisplay?.Display(new LyricEditorEditModeToast(Mode));\r\n                    return true;\r\n\r\n                case KaraokeEditAction.NextEditMode:\r\n                    SwitchMode(EnumUtils.GetNextValue(Mode));\r\n                    onScreenDisplay?.Display(new LyricEditorEditModeToast(Mode));\r\n                    return true;\r\n\r\n                default:\r\n                    return base.OnPressed(e);\r\n            }\r\n        }\r\n    }\r\n\r\n    public partial class LyricEditorEditModeToast : Toast\r\n    {\r\n        public LyricEditorEditModeToast(LyricEditorMode mode)\r\n            : base(getDescription(), getValue(mode))\r\n        {\r\n        }\r\n\r\n        private static LocalisableString getDescription()\r\n            => \"Lyric editor\";\r\n\r\n        private static LocalisableString getValue(LyricEditorMode mode)\r\n            => $\"{mode.GetDescription()} Mode\";\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/LyricEditorSkin.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing JetBrains.Annotations;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.IO;\r\nusing osu.Game.Rulesets.Karaoke.Extensions;\r\nusing osu.Game.Rulesets.Karaoke.Skinning;\r\nusing osu.Game.Rulesets.Karaoke.Skinning.Elements;\r\nusing osu.Game.Skinning;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics;\r\n\r\n/// <summary>\r\n/// This karaoke skin is using in lyric editor only.\r\n/// </summary>\r\npublic class LyricEditorSkin : KaraokeSkin\r\n{\r\n    public const int MIN_FONT_SIZE = 10;\r\n    public const int MAX_FONT_SIZE = 45;\r\n\r\n    internal static readonly Guid DEFAULT_SKIN = new(\"FEC5A290-5709-11EC-9F10-0800200C9A66\");\r\n\r\n    public static SkinInfo CreateInfo() => new()\r\n    {\r\n        ID = DEFAULT_SKIN,\r\n        Name = \"karaoke! (default editor skin)\",\r\n        Creator = \"team karaoke!\",\r\n        Protected = true,\r\n        InstantiationInfo = typeof(LyricEditorSkin).GetInvariantInstantiationInfo(),\r\n    };\r\n\r\n    public LyricEditorSkin(IStorageResourceProvider? resources)\r\n        : this(CreateInfo(), resources)\r\n    {\r\n    }\r\n\r\n    [UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)]\r\n    public LyricEditorSkin(SkinInfo skin, IStorageResourceProvider? resources)\r\n        : base(skin, resources)\r\n    {\r\n        DefaultElement[ElementType.LyricFontInfo] = LyricFontInfo.CreateDefault();\r\n        DefaultElement[ElementType.NoteStyle] = NoteStyle.CreateDefault();\r\n\r\n        // todo: should use better way to handle overall size.\r\n        FontSize = 26;\r\n    }\r\n\r\n    protected LyricFontInfo LyricFontInfo => (LyricFontInfo)DefaultElement[ElementType.LyricFontInfo];\r\n\r\n    public float FontSize\r\n    {\r\n        get => LyricFontInfo.MainTextFont.Size;\r\n        set\r\n        {\r\n            float textSize = Math.Clamp(value, MIN_FONT_SIZE, MAX_FONT_SIZE);\r\n            float changePercentage = textSize / FontSize;\r\n\r\n            LyricFontInfo.MainTextFont\r\n                = multipleSize(LyricFontInfo.MainTextFont, changePercentage);\r\n            LyricFontInfo.RubyTextFont\r\n                = multipleSize(LyricFontInfo.RubyTextFont, changePercentage);\r\n            LyricFontInfo.RomanisationTextFont\r\n                = multipleSize(LyricFontInfo.RomanisationTextFont, changePercentage);\r\n\r\n            // todo: change size might not working now.\r\n            // DefaultElement[ElementType.LyricConfig].TriggerChange();\r\n\r\n            static FontUsage multipleSize(FontUsage origin, float percentage)\r\n                => origin.With(size: origin.Size * percentage);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/LyricEditorVerifier.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Caching;\r\nusing osu.Framework.Extensions.ObjectExtensions;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Rulesets.Edit;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Objects;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics;\r\n\r\npublic partial class LyricEditorVerifier : EditorVerifier<LyricEditorMode>, ILyricEditorVerifier\r\n{\r\n    [Resolved]\r\n    private EditorBeatmap editorBeatmap { get; set; } = null!;\r\n\r\n    private readonly Dictionary<KaraokeHitObject, BindableList<Issue>> hitObjectIssues = new();\r\n\r\n    private readonly Cached editModeCache = new();\r\n\r\n    protected override IEnumerable<ICheck> CreateChecks(LyricEditorMode type) =>\r\n        type switch\r\n        {\r\n            LyricEditorMode.View => Array.Empty<ICheck>(),\r\n            LyricEditorMode.EditText => new ICheck[] { new CheckLyricText() },\r\n            LyricEditorMode.EditReferenceLyric => new ICheck[] { new CheckLyricReferenceLyric() },\r\n            LyricEditorMode.EditLanguage => new ICheck[] { new CheckLyricLanguage() },\r\n            LyricEditorMode.EditRuby => new ICheck[] { new CheckLyricRubyTag() },\r\n            LyricEditorMode.EditTimeTag => new ICheck[] { new CheckLyricTimeTagOnly() },\r\n            LyricEditorMode.EditRomanisation => new ICheck[] { new CheckLyricRomanisation() },\r\n            LyricEditorMode.EditNote => new ICheck[] { new CheckNoteReferenceLyric(), new CheckNoteText(), new CheckNoteTime() },\r\n            LyricEditorMode.EditSinger => Array.Empty<ICheck>(),\r\n            _ => throw new ArgumentOutOfRangeException(nameof(type), type, null),\r\n        };\r\n\r\n    public IBindableList<Issue> GetBindable(KaraokeHitObject hitObject)\r\n        => hitObjectIssues[hitObject];\r\n\r\n    public override void Refresh()\r\n        => recalculateIssues();\r\n\r\n    public void RefreshByHitObject(KaraokeHitObject hitObject)\r\n    {\r\n        if (hitObjectIssues.ContainsKey(hitObject))\r\n        {\r\n            hitObjectUpdated(hitObject);\r\n        }\r\n        else\r\n        {\r\n            hitObjectAdded(hitObject);\r\n        }\r\n    }\r\n\r\n    protected override void LoadComplete()\r\n    {\r\n        base.LoadComplete();\r\n\r\n        // need to check is there any lyric added or removed.\r\n        editorBeatmap.HitObjectAdded += hitObjectAdded;\r\n        editorBeatmap.HitObjectRemoved += hitObjectRemoved;\r\n        editorBeatmap.HitObjectUpdated += hitObjectUpdated;\r\n\r\n        recalculateIssues();\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(KaraokeRulesetEditCheckerConfigManager? rulesetEditCheckerConfigManager)\r\n    {\r\n        // todo: adjust the config in the config.\r\n    }\r\n\r\n    private void recalculateIssues()\r\n    {\r\n        var hitObjects = editorBeatmap.HitObjects.OfType<KaraokeHitObject>().ToArray();\r\n        var listedHitObjects = hitObjectIssues.Keys.ToArray();\r\n\r\n        var newHitObjects = hitObjects.Except(listedHitObjects);\r\n        var removeHitObjects = listedHitObjects.Except(hitObjects);\r\n        var updateHitObjects = hitObjects.Intersect(listedHitObjects);\r\n\r\n        foreach (var hitObject in newHitObjects)\r\n        {\r\n            hitObjectAdded(hitObject);\r\n        }\r\n\r\n        foreach (var hitObject in removeHitObjects)\r\n        {\r\n            hitObjectRemoved(hitObject);\r\n        }\r\n\r\n        foreach (var hitObject in updateHitObjects)\r\n        {\r\n            hitObjectUpdated(hitObject);\r\n        }\r\n\r\n        recalculateEditModeIssue();\r\n    }\r\n\r\n    private void hitObjectAdded(HitObject obj)\r\n    {\r\n        if (obj is not KaraokeHitObject karaokeHitObject)\r\n            return;\r\n\r\n        hitObjectIssues.Add(karaokeHitObject, new BindableList<Issue>());\r\n        hitObjectUpdated(obj);\r\n\r\n        editModeCache.Invalidate();\r\n    }\r\n\r\n    private void hitObjectRemoved(HitObject obj)\r\n    {\r\n        if (obj is not KaraokeHitObject karaokeHitObject)\r\n            return;\r\n\r\n        hitObjectIssues[karaokeHitObject].Clear();\r\n        hitObjectIssues.Remove(karaokeHitObject);\r\n\r\n        editModeCache.Invalidate();\r\n    }\r\n\r\n    private void hitObjectUpdated(HitObject obj)\r\n    {\r\n        if (obj is not KaraokeHitObject karaokeHitObject)\r\n            return;\r\n\r\n        var bindableIssues = hitObjectIssues[karaokeHitObject];\r\n\r\n        var issues = getIssueByHitObject(karaokeHitObject).ToArray();\r\n        bindableIssues.Clear();\r\n        bindableIssues.AddRange(issues);\r\n\r\n        editModeCache.Invalidate();\r\n    }\r\n\r\n    protected override void Update()\r\n    {\r\n        base.Update();\r\n\r\n        if (!editModeCache.IsValid)\r\n            recalculateEditModeIssue();\r\n    }\r\n\r\n    private void recalculateEditModeIssue()\r\n    {\r\n        var allIssues = hitObjectIssues.Values.SelectMany(x => x);\r\n        var groupByEditModeIssues = allIssues.GroupBy(ClassifyIssue).ToDictionary(x => x.Key, x => x.ToArray());\r\n\r\n        foreach (var editorMode in Enum.GetValues<LyricEditorMode>())\r\n        {\r\n            ClearChecks(editorMode);\r\n\r\n            if (groupByEditModeIssues.TryGetValue(editorMode, out var issues))\r\n                AddChecks(editorMode, issues);\r\n        }\r\n\r\n        editModeCache.Validate();\r\n    }\r\n\r\n    protected override BeatmapVerifierContext CreateBeatmapVerifierContext(IBeatmap beatmap, WorkingBeatmap workingBeatmap) => new(new Beatmap<HitObject>(), workingBeatmap);\r\n\r\n    private IEnumerable<Issue> getIssueByHitObject(KaraokeHitObject karaokeHitObject)\r\n    {\r\n        return CreateIssues(context =>\r\n        {\r\n            if (context.CurrentDifficulty.Playable is not Beatmap<HitObject> beatmap)\r\n                throw new InvalidCastException();\r\n\r\n            beatmap.HitObjects.Add(karaokeHitObject);\r\n        });\r\n    }\r\n\r\n    protected override void Dispose(bool isDisposing)\r\n    {\r\n        base.Dispose(isDisposing);\r\n\r\n        // todo: not very sure\r\n        if (editorBeatmap.IsNull())\r\n            return;\r\n\r\n        editorBeatmap.HitObjectAdded -= hitObjectAdded;\r\n        editorBeatmap.HitObjectRemoved -= hitObjectRemoved;\r\n        editorBeatmap.HitObjectUpdated -= hitObjectUpdated;\r\n    }\r\n}\r\n\r\npublic class CheckLyricTimeTagOnly : CheckLyricTimeTag\r\n{\r\n    protected override IEnumerable<Issue> Check(Lyric lyric)\r\n    {\r\n        return CheckTimeTag(lyric);\r\n    }\r\n}\r\n\r\npublic class CheckLyricRomanisation : CheckLyricTimeTag\r\n{\r\n    protected override IEnumerable<Issue> Check(Lyric lyric)\r\n    {\r\n        return CheckTimeTagRomanisedSyllable(lyric);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/OsuColourExtensions.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics;\r\n\r\npublic static class OsuColourExtensions\r\n{\r\n    // todo: this should be moved to a more appropriate place.\r\n    // or just delete it.\r\n    public static Color4 GetRecordingTimeTagCaretColour(this OsuColour colours, TimeTag timeTag)\r\n        => timeTag.Time.HasValue ? colours.Red : colours.Gray3;\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/Components/BlockSectionWrapper.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Extensions.Color4Extensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Cursor;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osuTK;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Components;\r\n\r\npublic partial class BlockSectionWrapper : CompositeDrawable, IHasTooltip\r\n{\r\n    public BlockSectionWrapper(IconUsage iconUsage, LocalisableString name, LocalisableString description, LocalisableString tooltip)\r\n    {\r\n        TooltipText = tooltip;\r\n\r\n        RelativeSizeAxes = Axes.Both;\r\n        Padding = new MarginPadding { Top = 30 };\r\n\r\n        InternalChildren = new Drawable[]\r\n        {\r\n            new Container\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n                Masking = true,\r\n                CornerRadius = 15,\r\n                Child = new Box\r\n                {\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    Alpha = 0.3f,\r\n                    Colour = Color4.Black,\r\n                },\r\n            },\r\n            new BlockSectionMessage(iconUsage, name, description)\r\n            {\r\n                Anchor = Anchor.Centre,\r\n                Origin = Anchor.Centre,\r\n            },\r\n        };\r\n    }\r\n\r\n    public LocalisableString TooltipText { get; }\r\n\r\n    public partial class BlockSectionMessage : CompositeDrawable\r\n    {\r\n        private FillFlowContainer textContainer { get; }\r\n\r\n        private readonly Container boxContainer;\r\n\r\n        private const double transition_time = 1000;\r\n\r\n        public BlockSectionMessage(IconUsage iconUsage, LocalisableString name, LocalisableString description)\r\n        {\r\n            AutoSizeAxes = Axes.Both;\r\n\r\n            Anchor = Anchor.Centre;\r\n            Origin = Anchor.Centre;\r\n\r\n            var colour = Color4.Gray;\r\n\r\n            InternalChildren = new Drawable[]\r\n            {\r\n                boxContainer = new Container\r\n                {\r\n                    CornerRadius = 12,\r\n                    Masking = true,\r\n                    AutoSizeAxes = Axes.Both,\r\n                    Anchor = Anchor.Centre,\r\n                    Origin = Anchor.Centre,\r\n                    Children = new Drawable[]\r\n                    {\r\n                        new Box\r\n                        {\r\n                            RelativeSizeAxes = Axes.Both,\r\n\r\n                            Colour = colour.Darken(0.8f),\r\n                            Alpha = 0.8f,\r\n                        },\r\n                        textContainer = new FillFlowContainer\r\n                        {\r\n                            AutoSizeAxes = Axes.Both,\r\n                            Anchor = Anchor.Centre,\r\n                            Origin = Anchor.Centre,\r\n                            Padding = new MarginPadding(20),\r\n                            Direction = FillDirection.Vertical,\r\n                            Children = new Drawable[]\r\n                            {\r\n                                new SpriteIcon\r\n                                {\r\n                                    Icon = iconUsage,\r\n                                    Anchor = Anchor.TopCentre,\r\n                                    Origin = Anchor.TopCentre,\r\n                                    Size = new Vector2(32),\r\n                                },\r\n                                new OsuSpriteText\r\n                                {\r\n                                    Anchor = Anchor.TopCentre,\r\n                                    Origin = Anchor.TopCentre,\r\n                                    Text = name,\r\n                                    Colour = colour,\r\n                                    Font = OsuFont.GetFont(size: 18),\r\n                                },\r\n                                new OsuSpriteText\r\n                                {\r\n                                    Anchor = Anchor.TopCentre,\r\n                                    Origin = Anchor.TopCentre,\r\n                                    Text = description,\r\n                                    Font = OsuFont.GetFont(size: 14),\r\n                                },\r\n                            },\r\n                        },\r\n                    },\r\n                },\r\n            };\r\n        }\r\n\r\n        protected override void LoadComplete()\r\n        {\r\n            base.LoadComplete();\r\n\r\n            textContainer.Position = new Vector2(DrawSize.X / 16, 0);\r\n\r\n            boxContainer.Hide();\r\n            boxContainer.ScaleTo(0.2f);\r\n            boxContainer.RotateTo(-20);\r\n\r\n            using (BeginDelayedSequence(300))\r\n            {\r\n                boxContainer.ScaleTo(1, transition_time, Easing.OutElastic);\r\n                boxContainer.RotateTo(0, transition_time / 2, Easing.OutQuint);\r\n\r\n                textContainer.MoveTo(Vector2.Zero, transition_time, Easing.OutExpo);\r\n                boxContainer.FadeIn(transition_time, Easing.OutExpo);\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/Components/Markdown/LyricEditorDescriptionTextFlowContainer.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Graphics.Containers.Markdown;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Markdown;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Components.Markdown;\r\n\r\npublic partial class LyricEditorDescriptionTextFlowContainer : DescriptionTextFlowContainer\r\n{\r\n    protected override OsuMarkdownLinkText GetLinkTextByDescriptionAction(IDescriptionAction descriptionAction) =>\r\n        descriptionAction switch\r\n        {\r\n            SwitchModeDescriptionAction switchMode => new SwitchMoteText(switchMode),\r\n            _ => base.GetLinkTextByDescriptionAction(descriptionAction),\r\n        };\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/Components/Markdown/SwitchModeDescriptionAction.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Markdown;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Components.Markdown;\r\n\r\npublic struct SwitchModeDescriptionAction : IDescriptionAction\r\n{\r\n    public LocalisableString Text { get; set; }\r\n\r\n    public LyricEditorMode Mode { get; set; }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/Components/Markdown/SwitchMoteText.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Diagnostics;\r\nusing System.Linq;\r\nusing Markdig.Syntax.Inlines;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Game.Graphics.Containers.Markdown;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Overlays;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Components.Markdown;\r\n\r\npublic partial class SwitchMoteText : OsuMarkdownLinkText\r\n{\r\n    [Resolved]\r\n    private ILyricEditorState? state { get; set; }\r\n\r\n    private readonly SwitchModeDescriptionAction switchModeDescriptionAction;\r\n\r\n    public SwitchMoteText(SwitchModeDescriptionAction switchModeDescriptionAction)\r\n        : base(switchModeDescriptionAction.Text.ToString(), new LinkInline { Title = \"Click to change the edit mode.\" })\r\n    {\r\n        this.switchModeDescriptionAction = switchModeDescriptionAction;\r\n\r\n        CornerRadius = 4;\r\n        Masking = true;\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OverlayColourProvider colourProvider)\r\n    {\r\n        AddInternal(new Box\r\n        {\r\n            Name = \"Background\",\r\n            Depth = 1,\r\n            RelativeSizeAxes = Axes.Both,\r\n            Colour = colourProvider.Background6,\r\n        });\r\n\r\n        var spriteText = InternalChildren.OfType<OsuSpriteText>().FirstOrDefault();\r\n        Debug.Assert(spriteText != null);\r\n\r\n        spriteText.Padding = new MarginPadding { Horizontal = 4 };\r\n    }\r\n\r\n    protected override void OnLinkPressed()\r\n    {\r\n        base.OnLinkPressed();\r\n\r\n        state?.NavigateToFix(switchModeDescriptionAction.Mode);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/LabelledObjectFieldSwitchButton.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.UserInterfaceV2;\r\nusing osu.Game.Overlays;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings;\r\n\r\npublic abstract partial class LabelledObjectFieldSwitchButton<T> : LabelledSwitchButton where T : class\r\n{\r\n    protected readonly BindableList<T> SelectedItems = new();\r\n\r\n    protected new ObjectFieldSwitchButton Component => (ObjectFieldSwitchButton)base.Component;\r\n\r\n    private readonly T item;\r\n\r\n    protected LabelledObjectFieldSwitchButton(T item)\r\n    {\r\n        this.item = item;\r\n\r\n        // apply current value from the field in the item.\r\n        Current.Value = GetFieldValue(item);\r\n\r\n        // should change preview text box if selected boolean property changed.\r\n        Component.OnCommit += (sender, edited) =>\r\n        {\r\n            if (!edited)\r\n                return;\r\n\r\n            ApplyValue(item, sender.Value);\r\n        };\r\n\r\n        // change style if focus.\r\n        SelectedItems.BindCollectionChanged((_, _) =>\r\n        {\r\n            bool highLight = SelectedItems.Contains(item);\r\n            Component.HighLight = highLight;\r\n        });\r\n    }\r\n\r\n    protected void TriggerSelect()\r\n    {\r\n        // not trigger again if already focus.\r\n        if (SelectedItems.Contains(item) && SelectedItems.Count == 1)\r\n            return;\r\n\r\n        // trigger selected.\r\n        SelectedItems.Clear();\r\n        SelectedItems.Add(item);\r\n    }\r\n\r\n    protected abstract bool GetFieldValue(T item);\r\n\r\n    protected abstract void ApplyValue(T item, bool value);\r\n\r\n    protected override SwitchButton CreateComponent() => new ObjectFieldSwitchButton\r\n    {\r\n        Selected = selected =>\r\n        {\r\n            if (selected)\r\n                TriggerSelect();\r\n        },\r\n    };\r\n\r\n    protected partial class ObjectFieldSwitchButton : SwitchButton\r\n    {\r\n        public Action<bool>? Selected;\r\n\r\n        public Action<ObjectFieldSwitchButton, bool>? OnCommit;\r\n\r\n        protected override bool OnHover(HoverEvent e)\r\n        {\r\n            Selected?.Invoke(true);\r\n            return base.OnHover(e);\r\n        }\r\n\r\n        protected override void OnHoverLost(HoverLostEvent e)\r\n        {\r\n            Selected?.Invoke(false);\r\n            base.OnHoverLost(e);\r\n        }\r\n\r\n        protected override void OnUserChange(bool value)\r\n        {\r\n            base.OnUserChange(value);\r\n            OnCommit?.Invoke(this, true);\r\n        }\r\n\r\n        private Color4 highLightColour;\r\n        private Color4 enabledColour;\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(OverlayColourProvider? colourProvider, OsuColour colours)\r\n        {\r\n            highLightColour = colours.Yellow;\r\n\r\n            // copied from SwitchButton\r\n            enabledColour = colourProvider?.Highlight1 ?? colours.BlueDark;\r\n        }\r\n\r\n        public bool Value\r\n        {\r\n            get => Current.Value;\r\n            set => Current.Value = value;\r\n        }\r\n\r\n        public bool HighLight\r\n        {\r\n            set\r\n            {\r\n                if (InternalChild is not CircularContainer circularContainer)\r\n                    throw new ArgumentNullException(nameof(circularContainer));\r\n\r\n                var switchContainer = circularContainer.Children.OfType<Container>().LastOrDefault()?.Child;\r\n                if (switchContainer == null)\r\n                    throw new ArgumentNullException(nameof(switchContainer));\r\n\r\n                // only change dot colour because border colour should consider off case.\r\n                switchContainer.Colour = value ? highLightColour : enabledColour;\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/LabelledObjectFieldTextBox.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Containers;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Graphics.UserInterfaceV2;\r\nusing osu.Game.Rulesets.Karaoke.Extensions;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings;\r\n\r\npublic abstract partial class LabelledObjectFieldTextBox<T> : LabelledTextBox where T : class\r\n{\r\n    protected readonly IBindableList<T> SelectedItems = new BindableList<T>();\r\n\r\n    protected new ObjectFieldTextBox Component => (ObjectFieldTextBox)base.Component;\r\n\r\n    private readonly T item;\r\n\r\n    protected LabelledObjectFieldTextBox(T item)\r\n    {\r\n        this.item = item;\r\n\r\n        // apply current value from the field in the item.\r\n        Current.Value = GetFieldValue(item);\r\n\r\n        // should change preview text box if selected string property changed.\r\n        OnCommit += (sender, newText) =>\r\n        {\r\n            if (!newText)\r\n                return;\r\n\r\n            ApplyValue(item, sender.Text);\r\n        };\r\n\r\n        // change style if focus.\r\n        SelectedItems.BindCollectionChanged((_, _) =>\r\n        {\r\n            bool highLight = SelectedItems.Contains(item);\r\n            Component.HighLight = highLight;\r\n\r\n            if (SelectedItems.Contains(item) && SelectedItems.Count == 1)\r\n                focus();\r\n        });\r\n\r\n        if (InternalChildren[1] is not FillFlowContainer fillFlowContainer)\r\n            return;\r\n\r\n        // change padding to place delete button.\r\n        fillFlowContainer.Padding = new MarginPadding\r\n        {\r\n            Horizontal = CONTENT_PADDING_HORIZONTAL,\r\n            Vertical = CONTENT_PADDING_VERTICAL,\r\n            Right = CONTENT_PADDING_HORIZONTAL + CONTENT_PADDING_HORIZONTAL,\r\n        };\r\n    }\r\n\r\n    protected abstract void TriggerSelect(T item);\r\n\r\n    protected abstract string GetFieldValue(T item);\r\n\r\n    protected abstract void ApplyValue(T item, string value);\r\n\r\n    protected override OsuTextBox CreateTextBox() => new ObjectFieldTextBox\r\n    {\r\n        CommitOnFocusLost = true,\r\n        Anchor = Anchor.Centre,\r\n        Origin = Anchor.Centre,\r\n        RelativeSizeAxes = Axes.X,\r\n        CornerRadius = CORNER_RADIUS,\r\n        Selected = selected =>\r\n        {\r\n            if (selected)\r\n                TriggerSelect(item);\r\n        },\r\n    };\r\n\r\n    private void focus()\r\n    {\r\n        Schedule(() =>\r\n        {\r\n            var focusedDrawable = GetContainingInputManager().FocusedDrawable;\r\n            if (focusedDrawable == null)\r\n                return;\r\n\r\n            // Make sure that view is visible in the scroll container.\r\n            // Give the top spacing larger space to let use able to see the previous item or the description text.\r\n            var parentScrollContainer = this.FindClosestParent<OsuScrollContainer>();\r\n            if (parentScrollContainer == null)\r\n                throw new InvalidOperationException(\"Should have a parent scroll container.\");\r\n\r\n            parentScrollContainer.ScrollIntoViewWithSpacing(this, new MarginPadding { Top = 150, Bottom = 50 });\r\n\r\n            if (IsFocused(focusedDrawable))\r\n                return;\r\n\r\n            GetContainingFocusManager().ChangeFocus(Component);\r\n        });\r\n    }\r\n\r\n    protected virtual bool IsFocused(Drawable focusedDrawable)\r\n        => focusedDrawable == Component;\r\n\r\n    protected partial class ObjectFieldTextBox : OsuTextBox\r\n    {\r\n        [Resolved]\r\n        private OsuColour colours { get; set; } = null!;\r\n\r\n        public Action<bool>? Selected;\r\n\r\n        protected override void OnFocus(FocusEvent e)\r\n        {\r\n            base.OnFocus(e);\r\n            Selected?.Invoke(true);\r\n        }\r\n\r\n        protected override void OnFocusLost(FocusLostEvent e)\r\n        {\r\n            base.OnFocusLost(e);\r\n\r\n            // should not change the border size because still need to highlight the textarea without focus.\r\n            BorderThickness = 3f;\r\n\r\n            // note: should trigger commit event first in the base class.\r\n            Selected?.Invoke(false);\r\n        }\r\n\r\n        private Color4 standardBorderColour;\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load()\r\n        {\r\n            standardBorderColour = BorderColour;\r\n        }\r\n\r\n        public bool HighLight\r\n        {\r\n            set\r\n            {\r\n                BorderColour = value ? colours.Yellow : standardBorderColour;\r\n                BorderThickness = value ? 3 : 0;\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/Language/AssignLanguageSubsection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Globalization;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Extensions;\r\nusing osu.Framework.Graphics.Cursor;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.UserInterfaceV2;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Language;\r\n\r\npublic partial class AssignLanguageSubsection : SelectLyricButton, IHasPopover\r\n{\r\n    private readonly Bindable<CultureInfo?> bindableLanguage = new();\r\n\r\n    [Resolved]\r\n    private ILyricLanguageChangeHandler lyricLanguageChangeHandler { get; set; } = null!;\r\n\r\n    public AssignLanguageSubsection()\r\n    {\r\n        bindableLanguage.BindValueChanged(e =>\r\n        {\r\n            var language = e.NewValue;\r\n            if (language == null)\r\n                return;\r\n\r\n            this.HidePopover();\r\n            StartSelectingLyrics();\r\n        });\r\n    }\r\n\r\n    protected override LocalisableString StandardText => \"Change language\";\r\n\r\n    protected override LocalisableString SelectingText => $\"Cancel change language({CultureInfoUtils.GetLanguageDisplayText(bindableLanguage.Value)})\";\r\n\r\n    protected override void Apply()\r\n    {\r\n        lyricLanguageChangeHandler.SetLanguage(bindableLanguage.Value);\r\n        bindableLanguage.Value = null;\r\n    }\r\n\r\n    protected override void Cancel()\r\n    {\r\n        bindableLanguage.Value = null;\r\n    }\r\n\r\n    protected override void StartSelectingLyrics()\r\n    {\r\n        // before start selecting, we should make sure that language has been assigned.\r\n        if (bindableLanguage.Value == null)\r\n        {\r\n            this.ShowPopover();\r\n            return;\r\n        }\r\n\r\n        base.StartSelectingLyrics();\r\n    }\r\n\r\n    public Popover GetPopover()\r\n        => new LanguageSelectorPopover(bindableLanguage)\r\n        {\r\n            EnableEmptyOption = true,\r\n        };\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/Language/LanguageAutoGenerateSubsection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Components.Markdown;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Markdown;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Language;\r\n\r\npublic partial class LanguageAutoGenerateSubsection : LyricEditorAutoGenerateSubsection\r\n{\r\n    private const string typing_mode = \"TYPING_MODE\";\r\n\r\n    public LanguageAutoGenerateSubsection()\r\n        : base(AutoGenerateType.DetectLanguage)\r\n    {\r\n    }\r\n\r\n    protected override DescriptionFormat CreateInvalidDescriptionFormat()\r\n        => new()\r\n        {\r\n            Text = $\"Seems some lyric has no texts, go to [{DescriptionFormat.LINK_KEY_ACTION}]({typing_mode}) to fill the text.\",\r\n            Actions = new Dictionary<string, IDescriptionAction>\r\n            {\r\n                {\r\n                    typing_mode, new SwitchModeDescriptionAction\r\n                    {\r\n                        Text = \"typing mode\",\r\n                        Mode = LyricEditorMode.EditText,\r\n                    }\r\n                },\r\n            },\r\n        };\r\n\r\n    protected override ConfigButton CreateConfigButton()\r\n        => new LanguageAutoGenerateConfigButton();\r\n\r\n    protected partial class LanguageAutoGenerateConfigButton : ConfigButton\r\n    {\r\n        public override Popover GetPopover()\r\n            => new GeneratorConfigPopover(KaraokeRulesetEditGeneratorSetting.LanguageDetectorConfig);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/Language/LanguageEditModeSpecialAction.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Language;\r\n\r\npublic enum LanguageEditModeSpecialAction\r\n{\r\n    AutoGenerate,\r\n\r\n    BatchSelect,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/Language/LanguageIssueSection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks.Issues;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Issues;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Language;\r\n\r\npublic partial class LanguageIssueSection : LyricEditorIssueSection\r\n{\r\n    protected override LyricEditorMode EditMode => LyricEditorMode.EditLanguage;\r\n\r\n    protected override LyricsIssueTable CreateLyricsIssueTable() => new LanguageIssueTable();\r\n\r\n    private partial class LanguageIssueTable : LyricsIssueTable\r\n    {\r\n        protected override TableColumn[] CreateHeaders() => new[]\r\n        {\r\n            new TableColumn(string.Empty, Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize, minSize: 30)),\r\n            new TableColumn(\"Lyric\", Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize, minSize: 40)),\r\n            new TableColumn(\"Message\", Anchor.CentreLeft),\r\n        };\r\n\r\n        protected override Drawable[] CreateContent(Issue issue)\r\n        {\r\n            var lyric = getInvalidByIssue(issue);\r\n\r\n            return new Drawable[]\r\n            {\r\n                new IssueIcon\r\n                {\r\n                    Origin = Anchor.Centre,\r\n                    Size = new Vector2(10),\r\n                    Margin = new MarginPadding { Left = 10 },\r\n                    Issue = issue,\r\n                },\r\n                new OsuSpriteText\r\n                {\r\n                    Text = $\"#{lyric.Order}\",\r\n                    Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold),\r\n                    Margin = new MarginPadding { Right = 10 },\r\n                },\r\n                new TruncatingSpriteText\r\n                {\r\n                    Text = issue.ToString(),\r\n                    RelativeSizeAxes = Axes.X,\r\n                    Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Medium),\r\n                },\r\n            };\r\n        }\r\n\r\n        private Lyric getInvalidByIssue(Issue issue)\r\n        {\r\n            if (issue is not LyricIssue lyricIssue)\r\n                throw new InvalidCastException();\r\n\r\n            return lyricIssue.Lyric;\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/Language/LanguageSettingsHeader.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Markdown;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Language;\r\n\r\npublic partial class LanguageSettingsHeader : LyricEditorSettingsHeader<LanguageEditStep>\r\n{\r\n    protected override OverlayColourScheme CreateColourScheme()\r\n        => OverlayColourScheme.Pink;\r\n\r\n    protected override EditStepTabControl CreateTabControl()\r\n        => new LanguageEditStepTabControl();\r\n\r\n    protected override DescriptionFormat GetSelectionDescription(LanguageEditStep step) =>\r\n        step switch\r\n        {\r\n            LanguageEditStep.Generate => \"Auto-generate language with just a click.\",\r\n            LanguageEditStep.Verify => \"Check if have lyric with no language.\",\r\n            _ => throw new ArgumentOutOfRangeException(nameof(step), step, null),\r\n        };\r\n\r\n    private partial class LanguageEditStepTabControl : EditStepTabControl\r\n    {\r\n        protected override StepTabButton CreateStepButton(OsuColour colours, LanguageEditStep value)\r\n        {\r\n            return value switch\r\n            {\r\n                LanguageEditStep.Generate => new StepTabButton(value)\r\n                {\r\n                    Text = \"Generate\",\r\n                    SelectedColour = colours.Blue,\r\n                    UnSelectedColour = colours.BlueDarker,\r\n                },\r\n                LanguageEditStep.Verify => new VerifyStepTabButton(value)\r\n                {\r\n                    Text = \"Verify\",\r\n                    EditMode = LyricEditorMode.EditLanguage,\r\n                    SelectedColour = colours.Yellow,\r\n                    UnSelectedColour = colours.YellowDarker,\r\n                },\r\n                _ => throw new ArgumentOutOfRangeException(nameof(value), value, null),\r\n            };\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/Language/LanguageSwitchSpecialActionSection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Language;\r\n\r\npublic partial class LanguageSwitchSpecialActionSection : SpecialActionSection<LanguageEditModeSpecialAction>\r\n{\r\n    protected override string SwitchActionTitle => \"Special actions\";\r\n    protected override string SwitchActionDescription => \"Auto-generate or batch change the language by hands.\";\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(IEditLanguageModeState editNoteModeState)\r\n    {\r\n        BindTo(editNoteModeState);\r\n    }\r\n\r\n    protected override void UpdateActionArea(LanguageEditModeSpecialAction action)\r\n    {\r\n        RemoveAll(x => x is LanguageAutoGenerateSubsection or AssignLanguageSubsection, true);\r\n\r\n        switch (action)\r\n        {\r\n            case LanguageEditModeSpecialAction.AutoGenerate:\r\n                Add(new LanguageAutoGenerateSubsection());\r\n                break;\r\n\r\n            case LanguageEditModeSpecialAction.BatchSelect:\r\n                Add(new AssignLanguageSubsection());\r\n                break;\r\n\r\n            default:\r\n                return;\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/LanguageSettings.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Language;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings;\r\n\r\npublic partial class LanguageSettings : LyricEditorSettings\r\n{\r\n    public override SettingsDirection Direction => SettingsDirection.Right;\r\n\r\n    public override float SettingsWidth => 300;\r\n\r\n    private readonly Bindable<LanguageEditStep> bindableEditStep = new();\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(IEditLanguageModeState editLanguageModeState)\r\n    {\r\n        bindableEditStep.BindTo(editLanguageModeState.BindableEditStep);\r\n    }\r\n\r\n    protected override EditorSettingsHeader CreateSettingHeader()\r\n        => new LanguageSettingsHeader\r\n        {\r\n            Current = bindableEditStep,\r\n        };\r\n\r\n    protected override IReadOnlyList<EditorSection> CreateEditorSections() => bindableEditStep.Value switch\r\n    {\r\n        LanguageEditStep.Generate => new[]\r\n        {\r\n            new LanguageSwitchSpecialActionSection(),\r\n        },\r\n        LanguageEditStep.Verify => new[]\r\n        {\r\n            new LanguageIssueSection(),\r\n        },\r\n        _ => throw new ArgumentOutOfRangeException(),\r\n    };\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/LyricEditorAutoGenerateSubsection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Components.Markdown;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Markdown;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings;\r\n\r\npublic abstract partial class LyricEditorAutoGenerateSubsection : AutoGenerateSubsection\r\n{\r\n    private readonly AutoGenerateType autoGenerateType;\r\n\r\n    protected LyricEditorAutoGenerateSubsection(AutoGenerateType generateType)\r\n    {\r\n        autoGenerateType = generateType;\r\n    }\r\n\r\n    protected override EditorSectionButton CreateGenerateButton()\r\n        => new AutoGenerateButton(autoGenerateType);\r\n\r\n    protected sealed override DescriptionTextFlowContainer CreateDescriptionTextFlowContainer()\r\n        => new LyricEditorDescriptionTextFlowContainer();\r\n\r\n    private partial class AutoGenerateButton : SelectLyricButton\r\n    {\r\n        [Resolved]\r\n        private ILyricPropertyAutoGenerateChangeHandler lyricPropertyAutoGenerateChangeHandler { get; set; } = null!;\r\n\r\n        private readonly AutoGenerateType autoGenerateType;\r\n\r\n        public AutoGenerateButton(AutoGenerateType generateType)\r\n        {\r\n            autoGenerateType = generateType;\r\n        }\r\n\r\n        protected override LocalisableString StandardText => \"Generate\";\r\n\r\n        protected override LocalisableString SelectingText => \"Cancel generate\";\r\n\r\n        protected override IDictionary<Lyric, LocalisableString> GetDisableSelectingLyrics()\r\n        {\r\n            return lyricPropertyAutoGenerateChangeHandler.GetGeneratorNotSupportedLyrics(autoGenerateType);\r\n        }\r\n\r\n        protected override void Apply()\r\n        {\r\n            lyricPropertyAutoGenerateChangeHandler.AutoGenerate(autoGenerateType);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/LyricEditorIssueSection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Extensions.Color4Extensions;\r\nusing osu.Framework.Graphics.Colour;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings;\r\n\r\npublic abstract partial class LyricEditorIssueSection : IssueSection\r\n{\r\n    protected abstract LyricEditorMode EditMode { get; }\r\n\r\n    protected abstract LyricsIssueTable CreateLyricsIssueTable();\r\n\r\n    protected sealed override EmptyIssue CreateEmptyIssue() => new LyricEditorEmptyIssue();\r\n\r\n    protected sealed override IssueNavigator CreateIssueNavigator() => new LyricEditorIssueNavigator();\r\n\r\n    protected sealed override IssueTable CreateIssueTable() => CreateLyricsIssueTable();\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(ILyricEditorVerifier verifier)\r\n    {\r\n        Issues.BindTo(verifier.GetIssueByType(EditMode));\r\n    }\r\n\r\n    private partial class LyricEditorEmptyIssue : EmptyIssue\r\n    {\r\n        [Resolved]\r\n        private ILyricEditorVerifier verifier { get; set; } = null!;\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(LyricEditorColourProvider colourProvider, ILyricEditorState state)\r\n        {\r\n            Background.Colour = colourProvider.Background5(state.Mode);\r\n            Text.Colour = colourProvider.Colour1(state.Mode);\r\n        }\r\n\r\n        protected override void OnRefreshButtonClicked()\r\n            => verifier.Refresh();\r\n    }\r\n\r\n    private partial class LyricEditorIssueNavigator : IssueNavigator\r\n    {\r\n        [Resolved]\r\n        private ILyricEditorVerifier verifier { get; set; } = null!;\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(LyricEditorColourProvider colourProvider, ILyricEditorState state)\r\n        {\r\n            var colour = colourProvider.Background5(state.Mode);\r\n            Background.Colour = colour;\r\n            BlockBox.Colour = ColourInfo.GradientHorizontal(colour.Opacity(0), colour);\r\n        }\r\n\r\n        protected override void OnRefreshButtonClicked()\r\n            => verifier.Refresh();\r\n    }\r\n\r\n    protected abstract partial class LyricsIssueTable : LyricEditorIssueTable;\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/LyricEditorSettings.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings;\r\n\r\npublic abstract partial class LyricEditorSettings : EditorSettings\r\n{\r\n    public abstract SettingsDirection Direction { get; }\r\n\r\n    public abstract float SettingsWidth { get; }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(ILyricEditorState state, LyricEditorColourProvider colourProvider)\r\n    {\r\n        // change the background colour to the lighter one.\r\n        ChangeBackgroundColour(colourProvider.Background3(state.Mode));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/LyricEditorSettingsHeader.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Allocation;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Components.Markdown;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Markdown;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings;\r\n\r\npublic abstract partial class LyricEditorSettingsHeader<TEditStep> : EditorSettingsHeader<TEditStep>\r\n    where TEditStep : struct, Enum\r\n{\r\n    [Resolved]\r\n    private ILyricSelectionState lyricSelectionState { get; set; } = null!;\r\n\r\n    protected override DescriptionTextFlowContainer CreateDescriptionTextFlowContainer()\r\n        => new LyricEditorDescriptionTextFlowContainer();\r\n\r\n    protected override void UpdateEditStep(TEditStep step)\r\n    {\r\n        // should cancel the selection after change to the new edit step.\r\n        lyricSelectionState.EndSelecting(LyricEditorSelectingAction.Cancel);\r\n    }\r\n\r\n    protected sealed partial class VerifyStepTabButton : IssueStepTabButton\r\n    {\r\n        [Resolved]\r\n        private ILyricEditorVerifier verifier { get; set; } = null!;\r\n\r\n        public VerifyStepTabButton(TEditStep value)\r\n            : base(value)\r\n        {\r\n        }\r\n\r\n        private LyricEditorMode editorMode;\r\n\r\n        public LyricEditorMode EditMode\r\n        {\r\n            get => editorMode;\r\n            set\r\n            {\r\n                editorMode = value;\r\n\r\n                Schedule(() =>\r\n                {\r\n                    Issues.BindTo(verifier.GetIssueByType(EditMode));\r\n                });\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/LyricPropertiesSection.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings;\r\n\r\npublic abstract partial class LyricPropertiesSection<TModel> : LyricPropertySection where TModel : class\r\n{\r\n    private readonly LyricPropertiesEditor itemsEditor;\r\n\r\n    protected LyricPropertiesSection()\r\n    {\r\n        Add(itemsEditor = CreateLyricPropertiesEditor());\r\n    }\r\n\r\n    protected sealed override void OnLyricChanged(Lyric? lyric)\r\n    {\r\n        itemsEditor.OnLyricChanged(lyric);\r\n    }\r\n\r\n    protected abstract LyricPropertiesEditor CreateLyricPropertiesEditor();\r\n\r\n    protected abstract partial class LyricPropertiesEditor : SectionItemsEditor<TModel>\r\n    {\r\n        private Lyric? currentLyric;\r\n\r\n        protected Lyric CurrentLyric => currentLyric ?? throw new InvalidOperationException();\r\n\r\n        public void OnLyricChanged(Lyric? lyric)\r\n        {\r\n            currentLyric = lyric;\r\n\r\n            Items.UnbindBindings();\r\n\r\n            if (lyric != null)\r\n                Items.BindTo(GetItems(lyric));\r\n        }\r\n\r\n        protected abstract IBindableList<TModel> GetItems(Lyric lyric);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/LyricPropertySection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Components;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings;\r\n\r\npublic abstract partial class LyricPropertySection : EditorSection\r\n{\r\n    private readonly IBindable<Lyric?> bindableFocusedLyric = new Bindable<Lyric?>();\r\n    private readonly IBindable<int> bindablePropertyWritableVersion = new Bindable<int>();\r\n\r\n    public override bool PropagateNonPositionalInputSubTree => base.PropagateNonPositionalInputSubTree && !Disabled;\r\n    public override bool PropagatePositionalInputSubTree => base.PropagatePositionalInputSubTree && !Disabled;\r\n\r\n    protected bool IsRebinding { get; private set; }\r\n\r\n    protected bool Disabled { get; private set; }\r\n\r\n    protected override void LoadComplete()\r\n    {\r\n        base.LoadComplete();\r\n\r\n        bindableFocusedLyric.BindValueChanged(x =>\r\n        {\r\n            var lyric = x.NewValue;\r\n\r\n            IsRebinding = true;\r\n\r\n            OnLyricChanged(lyric);\r\n\r\n            bindablePropertyWritableVersion.UnbindBindings();\r\n\r\n            if (lyric != null)\r\n            {\r\n                bindablePropertyWritableVersion.BindTo(lyric.LyricPropertyWritableVersion);\r\n                updateDisableStatus();\r\n            }\r\n\r\n            IsRebinding = false;\r\n        }, true);\r\n\r\n        bindablePropertyWritableVersion.BindValueChanged(x =>\r\n        {\r\n            updateDisableStatus();\r\n        });\r\n\r\n        updateDisableStatus();\r\n    }\r\n\r\n    private void updateDisableStatus()\r\n    {\r\n        var lyric = bindableFocusedLyric.Value;\r\n        var propertyLocked = lyric != null ? IsWriteLyricPropertyLocked(lyric) : null;\r\n        Disabled = propertyLocked != null;\r\n\r\n        UpdateDisabledState(Disabled);\r\n\r\n        // should show the block section and make the children looks not editable if disable edit.\r\n        Content.FadeTo(Disabled ? 0.5f : 1, 300);\r\n        updateBlockSectionMessage(propertyLocked);\r\n    }\r\n\r\n    private void updateBlockSectionMessage(LockLyricPropertyBy? propertyLocked)\r\n    {\r\n        var blockMaskingWrapper = InternalChildren.OfType<BlockSectionWrapper>().FirstOrDefault();\r\n\r\n        if (blockMaskingWrapper == null && propertyLocked != null)\r\n        {\r\n            var icon = getWriteLyricPropertyLockedIcon(propertyLocked.Value);\r\n            var title = getWriteLyricPropertyLockedDescriptionTitle(propertyLocked.Value);\r\n            var description = GetWriteLyricPropertyLockedDescription(propertyLocked.Value);\r\n            var tooltip = GetWriteLyricPropertyLockedTooltip(propertyLocked.Value);\r\n\r\n            AddInternal(new BlockSectionWrapper(icon, title, description, tooltip));\r\n        }\r\n        else if (blockMaskingWrapper != null && propertyLocked == null)\r\n        {\r\n            RemoveInternal(blockMaskingWrapper, true);\r\n        }\r\n\r\n        static IconUsage getWriteLyricPropertyLockedIcon(LockLyricPropertyBy lockLyricPropertyBy) =>\r\n            lockLyricPropertyBy switch\r\n            {\r\n                LockLyricPropertyBy.ReferenceLyricConfig => FontAwesome.Solid.Chair,\r\n                LockLyricPropertyBy.LockState => FontAwesome.Solid.Lock,\r\n                _ => throw new ArgumentOutOfRangeException(nameof(lockLyricPropertyBy), lockLyricPropertyBy, null),\r\n            };\r\n\r\n        static LocalisableString getWriteLyricPropertyLockedDescriptionTitle(LockLyricPropertyBy lockLyricPropertyBy) =>\r\n            lockLyricPropertyBy switch\r\n            {\r\n                LockLyricPropertyBy.ReferenceLyricConfig => \"Sync\",\r\n                LockLyricPropertyBy.LockState => \"Locked\",\r\n                _ => throw new ArgumentOutOfRangeException(nameof(lockLyricPropertyBy), lockLyricPropertyBy, null),\r\n            };\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(ILyricCaretState lyricCaretState)\r\n    {\r\n        bindableFocusedLyric.BindTo(lyricCaretState.BindableFocusedLyric);\r\n    }\r\n\r\n    protected abstract void OnLyricChanged(Lyric? lyric);\r\n\r\n    protected virtual void UpdateDisabledState(bool disabled)\r\n    {\r\n    }\r\n\r\n    protected abstract LockLyricPropertyBy? IsWriteLyricPropertyLocked(Lyric lyric);\r\n\r\n    protected abstract LocalisableString GetWriteLyricPropertyLockedDescription(LockLyricPropertyBy lockLyricPropertyBy);\r\n\r\n    protected abstract LocalisableString GetWriteLyricPropertyLockedTooltip(LockLyricPropertyBy lockLyricPropertyBy);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/NoteSettings.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Notes;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings;\r\n\r\npublic partial class NoteSettings : LyricEditorSettings\r\n{\r\n    public override SettingsDirection Direction => SettingsDirection.Right;\r\n\r\n    public override float SettingsWidth => 300;\r\n\r\n    private readonly Bindable<NoteEditStep> bindableEditStep = new();\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(IEditNoteModeState editNoteModeState)\r\n    {\r\n        bindableEditStep.BindTo(editNoteModeState.BindableEditStep);\r\n    }\r\n\r\n    protected override EditorSettingsHeader CreateSettingHeader()\r\n        => new NoteSettingsHeader\r\n        {\r\n            Current = bindableEditStep,\r\n        };\r\n\r\n    protected override IReadOnlyList<EditorSection> CreateEditorSections() => bindableEditStep.Value switch\r\n    {\r\n        NoteEditStep.Generate => new EditorSection[]\r\n        {\r\n            new NoteConfigSection(),\r\n            new NoteSwitchSpecialActionSection(),\r\n        },\r\n        NoteEditStep.Edit => new EditorSection[]\r\n        {\r\n            new NoteEditPropertyModeSection(),\r\n            new NoteEditPropertySection(),\r\n        },\r\n        NoteEditStep.Verify => new EditorSection[]\r\n        {\r\n            new NoteIssueSection(),\r\n        },\r\n        _ => throw new ArgumentOutOfRangeException(),\r\n    };\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/Notes/NoteAutoGenerateSubsection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks.Issues;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Components.Markdown;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Markdown;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Notes;\r\n\r\n/// <summary>\r\n/// In <see cref=\"NoteEditStep.Generate\"/> mode, able to let user generate notes by <see cref=\"TimeTag\"/>\r\n/// But need to make sure that lyric should not have any <see cref=\"LyricTimeTagIssue\"/>\r\n/// If found any issue, will navigate to target lyric.\r\n/// </summary>\r\npublic partial class NoteAutoGenerateSubsection : LyricEditorAutoGenerateSubsection\r\n{\r\n    private const string create_time_tag_mode = \"CREATE_TIME_TAG_MODE\";\r\n\r\n    public NoteAutoGenerateSubsection()\r\n        : base(AutoGenerateType.AutoGenerateNotes)\r\n    {\r\n    }\r\n\r\n    protected override DescriptionFormat CreateInvalidDescriptionFormat()\r\n        => new()\r\n        {\r\n            Text = $\"Seems some lyric contains invalid time-tag, go to [{DescriptionFormat.LINK_KEY_ACTION}]({create_time_tag_mode}) to fix those issue.\",\r\n            Actions = new Dictionary<string, IDescriptionAction>\r\n            {\r\n                {\r\n                    create_time_tag_mode, new SwitchModeDescriptionAction\r\n                    {\r\n                        Text = \"adjust time-tag mode\",\r\n                        Mode = LyricEditorMode.EditTimeTag,\r\n                    }\r\n                },\r\n            },\r\n        };\r\n\r\n    protected override ConfigButton CreateConfigButton()\r\n        => new NoteAutoGenerateConfigButton();\r\n\r\n    protected partial class NoteAutoGenerateConfigButton : ConfigButton\r\n    {\r\n        public override Popover GetPopover()\r\n            => new GeneratorConfigPopover(KaraokeRulesetEditGeneratorSetting.NoteGeneratorConfig);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/Notes/NoteClearSubsection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Notes;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Notes;\r\n\r\npublic partial class NoteClearSubsection : SelectLyricButton\r\n{\r\n    [Resolved]\r\n    private INotesChangeHandler notesChangeHandler { get; set; } = null!;\r\n\r\n    protected override LocalisableString StandardText => \"Clear\";\r\n\r\n    protected override LocalisableString SelectingText => \"Cancel clear\";\r\n\r\n    protected override IDictionary<Lyric, LocalisableString> GetDisableSelectingLyrics()\r\n    {\r\n        // todo: should not select the lyric that not contains the note.\r\n        return new Dictionary<Lyric, LocalisableString>();\r\n    }\r\n\r\n    protected override void Apply()\r\n    {\r\n        notesChangeHandler.Clear();\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/Notes/NoteConfigSection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.UserInterfaceV2;\r\nusing osu.Game.Rulesets.UI.Scrolling;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Notes;\r\n\r\npublic partial class NoteConfigSection : EditorSection\r\n{\r\n    protected override LocalisableString Title => \"Config\";\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(IScrollingInfo scrollingInfo)\r\n    {\r\n        if (scrollingInfo.TimeRange is not BindableDouble bindableDouble)\r\n            return;\r\n\r\n        Children = new[]\r\n        {\r\n            new LabelledRealTimeSliderBar<double>\r\n            {\r\n                Label = \"Time range\",\r\n                Description = \"Change time-range to zoom-in/zoom-out the notes.\",\r\n                Current = bindableDouble,\r\n            },\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/Notes/NoteEditModeSpecialAction.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Notes;\r\n\r\npublic enum NoteEditModeSpecialAction\r\n{\r\n    AutoGenerate,\r\n\r\n    SyncTime,\r\n\r\n    Clear,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/Notes/NoteEditPropertyMode.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Notes;\r\n\r\npublic enum NoteEditPropertyMode\r\n{\r\n    Text,\r\n\r\n    RubyText,\r\n\r\n    Display,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/Notes/NoteEditPropertyModeSection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Graphics.UserInterfaceV2;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Notes;\r\n\r\npublic partial class NoteEditPropertyModeSection : EditorSection\r\n{\r\n    protected override LocalisableString Title => \"Edit property\";\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(IEditNoteModeState editNoteModeState)\r\n    {\r\n        Children = new Drawable[]\r\n        {\r\n            new LabelledEnumDropdown<NoteEditPropertyMode>(true)\r\n            {\r\n                Label = \"Edit property\",\r\n                Description = \"Batch edit text, ruby(alternative) text or display from notes\",\r\n                Current = editNoteModeState.BindableNoteEditPropertyMode,\r\n            },\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/Notes/NoteEditPropertySection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Notes;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Extensions;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Notes;\r\n\r\npublic partial class NoteEditPropertySection : LyricPropertiesSection<Note>\r\n{\r\n    protected override LocalisableString Title => \"Properties\";\r\n\r\n    protected override LyricPropertiesEditor CreateLyricPropertiesEditor() => new NotePropertiesEditor();\r\n\r\n    protected override LockLyricPropertyBy? IsWriteLyricPropertyLocked(Lyric lyric)\r\n        => HitObjectWritableUtils.GetCreateOrRemoveNoteLockedBy(lyric); //todo: should reference by another utils.\r\n\r\n    protected override LocalisableString GetWriteLyricPropertyLockedDescription(LockLyricPropertyBy lockLyricPropertyBy) =>\r\n        lockLyricPropertyBy switch\r\n        {\r\n            LockLyricPropertyBy.ReferenceLyricConfig => \"Notes is sync to another notes.\",\r\n            LockLyricPropertyBy.LockState => \"Notes is locked.\",\r\n            _ => throw new ArgumentOutOfRangeException(nameof(lockLyricPropertyBy), lockLyricPropertyBy, null),\r\n        };\r\n\r\n    protected override LocalisableString GetWriteLyricPropertyLockedTooltip(LockLyricPropertyBy lockLyricPropertyBy) =>\r\n        lockLyricPropertyBy switch\r\n        {\r\n            LockLyricPropertyBy.ReferenceLyricConfig => \"Cannot edit the notes because it's sync to another lyric's notes.\",\r\n            LockLyricPropertyBy.LockState => \"The lyric is locked, so cannot edit the note.\",\r\n            _ => throw new ArgumentOutOfRangeException(nameof(lockLyricPropertyBy), lockLyricPropertyBy, null),\r\n        };\r\n\r\n    private partial class NotePropertiesEditor : LyricPropertiesEditor\r\n    {\r\n        private readonly Bindable<NoteEditPropertyMode> bindableNoteEditPropertyMode = new();\r\n\r\n        [Resolved]\r\n        private EditorBeatmap beatmap { get; set; } = null!;\r\n\r\n        public NotePropertiesEditor()\r\n        {\r\n            bindableNoteEditPropertyMode.BindValueChanged(e =>\r\n            {\r\n                RedrewContent();\r\n            });\r\n        }\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(IEditNoteModeState editNoteModeState)\r\n        {\r\n            bindableNoteEditPropertyMode.BindTo(editNoteModeState.BindableNoteEditPropertyMode);\r\n        }\r\n\r\n        protected override Drawable CreateDrawable(Note item)\r\n        {\r\n            // todo: deal with create or remove the notes.\r\n            int index = Items.IndexOf(item);\r\n            return bindableNoteEditPropertyMode.Value switch\r\n            {\r\n                NoteEditPropertyMode.Text => new LabelledNoteTextTextBox(item)\r\n                {\r\n                    Label = $\"#{index + 1}\",\r\n                    TabbableContentContainer = this,\r\n                },\r\n                NoteEditPropertyMode.RubyText => new LabelledNoteRubyTextTextBox(item)\r\n                {\r\n                    Label = item.Text,\r\n                    TabbableContentContainer = this,\r\n                },\r\n                NoteEditPropertyMode.Display => new LabelledNoteDisplaySwitchButton(item)\r\n                {\r\n                    Label = item.Text,\r\n                },\r\n                _ => throw new ArgumentOutOfRangeException(nameof(bindableNoteEditPropertyMode.Value)),\r\n            };\r\n        }\r\n\r\n        protected override EditorSectionButton? CreateCreateNewItemButton() => null;\r\n\r\n        protected override IBindableList<Note> GetItems(Lyric lyric)\r\n        {\r\n            var notes = EditorBeatmapUtils.GetNotesByLyric(beatmap, lyric);\r\n            return new BindableList<Note>(notes);\r\n        }\r\n    }\r\n\r\n    private partial class LabelledNoteTextTextBox : LabelledObjectFieldTextBox<Note>\r\n    {\r\n        [Resolved]\r\n        private INotePropertyChangeHandler notePropertyChangeHandler { get; set; } = null!;\r\n\r\n        [Resolved]\r\n        private IEditNoteModeState editNoteModeState { get; set; } = null!;\r\n\r\n        public LabelledNoteTextTextBox(Note item)\r\n            : base(item)\r\n        {\r\n        }\r\n\r\n        protected override void TriggerSelect(Note item)\r\n            => editNoteModeState.Select(item);\r\n\r\n        protected override string GetFieldValue(Note note)\r\n            => note.Text;\r\n\r\n        protected override void ApplyValue(Note note, string value)\r\n            => notePropertyChangeHandler.ChangeText(value);\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load()\r\n        {\r\n            SelectedItems.BindTo(editNoteModeState.SelectedItems);\r\n        }\r\n    }\r\n\r\n    private partial class LabelledNoteRubyTextTextBox : LabelledObjectFieldTextBox<Note>\r\n    {\r\n        [Resolved]\r\n        private INotePropertyChangeHandler notePropertyChangeHandler { get; set; } = null!;\r\n\r\n        [Resolved]\r\n        private IEditNoteModeState editNoteModeState { get; set; } = null!;\r\n\r\n        public LabelledNoteRubyTextTextBox(Note item)\r\n            : base(item)\r\n        {\r\n        }\r\n\r\n        protected override void TriggerSelect(Note item)\r\n            => editNoteModeState.Select(item);\r\n\r\n        protected override string GetFieldValue(Note note)\r\n            => note.RubyText ?? string.Empty;\r\n\r\n        protected override void ApplyValue(Note note, string value)\r\n            => notePropertyChangeHandler.ChangeRubyText(value);\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load()\r\n        {\r\n            SelectedItems.BindTo(editNoteModeState.SelectedItems);\r\n        }\r\n    }\r\n\r\n    private partial class LabelledNoteDisplaySwitchButton : LabelledObjectFieldSwitchButton<Note>\r\n    {\r\n        [Resolved]\r\n        private INotePropertyChangeHandler notePropertyChangeHandler { get; set; } = null!;\r\n\r\n        public LabelledNoteDisplaySwitchButton(Note item)\r\n            : base(item)\r\n        {\r\n        }\r\n\r\n        protected override bool GetFieldValue(Note note)\r\n            => note.Display;\r\n\r\n        protected override void ApplyValue(Note note, bool value)\r\n            => notePropertyChangeHandler.ChangeDisplayState(value);\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(IEditNoteModeState editNoteModeState)\r\n        {\r\n            SelectedItems.BindTo(editNoteModeState.SelectedItems);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/Notes/NoteIssueSection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks.Issues;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Issues;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Notes;\r\n\r\npublic partial class NoteIssueSection : LyricEditorIssueSection\r\n{\r\n    protected override LyricEditorMode EditMode => LyricEditorMode.EditNote;\r\n\r\n    protected override LyricsIssueTable CreateLyricsIssueTable() => new NoteIssueTable();\r\n\r\n    private partial class NoteIssueTable : LyricsIssueTable\r\n    {\r\n        protected override TableColumn[] CreateHeaders() => new[]\r\n        {\r\n            new TableColumn(string.Empty, Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize, minSize: 30)),\r\n            new TableColumn(\"Note\", Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize, minSize: 40)),\r\n            new TableColumn(\"Message\", Anchor.CentreLeft),\r\n        };\r\n\r\n        protected override Drawable[] CreateContent(Issue issue)\r\n        {\r\n            var note = getInvalidByIssue(issue);\r\n            string noteIndex = note.ReferenceLyric?.Order.ToString() ?? \"??\";\r\n\r\n            return new Drawable[]\r\n            {\r\n                new IssueIcon\r\n                {\r\n                    Origin = Anchor.Centre,\r\n                    Size = new Vector2(10),\r\n                    Margin = new MarginPadding { Left = 10 },\r\n                    Issue = issue,\r\n                },\r\n                new OsuSpriteText\r\n                {\r\n                    Text = $\"#{noteIndex}\",\r\n                    Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold),\r\n                    Margin = new MarginPadding { Right = 10 },\r\n                },\r\n                new TruncatingSpriteText\r\n                {\r\n                    Text = issue.ToString(),\r\n                    RelativeSizeAxes = Axes.X,\r\n                    Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Medium),\r\n                },\r\n            };\r\n        }\r\n\r\n        private Note getInvalidByIssue(Issue issue)\r\n        {\r\n            if (issue is not NoteIssue noteIssue)\r\n                throw new InvalidCastException();\r\n\r\n            return noteIssue.Note;\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/Notes/NoteSettingsHeader.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Markdown;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Notes;\r\n\r\npublic partial class NoteSettingsHeader : LyricEditorSettingsHeader<NoteEditStep>\r\n{\r\n    protected override OverlayColourScheme CreateColourScheme()\r\n        => OverlayColourScheme.Blue;\r\n\r\n    protected override EditStepTabControl CreateTabControl()\r\n        => new NoteEditStepTabControl();\r\n\r\n    protected override DescriptionFormat GetSelectionDescription(NoteEditStep step) =>\r\n        step switch\r\n        {\r\n            NoteEditStep.Generate => \"Using time-tag to create default notes.\",\r\n            NoteEditStep.Edit => \"Batch edit note property in here.\",\r\n            NoteEditStep.Verify => \"Check invalid notes in here.\",\r\n            _ => throw new ArgumentOutOfRangeException(nameof(step), step, null),\r\n        };\r\n\r\n    private partial class NoteEditStepTabControl : EditStepTabControl\r\n    {\r\n        protected override StepTabButton CreateStepButton(OsuColour colours, NoteEditStep value)\r\n        {\r\n            return value switch\r\n            {\r\n                NoteEditStep.Generate => new StepTabButton(value)\r\n                {\r\n                    Text = \"Generate\",\r\n                    SelectedColour = colours.Blue,\r\n                    UnSelectedColour = colours.BlueDarker,\r\n                },\r\n                NoteEditStep.Edit => new StepTabButton(value)\r\n                {\r\n                    Text = \"Edit\",\r\n                    SelectedColour = colours.Red,\r\n                    UnSelectedColour = colours.RedDarker,\r\n                },\r\n                NoteEditStep.Verify => new VerifyStepTabButton(value)\r\n                {\r\n                    Text = \"Verify\",\r\n                    EditMode = LyricEditorMode.EditNote,\r\n                    SelectedColour = colours.Yellow,\r\n                    UnSelectedColour = colours.YellowDarker,\r\n                },\r\n                _ => throw new ArgumentOutOfRangeException(nameof(value), value, null),\r\n            };\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/Notes/NoteSwitchSpecialActionSection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Notes;\r\n\r\npublic partial class NoteSwitchSpecialActionSection : SpecialActionSection<NoteEditModeSpecialAction>\r\n{\r\n    protected override string SwitchActionTitle => \"Special actions\";\r\n    protected override string SwitchActionDescription => \"Auto-generate, edit or clear the notes.\";\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(IEditNoteModeState editNoteModeState)\r\n    {\r\n        BindTo(editNoteModeState);\r\n    }\r\n\r\n    protected override void UpdateActionArea(NoteEditModeSpecialAction action)\r\n    {\r\n        RemoveAll(x => x is NoteAutoGenerateSubsection or NoteClearSubsection, true);\r\n\r\n        switch (action)\r\n        {\r\n            case NoteEditModeSpecialAction.AutoGenerate:\r\n                Add(new NoteAutoGenerateSubsection());\r\n                break;\r\n\r\n            case NoteEditModeSpecialAction.SyncTime:\r\n                // todo: implement\r\n                break;\r\n\r\n            case NoteEditModeSpecialAction.Clear:\r\n                Add(new NoteClearSubsection());\r\n                break;\r\n\r\n            default:\r\n                return;\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/Reference/LabelledReferenceLyricSelector.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Extensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Cursor;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Containers;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Graphics.UserInterfaceV2;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Components.UserInterfaceV2;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Reference;\r\n\r\npublic partial class LabelledReferenceLyricSelector : LabelledComponent<LabelledReferenceLyricSelector.SelectLyricButton, Lyric?>\r\n{\r\n    public LabelledReferenceLyricSelector()\r\n        : base(true)\r\n    {\r\n    }\r\n\r\n    protected override SelectLyricButton CreateComponent()\r\n        => new()\r\n        {\r\n            RelativeSizeAxes = Axes.X,\r\n        };\r\n\r\n    public Lyric? IgnoredLyric\r\n    {\r\n        get => Component.IgnoredLyric;\r\n        set => Component.IgnoredLyric = value;\r\n    }\r\n\r\n    public partial class SelectLyricButton : OsuButton, IHasCurrentValue<Lyric?>, IHasPopover\r\n    {\r\n        [Resolved]\r\n        private EditorBeatmap editorBeatmap { get; set; } = null!;\r\n\r\n        private readonly BindableWithCurrent<Lyric?> current = new();\r\n\r\n        public Bindable<Lyric?> Current\r\n        {\r\n            get => current.Current;\r\n            set => current.Current = value;\r\n        }\r\n\r\n        private Lyric? ignoredLyric;\r\n\r\n        public Lyric? IgnoredLyric\r\n        {\r\n            get => ignoredLyric;\r\n            set\r\n            {\r\n                ignoredLyric = value;\r\n\r\n                // should not enable the selection if current lyric is being referenced.\r\n                Enabled.Value = ignoredLyric != null && !EditorBeatmapUtils.GetAllReferenceLyrics(editorBeatmap, ignoredLyric).Any();\r\n            }\r\n        }\r\n\r\n        public SelectLyricButton()\r\n        {\r\n            Action = this.ShowPopover;\r\n            current.BindValueChanged(x =>\r\n            {\r\n                var lyric = x.NewValue;\r\n                Text = lyric == null\r\n                    ? \"Select lyric...\"\r\n                    : $\"#{lyric.Order} {lyric.Text}\";\r\n            }, true);\r\n        }\r\n\r\n        public Popover GetPopover()\r\n            => new LyricSelectorPopover(Current, IgnoredLyric);\r\n    }\r\n\r\n    private partial class LyricSelectorPopover : OsuPopover\r\n    {\r\n        private readonly ReferenceLyricSelector lyricSelector;\r\n\r\n        [Cached]\r\n        private readonly Lyric? ignoreLyric;\r\n\r\n        public LyricSelectorPopover(Bindable<Lyric?> bindable, Lyric? ignoreLyric)\r\n        {\r\n            this.ignoreLyric = ignoreLyric;\r\n\r\n            Child = lyricSelector = new ReferenceLyricSelector\r\n            {\r\n                Width = 400,\r\n                Height = 600,\r\n                Current = bindable,\r\n            };\r\n        }\r\n\r\n        protected override void LoadComplete()\r\n        {\r\n            base.LoadComplete();\r\n\r\n            GetContainingFocusManager().ChangeFocus(lyricSelector);\r\n        }\r\n    }\r\n\r\n    protected partial class ReferenceLyricSelector : LyricSelector\r\n    {\r\n        protected override RearrangeableLyricListContainer CreateRearrangeableLyricListContainer()\r\n            => new RearrangeableReferenceLyricListContainer();\r\n\r\n        protected partial class RearrangeableReferenceLyricListContainer : RearrangeableLyricListContainer\r\n        {\r\n            protected override DrawableTextListItem CreateDrawable(Lyric? item)\r\n                => new DrawableReferenceLyricListItem(item);\r\n\r\n            protected partial class DrawableReferenceLyricListItem : DrawableLyricListItem\r\n            {\r\n                [Resolved]\r\n                private OsuColour colours { get; set; } = null!;\r\n\r\n                [Resolved]\r\n                private EditorBeatmap editorBeatmap { get; set; } = null!;\r\n\r\n                [Resolved]\r\n                private Lyric? ignoredLyric { get; set; }\r\n\r\n                public DrawableReferenceLyricListItem(Lyric? item)\r\n                    : base(item)\r\n                {\r\n                }\r\n\r\n                protected override bool OnClick(ClickEvent e)\r\n                {\r\n                    // cannot select those lyric that already contains reference lyric.\r\n                    if (!selectable(Model))\r\n                        return false;\r\n\r\n                    return base.OnClick(e);\r\n                }\r\n\r\n                protected override void CreateDisplayContent(OsuTextFlowContainer textFlowContainer, Lyric? model)\r\n                {\r\n                    // should have disable style if lyric is not selectable.\r\n                    textFlowContainer.Alpha = selectable(model) ? 1 : 0.5f;\r\n\r\n                    base.CreateDisplayContent(textFlowContainer, model);\r\n\r\n                    if (model == null)\r\n                        return;\r\n\r\n                    // add reference text at the end of the text.\r\n                    int referenceLyricsAmount = EditorBeatmapUtils.GetAllReferenceLyrics(editorBeatmap, model).Count();\r\n\r\n                    if (referenceLyricsAmount > 0)\r\n                    {\r\n                        textFlowContainer.AddText($\"({referenceLyricsAmount} reference)\", x => x.Colour = colours.Red);\r\n                    }\r\n                }\r\n\r\n                private bool selectable(Lyric? lyric)\r\n                    => lyric != ignoredLyric && lyric?.ReferenceLyric == null;\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/Reference/ReferenceLyricAutoGenerateSection.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Markdown;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Reference;\r\n\r\npublic partial class ReferenceLyricAutoGenerateSection : AutoGenerateSection\r\n{\r\n    protected override AutoGenerateSubsection CreateAutoGenerateSubsection()\r\n        => new ReferenceLyricAutoGenerateSubsection();\r\n\r\n    private partial class ReferenceLyricAutoGenerateSubsection : LyricEditorAutoGenerateSubsection\r\n    {\r\n        public ReferenceLyricAutoGenerateSubsection()\r\n            : base(AutoGenerateType.DetectReferenceLyric)\r\n        {\r\n        }\r\n\r\n        protected override DescriptionFormat CreateInvalidDescriptionFormat()\r\n            => new()\r\n            {\r\n                Text = \"Seems every lyrics in the songs are unique. But don't worry, reference lyric can still link by hands.\",\r\n            };\r\n\r\n        protected override ConfigButton CreateConfigButton()\r\n            => new ReferenceLyricAutoGenerateConfigButton();\r\n\r\n        protected partial class ReferenceLyricAutoGenerateConfigButton : ConfigButton\r\n        {\r\n            public override Popover GetPopover()\r\n                => new GeneratorConfigPopover(KaraokeRulesetEditGeneratorSetting.ReferenceLyricDetectorConfig);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/Reference/ReferenceLyricConfigSection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Extensions.IEnumerableExtensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Graphics.UserInterfaceV2;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Properties;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Reference;\r\n\r\npublic partial class ReferenceLyricConfigSection : LyricPropertySection\r\n{\r\n    private const string sync = \"sync\";\r\n    private const string reference = \"reference\";\r\n\r\n    protected override LocalisableString Title => \"Config\";\r\n\r\n    [Resolved]\r\n    private ILyricReferenceChangeHandler lyricReferenceChangeHandler { get; set; } = null!;\r\n\r\n    private readonly IBindable<IReferenceLyricPropertyConfig?> bindableReferenceLyricPropertyConfig = new Bindable<IReferenceLyricPropertyConfig?>();\r\n\r\n    private readonly LabelledDropdown<string> labelledReferenceLyricConfig;\r\n    private readonly LabelledSwitchButton labelledSyncEverything;\r\n    private readonly LabelledSwitchButton labelledSyncSinger;\r\n    private readonly LabelledSwitchButton labelledSyncTimeTag;\r\n\r\n    private bool isConfigChanging;\r\n\r\n    public ReferenceLyricConfigSection()\r\n    {\r\n        Children = new Drawable[]\r\n        {\r\n            labelledReferenceLyricConfig = new LabelledDropdown<string>(true)\r\n            {\r\n                Label = \"Config\",\r\n                Description = \"Select the similar lyric that want to reference or sync the property.\",\r\n                Items = new[]\r\n                {\r\n                    sync,\r\n                    reference,\r\n                },\r\n            },\r\n            labelledSyncEverything = new LabelledSwitchButton\r\n            {\r\n                Label = \"Sync\",\r\n                Description = \"Sync most property.\",\r\n                Current =\r\n                {\r\n                    Value = true,\r\n                    Disabled = true,\r\n                },\r\n            },\r\n            labelledSyncSinger = new LabelledSwitchButton\r\n            {\r\n                Label = \"Sync singer.\",\r\n                Description = \"Un-select the selection if want to customize the singer.\",\r\n            },\r\n            labelledSyncTimeTag = new LabelledSwitchButton\r\n            {\r\n                Label = \"Sync time-tags.\",\r\n                Description = \"Un-select the selection if want to customize the time-tag.\",\r\n            },\r\n        };\r\n\r\n        bindableReferenceLyricPropertyConfig.BindValueChanged(e =>\r\n        {\r\n            onConfigChanged();\r\n        }, true);\r\n\r\n        labelledReferenceLyricConfig.Current.BindValueChanged(x =>\r\n        {\r\n            if (IsRebinding || isConfigChanging)\r\n                return;\r\n\r\n            switch (x.NewValue)\r\n            {\r\n                case sync:\r\n                    lyricReferenceChangeHandler.SwitchToSyncLyricConfig();\r\n                    break;\r\n\r\n                case reference:\r\n                    lyricReferenceChangeHandler.SwitchToReferenceLyricConfig();\r\n                    break;\r\n\r\n                default:\r\n                    throw new InvalidOperationException();\r\n            }\r\n        });\r\n\r\n        labelledSyncSinger.Current.BindValueChanged(x =>\r\n        {\r\n            if (!IsRebinding && !isConfigChanging)\r\n                lyricReferenceChangeHandler.AdjustLyricConfig<SyncLyricConfig>(config => config.SyncSingerProperty = x.NewValue);\r\n        });\r\n\r\n        labelledSyncTimeTag.Current.BindValueChanged(x =>\r\n        {\r\n            if (!IsRebinding && !isConfigChanging)\r\n                lyricReferenceChangeHandler.AdjustLyricConfig<SyncLyricConfig>(config => config.SyncTimeTagProperty = x.NewValue);\r\n        });\r\n    }\r\n\r\n    protected override void OnLyricChanged(Lyric? lyric)\r\n    {\r\n        bindableReferenceLyricPropertyConfig.UnbindBindings();\r\n\r\n        if (lyric != null)\r\n            bindableReferenceLyricPropertyConfig.BindTo(lyric.ReferenceLyricConfigBindable);\r\n    }\r\n\r\n    protected override LockLyricPropertyBy? IsWriteLyricPropertyLocked(Lyric lyric)\r\n        => HitObjectWritableUtils.GetLyricPropertyLockedBy(lyric, nameof(Lyric.ReferenceLyric), nameof(lyric.ReferenceLyricConfig));\r\n\r\n    protected override LocalisableString GetWriteLyricPropertyLockedDescription(LockLyricPropertyBy lockLyricPropertyBy) =>\r\n        lockLyricPropertyBy switch\r\n        {\r\n            // technically the property is always editable.\r\n            _ => throw new ArgumentOutOfRangeException(nameof(lockLyricPropertyBy), lockLyricPropertyBy, null),\r\n        };\r\n\r\n    protected override LocalisableString GetWriteLyricPropertyLockedTooltip(LockLyricPropertyBy lockLyricPropertyBy) =>\r\n        lockLyricPropertyBy switch\r\n        {\r\n            // technically the property is always editable.\r\n            _ => throw new ArgumentOutOfRangeException(nameof(lockLyricPropertyBy), lockLyricPropertyBy, null),\r\n        };\r\n\r\n    private void onConfigChanged()\r\n    {\r\n        isConfigChanging = true;\r\n\r\n        Children.ForEach(x => x.Hide());\r\n        Show();\r\n\r\n        var config = bindableReferenceLyricPropertyConfig.Value;\r\n\r\n        switch (config)\r\n        {\r\n            case ReferenceLyricConfig:\r\n                labelledReferenceLyricConfig.Current.Value = reference;\r\n                labelledReferenceLyricConfig.Show();\r\n                break;\r\n\r\n            case SyncLyricConfig syncLyricConfig:\r\n                labelledReferenceLyricConfig.Current.Value = sync;\r\n                labelledSyncSinger.Current = syncLyricConfig.SyncSingerPropertyBindable;\r\n                labelledSyncTimeTag.Current = syncLyricConfig.SyncTimeTagPropertyBindable;\r\n\r\n                labelledReferenceLyricConfig.Show();\r\n                labelledSyncEverything.Show();\r\n                labelledSyncSinger.Show();\r\n                labelledSyncTimeTag.Show();\r\n                break;\r\n\r\n            case null:\r\n                Hide();\r\n                break;\r\n\r\n            default:\r\n                throw new ArgumentOutOfRangeException(nameof(config), config, \"unknown config.\");\r\n        }\r\n\r\n        isConfigChanging = false;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/Reference/ReferenceLyricIssueSection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks.Issues;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Issues;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Reference;\r\n\r\npublic partial class ReferenceLyricIssueSection : LyricEditorIssueSection\r\n{\r\n    protected override LyricEditorMode EditMode => LyricEditorMode.EditReferenceLyric;\r\n\r\n    protected override LyricsIssueTable CreateLyricsIssueTable() => new ReferenceLyricIssueTable();\r\n\r\n    private partial class ReferenceLyricIssueTable : LyricsIssueTable\r\n    {\r\n        protected override TableColumn[] CreateHeaders() => new[]\r\n        {\r\n            new TableColumn(string.Empty, Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize, minSize: 30)),\r\n            new TableColumn(\"Lyric\", Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize, minSize: 40)),\r\n            new TableColumn(\"Message\", Anchor.CentreLeft),\r\n        };\r\n\r\n        protected override Drawable[] CreateContent(Issue issue)\r\n        {\r\n            var lyric = getInvalidByIssue(issue);\r\n\r\n            return new Drawable[]\r\n            {\r\n                new IssueIcon\r\n                {\r\n                    Origin = Anchor.Centre,\r\n                    Size = new Vector2(10),\r\n                    Margin = new MarginPadding { Left = 10 },\r\n                    Issue = issue,\r\n                },\r\n                new OsuSpriteText\r\n                {\r\n                    Text = $\"#{lyric.Order}\",\r\n                    Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold),\r\n                    Margin = new MarginPadding { Right = 10 },\r\n                },\r\n                new TruncatingSpriteText\r\n                {\r\n                    Text = issue.ToString(),\r\n                    RelativeSizeAxes = Axes.X,\r\n                    Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Medium),\r\n                },\r\n            };\r\n        }\r\n\r\n        private Lyric getInvalidByIssue(Issue issue)\r\n        {\r\n            if (issue is not LyricIssue lyricIssue)\r\n                throw new InvalidCastException();\r\n\r\n            return lyricIssue.Lyric;\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/Reference/ReferenceLyricSection.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Reference;\r\n\r\npublic partial class ReferenceLyricSection : LyricPropertySection\r\n{\r\n    protected override LocalisableString Title => \"Reference lyric\";\r\n\r\n    [Resolved]\r\n    private ILyricReferenceChangeHandler lyricReferenceChangeHandler { get; set; } = null!;\r\n\r\n    private readonly LabelledReferenceLyricSelector labelledReferenceLyricSelector;\r\n\r\n    public ReferenceLyricSection()\r\n    {\r\n        Children = new[]\r\n        {\r\n            labelledReferenceLyricSelector = new LabelledReferenceLyricSelector\r\n            {\r\n                Label = \"Referenced lyric\",\r\n                Description = \"Select the similar lyric that want to reference or sync the property.\",\r\n            },\r\n        };\r\n\r\n        labelledReferenceLyricSelector.Current.BindValueChanged(x =>\r\n        {\r\n            if (!IsRebinding)\r\n                lyricReferenceChangeHandler.UpdateReferenceLyric(x.NewValue);\r\n        });\r\n    }\r\n\r\n    protected override void OnLyricChanged(Lyric? lyric)\r\n    {\r\n        if (lyric == null)\r\n            return;\r\n\r\n        labelledReferenceLyricSelector.Current = lyric.ReferenceLyricBindable;\r\n        labelledReferenceLyricSelector.IgnoredLyric = lyric;\r\n    }\r\n\r\n    protected override LockLyricPropertyBy? IsWriteLyricPropertyLocked(Lyric lyric)\r\n        => HitObjectWritableUtils.GetLyricPropertyLockedBy(lyric, nameof(Lyric.ReferenceLyric), nameof(lyric.ReferenceLyricConfig));\r\n\r\n    protected override LocalisableString GetWriteLyricPropertyLockedDescription(LockLyricPropertyBy lockLyricPropertyBy) =>\r\n        lockLyricPropertyBy switch\r\n        {\r\n            // technically the property is always editable.\r\n            _ => throw new ArgumentOutOfRangeException(nameof(lockLyricPropertyBy), lockLyricPropertyBy, null),\r\n        };\r\n\r\n    protected override LocalisableString GetWriteLyricPropertyLockedTooltip(LockLyricPropertyBy lockLyricPropertyBy) =>\r\n        lockLyricPropertyBy switch\r\n        {\r\n            // technically the property is always editable.\r\n            _ => throw new ArgumentOutOfRangeException(nameof(lockLyricPropertyBy), lockLyricPropertyBy, null),\r\n        };\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/Reference/ReferenceLyricSettingsHeader.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Markdown;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Reference;\r\n\r\npublic partial class ReferenceLyricSettingsHeader : LyricEditorSettingsHeader<ReferenceLyricEditStep>\r\n{\r\n    protected override OverlayColourScheme CreateColourScheme()\r\n        => OverlayColourScheme.Pink;\r\n\r\n    protected override EditStepTabControl CreateTabControl()\r\n        => new ReferenceLyricEditStepTabControl();\r\n\r\n    protected override DescriptionFormat GetSelectionDescription(ReferenceLyricEditStep step) =>\r\n        step switch\r\n        {\r\n            ReferenceLyricEditStep.Edit => \"Assign the reference lyrics.\",\r\n            ReferenceLyricEditStep.Verify => \"Check any invalid reference lyric issue.\",\r\n            _ => throw new ArgumentOutOfRangeException(nameof(step), step, null),\r\n        };\r\n\r\n    private partial class ReferenceLyricEditStepTabControl : EditStepTabControl\r\n    {\r\n        protected override StepTabButton CreateStepButton(OsuColour colours, ReferenceLyricEditStep value)\r\n        {\r\n            return value switch\r\n            {\r\n                ReferenceLyricEditStep.Edit => new StepTabButton(value)\r\n                {\r\n                    Text = \"Edit\",\r\n                    SelectedColour = colours.Blue,\r\n                    UnSelectedColour = colours.BlueDarker,\r\n                },\r\n                ReferenceLyricEditStep.Verify => new VerifyStepTabButton(value)\r\n                {\r\n                    Text = \"Verify\",\r\n                    EditMode = LyricEditorMode.EditReferenceLyric,\r\n                    SelectedColour = colours.Yellow,\r\n                    UnSelectedColour = colours.YellowDarker,\r\n                },\r\n                _ => throw new ArgumentOutOfRangeException(nameof(value), value, null),\r\n            };\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/ReferenceSettings.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Reference;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings;\r\n\r\npublic partial class ReferenceSettings : LyricEditorSettings\r\n{\r\n    public override SettingsDirection Direction => SettingsDirection.Right;\r\n\r\n    public override float SettingsWidth => 300;\r\n\r\n    private readonly Bindable<ReferenceLyricEditStep> bindableEditStep = new();\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(IEditReferenceLyricModeState editReferenceLyricModeState)\r\n    {\r\n        bindableEditStep.BindTo(editReferenceLyricModeState.BindableEditStep);\r\n    }\r\n\r\n    protected override EditorSettingsHeader CreateSettingHeader()\r\n        => new ReferenceLyricSettingsHeader\r\n        {\r\n            Current = bindableEditStep,\r\n        };\r\n\r\n    protected override IReadOnlyList<EditorSection> CreateEditorSections() => bindableEditStep.Value switch\r\n    {\r\n        ReferenceLyricEditStep.Edit => new EditorSection[]\r\n        {\r\n            new ReferenceLyricAutoGenerateSection(),\r\n            new ReferenceLyricSection(),\r\n            new ReferenceLyricConfigSection(),\r\n        },\r\n        ReferenceLyricEditStep.Verify => new EditorSection[]\r\n        {\r\n            new ReferenceLyricIssueSection(),\r\n        },\r\n        _ => throw new ArgumentOutOfRangeException(),\r\n    };\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/Romanisation/Components/LabelledRomanisedTextBox.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Diagnostics;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Audio;\r\nusing osu.Framework.Audio.Sample;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Cursor;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\nusing osuTK;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Romanisation.Components;\r\n\r\npublic partial class LabelledRomanisedTextBox : LabelledObjectFieldTextBox<TimeTag>\r\n{\r\n    protected const float FIRST_SYLLABLE_BUTTON_SIZE = 20f;\r\n\r\n    [Resolved]\r\n    private ILyricTimeTagsChangeHandler timeTagsChangeHandler { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private IEditRomanisationModeState editRomanisationModeState { get; set; } = null!;\r\n\r\n    private readonly IBindable<int> bindableRomanisationVersion = new Bindable<int>();\r\n\r\n    public LabelledRomanisedTextBox(Lyric lyric, TimeTag timeTag)\r\n        : base(timeTag)\r\n    {\r\n        Debug.Assert(lyric.TimeTags.Contains(timeTag));\r\n\r\n        if (InternalChildren[1] is not FillFlowContainer fillFlowContainer)\r\n            throw new ArgumentNullException(nameof(fillFlowContainer));\r\n\r\n        // change padding to place first syllable button.\r\n        fillFlowContainer.Padding = new MarginPadding\r\n        {\r\n            Horizontal = CONTENT_PADDING_HORIZONTAL,\r\n            Vertical = CONTENT_PADDING_VERTICAL,\r\n            Right = CONTENT_PADDING_HORIZONTAL + FIRST_SYLLABLE_BUTTON_SIZE + CONTENT_PADDING_HORIZONTAL,\r\n        };\r\n\r\n        // add first syllable button.\r\n        AddInternal(new Container\r\n        {\r\n            RelativeSizeAxes = Axes.X,\r\n            AutoSizeAxes = Axes.Y,\r\n            Padding = new MarginPadding\r\n            {\r\n                Top = CONTENT_PADDING_VERTICAL + 10,\r\n                Right = CONTENT_PADDING_HORIZONTAL,\r\n            },\r\n            Child = new IconCheckbox(timeTag)\r\n            {\r\n                Anchor = Anchor.TopRight,\r\n                Origin = Anchor.TopRight,\r\n                Size = new Vector2(FIRST_SYLLABLE_BUTTON_SIZE),\r\n            },\r\n        });\r\n\r\n        bindableRomanisationVersion.BindTo(lyric.TimeTagsRomanisationVersion);\r\n        bindableRomanisationVersion.BindValueChanged(_ =>\r\n        {\r\n            // change the label and the description.\r\n            updateLabel(lyric, timeTag);\r\n            updateDescription(lyric, timeTag);\r\n        }, true);\r\n    }\r\n\r\n    private void updateLabel(Lyric lyric, TimeTag timeTag)\r\n    {\r\n        Label = !timeTag.FirstSyllable\r\n            ? \"  |  \"\r\n            : $\"#{getRomanisationIndex(lyric, timeTag) + 1}\";\r\n        return;\r\n\r\n        // get the index that mark as first syllable.\r\n        static int getRomanisationIndex(Lyric lyric, TimeTag timeTag)\r\n            => lyric.TimeTags.TakeWhile(x => x != timeTag).Count(x => x.FirstSyllable);\r\n    }\r\n\r\n    private void updateDescription(Lyric lyric, TimeTag timeTag)\r\n    {\r\n        // get the index and the calculated string.\r\n        string displayIndex = TextIndexUtils.PositionFormattedString(timeTag.Index);\r\n        string mainText = LyricUtils.GetTimeTagIndexDisplayText(lyric, timeTag.Index);\r\n        Description = $\"{displayIndex}, {mainText}\";\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        SelectedItems.BindTo(editRomanisationModeState.SelectedItems);\r\n    }\r\n\r\n    protected sealed override string GetFieldValue(TimeTag item)\r\n        => item.RomanisedSyllable ?? string.Empty;\r\n\r\n    protected override void TriggerSelect(TimeTag item)\r\n        => editRomanisationModeState.Select(item);\r\n\r\n    protected override void ApplyValue(TimeTag item, string value)\r\n        => timeTagsChangeHandler.SetTimeTagRomanisedSyllable(item, value);\r\n\r\n    public partial class IconCheckbox : Checkbox, IHasAccentColour, IHasTooltip\r\n    {\r\n        private readonly SpriteIcon selectedIcon;\r\n\r\n        [Resolved]\r\n        private ILyricTimeTagsChangeHandler timeTagsChangeHandler { get; set; } = null!;\r\n\r\n        private Sample? sampleChecked;\r\n        private Sample? sampleUnchecked;\r\n\r\n        public IconCheckbox(TimeTag timeTag)\r\n        {\r\n            Children = new Drawable[]\r\n            {\r\n                selectedIcon = new SpriteIcon\r\n                {\r\n                    Anchor = Anchor.Centre,\r\n                    Origin = Anchor.Centre,\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    Icon = FontAwesome.Solid.Tag,\r\n                    Scale = new Vector2(0),\r\n                },\r\n                new HoverSounds(),\r\n            };\r\n\r\n            Current.Value = timeTag.FirstSyllable;\r\n\r\n            Current.ValueChanged += e =>\r\n            {\r\n                updateSelected(e.NewValue);\r\n                timeTagsChangeHandler.SetTimeTagFirstSyllable(timeTag, e.NewValue);\r\n            };\r\n\r\n            updateSelected(Current.Value);\r\n        }\r\n\r\n        private void updateSelected(bool selected)\r\n        {\r\n            selectedIcon.ScaleTo(selected ? 1f : 0.8f, 200, Easing.OutElastic);\r\n            selectedIcon.FadeTo(selected ? 1f : 0.2f, 200, Easing.OutElastic);\r\n        }\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(AudioManager audio)\r\n        {\r\n            sampleChecked = audio.Samples.Get(\"UI/check-on\");\r\n            sampleUnchecked = audio.Samples.Get(\"UI/check-off\");\r\n        }\r\n\r\n        private Color4 accentColour;\r\n\r\n        public Color4 AccentColour\r\n        {\r\n            get => accentColour;\r\n            set\r\n            {\r\n                accentColour = value;\r\n                selectedIcon.Colour = AccentColour;\r\n            }\r\n        }\r\n\r\n        protected override void OnUserChange(bool value)\r\n        {\r\n            base.OnUserChange(value);\r\n\r\n            if (value)\r\n                sampleChecked?.Play();\r\n            else\r\n                sampleUnchecked?.Play();\r\n        }\r\n\r\n        public LocalisableString TooltipText => \"Mark as the first romanised syllable\";\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/Romanisation/RomanisationAutoGenerateSection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Components.Markdown;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Markdown;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Romanisation;\r\n\r\npublic partial class RomanisationAutoGenerateSection : AutoGenerateSection\r\n{\r\n    protected override AutoGenerateSubsection CreateAutoGenerateSubsection()\r\n        => new RomanisationAutoGenerateSubsection();\r\n\r\n    private partial class RomanisationAutoGenerateSubsection : LyricEditorAutoGenerateSubsection\r\n    {\r\n        private const string language_mode = \"LANGUAGE_MODE\";\r\n        private const string time_tag_mode = \"TIME_TAG_MODE\";\r\n\r\n        public RomanisationAutoGenerateSubsection()\r\n            : base(AutoGenerateType.AutoGenerateRomanisation)\r\n        {\r\n        }\r\n\r\n        protected override DescriptionFormat CreateInvalidDescriptionFormat()\r\n            => new()\r\n            {\r\n                Text = $\"Seems some lyric missing language or time-tag, go to [{DescriptionFormat.LINK_KEY_ACTION}]({language_mode}) to fill the language, or [{DescriptionFormat.LINK_KEY_ACTION}]({time_tag_mode}) to fill the time-tag.\",\r\n                Actions = new Dictionary<string, IDescriptionAction>\r\n                {\r\n                    {\r\n                        language_mode,\r\n                        new SwitchModeDescriptionAction\r\n                        {\r\n                            Text = \"edit language mode\",\r\n                            Mode = LyricEditorMode.EditLanguage,\r\n                        }\r\n                    },\r\n                    {\r\n                        time_tag_mode,\r\n                        new SwitchModeDescriptionAction\r\n                        {\r\n                            Text = \"edit time-tag mode\",\r\n                            Mode = LyricEditorMode.EditTimeTag,\r\n                        }\r\n                    },\r\n                },\r\n            };\r\n\r\n        protected override ConfigButton CreateConfigButton()\r\n            => new RomanisationAutoGenerateConfigButton();\r\n\r\n        protected partial class RomanisationAutoGenerateConfigButton : MultiConfigButton\r\n        {\r\n            protected override IEnumerable<KaraokeRulesetEditGeneratorSetting> AvailableSettings => new[]\r\n            {\r\n                KaraokeRulesetEditGeneratorSetting.JaRomanisationGeneratorConfig,\r\n            };\r\n\r\n            protected override string GetDisplayName(KaraokeRulesetEditGeneratorSetting setting) =>\r\n                setting switch\r\n                {\r\n                    KaraokeRulesetEditGeneratorSetting.JaRomanisationGeneratorConfig => \"Japanese\",\r\n                    _ => throw new ArgumentOutOfRangeException(nameof(setting)),\r\n                };\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/Romanisation/RomanisationEditSection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Romanisation.Components;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Romanisation;\r\n\r\npublic partial class RomanisationEditSection : LyricPropertiesSection<TimeTag>\r\n{\r\n    protected override LocalisableString Title => \"Romanisation\";\r\n\r\n    protected override LyricPropertiesEditor CreateLyricPropertiesEditor() => new RomanisationTagsEditor();\r\n\r\n    protected override LockLyricPropertyBy? IsWriteLyricPropertyLocked(Lyric lyric)\r\n        => HitObjectWritableUtils.GetLyricPropertyLockedBy(lyric, nameof(Lyric.TimeTags));\r\n\r\n    protected override LocalisableString GetWriteLyricPropertyLockedDescription(LockLyricPropertyBy lockLyricPropertyBy) =>\r\n        lockLyricPropertyBy switch\r\n        {\r\n            LockLyricPropertyBy.ReferenceLyricConfig => \"Romanisation is sync to another romanisation.\",\r\n            LockLyricPropertyBy.LockState => \"Romanisation is locked.\",\r\n            _ => throw new ArgumentOutOfRangeException(nameof(lockLyricPropertyBy), lockLyricPropertyBy, null),\r\n        };\r\n\r\n    protected override LocalisableString GetWriteLyricPropertyLockedTooltip(LockLyricPropertyBy lockLyricPropertyBy) =>\r\n        lockLyricPropertyBy switch\r\n        {\r\n            LockLyricPropertyBy.ReferenceLyricConfig => \"Cannot edit the romanisation because it's sync to another lyric's text.\",\r\n            LockLyricPropertyBy.LockState => \"The lyric is locked, so cannot edit the romanisation.\",\r\n            _ => throw new ArgumentOutOfRangeException(nameof(lockLyricPropertyBy), lockLyricPropertyBy, null),\r\n        };\r\n\r\n    private partial class RomanisationTagsEditor : LyricPropertiesEditor\r\n    {\r\n        protected override Drawable? CreateDrawable(TimeTag item)\r\n        {\r\n            if (!isEditable(item))\r\n            {\r\n                return null;\r\n            }\r\n\r\n            return new LabelledRomanisedTextBox(CurrentLyric, item)\r\n            {\r\n                TabbableContentContainer = this,\r\n            };\r\n\r\n            static bool isEditable(TimeTag timeTag)\r\n                => timeTag.Index.State == TextIndex.IndexState.Start;\r\n        }\r\n\r\n        protected override EditorSectionButton? CreateCreateNewItemButton() => null;\r\n\r\n        protected override IBindableList<TimeTag> GetItems(Lyric lyric)\r\n            => lyric.TimeTagsBindable;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/Romanisation/RomanisationIssueSection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks.Issues;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Issues;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Romanisation;\r\n\r\npublic partial class RomanisationIssueSection : LyricEditorIssueSection\r\n{\r\n    protected override LyricEditorMode EditMode => LyricEditorMode.EditRomanisation;\r\n\r\n    protected override LyricsIssueTable CreateLyricsIssueTable() => new RomanisationIssueTable();\r\n\r\n    private partial class RomanisationIssueTable : LyricsIssueTable\r\n    {\r\n        protected override TableColumn[] CreateHeaders() => new[]\r\n        {\r\n            new TableColumn(string.Empty, Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize, minSize: 30)),\r\n            new TableColumn(\"Lyric\", Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize, minSize: 40)),\r\n            new TableColumn(\"Position\", Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize, minSize: 60)),\r\n            new TableColumn(\"Message\", Anchor.CentreLeft),\r\n        };\r\n\r\n        protected override Drawable[] CreateContent(Issue issue)\r\n        {\r\n            (var lyric, TimeTag timeTag) = getInvalidByIssue(issue);\r\n\r\n            return new Drawable[]\r\n            {\r\n                new IssueIcon\r\n                {\r\n                    Origin = Anchor.Centre,\r\n                    Size = new Vector2(10),\r\n                    Margin = new MarginPadding { Left = 10 },\r\n                    Issue = issue,\r\n                },\r\n                new OsuSpriteText\r\n                {\r\n                    Text = $\"#{lyric.Order}\",\r\n                    Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold),\r\n                    Margin = new MarginPadding { Right = 10 },\r\n                },\r\n                new OsuSpriteText\r\n                {\r\n                    Text = TimeTagUtils.FormattedString(timeTag),\r\n                    Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold),\r\n                    Margin = new MarginPadding { Right = 10 },\r\n                },\r\n                new TruncatingSpriteText\r\n                {\r\n                    Text = issue.ToString(),\r\n                    RelativeSizeAxes = Axes.X,\r\n                    Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Medium),\r\n                },\r\n            };\r\n        }\r\n\r\n        private Tuple<Lyric, TimeTag> getInvalidByIssue(Issue issue)\r\n        {\r\n            if (issue is not LyricTimeTagIssue timeTagIssue)\r\n                throw new InvalidCastException();\r\n\r\n            return new Tuple<Lyric, TimeTag>(timeTagIssue.Lyric, timeTagIssue.TimeTag);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/Romanisation/RomanisationSettingsHeader.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Markdown;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Romanisation;\r\n\r\npublic partial class RomanisationSettingsHeader : LyricEditorSettingsHeader<RomanisationTagEditStep>\r\n{\r\n    protected override OverlayColourScheme CreateColourScheme()\r\n        => OverlayColourScheme.Orange;\r\n\r\n    protected override EditStepTabControl CreateTabControl()\r\n        => new RomanisationEditStepTabControl();\r\n\r\n    protected override DescriptionFormat GetSelectionDescription(RomanisationTagEditStep step) =>\r\n        step switch\r\n        {\r\n            RomanisationTagEditStep.Generate => \"Auto-generate romanisation in the lyric.\",\r\n            RomanisationTagEditStep.Edit => new DescriptionFormat\r\n            {\r\n                Text = \"Create / delete and edit lyric rubies in here.\\n\"\r\n                       + $\"Click [{DescriptionFormat.LINK_KEY_ACTION}](directions) to select the target lyric.\\n\"\r\n                       + \"Press `Tab` to switch between the romanised syllable tags.\\n\"\r\n                       + $\"Than, press [{DescriptionFormat.LINK_KEY_ACTION}](adjust_text_tag_index) or button to adjust romanised syllable index after hover to edit index area.\",\r\n                Actions = new Dictionary<string, IDescriptionAction>\r\n                {\r\n                    {\r\n                        \"directions\", new InputKeyDescriptionAction\r\n                        {\r\n                            Text = \"Up or down\",\r\n                            AdjustableActions = new List<KaraokeEditAction>\r\n                            {\r\n                                KaraokeEditAction.MoveToPreviousLyric,\r\n                                KaraokeEditAction.MoveToNextLyric,\r\n                            },\r\n                        }\r\n                    },\r\n                    {\r\n                        \"adjust_text_tag_index\", new InputKeyDescriptionAction\r\n                        {\r\n                            Text = \"Keys\",\r\n                            AdjustableActions = new List<KaraokeEditAction>\r\n                            {\r\n                                KaraokeEditAction.EditRubyTagReduceStartIndex,\r\n                                KaraokeEditAction.EditRubyTagIncreaseStartIndex,\r\n                                KaraokeEditAction.EditRubyTagReduceEndIndex,\r\n                                KaraokeEditAction.EditRubyTagIncreaseEndIndex,\r\n                            },\r\n                        }\r\n                    },\r\n                },\r\n            },\r\n            RomanisationTagEditStep.Verify => \"Check invalid romanisation in here.\",\r\n            _ => throw new ArgumentOutOfRangeException(nameof(step), step, null),\r\n        };\r\n\r\n    private partial class RomanisationEditStepTabControl : EditStepTabControl\r\n    {\r\n        protected override StepTabButton CreateStepButton(OsuColour colours, RomanisationTagEditStep value)\r\n        {\r\n            return value switch\r\n            {\r\n                RomanisationTagEditStep.Generate => new StepTabButton(value)\r\n                {\r\n                    Text = \"Generate\",\r\n                    SelectedColour = colours.Blue,\r\n                    UnSelectedColour = colours.BlueDarker,\r\n                },\r\n                RomanisationTagEditStep.Edit => new StepTabButton(value)\r\n                {\r\n                    Text = \"Edit\",\r\n                    SelectedColour = colours.Red,\r\n                    UnSelectedColour = colours.RedDarker,\r\n                },\r\n                RomanisationTagEditStep.Verify => new VerifyStepTabButton(value)\r\n                {\r\n                    Text = \"Verify\",\r\n                    EditMode = LyricEditorMode.EditRomanisation,\r\n                    SelectedColour = colours.Yellow,\r\n                    UnSelectedColour = colours.YellowDarker,\r\n                },\r\n                _ => throw new ArgumentOutOfRangeException(nameof(value), value, null),\r\n            };\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/RomanisationSettings.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Romanisation;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings;\r\n\r\npublic partial class RomanisationSettings : LyricEditorSettings\r\n{\r\n    public override SettingsDirection Direction => SettingsDirection.Right;\r\n\r\n    public override float SettingsWidth => 350;\r\n\r\n    private readonly Bindable<RomanisationTagEditStep> bindableEditStep = new();\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(IEditRomanisationModeState romanisationModeState)\r\n    {\r\n        bindableEditStep.BindTo(romanisationModeState.BindableEditStep);\r\n    }\r\n\r\n    protected override EditorSettingsHeader CreateSettingHeader()\r\n        => new RomanisationSettingsHeader\r\n        {\r\n            Current = bindableEditStep,\r\n        };\r\n\r\n    protected override IReadOnlyList<EditorSection> CreateEditorSections() => bindableEditStep.Value switch\r\n    {\r\n        RomanisationTagEditStep.Generate => new[]\r\n        {\r\n            new RomanisationAutoGenerateSection(),\r\n        },\r\n        RomanisationTagEditStep.Edit => new[]\r\n        {\r\n            new RomanisationEditSection(),\r\n        },\r\n        RomanisationTagEditStep.Verify => new[]\r\n        {\r\n            new RomanisationIssueSection(),\r\n        },\r\n        _ => throw new ArgumentOutOfRangeException(),\r\n    };\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/Ruby/Components/LabelledRubyTagTextBox.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Diagnostics;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Input.Bindings;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.UserInterface;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Ruby.Components;\r\n\r\npublic partial class LabelledRubyTagTextBox : LabelledObjectFieldTextBox<RubyTag>\r\n{\r\n    protected const float DELETE_BUTTON_SIZE = 20f;\r\n\r\n    [Resolved]\r\n    private ILyricRubyTagsChangeHandler rubyTagsChangeHandler { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private IEditRubyModeState editRubyModeState { get; set; } = null!;\r\n\r\n    private readonly IndexShiftingPart indexShiftingPart;\r\n\r\n    public LabelledRubyTagTextBox(Lyric lyric, RubyTag rubyTag)\r\n        : base(rubyTag)\r\n    {\r\n        Debug.Assert(lyric.RubyTags.Contains(rubyTag));\r\n\r\n        if (InternalChildren[1] is not FillFlowContainer fillFlowContainer)\r\n            throw new ArgumentNullException(nameof(fillFlowContainer));\r\n\r\n        // change padding to place delete button.\r\n        fillFlowContainer.Padding = new MarginPadding\r\n        {\r\n            Horizontal = CONTENT_PADDING_HORIZONTAL,\r\n            Vertical = CONTENT_PADDING_VERTICAL,\r\n            Right = CONTENT_PADDING_HORIZONTAL + DELETE_BUTTON_SIZE + CONTENT_PADDING_HORIZONTAL,\r\n        };\r\n\r\n        // add delete button.\r\n        AddInternal(new Container\r\n        {\r\n            RelativeSizeAxes = Axes.X,\r\n            AutoSizeAxes = Axes.Y,\r\n            Padding = new MarginPadding\r\n            {\r\n                Top = CONTENT_PADDING_VERTICAL + 10,\r\n                Right = CONTENT_PADDING_HORIZONTAL,\r\n            },\r\n            Child = new DeleteIconButton\r\n            {\r\n                Anchor = Anchor.TopRight,\r\n                Origin = Anchor.TopRight,\r\n                Size = new Vector2(DELETE_BUTTON_SIZE),\r\n                Action = () => removeRubyTag(rubyTag),\r\n                Hover = hover =>\r\n                {\r\n                    if (hover)\r\n                    {\r\n                        // trigger selected if hover on delete button.\r\n                        TriggerSelect(rubyTag);\r\n                    }\r\n                },\r\n            },\r\n        });\r\n\r\n        // add the index shifting component at the bottom of the text box.\r\n        AddInternal(new Container\r\n        {\r\n            RelativeSizeAxes = Axes.X,\r\n            AutoSizeAxes = Axes.Y,\r\n            Padding = new MarginPadding\r\n            {\r\n                Top = CONTENT_PADDING_VERTICAL + 45,\r\n                Right = CONTENT_PADDING_HORIZONTAL + DELETE_BUTTON_SIZE + CONTENT_PADDING_HORIZONTAL,\r\n            },\r\n            Child = indexShiftingPart = new IndexShiftingPart\r\n            {\r\n                Anchor = Anchor.TopRight,\r\n                Origin = Anchor.TopRight,\r\n                Width = 120,\r\n                Selected = selected =>\r\n                {\r\n                    if (selected)\r\n                        TriggerSelect(rubyTag);\r\n                },\r\n                Action = (indexType, action) =>\r\n                {\r\n                    int index = getNewIndex(rubyTag, indexType);\r\n                    int newIndex = calculateNewIndex(index, action);\r\n                    if (RubyTagUtils.OutOfRange(lyric.Text, newIndex))\r\n                        return;\r\n\r\n                    switch (indexType)\r\n                    {\r\n                        case AdjustIndex.Start:\r\n                            if (RubyTagUtils.ValidNewStartIndex(rubyTag, newIndex))\r\n                                setIndex(rubyTag, newIndex, null);\r\n                            break;\r\n\r\n                        case AdjustIndex.End:\r\n                            if (RubyTagUtils.ValidNewEndIndex(rubyTag, newIndex))\r\n                                setIndex(rubyTag, null, newIndex);\r\n                            break;\r\n\r\n                        default:\r\n                            throw new InvalidOperationException();\r\n                    }\r\n\r\n                    static int getNewIndex(RubyTag rubyTag, AdjustIndex index) =>\r\n                        index switch\r\n                        {\r\n                            AdjustIndex.Start => rubyTag.StartIndex,\r\n                            AdjustIndex.End => rubyTag.EndIndex,\r\n                            _ => throw new InvalidOperationException(),\r\n                        };\r\n\r\n                    static int calculateNewIndex(int index, AdjustAction action) =>\r\n                        action switch\r\n                        {\r\n                            AdjustAction.Decrease => index - 1,\r\n                            AdjustAction.Increase => index + 1,\r\n                            _ => throw new InvalidOperationException(),\r\n                        };\r\n                },\r\n            },\r\n        });\r\n    }\r\n\r\n    private void setIndex(RubyTag item, int? startIndex, int? endIndex)\r\n        => rubyTagsChangeHandler.SetIndex(item, startIndex, endIndex);\r\n\r\n    private void removeRubyTag(RubyTag rubyTag)\r\n        => rubyTagsChangeHandler.Remove(rubyTag);\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        SelectedItems.BindTo(editRubyModeState.SelectedItems);\r\n    }\r\n\r\n    protected sealed override string GetFieldValue(RubyTag item)\r\n        => item.Text;\r\n\r\n    protected override void TriggerSelect(RubyTag item)\r\n        => editRubyModeState.Select(item);\r\n\r\n    protected override void ApplyValue(RubyTag item, string value)\r\n        => rubyTagsChangeHandler.SetText(item, value);\r\n\r\n    protected override bool IsFocused(Drawable focusedDrawable)\r\n        => base.IsFocused(focusedDrawable) || focusedDrawable == indexShiftingPart;\r\n\r\n    public new CompositeDrawable TabbableContentContainer\r\n    {\r\n        set\r\n        {\r\n            base.TabbableContentContainer = value;\r\n            indexShiftingPart.TabbableContentContainer = value;\r\n        }\r\n    }\r\n\r\n    private partial class IndexShiftingPart : TabbableContainer, IKeyBindingHandler<KaraokeEditAction>\r\n    {\r\n        private const int button_size = 20;\r\n        private const int button_spacing = 5;\r\n\r\n        public override bool AcceptsFocus => true;\r\n\r\n        public Action<AdjustIndex, AdjustAction>? Action;\r\n\r\n        private readonly Box background;\r\n        private readonly IconButton reduceStartIndexButton;\r\n        private readonly IconButton increaseStartIndexButton;\r\n        private readonly IconButton reduceEndIndexButton;\r\n        private readonly IconButton increaseEndIndexButton;\r\n\r\n        public Action<bool>? Selected;\r\n\r\n        public IndexShiftingPart()\r\n        {\r\n            AutoSizeAxes = Axes.Y;\r\n            Masking = true;\r\n            CornerRadius = 5;\r\n\r\n            Children = new Drawable[]\r\n            {\r\n                background = new Box\r\n                {\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    Alpha = 0,\r\n                },\r\n                new Container\r\n                {\r\n                    RelativeSizeAxes = Axes.X,\r\n                    AutoSizeAxes = Axes.Y,\r\n                    Padding = new MarginPadding(5),\r\n                    Children = new[]\r\n                    {\r\n                        reduceStartIndexButton = new IconButton\r\n                        {\r\n                            Size = new Vector2(button_size),\r\n                            Anchor = Anchor.CentreLeft,\r\n                            Origin = Anchor.CentreLeft,\r\n                            Icon = FontAwesome.Regular.CaretSquareLeft,\r\n                            Action = () => Action?.Invoke(AdjustIndex.Start, AdjustAction.Decrease),\r\n                        },\r\n                        increaseStartIndexButton = new IconButton\r\n                        {\r\n                            Size = new Vector2(button_size),\r\n                            Anchor = Anchor.CentreLeft,\r\n                            Origin = Anchor.CentreLeft,\r\n                            X = button_size + button_spacing,\r\n                            Icon = FontAwesome.Regular.CaretSquareRight,\r\n                            Action = () => Action?.Invoke(AdjustIndex.Start, AdjustAction.Increase),\r\n                        },\r\n                        reduceEndIndexButton = new IconButton\r\n                        {\r\n                            Size = new Vector2(button_size),\r\n                            Anchor = Anchor.CentreRight,\r\n                            Origin = Anchor.CentreRight,\r\n                            X = -button_size - button_spacing,\r\n                            Icon = FontAwesome.Regular.CaretSquareLeft,\r\n                            Action = () => Action?.Invoke(AdjustIndex.End, AdjustAction.Decrease),\r\n                        },\r\n                        increaseEndIndexButton = new IconButton\r\n                        {\r\n                            Size = new Vector2(button_size),\r\n                            Anchor = Anchor.CentreRight,\r\n                            Origin = Anchor.CentreRight,\r\n                            Icon = FontAwesome.Regular.CaretSquareRight,\r\n                            Action = () => Action?.Invoke(AdjustIndex.End, AdjustAction.Increase),\r\n                        },\r\n                    },\r\n                },\r\n            };\r\n        }\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(OsuColour colours)\r\n        {\r\n            background.Colour = colours.Yellow;\r\n        }\r\n\r\n        protected override void OnFocus(FocusEvent e)\r\n        {\r\n            background.FadeTo(0.6f, 100);\r\n            Selected?.Invoke(true);\r\n            base.OnFocus(e);\r\n        }\r\n\r\n        protected override void OnFocusLost(FocusLostEvent e)\r\n        {\r\n            background.FadeOut(100);\r\n            Selected?.Invoke(false);\r\n            base.OnFocusLost(e);\r\n        }\r\n\r\n        public bool OnPressed(KeyBindingPressEvent<KaraokeEditAction> e)\r\n        {\r\n            if (!HasFocus)\r\n                return false;\r\n\r\n            return e.Action switch\r\n            {\r\n                KaraokeEditAction.EditRubyTagReduceStartIndex => reduceStartIndexButton.TriggerClick(),\r\n                KaraokeEditAction.EditRubyTagIncreaseStartIndex => increaseStartIndexButton.TriggerClick(),\r\n                KaraokeEditAction.EditRubyTagReduceEndIndex => reduceEndIndexButton.TriggerClick(),\r\n                KaraokeEditAction.EditRubyTagIncreaseEndIndex => increaseEndIndexButton.TriggerClick(),\r\n                _ => false,\r\n            };\r\n        }\r\n\r\n        public void OnReleased(KeyBindingReleaseEvent<KaraokeEditAction> e)\r\n        {\r\n        }\r\n    }\r\n\r\n    private enum AdjustIndex\r\n    {\r\n        Start,\r\n\r\n        End,\r\n    }\r\n\r\n    private enum AdjustAction\r\n    {\r\n        Decrease,\r\n\r\n        Increase,\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/Ruby/RubyTagAutoGenerateSection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Components.Markdown;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Markdown;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Ruby;\r\n\r\npublic partial class RubyTagAutoGenerateSection : AutoGenerateSection\r\n{\r\n    protected override AutoGenerateSubsection CreateAutoGenerateSubsection()\r\n        => new RubyTagAutoGenerateSubsection();\r\n\r\n    private partial class RubyTagAutoGenerateSubsection : LyricEditorAutoGenerateSubsection\r\n    {\r\n        private const string language_mode = \"LANGUAGE_MODE\";\r\n\r\n        public RubyTagAutoGenerateSubsection()\r\n            : base(AutoGenerateType.AutoGenerateRubyTags)\r\n        {\r\n        }\r\n\r\n        protected override DescriptionFormat CreateInvalidDescriptionFormat()\r\n            => new()\r\n            {\r\n                Text = $\"Seems some lyric missing language, go to [{DescriptionFormat.LINK_KEY_ACTION}]({language_mode}) to fill the language.\",\r\n                Actions = new Dictionary<string, IDescriptionAction>\r\n                {\r\n                    {\r\n                        language_mode, new SwitchModeDescriptionAction\r\n                        {\r\n                            Text = \"edit language mode\",\r\n                            Mode = LyricEditorMode.EditLanguage,\r\n                        }\r\n                    },\r\n                },\r\n            };\r\n\r\n        protected override ConfigButton CreateConfigButton()\r\n            => new RubyTagAutoGenerateConfigButton();\r\n\r\n        protected partial class RubyTagAutoGenerateConfigButton : MultiConfigButton\r\n        {\r\n            protected override IEnumerable<KaraokeRulesetEditGeneratorSetting> AvailableSettings => new[]\r\n            {\r\n                KaraokeRulesetEditGeneratorSetting.JaRubyTagGeneratorConfig,\r\n            };\r\n\r\n            protected override string GetDisplayName(KaraokeRulesetEditGeneratorSetting setting) =>\r\n                setting switch\r\n                {\r\n                    KaraokeRulesetEditGeneratorSetting.JaRubyTagGeneratorConfig => \"Japanese\",\r\n                    _ => throw new ArgumentOutOfRangeException(nameof(setting)),\r\n                };\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/Ruby/RubyTagConfigToolSection.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Ruby;\r\n\r\npublic partial class RubyTagConfigToolSection : EditorSection\r\n{\r\n    protected override LocalisableString Title => \"Config Tool\";\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(IEditRubyModeState editRubyModeState)\r\n    {\r\n        Children = new Drawable[]\r\n        {\r\n            new RubyTagEditModeSubsection\r\n            {\r\n                Current = editRubyModeState.BindableRubyTagEditMode,\r\n            },\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/Ruby/RubyTagEditModeSubsection.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Markdown;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Ruby;\r\n\r\npublic partial class RubyTagEditModeSubsection : SwitchSubsection<RubyTagEditMode>\r\n{\r\n    protected override SwitchTabControl CreateTabControl()\r\n        => new RubyTagTabControl();\r\n\r\n    protected override DescriptionFormat GetDescription(RubyTagEditMode mode) =>\r\n        mode switch\r\n        {\r\n            RubyTagEditMode.Create => \"Use mouse to select range of the lyric text to create the ruby tag.\",\r\n            RubyTagEditMode.Modify => \"Select ruby to change the start/end position or delete it.\",\r\n            _ => throw new InvalidOperationException(nameof(mode)),\r\n        };\r\n\r\n    private partial class RubyTagTabControl : SwitchTabControl\r\n    {\r\n        protected override SwitchTabItem CreateStepButton(OsuColour colours, RubyTagEditMode value)\r\n        {\r\n            return value switch\r\n            {\r\n                RubyTagEditMode.Create => new RubyTagTabButton(value)\r\n                {\r\n                    Text = \"Create\",\r\n                    SelectedColour = colours.Green,\r\n                    UnSelectedColour = colours.GreenDarker,\r\n                },\r\n                RubyTagEditMode.Modify => new RubyTagTabButton(value)\r\n                {\r\n                    Text = \"Modify\",\r\n                    SelectedColour = colours.Pink,\r\n                    UnSelectedColour = colours.PinkDarker,\r\n                },\r\n                _ => throw new ArgumentOutOfRangeException(nameof(value), value, null),\r\n            };\r\n        }\r\n\r\n        private partial class RubyTagTabButton : SwitchTabItem\r\n        {\r\n            private readonly Box background;\r\n            private readonly OsuSpriteText text;\r\n\r\n            public RubyTagTabButton(RubyTagEditMode value)\r\n                : base(value)\r\n            {\r\n                Child = new Container\r\n                {\r\n                    Masking = true,\r\n                    CornerRadius = 15,\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    Children = new Drawable[]\r\n                    {\r\n                        background = new Box\r\n                        {\r\n                            RelativeSizeAxes = Axes.Both,\r\n                        },\r\n                        text = new OsuSpriteText\r\n                        {\r\n                            Anchor = Anchor.Centre,\r\n                            Origin = Anchor.Centre,\r\n                            Font = OsuFont.GetFont(size: 16, weight: FontWeight.Bold),\r\n                        },\r\n                    },\r\n                };\r\n            }\r\n\r\n            public LocalisableString Text\r\n            {\r\n                get => text.Text;\r\n                set => text.Text = value;\r\n            }\r\n\r\n            public Color4 SelectedColour { get; init; }\r\n\r\n            public Color4 UnSelectedColour { get; init; }\r\n\r\n            protected override void UpdateState()\r\n            {\r\n                background.Colour = Active.Value ? SelectedColour : UnSelectedColour;\r\n                Child.Alpha = Active.Value ? 0.8f : 0.4f;\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/Ruby/RubyTagEditSection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Ruby.Components;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Ruby;\r\n\r\npublic partial class RubyTagEditSection : LyricPropertiesSection<RubyTag>\r\n{\r\n    protected override LocalisableString Title => \"Ruby\";\r\n\r\n    protected override LyricPropertiesEditor CreateLyricPropertiesEditor() => new RubyTagsEditor();\r\n\r\n    protected override LockLyricPropertyBy? IsWriteLyricPropertyLocked(Lyric lyric)\r\n        => HitObjectWritableUtils.GetLyricPropertyLockedBy(lyric, nameof(Lyric.RubyTags));\r\n\r\n    protected override LocalisableString GetWriteLyricPropertyLockedDescription(LockLyricPropertyBy lockLyricPropertyBy) =>\r\n        lockLyricPropertyBy switch\r\n        {\r\n            LockLyricPropertyBy.ReferenceLyricConfig => \"Ruby is sync to another ruby.\",\r\n            LockLyricPropertyBy.LockState => \"Ruby is locked.\",\r\n            _ => throw new ArgumentOutOfRangeException(nameof(lockLyricPropertyBy), lockLyricPropertyBy, null),\r\n        };\r\n\r\n    protected override LocalisableString GetWriteLyricPropertyLockedTooltip(LockLyricPropertyBy lockLyricPropertyBy) =>\r\n        lockLyricPropertyBy switch\r\n        {\r\n            LockLyricPropertyBy.ReferenceLyricConfig => \"Cannot edit the ruby because it's sync to another lyric's text.\",\r\n            LockLyricPropertyBy.LockState => \"The lyric is locked, so cannot edit the ruby.\",\r\n            _ => throw new ArgumentOutOfRangeException(nameof(lockLyricPropertyBy), lockLyricPropertyBy, null),\r\n        };\r\n\r\n    private partial class RubyTagsEditor : LyricPropertiesEditor\r\n    {\r\n        protected sealed override Drawable CreateDrawable(RubyTag item)\r\n        {\r\n            string relativeToLyricText = RubyTagUtils.GetTextFromLyric(item, CurrentLyric.Text);\r\n            string range = RubyTagUtils.PositionFormattedString(item);\r\n\r\n            return new LabelledRubyTagTextBox(CurrentLyric, item)\r\n            {\r\n                Label = relativeToLyricText,\r\n                Description = range,\r\n                TabbableContentContainer = this,\r\n            };\r\n        }\r\n\r\n        protected override EditorSectionButton? CreateCreateNewItemButton() => null;\r\n\r\n        protected override IBindableList<RubyTag> GetItems(Lyric lyric)\r\n            => lyric.RubyTagsBindable;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/Ruby/RubyTagIssueSection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks.Issues;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Issues;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Ruby;\r\n\r\npublic partial class RubyTagIssueSection : LyricEditorIssueSection\r\n{\r\n    protected override LyricEditorMode EditMode => LyricEditorMode.EditRuby;\r\n\r\n    protected override LyricsIssueTable CreateLyricsIssueTable() => new RubyTagIssueTable();\r\n\r\n    private partial class RubyTagIssueTable : LyricsIssueTable\r\n    {\r\n        protected override TableColumn[] CreateHeaders() => new[]\r\n        {\r\n            new TableColumn(string.Empty, Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize, minSize: 30)),\r\n            new TableColumn(\"Lyric\", Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize, minSize: 40)),\r\n            new TableColumn(\"Position\", Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize, minSize: 60)),\r\n            new TableColumn(\"Message\", Anchor.CentreLeft),\r\n        };\r\n\r\n        protected override Drawable[] CreateContent(Issue issue)\r\n        {\r\n            (var lyric, RubyTag rubyTag) = getInvalidByIssue(issue);\r\n\r\n            return new Drawable[]\r\n            {\r\n                new IssueIcon\r\n                {\r\n                    Origin = Anchor.Centre,\r\n                    Size = new Vector2(10),\r\n                    Margin = new MarginPadding { Left = 10 },\r\n                    Issue = issue,\r\n                },\r\n                new OsuSpriteText\r\n                {\r\n                    Text = $\"#{lyric.Order}\",\r\n                    Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold),\r\n                    Margin = new MarginPadding { Right = 10 },\r\n                },\r\n                new OsuSpriteText\r\n                {\r\n                    Text = RubyTagUtils.PositionFormattedString(rubyTag),\r\n                    Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold),\r\n                    Margin = new MarginPadding { Right = 10 },\r\n                },\r\n                new TruncatingSpriteText\r\n                {\r\n                    Text = issue.ToString(),\r\n                    RelativeSizeAxes = Axes.X,\r\n                    Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Medium),\r\n                },\r\n            };\r\n        }\r\n\r\n        private Tuple<Lyric, RubyTag> getInvalidByIssue(Issue issue)\r\n        {\r\n            if (issue is not LyricRubyTagIssue rubyTagIssue)\r\n                throw new InvalidCastException();\r\n\r\n            return new Tuple<Lyric, RubyTag>(rubyTagIssue.Lyric, rubyTagIssue.RubyTag);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/Ruby/RubyTagSettingsHeader.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Markdown;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Ruby;\r\n\r\npublic partial class RubyTagSettingsHeader : LyricEditorSettingsHeader<RubyTagEditStep>\r\n{\r\n    protected override OverlayColourScheme CreateColourScheme()\r\n        => OverlayColourScheme.Pink;\r\n\r\n    protected override EditStepTabControl CreateTabControl()\r\n        => new RubyTagEditStepTabControl();\r\n\r\n    protected override DescriptionFormat GetSelectionDescription(RubyTagEditStep step) =>\r\n        step switch\r\n        {\r\n            RubyTagEditStep.Generate => \"Auto-generate rubies in the lyric.\",\r\n            RubyTagEditStep.Edit => new DescriptionFormat\r\n            {\r\n                Text = \"Create / delete and edit lyric rubies in here.\\n\"\r\n                       + $\"Click [{DescriptionFormat.LINK_KEY_ACTION}](directions) to select the target lyric.\\n\"\r\n                       + \"Press `Tab` to switch between the ruby tags.\\n\"\r\n                       + $\"Than, press [{DescriptionFormat.LINK_KEY_ACTION}](adjust_text_tag_index) or button to adjust ruby index after hover to edit index area.\",\r\n                Actions = new Dictionary<string, IDescriptionAction>\r\n                {\r\n                    {\r\n                        \"directions\", new InputKeyDescriptionAction\r\n                        {\r\n                            Text = \"Up or down\",\r\n                            AdjustableActions = new List<KaraokeEditAction>\r\n                            {\r\n                                KaraokeEditAction.MoveToPreviousLyric,\r\n                                KaraokeEditAction.MoveToNextLyric,\r\n                            },\r\n                        }\r\n                    },\r\n                    {\r\n                        \"adjust_text_tag_index\", new InputKeyDescriptionAction\r\n                        {\r\n                            Text = \"Keys\",\r\n                            AdjustableActions = new List<KaraokeEditAction>\r\n                            {\r\n                                KaraokeEditAction.EditRubyTagReduceStartIndex,\r\n                                KaraokeEditAction.EditRubyTagIncreaseStartIndex,\r\n                                KaraokeEditAction.EditRubyTagReduceEndIndex,\r\n                                KaraokeEditAction.EditRubyTagIncreaseEndIndex,\r\n                            },\r\n                        }\r\n                    },\r\n                },\r\n            },\r\n            RubyTagEditStep.Verify => \"Check invalid rubies in here\",\r\n            _ => throw new ArgumentOutOfRangeException(nameof(step), step, null),\r\n        };\r\n\r\n    private partial class RubyTagEditStepTabControl : EditStepTabControl\r\n    {\r\n        protected override StepTabButton CreateStepButton(OsuColour colours, RubyTagEditStep value)\r\n        {\r\n            return value switch\r\n            {\r\n                RubyTagEditStep.Generate => new StepTabButton(value)\r\n                {\r\n                    Text = \"Generate\",\r\n                    SelectedColour = colours.Blue,\r\n                    UnSelectedColour = colours.BlueDarker,\r\n                },\r\n                RubyTagEditStep.Edit => new StepTabButton(value)\r\n                {\r\n                    Text = \"Edit\",\r\n                    SelectedColour = colours.Red,\r\n                    UnSelectedColour = colours.RedDarker,\r\n                },\r\n                RubyTagEditStep.Verify => new VerifyStepTabButton(value)\r\n                {\r\n                    Text = \"Verify\",\r\n                    EditMode = LyricEditorMode.EditRuby,\r\n                    SelectedColour = colours.Yellow,\r\n                    UnSelectedColour = colours.YellowDarker,\r\n                },\r\n                _ => throw new ArgumentOutOfRangeException(nameof(value), value, null),\r\n            };\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/RubyTagSettings.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Ruby;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings;\r\n\r\npublic partial class RubyTagSettings : LyricEditorSettings\r\n{\r\n    public override SettingsDirection Direction => SettingsDirection.Right;\r\n\r\n    public override float SettingsWidth => 350;\r\n\r\n    private readonly Bindable<RubyTagEditStep> bindableEditStep = new();\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(IEditRubyModeState editRubyModeState)\r\n    {\r\n        bindableEditStep.BindTo(editRubyModeState.BindableEditStep);\r\n    }\r\n\r\n    protected override EditorSettingsHeader CreateSettingHeader()\r\n        => new RubyTagSettingsHeader\r\n        {\r\n            Current = bindableEditStep,\r\n        };\r\n\r\n    protected override IReadOnlyList<EditorSection> CreateEditorSections() => bindableEditStep.Value switch\r\n    {\r\n        RubyTagEditStep.Generate => new EditorSection[]\r\n        {\r\n            new RubyTagConfigToolSection(),\r\n            new RubyTagAutoGenerateSection(),\r\n        },\r\n        RubyTagEditStep.Edit => new EditorSection[]\r\n        {\r\n            new RubyTagConfigToolSection(),\r\n            new RubyTagEditSection(),\r\n        },\r\n        RubyTagEditStep.Verify => new EditorSection[]\r\n        {\r\n            new RubyTagConfigToolSection(),\r\n            new RubyTagIssueSection(),\r\n        },\r\n        _ => throw new ArgumentOutOfRangeException(),\r\n    };\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/SelectLyricButton.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings;\r\n\r\npublic abstract partial class SelectLyricButton : EditorSectionButton\r\n{\r\n    private IBindable<bool> selecting = null!;\r\n\r\n    protected abstract LocalisableString StandardText { get; }\r\n\r\n    protected abstract LocalisableString SelectingText { get; }\r\n\r\n    protected virtual IDictionary<Lyric, LocalisableString> GetDisableSelectingLyrics()\r\n    {\r\n        return new Dictionary<Lyric, LocalisableString>();\r\n    }\r\n\r\n    protected abstract void Apply();\r\n\r\n    protected virtual void Cancel() { }\r\n\r\n    [Resolved]\r\n    private ILyricSelectionState lyricSelectionState { get; set; } = null!;\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OsuColour colours)\r\n    {\r\n        selecting = lyricSelectionState.Selecting.GetBoundCopy();\r\n        selecting.BindValueChanged(e =>\r\n        {\r\n            bool isSelecting = e.NewValue;\r\n            BackgroundColour = isSelecting ? colours.Blue : colours.Purple;\r\n            Text = isSelecting ? SelectingText : StandardText;\r\n        }, true);\r\n\r\n        Action = () =>\r\n        {\r\n            if (!selecting.Value)\r\n            {\r\n                StartSelectingLyrics();\r\n            }\r\n            else\r\n            {\r\n                EndSelectingLyrics();\r\n            }\r\n        };\r\n\r\n        lyricSelectionState.Action = e =>\r\n        {\r\n            switch (e)\r\n            {\r\n                case LyricEditorSelectingAction.Apply:\r\n                    Apply();\r\n                    return;\r\n\r\n                case LyricEditorSelectingAction.Cancel:\r\n                    Cancel();\r\n                    return;\r\n\r\n                default:\r\n                    throw new InvalidOperationException();\r\n            }\r\n        };\r\n    }\r\n\r\n    protected virtual void StartSelectingLyrics()\r\n    {\r\n        // update disabled lyrics list.\r\n        var disableLyrics = GetDisableSelectingLyrics();\r\n        lyricSelectionState.UpdateDisableLyricList(disableLyrics);\r\n\r\n        // then start selecting.\r\n        lyricSelectionState.StartSelecting();\r\n    }\r\n\r\n    protected virtual void EndSelectingLyrics()\r\n    {\r\n        lyricSelectionState.EndSelecting(LyricEditorSelectingAction.Cancel);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/SettingsDirection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings;\r\n\r\npublic enum SettingsDirection\r\n{\r\n    /// <summary>\r\n    /// At the right side of the main lyric editor.\r\n    /// </summary>\r\n    Left,\r\n\r\n    /// <summary>\r\n    /// At the right side of the main lyric editor.\r\n    /// </summary>\r\n    Right,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/SingerSettings.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Singers;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings;\r\n\r\npublic partial class SingerSettings : LyricEditorSettings\r\n{\r\n    public override SettingsDirection Direction => SettingsDirection.Left;\r\n\r\n    public override float SettingsWidth => 300;\r\n\r\n    protected override IReadOnlyList<EditorSection> CreateEditorSections() => new[]\r\n    {\r\n        new SingerEditSection(),\r\n    };\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/Singers/SingerEditSection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Cursor;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Graphics.UserInterfaceV2;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas.Types;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.Cursor;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.Drawables;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Singers;\r\n\r\npublic partial class SingerEditSection : LyricPropertySection\r\n{\r\n    private readonly IBindableList<Singer> bindableSingers = new BindableList<Singer>();\r\n    private readonly IBindableList<ElementId> singerIndexes = new BindableList<ElementId>();\r\n    protected override LocalisableString Title => \"Singer\";\r\n\r\n    [Resolved]\r\n    private ILyricSingerChangeHandler lyricSingerChangeHandler { get; set; } = null!;\r\n\r\n    public SingerEditSection()\r\n    {\r\n        // update singer list.\r\n        bindableSingers.BindCollectionChanged((_, _) =>\r\n        {\r\n            initialSingerList();\r\n        });\r\n\r\n        // update singer toggle state from lyric.\r\n        singerIndexes.BindCollectionChanged((_, _) =>\r\n        {\r\n            initialSingerList();\r\n        });\r\n    }\r\n\r\n    private void initialSingerList()\r\n    {\r\n        Content.Clear();\r\n        Content.AddRange(bindableSingers.Select(x =>\r\n        {\r\n            var switchButton = new LabelledSingerSwitchButton(x);\r\n            bool selected = singerIndexes.Contains(x.ID);\r\n\r\n            switchButton.Current.Value = selected;\r\n            switchButton.Current.BindValueChanged(e =>\r\n            {\r\n                if (e.NewValue)\r\n                {\r\n                    lyricSingerChangeHandler.Add(x);\r\n                }\r\n                else\r\n                {\r\n                    lyricSingerChangeHandler.Remove(x);\r\n                }\r\n            });\r\n\r\n            return switchButton;\r\n        }));\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(IBeatmapSingersChangeHandler beatmapSingersChangeHandler)\r\n    {\r\n        // update singer\r\n        bindableSingers.BindTo(beatmapSingersChangeHandler.Singers);\r\n    }\r\n\r\n    protected override void OnLyricChanged(Lyric? lyric)\r\n    {\r\n        singerIndexes.UnbindBindings();\r\n\r\n        if (lyric == null)\r\n            return;\r\n\r\n        // should bind from lyric.\r\n        // singer index might be able to change from other place like singer editor.\r\n        singerIndexes.BindTo(lyric.SingerIdsBindable);\r\n    }\r\n\r\n    protected override LockLyricPropertyBy? IsWriteLyricPropertyLocked(Lyric lyric)\r\n        => HitObjectWritableUtils.GetLyricPropertyLockedBy(lyric, nameof(Lyric.SingerIds));\r\n\r\n    protected override LocalisableString GetWriteLyricPropertyLockedDescription(LockLyricPropertyBy lockLyricPropertyBy) =>\r\n        lockLyricPropertyBy switch\r\n        {\r\n            LockLyricPropertyBy.ReferenceLyricConfig => \"Singers is sync to another notes.\",\r\n            LockLyricPropertyBy.LockState => \"Singers is locked.\",\r\n            _ => throw new ArgumentOutOfRangeException(nameof(lockLyricPropertyBy), lockLyricPropertyBy, null),\r\n        };\r\n\r\n    protected override LocalisableString GetWriteLyricPropertyLockedTooltip(LockLyricPropertyBy lockLyricPropertyBy) =>\r\n        lockLyricPropertyBy switch\r\n        {\r\n            LockLyricPropertyBy.ReferenceLyricConfig => \"Cannot edit the singers because it's sync to another lyric's singers.\",\r\n            LockLyricPropertyBy.LockState => \"The lyric is locked, so cannot edit the singers.\",\r\n            _ => throw new ArgumentOutOfRangeException(nameof(lockLyricPropertyBy), lockLyricPropertyBy, null),\r\n        };\r\n\r\n    public partial class LabelledSingerSwitchButton : LabelledSwitchButton, IHasCustomTooltip<ISinger>\r\n    {\r\n        private const float avatar_size = 48f;\r\n\r\n        private readonly IBindable<string> bindableName = new Bindable<string>();\r\n        private readonly IBindable<string> bindableEnglishName = new Bindable<string>();\r\n\r\n        public LabelledSingerSwitchButton(ISinger singer)\r\n        {\r\n            TooltipContent = singer;\r\n\r\n            if (singer is Singer mainSinger)\r\n            {\r\n                bindableName.BindTo(mainSinger.NameBindable);\r\n                bindableEnglishName.BindTo(mainSinger.EnglishNameBindable);\r\n            }\r\n\r\n            if (InternalChildren[1] is FillFlowContainer fillFlowContainer)\r\n            {\r\n                fillFlowContainer.Spacing = new Vector2(0, 6);\r\n                fillFlowContainer.Padding\r\n                    = new MarginPadding\r\n                    {\r\n                        Horizontal = CONTENT_PADDING_HORIZONTAL,\r\n                        Vertical = CONTENT_PADDING_VERTICAL,\r\n                        Left = CONTENT_PADDING_HORIZONTAL + 40 + CONTENT_PADDING_HORIZONTAL,\r\n                    };\r\n            }\r\n\r\n            AddInternal(new DrawableCircleSingerAvatar\r\n            {\r\n                Singer = singer,\r\n                Size = new Vector2(avatar_size),\r\n                Anchor = Anchor.CentreLeft,\r\n                Origin = Anchor.CentreLeft,\r\n                Margin = new MarginPadding\r\n                {\r\n                    Left = CONTENT_PADDING_HORIZONTAL,\r\n                },\r\n            });\r\n\r\n            bindableName.BindValueChanged(e => Label = e.NewValue, true);\r\n            bindableEnglishName.BindValueChanged(e => Description = string.IsNullOrEmpty(e.NewValue) ? \"<No english name>\" : e.NewValue, true);\r\n        }\r\n\r\n        public ITooltip<ISinger> GetCustomTooltip() => new SingerToolTip();\r\n\r\n        public ISinger TooltipContent { get; }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/SpecialActionSection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Graphics.UserInterfaceV2;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings;\r\n\r\npublic abstract partial class SpecialActionSection<TAction> : EditorSection where TAction : struct, Enum\r\n{\r\n    protected sealed override LocalisableString Title => \"Action\";\r\n\r\n    private readonly Bindable<TAction> bindableModeSpecialAction = new();\r\n\r\n    [Resolved]\r\n    private ILyricSelectionState lyricSelectionState { get; set; } = null!;\r\n\r\n    protected SpecialActionSection()\r\n    {\r\n        Children = new[]\r\n        {\r\n            new LabelledSpecialActionSelection\r\n            {\r\n                Label = SwitchActionTitle,\r\n                Description = SwitchActionDescription,\r\n                Current = bindableModeSpecialAction,\r\n            },\r\n        };\r\n\r\n        bindableModeSpecialAction.BindValueChanged(e =>\r\n        {\r\n            // should cancel the selection after change to the new action.\r\n            lyricSelectionState.EndSelecting(LyricEditorSelectingAction.Cancel);\r\n\r\n            UpdateActionArea(e.NewValue);\r\n        });\r\n\r\n        UpdateActionArea(bindableModeSpecialAction.Value);\r\n    }\r\n\r\n    protected void BindTo(IHasSpecialAction<TAction> specialAction)\r\n    {\r\n        bindableModeSpecialAction.BindTo(specialAction.BindableSpecialAction);\r\n    }\r\n\r\n    protected abstract string SwitchActionTitle { get; }\r\n\r\n    protected abstract string SwitchActionDescription { get; }\r\n\r\n    protected abstract void UpdateActionArea(TAction action);\r\n\r\n    private partial class LabelledSpecialActionSelection : LabelledEnumDropdown<TAction>\r\n    {\r\n        public LabelledSpecialActionSelection()\r\n            : base(true)\r\n        {\r\n            // should change the component size because LabelledDropdown use 0.5 as drawable with scale.\r\n            Component.Width = 1;\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/SwitchSubsection.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Components.Markdown;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Markdown;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings;\r\n\r\n/// <summary>\r\n/// A subsection that can switch between multiple modes.\r\n/// </summary>\r\n/// <typeparam name=\"TEnum\"></typeparam>\r\npublic abstract partial class SwitchSubsection<TEnum> : CompositeDrawable, IHasCurrentValue<TEnum>\r\n    where TEnum : struct, Enum\r\n{\r\n    private const int corner_radius = 15;\r\n\r\n    private const int tab_padding = 20;\r\n    private const int tab_height = 40;\r\n\r\n    private const int spacing = 10;\r\n\r\n    private const int description_horizontal_padding = 20;\r\n    private const int description_vertical_padding = 10;\r\n\r\n    public Bindable<TEnum> Current\r\n    {\r\n        get => tabControl.Current;\r\n        set => tabControl.Current = value;\r\n    }\r\n\r\n    private readonly Box background;\r\n    private readonly SwitchTabControl tabControl;\r\n    private readonly LyricEditorDescriptionTextFlowContainer lyricEditorDescription;\r\n\r\n    protected SwitchSubsection()\r\n    {\r\n        RelativeSizeAxes = Axes.X;\r\n        AutoSizeAxes = Axes.Y;\r\n\r\n        InternalChild = new FillFlowContainer\r\n        {\r\n            RelativeSizeAxes = Axes.X,\r\n            AutoSizeAxes = Axes.Y,\r\n            Direction = FillDirection.Vertical,\r\n            Spacing = new Vector2(spacing),\r\n            Children = new Drawable[]\r\n            {\r\n                new Container\r\n                {\r\n                    RelativeSizeAxes = Axes.X,\r\n                    AutoSizeAxes = Axes.Y,\r\n                    Masking = true,\r\n                    CornerRadius = corner_radius,\r\n                    Children = new Drawable[]\r\n                    {\r\n                        background = new Box\r\n                        {\r\n                            RelativeSizeAxes = Axes.Both,\r\n                        },\r\n                        new Container\r\n                        {\r\n                            RelativeSizeAxes = Axes.X,\r\n                            AutoSizeAxes = Axes.Y,\r\n                            Padding = new MarginPadding(tab_padding),\r\n                            Child = tabControl = CreateTabControl().With(x =>\r\n                            {\r\n                                x.RelativeSizeAxes = Axes.X;\r\n                                x.Height = tab_height;\r\n                            }),\r\n                        },\r\n                    },\r\n                },\r\n                lyricEditorDescription = new LyricEditorDescriptionTextFlowContainer\r\n                {\r\n                    RelativeSizeAxes = Axes.X,\r\n                    AutoSizeAxes = Axes.Y,\r\n                    Padding = new MarginPadding\r\n                    {\r\n                        Vertical = description_vertical_padding,\r\n                        Horizontal = description_horizontal_padding,\r\n                    },\r\n                },\r\n            },\r\n        };\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(LyricEditorColourProvider colourProvider, ILyricEditorState state)\r\n    {\r\n        background.Colour = colourProvider.Background4(state.Mode);\r\n\r\n        tabControl.Items = Enum.GetValues<TEnum>();\r\n        tabControl.Current.BindValueChanged(x =>\r\n        {\r\n            // update description text.\r\n            lyricEditorDescription.Description = GetDescription(x.NewValue);\r\n        }, true);\r\n    }\r\n\r\n    protected abstract SwitchTabControl CreateTabControl();\r\n\r\n    protected abstract DescriptionFormat GetDescription(TEnum @enum);\r\n\r\n    protected abstract partial class SwitchTabControl : TabControl<TEnum>\r\n    {\r\n        public const int SPACING = 20;\r\n\r\n        protected override TabFillFlowContainer CreateTabFlow() => new()\r\n        {\r\n            RelativeSizeAxes = Axes.Y,\r\n            AutoSizeAxes = Axes.X,\r\n            Direction = FillDirection.Horizontal,\r\n            Spacing = new Vector2(SPACING, 0),\r\n        };\r\n\r\n        protected override Dropdown<TEnum>? CreateDropdown() => null;\r\n\r\n        protected sealed override TabItem<TEnum> CreateTabItem(TEnum value) => CreateStepButton(new OsuColour(), value);\r\n\r\n        protected abstract SwitchTabItem CreateStepButton(OsuColour colour, TEnum step);\r\n    }\r\n\r\n    protected abstract partial class SwitchTabItem : TabItem<TEnum>\r\n    {\r\n        protected SwitchTabItem(TEnum value)\r\n            : base(value)\r\n        {\r\n            RelativeSizeAxes = Axes.Y;\r\n        }\r\n\r\n        protected override void LoadComplete()\r\n        {\r\n            base.LoadComplete();\r\n\r\n            UpdateState();\r\n            updateTabSize();\r\n        }\r\n\r\n        private void updateTabSize()\r\n        {\r\n            if (Parent?.Parent is not SwitchTabControl control)\r\n                throw new InvalidOperationException();\r\n\r\n            int tabAmount = Enum.GetValues<TEnum>().Length;\r\n            Width = (control.DrawWidth - (tabAmount - 1) * SwitchTabControl.SPACING) / tabAmount;\r\n        }\r\n\r\n        protected sealed override void OnActivated() => UpdateState();\r\n\r\n        protected sealed override void OnDeactivated() => UpdateState();\r\n\r\n        protected abstract void UpdateState();\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/Text/TextDeleteSubsection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Text;\r\n\r\npublic partial class TextDeleteSubsection : SelectLyricButton\r\n{\r\n    [Resolved]\r\n    private ILyricsChangeHandler lyricsChangeHandler { get; set; } = null!;\r\n\r\n    protected override LocalisableString StandardText => \"Delete\";\r\n\r\n    protected override LocalisableString SelectingText => \"Cancel delete\";\r\n\r\n    protected override void Apply()\r\n    {\r\n        lyricsChangeHandler.Remove();\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/Text/TextEditModeSpecialAction.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Text;\r\n\r\npublic enum TextEditModeSpecialAction\r\n{\r\n    Copy,\r\n\r\n    Delete,\r\n\r\n    Move,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/Text/TextIssueSection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks.Issues;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Issues;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Text;\r\n\r\npublic partial class TextIssueSection : LyricEditorIssueSection\r\n{\r\n    protected override LyricEditorMode EditMode => LyricEditorMode.EditText;\r\n\r\n    protected override LyricsIssueTable CreateLyricsIssueTable() => new TextIssueTable();\r\n\r\n    private partial class TextIssueTable : LyricsIssueTable\r\n    {\r\n        protected override TableColumn[] CreateHeaders() => new[]\r\n        {\r\n            new TableColumn(string.Empty, Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize, minSize: 30)),\r\n            new TableColumn(\"Lyric\", Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize, minSize: 40)),\r\n            new TableColumn(\"Message\", Anchor.CentreLeft),\r\n        };\r\n\r\n        protected override Drawable[] CreateContent(Issue issue)\r\n        {\r\n            var lyric = getInvalidByIssue(issue);\r\n\r\n            return new Drawable[]\r\n            {\r\n                new IssueIcon\r\n                {\r\n                    Origin = Anchor.Centre,\r\n                    Size = new Vector2(10),\r\n                    Margin = new MarginPadding { Left = 10 },\r\n                    Issue = issue,\r\n                },\r\n                new OsuSpriteText\r\n                {\r\n                    Text = $\"#{lyric.Order}\",\r\n                    Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold),\r\n                    Margin = new MarginPadding { Right = 10 },\r\n                },\r\n                new TruncatingSpriteText\r\n                {\r\n                    Text = issue.ToString(),\r\n                    RelativeSizeAxes = Axes.X,\r\n                    Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Medium),\r\n                },\r\n            };\r\n        }\r\n\r\n        private Lyric getInvalidByIssue(Issue issue)\r\n        {\r\n            if (issue is not LyricIssue lyricIssue)\r\n                throw new InvalidCastException();\r\n\r\n            return lyricIssue.Lyric;\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/Text/TextSettingsHeader.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Markdown;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Text;\r\n\r\npublic partial class TextSettingsHeader : LyricEditorSettingsHeader<TextEditStep>\r\n{\r\n    protected override OverlayColourScheme CreateColourScheme()\r\n        => OverlayColourScheme.Red;\r\n\r\n    protected override EditStepTabControl CreateTabControl()\r\n        => new TextEditStepTabControl();\r\n\r\n    protected override DescriptionFormat GetSelectionDescription(TextEditStep step) =>\r\n        step switch\r\n        {\r\n            TextEditStep.Typing => \"Edit the lyric text.\",\r\n            TextEditStep.Split => \"Create/delete or split/combine the lyric.\",\r\n            TextEditStep.Verify => \"Check if have lyric with no text.\",\r\n            _ => throw new ArgumentOutOfRangeException(nameof(step), step, null),\r\n        };\r\n\r\n    private partial class TextEditStepTabControl : EditStepTabControl\r\n    {\r\n        protected override StepTabButton CreateStepButton(OsuColour colours, TextEditStep value)\r\n        {\r\n            return value switch\r\n            {\r\n                TextEditStep.Typing => new StepTabButton(value)\r\n                {\r\n                    Text = \"Typing\",\r\n                    SelectedColour = colours.Blue,\r\n                    UnSelectedColour = colours.BlueDarker,\r\n                },\r\n                TextEditStep.Split => new StepTabButton(value)\r\n                {\r\n                    Text = \"Split\",\r\n                    SelectedColour = colours.Red,\r\n                    UnSelectedColour = colours.RedDarker,\r\n                },\r\n                TextEditStep.Verify => new VerifyStepTabButton(value)\r\n                {\r\n                    Text = \"Verify\",\r\n                    EditMode = LyricEditorMode.EditText,\r\n                    SelectedColour = colours.Yellow,\r\n                    UnSelectedColour = colours.YellowDarker,\r\n                },\r\n                _ => throw new ArgumentOutOfRangeException(nameof(value), value, null),\r\n            };\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/Text/TextSwitchSpecialActionSection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Text;\r\n\r\npublic partial class TextSwitchSpecialActionSection : SpecialActionSection<TextEditModeSpecialAction>\r\n{\r\n    protected override string SwitchActionTitle => \"Special actions\";\r\n\r\n    protected override string SwitchActionDescription => \"Copy, delete or move the lyrics.\";\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(IEditTextModeState editTextModeState)\r\n    {\r\n        BindTo(editTextModeState);\r\n    }\r\n\r\n    protected override void UpdateActionArea(TextEditModeSpecialAction action)\r\n    {\r\n        RemoveAll(x => x is TextDeleteSubsection, true);\r\n\r\n        switch (action)\r\n        {\r\n            case TextEditModeSpecialAction.Copy:\r\n                // todo: implement\r\n                break;\r\n\r\n            case TextEditModeSpecialAction.Delete:\r\n                Add(new TextDeleteSubsection());\r\n                break;\r\n\r\n            case TextEditModeSpecialAction.Move:\r\n                // todo: implement\r\n                break;\r\n\r\n            default:\r\n                return;\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/TextSettings.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Text;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings;\r\n\r\npublic partial class TextSettings : LyricEditorSettings\r\n{\r\n    public override SettingsDirection Direction => SettingsDirection.Right;\r\n\r\n    public override float SettingsWidth => 300;\r\n\r\n    private readonly Bindable<TextEditStep> bindableEditStep = new();\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(IEditTextModeState editTextModeState)\r\n    {\r\n        bindableEditStep.BindTo(editTextModeState.BindableEditStep);\r\n    }\r\n\r\n    protected override EditorSettingsHeader CreateSettingHeader()\r\n        => new TextSettingsHeader\r\n        {\r\n            Current = bindableEditStep,\r\n        };\r\n\r\n    protected override IReadOnlyList<EditorSection> CreateEditorSections() => bindableEditStep.Value switch\r\n    {\r\n        TextEditStep.Typing => new[]\r\n        {\r\n            new TextSwitchSpecialActionSection(),\r\n        },\r\n        TextEditStep.Split => new[]\r\n        {\r\n            new TextSwitchSpecialActionSection(),\r\n        },\r\n        TextEditStep.Verify => new[]\r\n        {\r\n            new TextIssueSection(),\r\n        },\r\n        _ => throw new ArgumentOutOfRangeException(),\r\n    };\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/TimeTagSettings.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.TimeTags;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings;\r\n\r\npublic partial class TimeTagSettings : LyricEditorSettings\r\n{\r\n    public override SettingsDirection Direction => SettingsDirection.Right;\r\n    public override float SettingsWidth => 300;\r\n\r\n    private readonly Bindable<TimeTagEditStep> bindableEditStep = new();\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(IEditTimeTagModeState editTimeTagModeState)\r\n    {\r\n        bindableEditStep.BindTo(editTimeTagModeState.BindableEditStep);\r\n    }\r\n\r\n    protected override EditorSettingsHeader CreateSettingHeader()\r\n        => new TimeTagSettingsHeader\r\n        {\r\n            Current = bindableEditStep,\r\n        };\r\n\r\n    protected override IReadOnlyList<EditorSection> CreateEditorSections() => bindableEditStep.Value switch\r\n    {\r\n        TimeTagEditStep.Create => new EditorSection[]\r\n        {\r\n            new TimeTagAutoGenerateSection(),\r\n            new CreateTimeTagActionSection(),\r\n        },\r\n        TimeTagEditStep.Recording => new EditorSection[]\r\n        {\r\n            new TimeTagRecordingToolSection(),\r\n            new TimeTagRecordingConfigSection(),\r\n        },\r\n        TimeTagEditStep.Adjust => new EditorSection[]\r\n        {\r\n            new TimeTagAdjustConfigSection(),\r\n            new TimeTagIssueSection(),\r\n        },\r\n        _ => throw new ArgumentOutOfRangeException(),\r\n    };\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/TimeTags/Components/LabelledOpacityAdjustment.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Extensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Cursor;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Graphics.UserInterfaceV2;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.TimeTags.Components;\r\n\r\npublic partial class LabelledOpacityAdjustment : LabelledSwitchButton\r\n{\r\n    protected const float CONFIG_BUTTON_SIZE = 20f;\r\n\r\n    private readonly OpacityButton opacityButton;\r\n\r\n    public LabelledOpacityAdjustment()\r\n    {\r\n        if (InternalChildren[1] is not FillFlowContainer fillFlowContainer)\r\n            throw new ArgumentNullException(nameof(fillFlowContainer));\r\n\r\n        // change padding to place config button.\r\n        fillFlowContainer.Padding = new MarginPadding\r\n        {\r\n            Horizontal = CONTENT_PADDING_HORIZONTAL,\r\n            Vertical = CONTENT_PADDING_VERTICAL,\r\n            Right = CONTENT_PADDING_HORIZONTAL + CONFIG_BUTTON_SIZE + CONTENT_PADDING_HORIZONTAL,\r\n        };\r\n\r\n        // add config button.\r\n        AddInternal(new Container\r\n        {\r\n            RelativeSizeAxes = Axes.X,\r\n            AutoSizeAxes = Axes.Y,\r\n            Padding = new MarginPadding\r\n            {\r\n                Top = CONTENT_PADDING_VERTICAL,\r\n                Right = CONTENT_PADDING_HORIZONTAL,\r\n            },\r\n            Child = opacityButton = new OpacityButton\r\n            {\r\n                Anchor = Anchor.TopRight,\r\n                Origin = Anchor.TopRight,\r\n                Size = new Vector2(CONFIG_BUTTON_SIZE),\r\n            },\r\n        });\r\n\r\n        Component.Current.BindValueChanged(_ => updateConfigButtonOpacity(), true);\r\n    }\r\n\r\n    private void updateConfigButtonOpacity()\r\n    {\r\n        bool showOpacityButton = Component.Current.Value;\r\n        opacityButton.FadeTo(showOpacityButton ? 1 : 0.3f, 200, Easing.OutQuint);\r\n        opacityButton.Enabled.Value = showOpacityButton;\r\n    }\r\n\r\n    public Bindable<float> Opacity\r\n    {\r\n        set => opacityButton.Current = value;\r\n    }\r\n\r\n    private partial class OpacityButton : IconButton, IHasPopover, IHasCurrentValue<float>\r\n    {\r\n        private readonly BindableNumberWithCurrent<float> current = new();\r\n\r\n        public Bindable<float> Current\r\n        {\r\n            get => current.Current;\r\n            set => current.Current = value;\r\n        }\r\n\r\n        public OpacityButton()\r\n        {\r\n            Icon = FontAwesome.Solid.Cog;\r\n            Action = this.ShowPopover;\r\n        }\r\n\r\n        public Popover GetPopover()\r\n            => new OsuPopover\r\n            {\r\n                Child = new OpacitySliderBar\r\n                {\r\n                    Width = 150,\r\n                    Current = { BindTarget = Current },\r\n                },\r\n            };\r\n\r\n        private partial class OpacitySliderBar : RoundedSliderBar<float>\r\n        {\r\n            public override LocalisableString TooltipText => (Current.Value * 100).ToString(\"N0\") + \"%\";\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/TimeTags/CreateTimeTagActionSection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Input.Bindings;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.TimeTags;\r\n\r\npublic partial class CreateTimeTagActionSection : EditorSection, IKeyBindingHandler<KaraokeEditAction>\r\n{\r\n    protected override LocalisableString Title => \"Action\";\r\n\r\n    [Resolved]\r\n    private ILyricTimeTagsChangeHandler lyricTimeTagsChangeHandler { get; set; } = null!;\r\n\r\n    private readonly IBindable<ICaretPosition?> bindableCaretPosition = new Bindable<ICaretPosition?>();\r\n    private readonly Bindable<CreateTimeTagType> bindableCreateType = new();\r\n\r\n    public CreateTimeTagActionSection()\r\n    {\r\n        Children = new[]\r\n        {\r\n            new CreateTimeTagTypeSubsection\r\n            {\r\n                Current = bindableCreateType,\r\n            },\r\n        };\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(IEditTimeTagModeState editTimeTagModeState, ILyricCaretState lyricCaretState)\r\n    {\r\n        bindableCaretPosition.BindTo(lyricCaretState.BindableCaretPosition);\r\n        bindableCreateType.BindTo(editTimeTagModeState.BindableCreateType);\r\n    }\r\n\r\n    public bool OnPressed(KeyBindingPressEvent<KaraokeEditAction> e)\r\n    {\r\n        var action = e.Action;\r\n        var caretPosition = bindableCaretPosition.Value;\r\n\r\n        if (caretPosition is not CreateRemoveTimeTagCaretPosition createRemoveTimeTagCaretPosition)\r\n            return false;\r\n\r\n        if (LyricEditor.ToMovingCaretAction(e.Action) != null)\r\n        {\r\n            bindableCreateType.Value = CreateTimeTagType.Keyboard;\r\n            return false;\r\n        }\r\n\r\n        if (createTimeTagByKeyboard(createRemoveTimeTagCaretPosition.CharIndex, action))\r\n        {\r\n            bindableCreateType.Value = CreateTimeTagType.Keyboard;\r\n            return true;\r\n        }\r\n\r\n        return false;\r\n    }\r\n\r\n    private bool createTimeTagByKeyboard(int charIndex, KaraokeEditAction action)\r\n    {\r\n        switch (action)\r\n        {\r\n            case KaraokeEditAction.CreateStartTimeTag:\r\n                lyricTimeTagsChangeHandler.AddByPosition(new TextIndex(charIndex));\r\n                return true;\r\n\r\n            case KaraokeEditAction.CreateEndTimeTag:\r\n                lyricTimeTagsChangeHandler.AddByPosition(new TextIndex(charIndex, TextIndex.IndexState.End));\r\n                return true;\r\n\r\n            case KaraokeEditAction.RemoveStartTimeTag:\r\n                lyricTimeTagsChangeHandler.RemoveByPosition(new TextIndex(charIndex));\r\n                return true;\r\n\r\n            case KaraokeEditAction.RemoveEndTimeTag:\r\n                lyricTimeTagsChangeHandler.RemoveByPosition(new TextIndex(charIndex, TextIndex.IndexState.End));\r\n                return true;\r\n\r\n            default:\r\n                return false;\r\n        }\r\n    }\r\n\r\n    public void OnReleased(KeyBindingReleaseEvent<KaraokeEditAction> e)\r\n    {\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/TimeTags/CreateTimeTagTypeSubsection.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Cursor;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Markdown;\r\nusing osuTK;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.TimeTags;\r\n\r\npublic partial class CreateTimeTagTypeSubsection : SwitchSubsection<CreateTimeTagType>\r\n{\r\n    protected override SwitchTabControl CreateTabControl()\r\n        => new CreateTimeTagTypeTabControl();\r\n\r\n    protected override DescriptionFormat GetDescription(CreateTimeTagType mode) =>\r\n        mode switch\r\n        {\r\n            CreateTimeTagType.Mouse => \"Use mouse to move the caret, and click the button in the UI to create/remove the start/end time tag. It's for the beginner.\",\r\n            CreateTimeTagType.HotkeyThenPress => \"Press the hotkey to prepare create/remove the start/end time tag. This kind of create mode is still implementing.\",\r\n            CreateTimeTagType.Keyboard => new DescriptionFormat\r\n            {\r\n                Text =\r\n                    $\"Use [{DescriptionFormat.LINK_KEY_ACTION}](navigate_time_tag) to control caret position, press [{DescriptionFormat.LINK_KEY_ACTION}](create_time_tag) to create new time-tag and press [{DescriptionFormat.LINK_KEY_ACTION}](remove_time_tag) to delete exist time-tag.\",\r\n                Actions = new Dictionary<string, IDescriptionAction>\r\n                {\r\n                    {\r\n                        \"navigate_time_tag\", new InputKeyDescriptionAction\r\n                        {\r\n                            Text = \"Keyboard\",\r\n                            AdjustableActions = new List<KaraokeEditAction>\r\n                            {\r\n                                KaraokeEditAction.MoveToPreviousLyric,\r\n                                KaraokeEditAction.MoveToNextLyric,\r\n                                KaraokeEditAction.MoveToPreviousIndex,\r\n                                KaraokeEditAction.MoveToNextIndex,\r\n                            },\r\n                        }\r\n                    },\r\n                    {\r\n                        \"create_time_tag\", new InputKeyDescriptionAction\r\n                        {\r\n                            Text = \"Create Time-tag keys\",\r\n                            AdjustableActions = new List<KaraokeEditAction>\r\n                            {\r\n                                KaraokeEditAction.CreateStartTimeTag,\r\n                                KaraokeEditAction.CreateEndTimeTag,\r\n                            },\r\n                        }\r\n                    },\r\n                    {\r\n                        \"remove_time_tag\", new InputKeyDescriptionAction\r\n                        {\r\n                            Text = \"Remove Time-tag keys\",\r\n                            AdjustableActions = new List<KaraokeEditAction>\r\n                            {\r\n                                KaraokeEditAction.RemoveStartTimeTag,\r\n                                KaraokeEditAction.RemoveEndTimeTag,\r\n                            },\r\n                        }\r\n                    },\r\n                },\r\n            },\r\n            _ => throw new InvalidOperationException(nameof(mode)),\r\n        };\r\n\r\n    private partial class CreateTimeTagTypeTabControl : SwitchTabControl\r\n    {\r\n        protected override SwitchTabItem CreateStepButton(OsuColour colours, CreateTimeTagType value)\r\n        {\r\n            return value switch\r\n            {\r\n                CreateTimeTagType.Mouse => new CreateTimeTagTypeTabButton(value)\r\n                {\r\n                    Icon = FontAwesome.Solid.MousePointer,\r\n                    TooltipText = \"Mouse\",\r\n                    SelectedColour = colours.Green,\r\n                    UnSelectedColour = colours.GreenDarker,\r\n                },\r\n                CreateTimeTagType.HotkeyThenPress => new CreateTimeTagTypeTabButton(value)\r\n                {\r\n                    Icon = FontAwesome.Solid.PencilAlt,\r\n                    TooltipText = \"Keyboard + Mouse\",\r\n                    SelectedColour = colours.Yellow,\r\n                    UnSelectedColour = colours.YellowDarker,\r\n                },\r\n                CreateTimeTagType.Keyboard => new CreateTimeTagTypeTabButton(value)\r\n                {\r\n                    Icon = FontAwesome.Solid.Keyboard,\r\n                    TooltipText = \"Keyboard\",\r\n                    SelectedColour = colours.Blue,\r\n                    UnSelectedColour = colours.BlueDarker,\r\n                },\r\n                _ => throw new ArgumentOutOfRangeException(nameof(value), value, null),\r\n            };\r\n        }\r\n\r\n        private partial class CreateTimeTagTypeTabButton : SwitchTabItem, IHasTooltip\r\n        {\r\n            private readonly Box background;\r\n            private readonly SpriteIcon spriteIcon;\r\n\r\n            public CreateTimeTagTypeTabButton(CreateTimeTagType value)\r\n                : base(value)\r\n            {\r\n                Child = new Container\r\n                {\r\n                    Masking = true,\r\n                    CornerRadius = 15,\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    Children = new Drawable[]\r\n                    {\r\n                        background = new Box\r\n                        {\r\n                            RelativeSizeAxes = Axes.Both,\r\n                        },\r\n                        spriteIcon = new SpriteIcon\r\n                        {\r\n                            Anchor = Anchor.Centre,\r\n                            Origin = Anchor.Centre,\r\n                            Size = new Vector2(25),\r\n                        },\r\n                    },\r\n                };\r\n            }\r\n\r\n            public IconUsage Icon\r\n            {\r\n                get => spriteIcon.Icon;\r\n                set => spriteIcon.Icon = value;\r\n            }\r\n\r\n            public LocalisableString TooltipText { get; init; }\r\n\r\n            public Color4 SelectedColour { get; init; }\r\n\r\n            public Color4 UnSelectedColour { get; init; }\r\n\r\n            protected override void UpdateState()\r\n            {\r\n                background.Colour = Active.Value ? SelectedColour : UnSelectedColour;\r\n                Child.Alpha = Active.Value ? 0.8f : 0.4f;\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/TimeTags/RecordingTapControl.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Input.Bindings;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\nusing osu.Game.Screens.Edit;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.TimeTags;\r\n\r\npublic partial class RecordingTapControl : CompositeDrawable, IKeyBindingHandler<KaraokeEditAction>\r\n{\r\n    [Resolved]\r\n    private KaraokeRulesetLyricEditorConfigManager lyricEditorConfigManager { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private ILyricCaretState lyricCaretState { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private ILyricTimeTagsChangeHandler lyricTimeTagsChangeHandler { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private EditorClock editorClock { get; set; } = null!;\r\n\r\n    private InlineButton undoButton = null!;\r\n    private InlineButton resetButton = null!;\r\n    private TapButton tapButton = null!;\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(ILyricEditorState state, LyricEditorColourProvider colourProvider)\r\n    {\r\n        RelativeSizeAxes = Axes.X;\r\n        AutoSizeAxes = Axes.Y;\r\n\r\n        InternalChildren = new Drawable[]\r\n        {\r\n            new Container\r\n            {\r\n                RelativeSizeAxes = Axes.Y,\r\n                Anchor = Anchor.Centre,\r\n                Origin = Anchor.CentreRight,\r\n                Height = 0.98f,\r\n                Width = TapButton.SIZE / 1.3f,\r\n                Masking = true,\r\n                CornerRadius = 15,\r\n                Children = new Drawable[]\r\n                {\r\n                    undoButton = new InlineButton(FontAwesome.Solid.Trash, Anchor.TopLeft)\r\n                    {\r\n                        BackgroundColour = colourProvider.Background1(state.Mode),\r\n                        RelativeSizeAxes = Axes.Both,\r\n                        Height = 0.49f,\r\n                        Action = reset,\r\n                    },\r\n                    resetButton = new InlineButton(FontAwesome.Solid.AngleLeft, Anchor.BottomLeft)\r\n                    {\r\n                        BackgroundColour = colourProvider.Background1(state.Mode),\r\n                        RelativeSizeAxes = Axes.Both,\r\n                        Height = 0.49f,\r\n                        Anchor = Anchor.BottomLeft,\r\n                        Origin = Anchor.BottomLeft,\r\n                        Action = undo,\r\n                    },\r\n                },\r\n            },\r\n            tapButton = new TapButton\r\n            {\r\n                Anchor = Anchor.Centre,\r\n                Origin = Anchor.Centre,\r\n                Tapped = onTapped,\r\n            },\r\n        };\r\n    }\r\n\r\n    public bool OnPressed(KeyBindingPressEvent<KaraokeEditAction> e)\r\n    {\r\n        switch (e.Action)\r\n        {\r\n            case KaraokeEditAction.ClearTime:\r\n                resetButton.TriggerClick();\r\n\r\n                return true;\r\n\r\n            case KaraokeEditAction.SetTime:\r\n                tapButton.TriggerClick();\r\n\r\n                return true;\r\n\r\n            default:\r\n                return false;\r\n        }\r\n    }\r\n\r\n    public void OnReleased(KeyBindingReleaseEvent<KaraokeEditAction> e)\r\n    {\r\n    }\r\n\r\n    private void reset()\r\n    {\r\n        if (lyricCaretState.CaretPosition is not RecordingTimeTagCaretPosition timeTagCaretPosition)\r\n            throw new InvalidOperationException();\r\n\r\n        lyricTimeTagsChangeHandler.ClearAllTimeTagTime();\r\n\r\n        var lyric = timeTagCaretPosition.Lyric;\r\n        if (lyric.TimeValid)\r\n        {\r\n            editorClock.Seek(lyric.StartTime - 1000);\r\n        }\r\n\r\n        if (lyricCaretState.GetCaretPositionByAction(MovingCaretAction.FirstIndex)?.Lyric != timeTagCaretPosition.Lyric)\r\n            return;\r\n\r\n        lyricCaretState.MoveCaret(MovingCaretAction.FirstIndex);\r\n    }\r\n\r\n    private void undo()\r\n    {\r\n        if (lyricCaretState.CaretPosition is not RecordingTimeTagCaretPosition timeTagCaretPosition)\r\n            throw new InvalidOperationException();\r\n\r\n        double? currentTimeTagTime = timeTagCaretPosition.TimeTag.Time;\r\n\r\n        var timeTag = timeTagCaretPosition.TimeTag;\r\n        lyricTimeTagsChangeHandler.ClearTimeTagTime(timeTag);\r\n\r\n        if (lyricCaretState.GetCaretPositionByAction(MovingCaretAction.PreviousIndex)?.Lyric != timeTagCaretPosition.Lyric)\r\n            return;\r\n\r\n        lyricCaretState.MoveCaret(MovingCaretAction.PreviousIndex);\r\n\r\n        if (currentTimeTagTime != null)\r\n        {\r\n            editorClock.Seek(currentTimeTagTime.Value - 1000);\r\n        }\r\n        else\r\n        {\r\n            editorClock.Seek(editorClock.CurrentTime - 1000);\r\n        }\r\n    }\r\n\r\n    private void onTapped(double currentTime)\r\n    {\r\n        if (lyricCaretState.CaretPosition is not RecordingTimeTagCaretPosition timeTagCaretPosition)\r\n            throw new InvalidOperationException();\r\n\r\n        var timeTag = timeTagCaretPosition.TimeTag;\r\n        lyricTimeTagsChangeHandler.SetTimeTagTime(timeTag, currentTime);\r\n\r\n        if (lyricEditorConfigManager.Get<bool>(KaraokeRulesetLyricEditorSetting.RecordingAutoMoveToNextTimeTag))\r\n            lyricCaretState.MoveCaret(MovingCaretAction.NextIndex);\r\n    }\r\n\r\n    private partial class InlineButton : OsuButton\r\n    {\r\n        private readonly IconUsage icon;\r\n        private readonly Anchor anchor;\r\n\r\n        private SpriteIcon spriteIcon = null!;\r\n\r\n        [Resolved]\r\n        private ILyricEditorState lyricEditorState { get; set; } = null!;\r\n\r\n        [Resolved]\r\n        private LyricEditorColourProvider colourProvider { get; set; } = null!;\r\n\r\n        public InlineButton(IconUsage icon, Anchor anchor)\r\n        {\r\n            this.icon = icon;\r\n            this.anchor = anchor;\r\n        }\r\n\r\n        protected override void LoadComplete()\r\n        {\r\n            base.LoadComplete();\r\n\r\n            Content.CornerRadius = 0;\r\n            Content.Masking = false;\r\n\r\n            BackgroundColour = colourProvider.Background2(lyricEditorState.Mode);\r\n\r\n            Content.Add(new Container\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n                Padding = new MarginPadding(15),\r\n                Children = new Drawable[]\r\n                {\r\n                    spriteIcon = new SpriteIcon\r\n                    {\r\n                        Icon = icon,\r\n                        Size = new Vector2(22),\r\n                        Anchor = anchor,\r\n                        Origin = anchor,\r\n                        Colour = colourProvider.Background1(lyricEditorState.Mode),\r\n                    },\r\n                },\r\n            });\r\n        }\r\n\r\n        protected override bool OnMouseDown(MouseDownEvent e)\r\n        {\r\n            // scale looks bad so don't call base.\r\n            return false;\r\n        }\r\n\r\n        protected override bool OnHover(HoverEvent e)\r\n        {\r\n            spriteIcon.FadeColour(colourProvider.Content2(lyricEditorState.Mode), 200, Easing.OutQuint);\r\n            return base.OnHover(e);\r\n        }\r\n\r\n        protected override void OnHoverLost(HoverLostEvent e)\r\n        {\r\n            spriteIcon.FadeColour(colourProvider.Background1(lyricEditorState.Mode), 200, Easing.OutQuint);\r\n            base.OnHoverLost(e);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/TimeTags/TapButton.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Extensions.Color4Extensions;\r\nusing osu.Framework.Extensions.IEnumerableExtensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Colour;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Effects;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Framework.Threading;\r\nusing osu.Game.Extensions;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\nusing osu.Game.Screens.Edit;\r\nusing osuTK;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.TimeTags;\r\n\r\ninternal partial class TapButton : CircularContainer\r\n{\r\n    public const float SIZE = 140;\r\n\r\n    public Action<double>? Tapped;\r\n\r\n    [Resolved]\r\n    private EditorClock editorClock { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private ILyricEditorState lyricEditorState { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private LyricEditorColourProvider colourProvider { get; set; } = null!;\r\n\r\n    private Circle hoverLayer = null!;\r\n\r\n    private CircularContainer innerCircle = null!;\r\n    private Box innerCircleHighlight = null!;\r\n\r\n    private int currentIndex;\r\n\r\n    private Container scaleContainer = null!;\r\n    private Container<Light> lights = null!;\r\n    private Container lightsGlow = null!;\r\n    private OsuSpriteText timeTagInfoText = null!;\r\n    private Container textContainer = null!;\r\n\r\n    private bool grabbedMouseDown;\r\n\r\n    private ScheduledDelegate? resetDelegate;\r\n\r\n    private const double transition_length = 500;\r\n\r\n    private const float angular_light_gap = 0.007f;\r\n\r\n    private readonly IBindable<ICaretPosition?> bindableCaret = new Bindable<ICaretPosition?>();\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(ILyricCaretState lyricCaretState)\r\n    {\r\n        bindableCaret.BindTo(lyricCaretState.BindableCaretPosition);\r\n\r\n        Size = new Vector2(SIZE);\r\n\r\n        const float ring_width = 10;\r\n        const float light_padding = 3;\r\n\r\n        InternalChild = scaleContainer = new Container\r\n        {\r\n            Anchor = Anchor.Centre,\r\n            Origin = Anchor.Centre,\r\n            RelativeSizeAxes = Axes.Both,\r\n            Children = new Drawable[]\r\n            {\r\n                new Circle\r\n                {\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    Colour = colourProvider.Background4(lyricEditorState.Mode),\r\n                },\r\n                lights = new Container<Light>\r\n                {\r\n                    RelativeSizeAxes = Axes.Both,\r\n                },\r\n                new CircularContainer\r\n                {\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    Name = \"outer masking\",\r\n                    Masking = true,\r\n                    BorderThickness = light_padding,\r\n                    BorderColour = colourProvider.Background4(lyricEditorState.Mode),\r\n                    Children = new Drawable[]\r\n                    {\r\n                        new Box\r\n                        {\r\n                            Colour = Color4.Black,\r\n                            RelativeSizeAxes = Axes.Both,\r\n                            Alpha = 0,\r\n                            AlwaysPresent = true,\r\n                        },\r\n                    },\r\n                },\r\n                new Circle\r\n                {\r\n                    Name = \"inner masking\",\r\n                    Size = new Vector2(SIZE - ring_width * 2 + light_padding * 2),\r\n                    Anchor = Anchor.Centre,\r\n                    Origin = Anchor.Centre,\r\n                    Colour = colourProvider.Background4(lyricEditorState.Mode),\r\n                },\r\n                lightsGlow = new Container\r\n                {\r\n                    RelativeSizeAxes = Axes.Both,\r\n                },\r\n                innerCircle = new CircularContainer\r\n                {\r\n                    Size = new Vector2(SIZE - ring_width * 2),\r\n                    Anchor = Anchor.Centre,\r\n                    Origin = Anchor.Centre,\r\n                    Masking = true,\r\n                    Children = new Drawable[]\r\n                    {\r\n                        new Box\r\n                        {\r\n                            Colour = colourProvider.Background2(lyricEditorState.Mode),\r\n                            RelativeSizeAxes = Axes.Both,\r\n                        },\r\n                        innerCircleHighlight = new Box\r\n                        {\r\n                            Colour = colourProvider.Colour3(lyricEditorState.Mode),\r\n                            Blending = BlendingParameters.Additive,\r\n                            RelativeSizeAxes = Axes.Both,\r\n                            Alpha = 0,\r\n                        },\r\n                        textContainer = new Container\r\n                        {\r\n                            RelativeSizeAxes = Axes.Both,\r\n                            Colour = colourProvider.Background1(lyricEditorState.Mode),\r\n                            Children = new Drawable[]\r\n                            {\r\n                                new OsuSpriteText\r\n                                {\r\n                                    Font = OsuFont.Torus.With(size: 34, weight: FontWeight.SemiBold),\r\n                                    Anchor = Anchor.Centre,\r\n                                    Origin = Anchor.BottomCentre,\r\n                                    Y = 5,\r\n                                    Text = \"Tap\",\r\n                                },\r\n                                timeTagInfoText = new OsuSpriteText\r\n                                {\r\n                                    Font = OsuFont.Torus.With(size: 18, weight: FontWeight.Regular),\r\n                                    Anchor = Anchor.Centre,\r\n                                    Origin = Anchor.TopCentre,\r\n                                    Y = 2,\r\n                                },\r\n                            },\r\n                        },\r\n                        hoverLayer = new Circle\r\n                        {\r\n                            RelativeSizeAxes = Axes.Both,\r\n                            Colour = colourProvider.Background1(lyricEditorState.Mode).Opacity(0.3f),\r\n                            Blending = BlendingParameters.Additive,\r\n                            Alpha = 0,\r\n                        },\r\n                    },\r\n                },\r\n            },\r\n        };\r\n\r\n        reset();\r\n\r\n        bindableCaret.BindValueChanged(x =>\r\n        {\r\n            if (x.NewValue is RecordingTimeTagCaretPosition newCaret)\r\n            {\r\n                updateTimeTagAmount(newCaret.GetTotalTimeTags());\r\n                updateCurrentTimeTag(newCaret.GetCurrentTimeTagIndex());\r\n            }\r\n            else\r\n            {\r\n                updateTimeTagAmount(0);\r\n            }\r\n        }, true);\r\n    }\r\n\r\n    private void updateTimeTagAmount(int amount)\r\n    {\r\n        if (lights.Children.Count == amount)\r\n            return;\r\n\r\n        lights.Clear();\r\n        lightsGlow.Clear();\r\n\r\n        for (int i = 0; i < amount; i++)\r\n        {\r\n            var light = new Light(amount)\r\n            {\r\n                Rotation = i * (360f / amount) + 360 * angular_light_gap / 2,\r\n            };\r\n\r\n            lights.Add(light);\r\n            lightsGlow.Add(light.Glow.CreateProxy());\r\n        }\r\n    }\r\n\r\n    private void updateCurrentTimeTag(int currentTimeTagIndex)\r\n    {\r\n        currentIndex = currentTimeTagIndex;\r\n\r\n        for (int i = 0; i < lights.Children.Count; i++)\r\n        {\r\n            bool isTapped = i <= currentIndex;\r\n            lights.Children[i].IsTapped = isTapped;\r\n        }\r\n    }\r\n\r\n    public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>\r\n        hoverLayer.ReceivePositionalInputAt(screenSpacePos);\r\n\r\n    private ColourInfo textColour\r\n    {\r\n        get\r\n        {\r\n            if (grabbedMouseDown)\r\n                return colourProvider.Background4(lyricEditorState.Mode);\r\n\r\n            if (IsHovered)\r\n                return colourProvider.Content2(lyricEditorState.Mode);\r\n\r\n            return colourProvider.Background1(lyricEditorState.Mode);\r\n        }\r\n    }\r\n\r\n    protected override bool OnHover(HoverEvent e)\r\n    {\r\n        hoverLayer.FadeIn(transition_length, Easing.OutQuint);\r\n        textContainer.FadeColour(textColour, transition_length, Easing.OutQuint);\r\n        return true;\r\n    }\r\n\r\n    protected override void OnHoverLost(HoverLostEvent e)\r\n    {\r\n        hoverLayer.FadeOut(transition_length, Easing.OutQuint);\r\n        textContainer.FadeColour(textColour, transition_length, Easing.OutQuint);\r\n        base.OnHoverLost(e);\r\n    }\r\n\r\n    protected override bool OnClick(ClickEvent e)\r\n    {\r\n        Tapped?.Invoke(editorClock.CurrentTime);\r\n\r\n        mouseDownAnimation();\r\n        mouseUpAnimation();\r\n\r\n        return true;\r\n    }\r\n\r\n    private void mouseDownAnimation()\r\n    {\r\n        const double in_duration = 100;\r\n\r\n        grabbedMouseDown = true;\r\n\r\n        resetDelegate?.Cancel();\r\n\r\n        textContainer.FadeColour(textColour, in_duration, Easing.OutQuint);\r\n\r\n        scaleContainer.ScaleTo(0.99f, in_duration, Easing.OutQuint);\r\n        innerCircle.ScaleTo(0.96f, in_duration, Easing.OutQuint);\r\n\r\n        innerCircleHighlight\r\n            .FadeIn(50, Easing.OutQuint)\r\n            .FlashColour(Color4.White, 1000, Easing.OutQuint);\r\n\r\n        lights.ForEach(x => x.Hide());\r\n        lights[currentIndex].Show();\r\n    }\r\n\r\n    private void mouseUpAnimation()\r\n    {\r\n        const double out_duration = 800;\r\n\r\n        grabbedMouseDown = false;\r\n\r\n        textContainer.FadeColour(textColour, out_duration, Easing.OutQuint);\r\n\r\n        scaleContainer.ScaleTo(1, out_duration, Easing.OutQuint);\r\n        innerCircle.ScaleTo(1, out_duration, Easing.OutQuint);\r\n\r\n        innerCircleHighlight.FadeOut(out_duration, Easing.OutQuint);\r\n\r\n        resetDelegate = Scheduler.AddDelayed(reset, 1000);\r\n    }\r\n\r\n    private void reset()\r\n    {\r\n        timeTagInfoText.FadeOut(transition_length, Easing.OutQuint);\r\n        timeTagInfoText.FadeIn(800, Easing.OutQuint);\r\n\r\n        foreach (var light in lights)\r\n            light.Hide();\r\n    }\r\n\r\n    protected override void Update()\r\n    {\r\n        base.Update();\r\n\r\n        timeTagInfoText.Text = editorClock.CurrentTime.ToEditorFormattedString();\r\n    }\r\n\r\n    // todo: light should have states:\r\n    // 1. pending\r\n    // 2. finished\r\n    private partial class Light : CompositeDrawable\r\n    {\r\n        public Drawable Glow { get; private set; } = null!;\r\n\r\n        private CircularProgress circularProgress = null!;\r\n        private Container fillContent = null!;\r\n\r\n        [Resolved]\r\n        private ILyricEditorState lyricEditorState { get; set; } = null!;\r\n\r\n        [Resolved]\r\n        private LyricEditorColourProvider colourProvider { get; set; } = null!;\r\n\r\n        private readonly int lightAmount;\r\n\r\n        public Light(int lightAmount)\r\n        {\r\n            this.lightAmount = lightAmount;\r\n        }\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load()\r\n        {\r\n            RelativeSizeAxes = Axes.Both;\r\n            Anchor = Anchor.Centre;\r\n            Origin = Anchor.Centre;\r\n\r\n            Size = new Vector2(0.98f); // Avoid bleed into masking edge.\r\n\r\n            InternalChildren = new Drawable[]\r\n            {\r\n                circularProgress = new CircularProgress\r\n                {\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    Progress = 1f / lightAmount - angular_light_gap,\r\n                },\r\n                fillContent = new Container\r\n                {\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    Alpha = 0,\r\n                    Colour = colourProvider.Colour1(lyricEditorState.Mode),\r\n                    Children = new[]\r\n                    {\r\n                        new CircularProgress\r\n                        {\r\n                            RelativeSizeAxes = Axes.Both,\r\n                            Progress = 1f / lightAmount - angular_light_gap,\r\n                            Blending = BlendingParameters.Additive,\r\n                        },\r\n                        // Please do not try and make sense of this.\r\n                        // Getting the visual effect I was going for relies on what I can only imagine is broken implementation\r\n                        // of `PadExtent`. If that's ever fixed in the future this will likely need to be adjusted.\r\n                        Glow = new CircularProgress\r\n                        {\r\n                            RelativeSizeAxes = Axes.Both,\r\n                            Progress = 1f / lightAmount - 0.01f,\r\n                            Blending = BlendingParameters.Additive,\r\n                        }.WithEffect(new GlowEffect\r\n                        {\r\n                            Colour = colourProvider.Colour1(lyricEditorState.Mode).Opacity(0.4f),\r\n                            BlurSigma = new Vector2(9f),\r\n                            Strength = 10,\r\n                            PadExtent = true,\r\n                        }),\r\n                    },\r\n                },\r\n            };\r\n\r\n            updateColour();\r\n        }\r\n\r\n        private bool isTapped;\r\n\r\n        public bool IsTapped\r\n        {\r\n            get => isTapped;\r\n            set\r\n            {\r\n                if (value == isTapped)\r\n                    return;\r\n\r\n                isTapped = value;\r\n\r\n                updateColour();\r\n            }\r\n        }\r\n\r\n        private void updateColour()\r\n        {\r\n            circularProgress.Colour = isTapped ? colourProvider.Colour1(lyricEditorState.Mode) : colourProvider.Background2(lyricEditorState.Mode);\r\n        }\r\n\r\n        public override void Show()\r\n        {\r\n            fillContent\r\n                .FadeIn(50, Easing.OutQuint)\r\n                .FlashColour(Color4.White, 1000, Easing.OutQuint);\r\n        }\r\n\r\n        public override void Hide()\r\n        {\r\n            fillContent\r\n                .FadeOut(300, Easing.OutQuint);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/TimeTags/TimeTagAdjustConfigSection.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.UserInterfaceV2;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.TimeTags.Components;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.TimeTags;\r\n\r\npublic partial class TimeTagAdjustConfigSection : EditorSection\r\n{\r\n    protected override LocalisableString Title => \"Config\";\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(KaraokeRulesetLyricEditorConfigManager lyricEditorConfigManager, IEditTimeTagModeState editTimeTagModeState)\r\n    {\r\n        Children = new Drawable[]\r\n        {\r\n            new LabelledRealTimeSliderBar<float>\r\n            {\r\n                Label = \"Time range\",\r\n                Description = \"Change time-range to zoom-in/zoom-out the adjust area.\",\r\n                Current = editTimeTagModeState.BindableAdjustZoom,\r\n            },\r\n            new LabelledOpacityAdjustment\r\n            {\r\n                Label = \"Waveform\",\r\n                Description = \"Show/hide or change the opacity of the waveform.\",\r\n                Current = lyricEditorConfigManager.GetBindable<bool>(KaraokeRulesetLyricEditorSetting.AdjustTimeTagShowWaveform),\r\n                Opacity = lyricEditorConfigManager.GetBindable<float>(KaraokeRulesetLyricEditorSetting.AdjustTimeTagWaveformOpacity),\r\n            },\r\n            new LabelledOpacityAdjustment\r\n            {\r\n                Label = \"Ticks\",\r\n                Description = \"Show/hide or change the opacity of the ticks.\",\r\n                Current = lyricEditorConfigManager.GetBindable<bool>(KaraokeRulesetLyricEditorSetting.AdjustTimeTagShowTick),\r\n                Opacity = lyricEditorConfigManager.GetBindable<float>(KaraokeRulesetLyricEditorSetting.AdjustTimeTagTickOpacity),\r\n            },\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/TimeTags/TimeTagAutoGenerateSection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Components.Markdown;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Markdown;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.TimeTags;\r\n\r\npublic partial class TimeTagAutoGenerateSection : AutoGenerateSection\r\n{\r\n    protected override AutoGenerateSubsection CreateAutoGenerateSubsection()\r\n        => new TimeTageAutoGenerateSubsection();\r\n\r\n    private partial class TimeTageAutoGenerateSubsection : LyricEditorAutoGenerateSubsection\r\n    {\r\n        private const string language_mode = \"LANGUAGE_MODE\";\r\n\r\n        public TimeTageAutoGenerateSubsection()\r\n            : base(AutoGenerateType.AutoGenerateTimeTags)\r\n        {\r\n        }\r\n\r\n        protected override DescriptionFormat CreateInvalidDescriptionFormat()\r\n            => new()\r\n            {\r\n                Text = $\"Seems some lyric missing language, go to [{DescriptionFormat.LINK_KEY_ACTION}]({language_mode}) to fill the language.\",\r\n                Actions = new Dictionary<string, IDescriptionAction>\r\n                {\r\n                    {\r\n                        language_mode, new SwitchModeDescriptionAction\r\n                        {\r\n                            Text = \"edit language mode\",\r\n                            Mode = LyricEditorMode.EditLanguage,\r\n                        }\r\n                    },\r\n                },\r\n            };\r\n\r\n        protected override ConfigButton CreateConfigButton()\r\n            => new TimeTagAutoGenerateConfigButton();\r\n\r\n        protected partial class TimeTagAutoGenerateConfigButton : MultiConfigButton\r\n        {\r\n            protected override IEnumerable<KaraokeRulesetEditGeneratorSetting> AvailableSettings => new[]\r\n            {\r\n                KaraokeRulesetEditGeneratorSetting.JaTimeTagGeneratorConfig,\r\n                KaraokeRulesetEditGeneratorSetting.ZhTimeTagGeneratorConfig,\r\n            };\r\n\r\n            protected override string GetDisplayName(KaraokeRulesetEditGeneratorSetting setting) =>\r\n                setting switch\r\n                {\r\n                    KaraokeRulesetEditGeneratorSetting.JaTimeTagGeneratorConfig => \"Japanese\",\r\n                    KaraokeRulesetEditGeneratorSetting.ZhTimeTagGeneratorConfig => \"Chinese\",\r\n                    _ => throw new ArgumentOutOfRangeException(nameof(setting)),\r\n                };\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/TimeTags/TimeTagIssueSection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks.Issues;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Issues;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.TimeTags;\r\n\r\npublic partial class TimeTagIssueSection : LyricEditorIssueSection\r\n{\r\n    protected override LyricEditorMode EditMode => LyricEditorMode.EditTimeTag;\r\n\r\n    protected override LyricsIssueTable CreateLyricsIssueTable() => new TimeTagIssueTable();\r\n\r\n    private partial class TimeTagIssueTable : LyricsIssueTable\r\n    {\r\n        protected override TableColumn[] CreateHeaders() => new[]\r\n        {\r\n            new TableColumn(string.Empty, Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize, minSize: 30)),\r\n            new TableColumn(\"Lyric\", Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize, minSize: 40)),\r\n            new TableColumn(\"Position\", Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize, minSize: 60)),\r\n            new TableColumn(\"Message\", Anchor.CentreLeft),\r\n        };\r\n\r\n        protected override Drawable[] CreateContent(Issue issue)\r\n        {\r\n            (var lyric, TimeTag? timeTag) = getInvalidByIssue(issue);\r\n\r\n            // show the issue with the invalid time-tag.\r\n            if (timeTag != null)\r\n            {\r\n                return new Drawable[]\r\n                {\r\n                    new IssueIcon\r\n                    {\r\n                        Origin = Anchor.Centre,\r\n                        Size = new Vector2(10),\r\n                        Margin = new MarginPadding { Left = 10 },\r\n                        Issue = issue,\r\n                    },\r\n                    new OsuSpriteText\r\n                    {\r\n                        Text = $\"#{lyric.Order}\",\r\n                        Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold),\r\n                        Margin = new MarginPadding { Right = 10 },\r\n                    },\r\n                    new OsuSpriteText\r\n                    {\r\n                        Text = TextIndexUtils.PositionFormattedString(timeTag.Index),\r\n                        Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold),\r\n                        Margin = new MarginPadding { Right = 10 },\r\n                    },\r\n                    new TruncatingSpriteText\r\n                    {\r\n                        Text = issue.ToString(),\r\n                        RelativeSizeAxes = Axes.X,\r\n                        Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Medium),\r\n                    },\r\n                };\r\n            }\r\n\r\n            // show the default issue if not able to get the time-tag.\r\n            return new Drawable[]\r\n            {\r\n                new SpriteIcon\r\n                {\r\n                    Origin = Anchor.Centre,\r\n                    Size = new Vector2(10),\r\n                    Colour = issue.Template.Colour,\r\n                    Margin = new MarginPadding { Left = 10 },\r\n                    Icon = FontAwesome.Solid.AlignLeft,\r\n                },\r\n                new OsuSpriteText\r\n                {\r\n                    Text = $\"#{lyric.Order}\",\r\n                    Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold),\r\n                    Margin = new MarginPadding { Right = 10 },\r\n                },\r\n                new OsuSpriteText\r\n                {\r\n                    Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold),\r\n                    Margin = new MarginPadding { Right = 10 },\r\n                },\r\n                new TruncatingSpriteText\r\n                {\r\n                    Text = issue.ToString(),\r\n                    RelativeSizeAxes = Axes.X,\r\n                    Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Medium),\r\n                },\r\n            };\r\n        }\r\n\r\n        private Tuple<Lyric, TimeTag?> getInvalidByIssue(Issue issue) =>\r\n            issue switch\r\n            {\r\n                LyricTimeTagIssue timeTagIssue => new Tuple<Lyric, TimeTag?>(timeTagIssue.Lyric, timeTagIssue.TimeTag),\r\n                LyricIssue lyricIssue => new Tuple<Lyric, TimeTag?>(lyricIssue.Lyric, null),\r\n                _ => throw new InvalidCastException(),\r\n            };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/TimeTags/TimeTagRecordingConfigSection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Graphics.UserInterfaceV2;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.UserInterfaceV2;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition.Algorithms;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.TimeTags.Components;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.TimeTags;\r\n\r\npublic partial class TimeTagRecordingConfigSection : EditorSection\r\n{\r\n    protected override LocalisableString Title => \"Config\";\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(KaraokeRulesetLyricEditorConfigManager lyricEditorConfigManager, IEditTimeTagModeState editTimeTagModeState)\r\n    {\r\n        Children = new Drawable[]\r\n        {\r\n            new LabelledEnumDropdown<RecordingTimeTagCaretMoveMode>(true)\r\n            {\r\n                Label = \"Record tag mode\",\r\n                Description = \"Only record time with start/end time-tag while recording.\",\r\n                Current = lyricEditorConfigManager.GetBindable<RecordingTimeTagCaretMoveMode>(KaraokeRulesetLyricEditorSetting.RecordingTimeTagMovingCaretMode),\r\n            },\r\n            new LabelledSwitchButton\r\n            {\r\n                Label = \"Auto move to next tag\",\r\n                Description = \"Auto move to next time-tag if set time to current time-tag.\",\r\n                Current = lyricEditorConfigManager.GetBindable<bool>(KaraokeRulesetLyricEditorSetting.RecordingAutoMoveToNextTimeTag),\r\n            },\r\n            new LabelledSwitchButton\r\n            {\r\n                Label = \"Change the time by time-tag.\",\r\n                Description = \"Change the track time if change the recording caret while pausing.\",\r\n                Current = lyricEditorConfigManager.GetBindable<bool>(KaraokeRulesetLyricEditorSetting.RecordingChangeTimeWhileMovingTheCaret),\r\n            },\r\n            new LabelledRealTimeSliderBar<float>\r\n            {\r\n                Label = \"Time range\",\r\n                Description = \"Change time-range to zoom-in/zoom-out the recording area.\",\r\n                Current = editTimeTagModeState.BindableRecordZoom,\r\n            },\r\n            new LabelledOpacityAdjustment\r\n            {\r\n                Label = \"Waveform\",\r\n                Description = \"Show/hide or change the opacity of the waveform.\",\r\n                Current = lyricEditorConfigManager.GetBindable<bool>(KaraokeRulesetLyricEditorSetting.RecordingTimeTagShowWaveform),\r\n                Opacity = lyricEditorConfigManager.GetBindable<float>(KaraokeRulesetLyricEditorSetting.RecordingTimeTagWaveformOpacity),\r\n            },\r\n            new LabelledOpacityAdjustment\r\n            {\r\n                Label = \"Ticks\",\r\n                Description = \"Show/hide or change the opacity of the ticks.\",\r\n                Current = lyricEditorConfigManager.GetBindable<bool>(KaraokeRulesetLyricEditorSetting.RecordingTimeTagShowTick),\r\n                Opacity = lyricEditorConfigManager.GetBindable<float>(KaraokeRulesetLyricEditorSetting.RecordingTimeTagTickOpacity),\r\n            },\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/TimeTags/TimeTagRecordingToolSection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Localisation;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.TimeTags;\r\n\r\npublic partial class TimeTagRecordingToolSection : EditorSection\r\n{\r\n    protected override LocalisableString Title => \"Tool\";\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        Children = new Drawable[]\r\n        {\r\n            new RecordingTapControl(),\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/Settings/TimeTags/TimeTagSettingsHeader.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Markdown;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.TimeTags;\r\n\r\npublic partial class TimeTagSettingsHeader : LyricEditorSettingsHeader<TimeTagEditStep>\r\n{\r\n    protected override OverlayColourScheme CreateColourScheme()\r\n        => OverlayColourScheme.Orange;\r\n\r\n    protected override EditStepTabControl CreateTabControl()\r\n        => new RubyTagEditStepTabControl();\r\n\r\n    protected override DescriptionFormat GetSelectionDescription(TimeTagEditStep step) =>\r\n        step switch\r\n        {\r\n            TimeTagEditStep.Create => \"Create the time-tag or adjust the position.\",\r\n            TimeTagEditStep.Recording => new DescriptionFormat\r\n            {\r\n                Text =\r\n                    $\"Press [{DescriptionFormat.LINK_KEY_ACTION}](set_time_tag_time) at the right time to set current time to time-tag. Press [{DescriptionFormat.LINK_KEY_ACTION}](clear_time_tag_time) to clear the time-tag time.\",\r\n                Actions = new Dictionary<string, IDescriptionAction>\r\n                {\r\n                    {\r\n                        \"set_time_tag_time\", new InputKeyDescriptionAction\r\n                        {\r\n                            AdjustableActions = new List<KaraokeEditAction> { KaraokeEditAction.SetTime },\r\n                        }\r\n                    },\r\n                    {\r\n                        \"clear_time_tag_time\", new InputKeyDescriptionAction\r\n                        {\r\n                            AdjustableActions = new List<KaraokeEditAction> { KaraokeEditAction.ClearTime },\r\n                        }\r\n                    },\r\n                },\r\n            },\r\n            TimeTagEditStep.Adjust => \"Drag to adjust time-tag time precisely.\",\r\n            _ => throw new ArgumentOutOfRangeException(nameof(step), step, null),\r\n        };\r\n\r\n    private partial class RubyTagEditStepTabControl : EditStepTabControl\r\n    {\r\n        protected override StepTabButton CreateStepButton(OsuColour colours, TimeTagEditStep value)\r\n        {\r\n            return value switch\r\n            {\r\n                TimeTagEditStep.Create => new StepTabButton(value)\r\n                {\r\n                    Text = \"Create\",\r\n                    SelectedColour = colours.Blue,\r\n                    UnSelectedColour = colours.BlueDarker,\r\n                },\r\n                TimeTagEditStep.Recording => new StepTabButton(value)\r\n                {\r\n                    Text = \"Recording\",\r\n                    SelectedColour = colours.Red,\r\n                    UnSelectedColour = colours.RedDarker,\r\n                },\r\n                TimeTagEditStep.Adjust => new VerifyStepTabButton(value)\r\n                {\r\n                    Text = \"Adjust\",\r\n                    EditMode = LyricEditorMode.EditTimeTag,\r\n                    SelectedColour = colours.Yellow,\r\n                    UnSelectedColour = colours.YellowDarker,\r\n                },\r\n                _ => throw new ArgumentOutOfRangeException(nameof(value), value, null),\r\n            };\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/States/ILyricCaretState.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition.Algorithms;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\n\r\npublic interface ILyricCaretState\r\n{\r\n    ICaretPosition? HoverCaretPosition => BindableHoverCaretPosition.Value;\r\n\r\n    ICaretPosition? CaretPosition => BindableCaretPosition.Value;\r\n\r\n    RangeCaretPosition? RangeCaretPosition => BindableRangeCaretPosition.Value;\r\n\r\n    IBindable<ICaretPosition?> BindableHoverCaretPosition { get; }\r\n\r\n    IBindable<ICaretPosition?> BindableCaretPosition { get; }\r\n\r\n    IBindable<RangeCaretPosition?> BindableRangeCaretPosition { get; }\r\n\r\n    IBindable<Lyric?> BindableFocusedLyric { get; }\r\n\r\n    bool MoveCaret(MovingCaretAction action);\r\n\r\n    ICaretPosition? GetCaretPositionByAction(MovingCaretAction action);\r\n\r\n    bool MoveHoverCaretToTargetPosition(Lyric lyric);\r\n\r\n    bool MoveHoverCaretToTargetPosition<TIndex>(Lyric lyric, TIndex index) where TIndex : notnull;\r\n\r\n    bool ConfirmHoverCaretPosition();\r\n\r\n    bool ClearHoverCaretPosition();\r\n\r\n    bool MoveCaretToTargetPosition(Lyric lyric);\r\n\r\n    bool MoveCaretToTargetPosition<TIndex>(Lyric lyric, TIndex index) where TIndex : notnull;\r\n\r\n    bool StartDragging();\r\n\r\n    bool MoveDraggingCaretIndex<TIndex>(TIndex index) where TIndex : notnull;\r\n\r\n    bool EndDragging();\r\n\r\n    void SyncSelectedHitObjectWithCaret();\r\n\r\n    bool CaretEnabled { get; }\r\n\r\n    bool CaretDraggable { get; }\r\n\r\n    ICaretPositionAlgorithm? CaretPositionAlgorithm => BindableCaretPositionAlgorithm.Value;\r\n\r\n    IBindable<ICaretPositionAlgorithm?> BindableCaretPositionAlgorithm { get; }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/States/ILyricSelectionState.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\n\r\npublic interface ILyricSelectionState\r\n{\r\n    IBindable<bool> Selecting { get; }\r\n\r\n    IBindableDictionary<Lyric, LocalisableString> DisableSelectingLyric { get; }\r\n\r\n    IBindableList<Lyric> SelectedLyrics { get; }\r\n\r\n    Action<LyricEditorSelectingAction>? Action { get; set; }\r\n\r\n    void StartSelecting();\r\n\r\n    void EndSelecting(LyricEditorSelectingAction action);\r\n\r\n    void Select(Lyric lyric);\r\n\r\n    void UnSelect(Lyric lyric);\r\n\r\n    void SelectAll();\r\n\r\n    void UnSelectAll();\r\n\r\n    void UpdateDisableLyricList(IDictionary<Lyric, LocalisableString> disableLyrics);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/States/LyricCaretState.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition.Algorithms;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\n\r\npublic partial class LyricCaretState : Component, ILyricCaretState\r\n{\r\n    public IBindable<ICaretPosition?> BindableHoverCaretPosition => bindableHoverCaretPosition;\r\n    public IBindable<ICaretPosition?> BindableCaretPosition => bindableCaretPosition;\r\n    public IBindable<RangeCaretPosition?> BindableRangeCaretPosition => bindableRangeCaretPosition;\r\n    public IBindable<Lyric?> BindableFocusedLyric => bindableFocusedLyric;\r\n\r\n    private readonly Bindable<ICaretPosition?> bindableHoverCaretPosition = new();\r\n    private readonly Bindable<ICaretPosition?> bindableCaretPosition = new();\r\n    private readonly Bindable<RangeCaretPosition?> bindableRangeCaretPosition = new();\r\n    private readonly Bindable<Lyric?> bindableFocusedLyric = new();\r\n\r\n    public IBindable<ICaretPositionAlgorithm?> BindableCaretPositionAlgorithm => bindableCaretPositionAlgorithm;\r\n\r\n    private ICaretPositionAlgorithm? algorithm => bindableCaretPositionAlgorithm.Value;\r\n    private readonly Bindable<ICaretPositionAlgorithm?> bindableCaretPositionAlgorithm = new();\r\n\r\n    private readonly IBindableList<Lyric> bindableLyrics = new BindableList<Lyric>();\r\n\r\n    private readonly IBindable<EditorModeWithEditStep> bindableModeWithEditStep = new Bindable<EditorModeWithEditStep>();\r\n\r\n    // it might be special for create time-tag mode.\r\n    private readonly IBindable<RubyTagEditMode> bindableRubyTagEditMode = new Bindable<RubyTagEditMode>();\r\n    private readonly IBindable<RecordingTimeTagCaretMoveMode> bindableRecordingMovingCaretMode = new Bindable<RecordingTimeTagCaretMoveMode>();\r\n    private readonly IBindable<bool> bindableRecordingChangeTimeWhileMovingTheCaret = new Bindable<bool>();\r\n\r\n    [Resolved]\r\n    private EditorBeatmap beatmap { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private EditorClock editorClock { get; set; } = null!;\r\n\r\n    public LyricCaretState()\r\n    {\r\n        bindableLyrics.BindCollectionChanged((a, b) =>\r\n        {\r\n            // should reset caret position if not in the list.\r\n            var caretLyric = BindableFocusedLyric.Value;\r\n\r\n            // should adjust hover lyric if lyric has been deleted.\r\n            if (caretLyric != null && !bindableLyrics.Contains(caretLyric))\r\n            {\r\n                // if delete the current lyric, most of cases should move up.\r\n                MoveCaret(MovingCaretAction.PreviousLyric);\r\n            }\r\n\r\n            refreshAlgorithmAndCaretPosition();\r\n        });\r\n\r\n        bindableModeWithEditStep.BindValueChanged(e =>\r\n        {\r\n            // Should refresh algorithm until all component loaded.\r\n            Schedule(refreshAlgorithmAndCaretPosition);\r\n        });\r\n\r\n        bindableRubyTagEditMode.BindValueChanged(_ =>\r\n        {\r\n            refreshAlgorithmAndCaretPosition();\r\n        });\r\n\r\n        bindableRecordingMovingCaretMode.BindValueChanged(_ =>\r\n        {\r\n            refreshAlgorithmAndCaretPosition();\r\n        });\r\n\r\n        bindableCaretPosition.BindValueChanged(e =>\r\n        {\r\n            bindableFocusedLyric.Value = e.NewValue?.Lyric;\r\n        });\r\n\r\n        refreshAlgorithmAndCaretPosition();\r\n\r\n        // should move the caret to first.\r\n        MoveCaret(MovingCaretAction.FirstLyric);\r\n    }\r\n\r\n    private void refreshAlgorithmAndCaretPosition()\r\n    {\r\n        // refresh algorithm\r\n        bindableCaretPositionAlgorithm.Value = getAlgorithmByMode(bindableModeWithEditStep.Value);\r\n\r\n        // refresh caret position\r\n        var lyric = bindableCaretPosition.Value?.Lyric;\r\n        bindableHoverCaretPosition.Value = null;\r\n        bindableCaretPosition.Value = getCaretPosition(algorithm, lyric);\r\n        bindableRangeCaretPosition.Value = null;\r\n\r\n        // should update selection if selected lyric changed.\r\n        postProcess();\r\n\r\n        static ICaretPosition? getCaretPosition(ICaretPositionAlgorithm? algorithm, Lyric? lyric)\r\n        {\r\n            if (algorithm == null)\r\n                return null;\r\n\r\n            if (lyric == null)\r\n                return algorithm.MoveToFirstLyric();\r\n\r\n            return algorithm.MoveToTargetLyric(lyric);\r\n        }\r\n    }\r\n\r\n    private ICaretPositionAlgorithm? getAlgorithmByMode(EditorModeWithEditStep editorModeWithEditStep)\r\n    {\r\n        var lyrics = bindableLyrics.ToArray();\r\n        var mode = editorModeWithEditStep.Mode;\r\n\r\n        return mode switch\r\n        {\r\n            LyricEditorMode.View => null,\r\n            LyricEditorMode.EditText => getTextModeAlgorithm(editorModeWithEditStep.GetEditStep<TextEditStep>()),\r\n            LyricEditorMode.EditReferenceLyric => new NavigateCaretPositionAlgorithm(lyrics),\r\n            LyricEditorMode.EditLanguage => new ClickingCaretPositionAlgorithm(lyrics),\r\n            LyricEditorMode.EditRuby => getRubyTagModeAlgorithm(),\r\n            LyricEditorMode.EditTimeTag => getTimeTagModeAlgorithm(editorModeWithEditStep.GetEditStep<TimeTagEditStep>()),\r\n            LyricEditorMode.EditRomanisation => new NavigateCaretPositionAlgorithm(lyrics),\r\n            LyricEditorMode.EditNote => new NavigateCaretPositionAlgorithm(lyrics),\r\n            LyricEditorMode.EditSinger => new NavigateCaretPositionAlgorithm(lyrics),\r\n            _ => throw new InvalidOperationException(nameof(mode)),\r\n        };\r\n\r\n        ICaretPositionAlgorithm getTextModeAlgorithm(TextEditStep textEditMode) =>\r\n            textEditMode switch\r\n            {\r\n                TextEditStep.Typing => new TypingCaretPositionAlgorithm(lyrics),\r\n                TextEditStep.Split => new CuttingCaretPositionAlgorithm(lyrics),\r\n                TextEditStep.Verify => new NavigateCaretPositionAlgorithm(lyrics),\r\n                _ => throw new InvalidOperationException(nameof(textEditMode)),\r\n            };\r\n\r\n        ICaretPositionAlgorithm getRubyTagModeAlgorithm() =>\r\n            bindableRubyTagEditMode.Value switch\r\n            {\r\n                RubyTagEditMode.Create => new CreateRubyTagCaretPositionAlgorithm(lyrics),\r\n                RubyTagEditMode.Modify => new NavigateCaretPositionAlgorithm(lyrics),\r\n                _ => throw new InvalidOperationException(nameof(bindableRubyTagEditMode.Value)),\r\n            };\r\n\r\n        ICaretPositionAlgorithm getTimeTagModeAlgorithm(TimeTagEditStep timeTagEditMode) =>\r\n            timeTagEditMode switch\r\n            {\r\n                TimeTagEditStep.Create => new CreateRemoveTimeTagCaretPositionAlgorithm(lyrics),\r\n                TimeTagEditStep.Recording => new RecordingTimeTagCaretPositionAlgorithm(lyrics) { Mode = bindableRecordingMovingCaretMode.Value },\r\n                TimeTagEditStep.Adjust => new NavigateCaretPositionAlgorithm(lyrics),\r\n                _ => throw new InvalidOperationException(nameof(timeTagEditMode)),\r\n            };\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(ILyricsProvider lyricsProvider,\r\n                      ILyricEditorState state,\r\n                      KaraokeRulesetLyricEditorConfigManager lyricEditorConfigManager,\r\n                      IEditRubyModeState editRubyModeState)\r\n    {\r\n        bindableLyrics.BindTo(lyricsProvider.BindableLyrics);\r\n\r\n        bindableModeWithEditStep.BindTo(state.BindableModeWithEditStep);\r\n\r\n        bindableRubyTagEditMode.BindTo(editRubyModeState.BindableRubyTagEditMode);\r\n        lyricEditorConfigManager.BindWith(KaraokeRulesetLyricEditorSetting.RecordingTimeTagMovingCaretMode, bindableRecordingMovingCaretMode);\r\n        lyricEditorConfigManager.BindWith(KaraokeRulesetLyricEditorSetting.RecordingChangeTimeWhileMovingTheCaret, bindableRecordingChangeTimeWhileMovingTheCaret);\r\n    }\r\n\r\n    public bool MoveCaret(MovingCaretAction action)\r\n    {\r\n        var position = GetCaretPositionByAction(action);\r\n        return moveCaretToTargetPosition(position);\r\n    }\r\n\r\n    public ICaretPosition? GetCaretPositionByAction(MovingCaretAction action)\r\n    {\r\n        if (algorithm == null)\r\n            return null;\r\n\r\n        var rangeCaretPosition = bindableRangeCaretPosition.Value;\r\n\r\n        if (rangeCaretPosition != null)\r\n        {\r\n            if (algorithm is not IIndexCaretPositionAlgorithm indexCaretPositionAlgorithm)\r\n                throw new InvalidOperationException(\"Must using the index caret position algorithm if has range caret position.\");\r\n\r\n            var startPosition = rangeCaretPosition.Start;\r\n            var (smallerPosition, largerPosition) = rangeCaretPosition.GetRangeCaretPosition();\r\n\r\n            return action switch\r\n            {\r\n                MovingCaretAction.PreviousLyric => algorithm.MoveToPreviousLyric(startPosition),\r\n                MovingCaretAction.NextLyric => algorithm.MoveToNextLyric(startPosition),\r\n                MovingCaretAction.FirstLyric => algorithm.MoveToFirstLyric(),\r\n                MovingCaretAction.LastLyric => algorithm.MoveToLastLyric(),\r\n                MovingCaretAction.PreviousIndex => isTypingCaret(algorithm) ? smallerPosition : performMoveToPreviousIndex(algorithm, smallerPosition),\r\n                MovingCaretAction.NextIndex => isTypingCaret(algorithm) ? largerPosition : performMoveToNextIndex(algorithm, largerPosition),\r\n                MovingCaretAction.FirstIndex => indexCaretPositionAlgorithm.MoveToFirstIndex(smallerPosition.Lyric),\r\n                MovingCaretAction.LastIndex => indexCaretPositionAlgorithm.MoveToLastIndex(largerPosition.Lyric),\r\n                _ => throw new InvalidOperationException(nameof(action)),\r\n            };\r\n\r\n            static bool isTypingCaret(ICaretPositionAlgorithm algorithm)\r\n                => algorithm is TypingCaretPositionAlgorithm;\r\n        }\r\n\r\n        var currentPosition = bindableCaretPosition.Value;\r\n\r\n        if (currentPosition != null)\r\n        {\r\n            return action switch\r\n            {\r\n                MovingCaretAction.PreviousLyric => algorithm.MoveToPreviousLyric(currentPosition),\r\n                MovingCaretAction.NextLyric => algorithm.MoveToNextLyric(currentPosition),\r\n                MovingCaretAction.FirstLyric => algorithm.MoveToFirstLyric(),\r\n                MovingCaretAction.LastLyric => algorithm.MoveToLastLyric(),\r\n                MovingCaretAction.PreviousIndex => performMoveToPreviousIndex(algorithm, currentPosition),\r\n                MovingCaretAction.NextIndex => performMoveToNextIndex(algorithm, currentPosition),\r\n                MovingCaretAction.FirstIndex => performMoveToFirstIndex(algorithm, currentPosition),\r\n                MovingCaretAction.LastIndex => performMoveToLastIndex(algorithm, currentPosition),\r\n                _ => throw new InvalidOperationException(nameof(action)),\r\n            };\r\n        }\r\n\r\n        return null;\r\n\r\n        static ICaretPosition? performMoveToPreviousIndex(ICaretPositionAlgorithm algorithm, ICaretPosition caretPosition) =>\r\n            performMoveCaret(algorithm, caretPosition,\r\n                (a, c) => a.MoveToPreviousIndex(c),\r\n                (a, c) => a.MoveToPreviousLyric(c),\r\n                (a, c) => a.MoveToLastIndex(c.Lyric));\r\n\r\n        static ICaretPosition? performMoveToNextIndex(ICaretPositionAlgorithm algorithm, ICaretPosition caretPosition) =>\r\n            performMoveCaret(algorithm, caretPosition,\r\n                (a, c) => a.MoveToNextIndex(c),\r\n                (a, c) => a.MoveToNextLyric(c),\r\n                (a, c) => a.MoveToFirstIndex(c.Lyric));\r\n\r\n        static ICaretPosition? performMoveToFirstIndex(ICaretPositionAlgorithm algorithm, ICaretPosition caretPosition) =>\r\n            performMoveCaret(algorithm, caretPosition,\r\n                (a, c) => a.MoveToFirstIndex(c.Lyric),\r\n                (a, c) => a.MoveToPreviousLyric(c),\r\n                (a, c) => a.MoveToFirstIndex(c.Lyric));\r\n\r\n        static ICaretPosition? performMoveToLastIndex(ICaretPositionAlgorithm algorithm, ICaretPosition caretPosition) =>\r\n            performMoveCaret(algorithm, caretPosition,\r\n                (a, c) => a.MoveToLastIndex(c.Lyric),\r\n                (a, c) => a.MoveToNextLyric(c),\r\n                (a, c) => a.MoveToLastIndex(c.Lyric));\r\n\r\n        static ICaretPosition? performMoveCaret(ICaretPositionAlgorithm algorithm, ICaretPosition caretPosition,\r\n                                                Func<IIndexCaretPositionAlgorithm, IIndexCaretPosition, ICaretPosition?> action,\r\n                                                Func<IIndexCaretPositionAlgorithm, ICaretPosition, ICaretPosition?> switchLyricAction,\r\n                                                Func<IIndexCaretPositionAlgorithm, ICaretPosition, ICaretPosition?> getNewCaretAction)\r\n        {\r\n            if (algorithm is not IIndexCaretPositionAlgorithm indexCaretPositionAlgorithm)\r\n                return null;\r\n\r\n            if (caretPosition is not IIndexCaretPosition indexCaretPosition)\r\n                throw new InvalidOperationException();\r\n\r\n            // will have three cases in here:\r\n            // 1. got duplicated value (means it's not valid to move left and right)\r\n            // 2. got the same value (means it's not valid to move left and right)\r\n            // 3. got unique value (OK to return the value)\r\n            var movedCaretPosition = action(indexCaretPositionAlgorithm, indexCaretPosition);\r\n            if (movedCaretPosition != null && !EqualityComparer<ICaretPosition>.Default.Equals(movedCaretPosition, caretPosition))\r\n                return movedCaretPosition;\r\n\r\n            // if the caret is not valid to go, then trying to find the valid caret position in the different lyric.\r\n            var newLyricCaretPosition = switchLyricAction(indexCaretPositionAlgorithm, caretPosition);\r\n            return newLyricCaretPosition == null ? null : getNewCaretAction(indexCaretPositionAlgorithm, newLyricCaretPosition);\r\n        }\r\n    }\r\n\r\n    public bool MoveHoverCaretToTargetPosition(Lyric lyric)\r\n    {\r\n        var caretPosition = algorithm?.MoveToTargetLyric(lyric);\r\n        return moveHoverCaretToTargetPosition(caretPosition);\r\n    }\r\n\r\n    public bool MoveHoverCaretToTargetPosition<TIndex>(Lyric lyric, TIndex index)\r\n        where TIndex : notnull\r\n    {\r\n        if (algorithm is not IIndexCaretPositionAlgorithm indexCaretPositionAlgorithm)\r\n            return false;\r\n\r\n        var caretPosition = indexCaretPositionAlgorithm.MoveToTargetLyric(lyric, index);\r\n        return moveHoverCaretToTargetPosition(caretPosition);\r\n    }\r\n\r\n    private bool moveHoverCaretToTargetPosition(ICaretPosition? position)\r\n    {\r\n        if (position == null)\r\n            return false;\r\n\r\n        bindableHoverCaretPosition.Value = position;\r\n\r\n        return true;\r\n    }\r\n\r\n    public bool ConfirmHoverCaretPosition()\r\n    {\r\n        // place hover caret to target position.\r\n        var position = BindableHoverCaretPosition.Value;\r\n        return moveCaretToTargetPosition(position);\r\n    }\r\n\r\n    public bool ClearHoverCaretPosition()\r\n    {\r\n        bindableHoverCaretPosition.Value = null;\r\n\r\n        return true;\r\n    }\r\n\r\n    public bool MoveCaretToTargetPosition(Lyric lyric)\r\n    {\r\n        var caretPosition = algorithm?.MoveToTargetLyric(lyric);\r\n        return moveCaretToTargetPosition(caretPosition);\r\n    }\r\n\r\n    public bool MoveCaretToTargetPosition<TIndex>(Lyric lyric, TIndex index)\r\n        where TIndex : notnull\r\n    {\r\n        if (algorithm is not IIndexCaretPositionAlgorithm indexCaretPositionAlgorithm)\r\n            return false;\r\n\r\n        var caretPosition = indexCaretPositionAlgorithm.MoveToTargetLyric(lyric, index);\r\n        return moveCaretToTargetPosition(caretPosition);\r\n    }\r\n\r\n    public bool StartDragging()\r\n    {\r\n        if (!CaretDraggable)\r\n            throw new InvalidOperationException(\"Should not call this method if the caret is not draggable\");\r\n\r\n        var caretPosition = bindableHoverCaretPosition.Value;\r\n        if (caretPosition is not IIndexCaretPosition indexCaretPosition)\r\n            throw new InvalidOperationException($\"Binding caret position should have value and the type should be {typeof(IIndexCaretPosition)}.\");\r\n\r\n        return moveRangeCaretToTargetPosition(indexCaretPosition, indexCaretPosition, RangeCaretDraggingState.StartDrag);\r\n    }\r\n\r\n    public bool MoveDraggingCaretIndex<TIndex>(TIndex index)\r\n        where TIndex : notnull\r\n    {\r\n        if (!CaretDraggable)\r\n            throw new InvalidOperationException(\"Should not call this method if the caret is not draggable\");\r\n\r\n        var rangeCaretPosition = bindableRangeCaretPosition.Value;\r\n        if (rangeCaretPosition == null)\r\n            throw new InvalidOperationException(\"Binding range caret position should not be null.\");\r\n\r\n        if (algorithm is not IIndexCaretPositionAlgorithm indexCaretPositionAlgorithm)\r\n            throw new InvalidOperationException(\"Algorithm should be index caret position algorithm.\");\r\n\r\n        var startCaretPosition = rangeCaretPosition.Start;\r\n        var endCaretPosition = indexCaretPositionAlgorithm.MoveToTargetLyric(startCaretPosition.Lyric, index);\r\n        return moveRangeCaretToTargetPosition(startCaretPosition, endCaretPosition, RangeCaretDraggingState.Dragging);\r\n    }\r\n\r\n    public bool EndDragging()\r\n    {\r\n        if (!CaretDraggable)\r\n            throw new InvalidOperationException(\"Should not call this method if the caret is not draggable\");\r\n\r\n        var rangeCaretPosition = bindableRangeCaretPosition.Value;\r\n        if (rangeCaretPosition == null)\r\n            throw new InvalidOperationException(\"Binding range caret position should not be null.\");\r\n\r\n        return moveRangeCaretToTargetPosition(rangeCaretPosition.Start, rangeCaretPosition.End, RangeCaretDraggingState.EndDrag);\r\n    }\r\n\r\n    private bool moveCaretToTargetPosition(ICaretPosition? position)\r\n    {\r\n        if (position == null)\r\n            return false;\r\n\r\n        bindableHoverCaretPosition.Value = null;\r\n        bindableCaretPosition.Value = position;\r\n        bindableRangeCaretPosition.Value = null;\r\n\r\n        postProcess();\r\n\r\n        return true;\r\n    }\r\n\r\n    private bool moveRangeCaretToTargetPosition(IIndexCaretPosition startCaretPosition, IIndexCaretPosition? endCaretPosition, RangeCaretDraggingState draggingState)\r\n    {\r\n        if (endCaretPosition == null)\r\n            return false;\r\n\r\n        bindableHoverCaretPosition.Value = null;\r\n        bindableCaretPosition.Value = null;\r\n        bindableRangeCaretPosition.Value = new RangeCaretPosition(startCaretPosition, endCaretPosition, draggingState);\r\n\r\n        if (draggingState == RangeCaretDraggingState.EndDrag)\r\n            postProcess();\r\n\r\n        return true;\r\n    }\r\n\r\n    public void SyncSelectedHitObjectWithCaret()\r\n    {\r\n        // should wait until beatmap loaded.\r\n        Schedule(() =>\r\n        {\r\n            beatmap.SelectedHitObjects.Clear();\r\n\r\n            if (bindableRangeCaretPosition.Value is RangeCaretPosition rangeCaretPosition)\r\n            {\r\n                var selectedLyrics = beatmap.HitObjects.OfType<Lyric>().Where(x => rangeCaretPosition.IsInRange(x));\r\n                beatmap.SelectedHitObjects.AddRange(selectedLyrics);\r\n            }\r\n            else if (bindableCaretPosition.Value?.Lyric != null)\r\n            {\r\n                beatmap.SelectedHitObjects.Add(bindableCaretPosition.Value.Lyric);\r\n            }\r\n        });\r\n    }\r\n\r\n    public bool CaretEnabled => algorithm != null;\r\n\r\n    public bool CaretDraggable =>\r\n        algorithm switch\r\n        {\r\n            TypingCaretPositionAlgorithm => true,\r\n            CreateRubyTagCaretPositionAlgorithm => true,\r\n            _ => false,\r\n        };\r\n\r\n    private void postProcess()\r\n    {\r\n        SyncSelectedHitObjectWithCaret();\r\n\r\n        var caretPosition = bindableCaretPosition.Value;\r\n        navigateToTimeByCaretPosition(caretPosition);\r\n\r\n        void navigateToTimeByCaretPosition(ICaretPosition? position)\r\n        {\r\n            if (position is not RecordingTimeTagCaretPosition recordingTimeTagCaretPosition)\r\n                return;\r\n\r\n            double? timeTagTime = recordingTimeTagCaretPosition.TimeTag.Time;\r\n            if (timeTagTime.HasValue && !editorClock.IsRunning && bindableRecordingChangeTimeWhileMovingTheCaret.Value)\r\n                editorClock.SeekSmoothlyTo(timeTagTime.Value);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/States/LyricEditorSelectingAction.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\n\r\npublic enum LyricEditorSelectingAction\r\n{\r\n    Apply,\r\n\r\n    Cancel,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/States/LyricSelectionState.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\n\r\npublic partial class LyricSelectionState : Component, ILyricSelectionState\r\n{\r\n    public IBindable<bool> Selecting => selecting;\r\n\r\n    private readonly BindableDictionary<Lyric, LocalisableString> bindableDisableSelectingLyric = new();\r\n    private readonly BindableList<Lyric> bindableSelectedLyrics = new();\r\n\r\n    public IBindableDictionary<Lyric, LocalisableString> DisableSelectingLyric => bindableDisableSelectingLyric;\r\n\r\n    public IBindableList<Lyric> SelectedLyrics => bindableSelectedLyrics;\r\n\r\n    public Action<LyricEditorSelectingAction>? Action { get; set; }\r\n\r\n    [Resolved]\r\n    private EditorBeatmap beatmap { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private ILyricCaretState lyricCaretState { get; set; } = null!;\r\n\r\n    private readonly BindableBool selecting = new();\r\n\r\n    public void StartSelecting()\r\n    {\r\n        if (selecting.Value)\r\n            throw new NotSupportedException(\"Selecting already started.\");\r\n\r\n        selecting.Value = true;\r\n    }\r\n\r\n    public void EndSelecting(LyricEditorSelectingAction action)\r\n    {\r\n        if (!selecting.Value)\r\n            return;\r\n\r\n        // should sync selection to editor beatmap because auto-generate will be apply to those lyric that being selected.\r\n        var selectedLyrics = bindableSelectedLyrics.ToArray();\r\n        beatmap.SelectedHitObjects.Clear();\r\n        beatmap.SelectedHitObjects.AddRange(selectedLyrics);\r\n\r\n        Action?.Invoke(action);\r\n\r\n        // after being applied, should clear the selection.\r\n        beatmap.SelectedHitObjects.Clear();\r\n\r\n        // should clear the selection after finish.\r\n        bindableSelectedLyrics.Clear();\r\n\r\n        // for able to check if still selecting, should make sure that every process step has been finished.\r\n        selecting.Value = false;\r\n\r\n        // should add selected lyric back.\r\n        lyricCaretState.SyncSelectedHitObjectWithCaret();\r\n    }\r\n\r\n    public void Select(Lyric lyric)\r\n    {\r\n        if (!selecting.Value)\r\n            throw new NotSupportedException(\"Should not add the lyric if not in the selecting state.\");\r\n\r\n        if (bindableSelectedLyrics.Contains(lyric))\r\n            return;\r\n\r\n        if (bindableDisableSelectingLyric.ContainsKey(lyric))\r\n            return;\r\n\r\n        bindableSelectedLyrics.Add(lyric);\r\n    }\r\n\r\n    public void UnSelect(Lyric lyric)\r\n    {\r\n        if (!selecting.Value)\r\n            throw new NotSupportedException(\"Should not remove the lyric if not in the selecting state.\");\r\n\r\n        bindableSelectedLyrics.Remove(lyric);\r\n    }\r\n\r\n    public void SelectAll()\r\n    {\r\n        if (!selecting.Value)\r\n            throw new NotSupportedException(\"Should not select the lyric if not in the selecting state.\");\r\n\r\n        var lyrics = beatmap.HitObjects.OfType<Lyric>();\r\n\r\n        foreach (var lyric in lyrics)\r\n        {\r\n            Select(lyric);\r\n        }\r\n    }\r\n\r\n    public void UnSelectAll()\r\n    {\r\n        if (!selecting.Value)\r\n            throw new NotSupportedException(\"Should not clear the selected lyric if not in the selecting state.\");\r\n\r\n        bindableSelectedLyrics.Clear();\r\n    }\r\n\r\n    public void UpdateDisableLyricList(IDictionary<Lyric, LocalisableString> disableLyrics)\r\n    {\r\n        if (selecting.Value)\r\n            throw new NotSupportedException(\"Should not update the disable lyric list while selecting.\");\r\n\r\n        bindableDisableSelectingLyric.Clear();\r\n\r\n        foreach (var (lyric, reason) in disableLyrics)\r\n            bindableDisableSelectingLyric.Add(lyric, reason);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/States/Modes/CreateTimeTagType.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\n\r\npublic enum CreateTimeTagType\r\n{\r\n    /// <summary>\r\n    /// Use mouse to move the caret, and click the button in the UI to create/remove the start/end time tag.\r\n    /// It's the slowest way to create the time tag.\r\n    /// </summary>\r\n    Mouse,\r\n\r\n    /// <summary>\r\n    /// Press the hotkey to prepare create/remove the start/end time tag, click the character in the lyric to confirm.\r\n    /// It might be useful for those english-like lyric.\r\n    /// </summary>\r\n    HotkeyThenPress,\r\n\r\n    /// <summary>\r\n    /// Use keyboard to move the caret, and press hotkey to create/remove the start/end time tag.\r\n    /// It's the fastest way to create the time tag for Japanese/Chinses lyric.\r\n    /// </summary>\r\n    Keyboard,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/States/Modes/EditLanguageModeState.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Language;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\n\r\npublic partial class EditLanguageModeState : Component, IEditLanguageModeState\r\n{\r\n    public Bindable<LanguageEditModeSpecialAction> BindableSpecialAction { get; } = new();\r\n\r\n    public Bindable<LanguageEditStep> BindableEditStep { get; } = new();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/States/Modes/EditNoteModeState.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Notes;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\nusing osu.Game.Rulesets.Objects;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\n\r\npublic partial class EditNoteModeState : ModeStateWithBlueprintContainer<Note>, IEditNoteModeState\r\n{\r\n    private readonly BindableList<HitObject> selectedHitObjects = new();\r\n\r\n    [Resolved]\r\n    private EditorBeatmap editorBeatmap { get; set; } = null!;\r\n\r\n    public Bindable<NoteEditStep> BindableEditStep { get; } = new();\r\n\r\n    public Bindable<NoteEditModeSpecialAction> BindableSpecialAction { get; } = new();\r\n\r\n    public Bindable<NoteEditPropertyMode> BindableNoteEditPropertyMode { get; } = new();\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        BindablesUtils.Sync(SelectedItems, selectedHitObjects);\r\n        selectedHitObjects.BindTo(editorBeatmap.SelectedHitObjects);\r\n    }\r\n\r\n    protected override bool IsWriteLyricPropertyLocked(Lyric lyric)\r\n        => HitObjectWritableUtils.IsCreateOrRemoveNoteLocked(lyric);\r\n\r\n    protected override bool SelectFirstProperty(Lyric lyric)\r\n        => BindableEditStep.Value == NoteEditStep.Edit;\r\n\r\n    protected override IEnumerable<Note> SelectableProperties(Lyric lyric)\r\n        => EditorBeatmapUtils.GetNotesByLyric(editorBeatmap, lyric);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/States/Modes/EditReferenceLyricModeState.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\n\r\npublic partial class EditReferenceLyricModeState : Component, IEditReferenceLyricModeState\r\n{\r\n    public Bindable<ReferenceLyricEditStep> BindableEditStep { get; } = new();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/States/Modes/EditRomanisationModeState.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\n\r\npublic partial class EditRomanisationModeState : ModeStateWithBlueprintContainer<TimeTag>, IEditRomanisationModeState\r\n{\r\n    public Bindable<RomanisationTagEditStep> BindableEditStep { get; } = new();\r\n\r\n    protected override bool IsWriteLyricPropertyLocked(Lyric lyric)\r\n        => HitObjectWritableUtils.IsWriteLyricPropertyLocked(lyric, nameof(Lyric.TimeTags));\r\n\r\n    protected override bool SelectFirstProperty(Lyric lyric)\r\n        => BindableEditStep.Value == RomanisationTagEditStep.Edit;\r\n\r\n    protected override IEnumerable<TimeTag> SelectableProperties(Lyric lyric)\r\n        => lyric.TimeTags;\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/States/Modes/EditRubyModeState.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\n\r\npublic partial class EditRubyModeState : ModeStateWithBlueprintContainer<RubyTag>, IEditRubyModeState\r\n{\r\n    public Bindable<RubyTagEditStep> BindableEditStep { get; } = new();\r\n\r\n    public Bindable<RubyTagEditMode> BindableRubyTagEditMode { get; } = new();\r\n\r\n    protected override bool IsWriteLyricPropertyLocked(Lyric lyric)\r\n        => HitObjectWritableUtils.IsWriteLyricPropertyLocked(lyric, nameof(Lyric.RubyTags));\r\n\r\n    protected override bool SelectFirstProperty(Lyric lyric)\r\n        => BindableEditStep.Value == RubyTagEditStep.Edit;\r\n\r\n    protected override IEnumerable<RubyTag> SelectableProperties(Lyric lyric)\r\n        => lyric.RubyTags;\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/States/Modes/EditTextModeState.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Text;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\n\r\npublic partial class EditTextModeState : Component, IEditTextModeState\r\n{\r\n    public Bindable<TextEditStep> BindableEditStep { get; } = new();\r\n\r\n    public Bindable<TextEditModeSpecialAction> BindableSpecialAction { get; } = new();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/States/Modes/EditTimeTagModeState.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\n\r\npublic partial class EditTimeTagModeState : ModeStateWithBlueprintContainer<TimeTag>, IEditTimeTagModeState\r\n{\r\n    public Bindable<TimeTagEditStep> BindableEditStep { get; } = new();\r\n\r\n    public BindableFloat BindableRecordZoom { get; } = new();\r\n\r\n    public BindableFloat BindableAdjustZoom { get; } = new();\r\n\r\n    public Bindable<CreateTimeTagType> BindableCreateType { get; } = new();\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(EditorClock editorClock)\r\n    {\r\n        BindableRecordZoom.MaxValue = ZoomableScrollContainerUtils.GetZoomLevelForVisibleMilliseconds(editorClock, 800);\r\n        BindableRecordZoom.MinValue = ZoomableScrollContainerUtils.GetZoomLevelForVisibleMilliseconds(editorClock, 4000);\r\n        BindableRecordZoom.Value = BindableRecordZoom.Default = ZoomableScrollContainerUtils.GetZoomLevelForVisibleMilliseconds(editorClock, 2000);\r\n\r\n        BindableAdjustZoom.MaxValue = ZoomableScrollContainerUtils.GetZoomLevelForVisibleMilliseconds(editorClock, 1600);\r\n        BindableAdjustZoom.MinValue = ZoomableScrollContainerUtils.GetZoomLevelForVisibleMilliseconds(editorClock, 8000);\r\n        BindableAdjustZoom.Value = BindableAdjustZoom.Default = ZoomableScrollContainerUtils.GetZoomLevelForVisibleMilliseconds(editorClock, 4000);\r\n    }\r\n\r\n    protected override bool IsWriteLyricPropertyLocked(Lyric lyric)\r\n        => HitObjectWritableUtils.IsWriteLyricPropertyLocked(lyric, nameof(Lyric.TimeTags));\r\n\r\n    protected override bool SelectFirstProperty(Lyric lyric)\r\n        => false;\r\n\r\n    protected override IEnumerable<TimeTag> SelectableProperties(Lyric lyric)\r\n        => Array.Empty<TimeTag>();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/States/Modes/IEditLanguageModeState.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Language;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\n\r\npublic interface IEditLanguageModeState : IHasEditStep<LanguageEditStep>, IHasSpecialAction<LanguageEditModeSpecialAction>;\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/States/Modes/IEditNoteModeState.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Notes;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\n\r\npublic interface IEditNoteModeState : IHasEditStep<NoteEditStep>, IHasSpecialAction<NoteEditModeSpecialAction>, IHasBlueprintSelection<Note>\r\n{\r\n    Bindable<NoteEditPropertyMode> BindableNoteEditPropertyMode { get; }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/States/Modes/IEditReferenceLyricModeState.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\n\r\npublic interface IEditReferenceLyricModeState : IHasEditStep<ReferenceLyricEditStep>;\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/States/Modes/IEditRomanisationModeState.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\n\r\npublic interface IEditRomanisationModeState : IHasEditStep<RomanisationTagEditStep>, IHasBlueprintSelection<TimeTag>;\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/States/Modes/IEditRubyModeState.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\n\r\npublic interface IEditRubyModeState : IHasEditStep<RubyTagEditStep>, IHasBlueprintSelection<RubyTag>\r\n{\r\n    Bindable<RubyTagEditMode> BindableRubyTagEditMode { get; }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/States/Modes/IEditTextModeState.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Text;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\n\r\npublic interface IEditTextModeState : IHasEditStep<TextEditStep>, IHasSpecialAction<TextEditModeSpecialAction>;\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/States/Modes/IEditTimeTagModeState.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\n\r\npublic interface IEditTimeTagModeState : IHasEditStep<TimeTagEditStep>, IHasBlueprintSelection<TimeTag>\r\n{\r\n    BindableFloat BindableRecordZoom { get; }\r\n\r\n    BindableFloat BindableAdjustZoom { get; }\r\n\r\n    Bindable<CreateTimeTagType> BindableCreateType { get; }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/States/Modes/IHasBlueprintSelection.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Bindables;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\n\r\npublic interface IHasBlueprintSelection<T> where T : class\r\n{\r\n    BindableList<T> SelectedItems { get; }\r\n\r\n    void Select(T item);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/States/Modes/IHasEditStep.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Bindables;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\n\r\npublic interface IHasEditStep<TEditStep> where TEditStep : Enum\r\n{\r\n    Bindable<TEditStep> BindableEditStep { get; }\r\n\r\n    TEditStep EditStep => BindableEditStep.Value;\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/States/Modes/IHasSpecialAction.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Bindables;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\n\r\npublic interface IHasSpecialAction<TSpecialAction> where TSpecialAction : Enum\r\n{\r\n    Bindable<TSpecialAction> BindableSpecialAction { get; }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/States/Modes/LanguageEditStep.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\n\r\npublic enum LanguageEditStep\r\n{\r\n    Generate,\r\n\r\n    Verify,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/States/Modes/ModeStateWithBlueprintContainer.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\n\r\npublic abstract partial class ModeStateWithBlueprintContainer<TObject> : Component, IHasBlueprintSelection<TObject> where TObject : class\r\n{\r\n    private readonly IBindable<LyricEditorMode> bindableMode = new Bindable<LyricEditorMode>();\r\n    private readonly IBindable<ICaretPosition?> bindableCaretPosition = new Bindable<ICaretPosition?>();\r\n    private readonly IBindable<int> bindableLyricPropertyWritableVersion = new Bindable<int>();\r\n\r\n    public BindableList<TObject> SelectedItems { get; } = new();\r\n\r\n    protected ModeStateWithBlueprintContainer()\r\n    {\r\n        bindableMode.BindValueChanged(e =>\r\n        {\r\n            TriggerDisableStateChanged();\r\n        });\r\n\r\n        bindableCaretPosition.BindValueChanged(e =>\r\n        {\r\n            bindableLyricPropertyWritableVersion.UnbindBindings();\r\n\r\n            var lyric = e.NewValue?.Lyric;\r\n\r\n            if (lyric == null)\r\n                return;\r\n\r\n            bindableLyricPropertyWritableVersion.BindTo(lyric.LyricPropertyWritableVersion);\r\n            TriggerDisableStateChanged();\r\n        });\r\n\r\n        bindableLyricPropertyWritableVersion.BindValueChanged(_ =>\r\n        {\r\n            TriggerDisableStateChanged();\r\n        });\r\n    }\r\n\r\n    protected virtual void TriggerDisableStateChanged()\r\n    {\r\n        var caret = bindableCaretPosition.Value;\r\n        if (caret == null)\r\n            return;\r\n\r\n        var lyric = caret.Lyric;\r\n\r\n        SelectedItems.Clear();\r\n        bool locked = IsWriteLyricPropertyLocked(lyric);\r\n        if (locked)\r\n            return;\r\n\r\n        // should not select the items from the lyric when mouse click to the lyric.\r\n        // because selected items will be clear when mouse up.\r\n        bool mousePressed = isMousePressed();\r\n        if (mousePressed || !SelectFirstProperty(lyric))\r\n            return;\r\n\r\n        var firstItem = SelectableProperties(lyric).FirstOrDefault();\r\n        if (firstItem != null)\r\n            SelectedItems.Add(firstItem);\r\n    }\r\n\r\n    private bool isMousePressed() => GetContainingInputManager().CurrentState.Mouse.Buttons.HasAnyButtonPressed;\r\n\r\n    protected abstract bool IsWriteLyricPropertyLocked(Lyric lyric);\r\n\r\n    protected abstract bool SelectFirstProperty(Lyric lyric);\r\n\r\n    protected abstract IEnumerable<TObject> SelectableProperties(Lyric lyric);\r\n\r\n    public void Select(TObject item)\r\n    {\r\n        // not trigger again if already focus.\r\n        if (SelectedItems.Contains(item) && SelectedItems.Count == 1)\r\n            return;\r\n\r\n        // trigger selected.\r\n        SelectedItems.Clear();\r\n        SelectedItems.Add(item);\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(ILyricEditorState state, ILyricCaretState lyricCaretState)\r\n    {\r\n        bindableMode.BindTo(state.BindableMode);\r\n        bindableCaretPosition.BindTo(lyricCaretState.BindableCaretPosition);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/States/Modes/NoteEditStep.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\n\r\npublic enum NoteEditStep\r\n{\r\n    Generate,\r\n\r\n    Edit,\r\n\r\n    Verify,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/States/Modes/ReferenceLyricEditStep.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\n\r\npublic enum ReferenceLyricEditStep\r\n{\r\n    Edit,\r\n\r\n    Verify,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/States/Modes/RomanisationTagEditStep.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\n\r\npublic enum RomanisationTagEditStep\r\n{\r\n    Generate,\r\n\r\n    Edit,\r\n\r\n    Verify,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/States/Modes/RubyTagEditMode.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\n\r\npublic enum RubyTagEditMode\r\n{\r\n    Create,\r\n\r\n    Modify,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/States/Modes/RubyTagEditStep.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\n\r\npublic enum RubyTagEditStep\r\n{\r\n    Generate,\r\n\r\n    Edit,\r\n\r\n    Verify,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/States/Modes/TextEditStep.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\n\r\npublic enum TextEditStep\r\n{\r\n    Typing,\r\n\r\n    Split,\r\n\r\n    Verify,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/States/Modes/TimeTagEditStep.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\n\r\npublic enum TimeTagEditStep\r\n{\r\n    Create,\r\n\r\n    Recording,\r\n\r\n    Adjust,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/States/MovingCaretAction.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\n\r\npublic enum MovingCaretAction\r\n{\r\n    PreviousLyric,\r\n\r\n    NextLyric,\r\n\r\n    FirstLyric,\r\n\r\n    LastLyric,\r\n\r\n    PreviousIndex,\r\n\r\n    NextIndex,\r\n\r\n    FirstIndex,\r\n\r\n    LastIndex,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Lyrics/States/RangeCaretPosition.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\n\r\npublic class RangeCaretPosition : RangeCaretPosition<IIndexCaretPosition>\r\n{\r\n    public RangeCaretPosition(IIndexCaretPosition start, IIndexCaretPosition end, RangeCaretDraggingState draggingState)\r\n        : base(start, end, draggingState)\r\n    {\r\n    }\r\n\r\n    public RangeCaretPosition<TIndexCaretPosition> GetRangeCaretPositionWithType<TIndexCaretPosition>()\r\n        where TIndexCaretPosition : struct, IIndexCaretPosition\r\n    {\r\n        if (Start is not TIndexCaretPosition start || End is not TIndexCaretPosition end)\r\n            throw new InvalidCastException();\r\n\r\n        return new RangeCaretPosition<TIndexCaretPosition>(start, end, DraggingState);\r\n    }\r\n}\r\n\r\npublic class RangeCaretPosition<TIndexCaretPosition> : IEquatable<RangeCaretPosition<TIndexCaretPosition>> where TIndexCaretPosition : IIndexCaretPosition\r\n{\r\n    public RangeCaretPosition(TIndexCaretPosition start, TIndexCaretPosition end, RangeCaretDraggingState draggingState)\r\n    {\r\n        if (start.GetType() != end.GetType())\r\n            throw new InvalidOperationException(\"Start and end caret index should be the same type.\");\r\n\r\n        Start = start;\r\n        End = end;\r\n        DraggingState = draggingState;\r\n    }\r\n\r\n    public TIndexCaretPosition Start { get; }\r\n\r\n    public TIndexCaretPosition End { get; }\r\n\r\n    public RangeCaretDraggingState DraggingState { get; }\r\n\r\n    /// <summary>\r\n    /// Get the range caret position with ordered.\r\n    /// </summary>\r\n    /// <returns></returns>\r\n    public Tuple<TIndexCaretPosition, TIndexCaretPosition> GetRangeCaretPosition()\r\n    {\r\n        return Start < End\r\n            ? new Tuple<TIndexCaretPosition, TIndexCaretPosition>(Start, End)\r\n            : new Tuple<TIndexCaretPosition, TIndexCaretPosition>(End, Start);\r\n    }\r\n\r\n    public bool Equals(RangeCaretPosition<TIndexCaretPosition>? other)\r\n    {\r\n        if (ReferenceEquals(null, other))\r\n            return false;\r\n\r\n        if (ReferenceEquals(this, other))\r\n            return true;\r\n\r\n        return EqualityComparer<TIndexCaretPosition>.Default.Equals(Start, other.Start)\r\n               && EqualityComparer<TIndexCaretPosition>.Default.Equals(End, other.End)\r\n               && DraggingState == other.DraggingState;\r\n    }\r\n\r\n    public override bool Equals(object? obj)\r\n    {\r\n        if (ReferenceEquals(null, obj))\r\n            return false;\r\n\r\n        if (ReferenceEquals(this, obj))\r\n            return true;\r\n\r\n        return obj.GetType() == GetType() && Equals((RangeCaretPosition<TIndexCaretPosition>)obj);\r\n    }\r\n\r\n    public override int GetHashCode()\r\n    {\r\n        return HashCode.Combine(Start, End);\r\n    }\r\n\r\n    public bool IsInRange(Lyric lyric)\r\n    {\r\n        int minOrder = Math.Min(Start.Lyric.Order, End.Lyric.Order);\r\n        int maxOrder = Math.Max(Start.Lyric.Order, End.Lyric.Order);\r\n\r\n        return lyric.Order >= minOrder && lyric.Order <= maxOrder;\r\n    }\r\n\r\n    public Type GetCaretPositionType()\r\n    {\r\n        return Start.GetType();\r\n    }\r\n}\r\n\r\npublic enum RangeCaretDraggingState\r\n{\r\n    StartDrag,\r\n\r\n    Dragging,\r\n\r\n    EndDrag,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/LyricsProvider.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Utils;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps;\r\n\r\npublic partial class LyricsProvider : Component, ILyricsProvider\r\n{\r\n    /// <summary>\r\n    /// Get the bindable lyrics with sorted order.\r\n    /// </summary>\r\n    public BindableList<Lyric> BindableLyrics { get; } = new();\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(EditorBeatmap beatmap)\r\n    {\r\n        // Load the lyric into bindable list.\r\n        // And notice that order change in the bindable will not affect the order in the editor beatmap.\r\n        // The hit object in the editor beatmap will auto sort by the time.\r\n        var lyrics = OrderUtils.Sorted(beatmap.HitObjects.OfType<Lyric>());\r\n        BindableLyrics.AddRange(lyrics);\r\n\r\n        // need to check is there any lyric added or removed.\r\n        beatmap.HitObjectAdded += e =>\r\n        {\r\n            if (e is not Lyric lyric)\r\n                return;\r\n\r\n            var previousLyric = BindableLyrics.LastOrDefault(x => x.Order < lyric.Order);\r\n\r\n            if (previousLyric != null)\r\n            {\r\n                int insertIndex = BindableLyrics.IndexOf(previousLyric) + 1;\r\n                BindableLyrics.Insert(insertIndex, lyric);\r\n            }\r\n            else\r\n            {\r\n                // insert to first.\r\n                BindableLyrics.Insert(0, lyric);\r\n            }\r\n        };\r\n        beatmap.HitObjectRemoved += e =>\r\n        {\r\n            if (e is not Lyric lyric)\r\n                return;\r\n\r\n            BindableLyrics.Remove(lyric);\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Pages/Components/Timeline/LyricBlueprintContainer.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Game.Rulesets.Edit;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Timeline;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Pages.Components.Timeline;\r\n\r\npublic partial class LyricBlueprintContainer : EditableTimelineBlueprintContainer<Lyric>\r\n{\r\n    [BackgroundDependencyLoader]\r\n    private void load(ILyricsProvider lyricsProvider)\r\n    {\r\n        Items.BindTo(lyricsProvider.BindableLyrics);\r\n    }\r\n\r\n    protected override SelectionBlueprint<Lyric> CreateBlueprintFor(Lyric item)\r\n        => new PreviewLyricSelectionBlueprint(item);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Pages/Components/Timeline/PageBlueprintContainer.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Game.Rulesets.Edit;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Timeline;\r\nusing osu.Game.Screens.Edit.Compose.Components;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Pages.Components.Timeline;\r\n\r\npublic partial class PageBlueprintContainer : EditableTimelineBlueprintContainer<Page>\r\n{\r\n    [Resolved]\r\n    private IBeatmapPagesChangeHandler beatmapPagesChangeHandler { get; set; } = null!;\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(IPageStateProvider pageStateProvider)\r\n    {\r\n        Items.BindTo(pageStateProvider.PageInfo.Pages);\r\n    }\r\n\r\n    protected override bool ApplyOffsetResult(Page[] items, double time)\r\n    {\r\n        double offset = time - items.First().Time;\r\n        beatmapPagesChangeHandler.ShiftingPageTime(items, offset);\r\n        return true;\r\n    }\r\n\r\n    protected override IEnumerable<SelectionBlueprint<Page>> SortForMovement(IReadOnlyList<SelectionBlueprint<Page>> blueprints)\r\n        => blueprints.OrderBy(b => b.Item.Time);\r\n\r\n    protected override SelectionHandler<Page> CreateSelectionHandler()\r\n        => new PageSelectionHandler();\r\n\r\n    protected override SelectionBlueprint<Page> CreateBlueprintFor(Page item)\r\n        => new PageSelectionBlueprint(item);\r\n\r\n    protected partial class PageSelectionHandler : EditableTimelineSelectionHandler\r\n    {\r\n        [Resolved]\r\n        private IBeatmapPagesChangeHandler beatmapPagesChangeHandler { get; set; } = null!;\r\n\r\n        [Resolved]\r\n        private IPageStateProvider pageStateProvider { get; set; } = null!;\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load()\r\n        {\r\n            SelectedItems.BindTo(pageStateProvider.SelectedItems);\r\n        }\r\n\r\n        // for now we always allow movement. snapping is provided by the Timeline's \"distance\" snap implementation\r\n        public override bool HandleMovement(MoveSelectionEvent<Page> moveEvent) => true;\r\n\r\n        protected override void DeleteItems(IEnumerable<Page> items)\r\n        {\r\n            beatmapPagesChangeHandler.RemoveRange(items);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Pages/Components/Timeline/PageSelectionBlueprint.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Timeline;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Pages.Components.Timeline;\r\n\r\npublic partial class PageSelectionBlueprint : EditableTimelineSelectionBlueprint<Page>\r\n{\r\n    private const float body_width = 4;\r\n\r\n    private readonly IBindable<double> startTime;\r\n\r\n    private readonly PageInfoPiece pageInfoPiece;\r\n    private readonly PageBodyPiece pageBodyPiece;\r\n\r\n    public PageSelectionBlueprint(Page item)\r\n        : base(item)\r\n    {\r\n        startTime = item.TimeBindable.GetBoundCopy();\r\n        RelativeSizeAxes = Axes.None;\r\n\r\n        Width = body_width;\r\n\r\n        // todo: not really sure why it fix the issue. should have more checks about this.\r\n        Height = PagesTimeLine.TIMELINE_HEIGHT - 1;\r\n\r\n        AddRangeInternal(new Drawable[]\r\n        {\r\n            pageInfoPiece = new PageInfoPiece(item)\r\n            {\r\n                Anchor = Anchor.TopCentre,\r\n                Origin = Anchor.BottomCentre,\r\n            },\r\n            pageBodyPiece = new PageBodyPiece(item)\r\n            {\r\n                Anchor = Anchor.CentreLeft,\r\n                Origin = Anchor.CentreLeft,\r\n                RelativeSizeAxes = Axes.Both,\r\n            },\r\n        });\r\n\r\n        startTime.BindValueChanged(_ => X = (float)Item.Time, true);\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(IPageEditorVerifier pageEditorVerifier)\r\n    {\r\n    }\r\n\r\n    private partial class PageInfoPiece : CompositeDrawable\r\n    {\r\n        private readonly Page page;\r\n        private readonly IBindable<int> pagesVersion = new Bindable<int>();\r\n\r\n        public PageInfoPiece(Page page)\r\n        {\r\n            this.page = page;\r\n\r\n            AutoSizeAxes = Axes.X;\r\n            Height = 16;\r\n            Margin = new MarginPadding(4);\r\n\r\n            Masking = true;\r\n            CornerRadius = Height / 2;\r\n\r\n            Origin = Anchor.TopCentre;\r\n            Anchor = Anchor.TopCentre;\r\n        }\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(OsuColour colours, IPageStateProvider pageStateProvider)\r\n        {\r\n            OsuSpriteText spriteText;\r\n\r\n            InternalChildren = new Drawable[]\r\n            {\r\n                new Box\r\n                {\r\n                    Colour = colours.Yellow,\r\n                    RelativeSizeAxes = Axes.Both,\r\n                },\r\n                spriteText = new OsuSpriteText\r\n                {\r\n                    Anchor = Anchor.Centre,\r\n                    Origin = Anchor.Centre,\r\n                    Padding = new MarginPadding(3),\r\n                    Font = OsuFont.Default.With(size: 14, weight: FontWeight.SemiBold),\r\n                    Colour = colours.B5,\r\n                },\r\n            };\r\n\r\n            pagesVersion.BindTo(pageStateProvider.PageInfo.PagesVersion);\r\n            pagesVersion.BindValueChanged(x =>\r\n            {\r\n                int? order = pageStateProvider.PageInfo.GetPageOrder(page);\r\n                spriteText.Text = $\" #{order} \";\r\n            }, true);\r\n        }\r\n    }\r\n\r\n    private partial class PageBodyPiece : CompositeDrawable\r\n    {\r\n        private readonly Page page;\r\n\r\n        public PageBodyPiece(Page page)\r\n        {\r\n            this.page = page;\r\n        }\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(OsuColour colours)\r\n        {\r\n            InternalChildren = new Drawable[]\r\n            {\r\n                new Box\r\n                {\r\n                    Colour = colours.Yellow,\r\n                    RelativeSizeAxes = Axes.Both,\r\n                },\r\n            };\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Pages/Components/Timeline/PagesTimeLine.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Timeline;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Pages.Components.Timeline;\r\n\r\npublic partial class PagesTimeLine : EditableTimeline\r\n{\r\n    public const float TIMELINE_HEIGHT = 38;\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OsuColour colours, IPageStateProvider pageStateProvider)\r\n    {\r\n        AddInternal(new Box\r\n        {\r\n            Name = \"Background\",\r\n            Depth = 1,\r\n            RelativeSizeAxes = Axes.X,\r\n            Height = TIMELINE_HEIGHT,\r\n            Anchor = Anchor.CentreLeft,\r\n            Origin = Anchor.CentreLeft,\r\n            Colour = colours.Gray3,\r\n        });\r\n\r\n        BindableZoom.BindTo(pageStateProvider.BindableZoom);\r\n        BindableCurrent.BindTo(pageStateProvider.BindableCurrent);\r\n    }\r\n\r\n    protected override Container CreateMainContainer()\r\n    {\r\n        return base.CreateMainContainer().With(c => c.Height = TIMELINE_HEIGHT);\r\n    }\r\n\r\n    protected override IEnumerable<Drawable> CreateBlueprintContainer()\r\n    {\r\n        yield return new LyricBlueprintContainer();\r\n        yield return new PageBlueprintContainer();\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Pages/Components/Timeline/PreviewLyricSelectionBlueprint.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Timeline;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Pages.Components.Timeline;\r\n\r\npublic partial class PreviewLyricSelectionBlueprint : EditableLyricTimelineSelectionBlueprint\r\n{\r\n    public PreviewLyricSelectionBlueprint(Lyric item)\r\n        : base(item)\r\n    {\r\n        Selectable = false;\r\n    }\r\n\r\n    protected override void OnSelectableStatusChanged(bool selectable)\r\n    {\r\n        Alpha = 0.5f;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Pages/IPageEditorVerifier.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Pages;\r\n\r\npublic interface IPageEditorVerifier : IEditorVerifier\r\n{\r\n    void Navigate(Issue issue);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Pages/IPageStateProvider.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Pages;\r\n\r\npublic interface IPageStateProvider\r\n{\r\n    Bindable<PageEditorEditMode> BindableEditMode { get; }\r\n\r\n    PageEditorEditMode EditMode => BindableEditMode.Value;\r\n\r\n    PageInfo PageInfo { get; }\r\n\r\n    BindableList<Page> SelectedItems { get; }\r\n\r\n    void Select(Page item);\r\n\r\n    BindableFloat BindableZoom { get; }\r\n\r\n    BindableFloat BindableCurrent { get; }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Pages/PageEditor.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Pages.Components.Timeline;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Pages;\r\n\r\npublic partial class PageEditor : CompositeDrawable\r\n{\r\n    private readonly Box background;\r\n\r\n    public PageEditor()\r\n    {\r\n        InternalChildren = new Drawable[]\r\n        {\r\n            background = new Box\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n            },\r\n            new PagesTimeLine\r\n            {\r\n                Anchor = Anchor.BottomCentre,\r\n                Origin = Anchor.BottomCentre,\r\n                RelativeSizeAxes = Axes.X,\r\n                Height = 100,\r\n            },\r\n        };\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OverlayColourProvider colourProvider)\r\n    {\r\n        background.Colour = colourProvider.Background5;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Pages/PageEditorEditMode.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Pages;\r\n\r\npublic enum PageEditorEditMode\r\n{\r\n    Generate,\r\n\r\n    Edit,\r\n\r\n    Verify,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Pages/PageEditorVerifier.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Allocation;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks.Issues;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Pages;\r\n\r\npublic partial class PageEditorVerifier : EditorVerifier, IPageEditorVerifier\r\n{\r\n    [Resolved]\r\n    private EditorClock clock { get; set; } = null!;\r\n\r\n    protected override IEnumerable<ICheck> CreateChecks() => new ICheck[] { new CheckBeatmapPageInfo() };\r\n\r\n    protected override void LoadComplete()\r\n    {\r\n        base.LoadComplete();\r\n        Refresh();\r\n    }\r\n\r\n    public override void Refresh()\r\n    {\r\n        ClearChecks();\r\n        AddChecks(getIssues());\r\n    }\r\n\r\n    private IEnumerable<Issue> getIssues()\r\n    {\r\n        return CreateIssues();\r\n    }\r\n\r\n    public void Navigate(Issue issue)\r\n    {\r\n        if (issue.Time.HasValue)\r\n            clock.Seek(issue.Time.Value);\r\n\r\n        switch (issue)\r\n        {\r\n            case LyricIssue lyricIssue:\r\n                // todo: select the lyric.\r\n                var lyric = lyricIssue.Lyric;\r\n                break;\r\n\r\n            case BeatmapPageIssue beatmapPageIssue:\r\n                // todo: select the pages.\r\n                break;\r\n\r\n            case Issue:\r\n                break;\r\n\r\n            default:\r\n                throw new NotSupportedException();\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Pages/PageScreen.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Containers;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Pages.Settings;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Pages;\r\n\r\n[Cached(typeof(IPageStateProvider))]\r\npublic partial class PageScreen : BeatmapEditorRoundedScreen, IPageStateProvider\r\n{\r\n    [Cached(typeof(IBeatmapPagesChangeHandler))]\r\n    private readonly BeatmapPagesChangeHandler beatmapPagesChangeHandler;\r\n\r\n    [Cached(typeof(IPageEditorVerifier))]\r\n    private readonly PageEditorVerifier pageEditorVerifier;\r\n\r\n    public Bindable<PageEditorEditMode> BindableEditMode { get; } = new();\r\n\r\n    [Resolved]\r\n    private EditorBeatmap editorBeatmap { get; set; } = null!;\r\n\r\n    public PageInfo PageInfo => EditorBeatmapUtils.GetPlayableBeatmap(editorBeatmap).PageInfo;\r\n\r\n    public BindableList<Page> SelectedItems { get; } = new();\r\n\r\n    public BindableFloat BindableZoom { get; } = new();\r\n\r\n    public BindableFloat BindableCurrent { get; } = new();\r\n\r\n    public PageScreen()\r\n        : base(KaraokeBeatmapEditorScreenMode.Page)\r\n    {\r\n        AddInternal(beatmapPagesChangeHandler = new BeatmapPagesChangeHandler());\r\n        AddInternal(pageEditorVerifier = new PageEditorVerifier());\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(EditorClock editorClock)\r\n    {\r\n        BindableZoom.MaxValue = ZoomableScrollContainerUtils.GetZoomLevelForVisibleMilliseconds(editorClock, 8000);\r\n        BindableZoom.MinValue = ZoomableScrollContainerUtils.GetZoomLevelForVisibleMilliseconds(editorClock, 80000);\r\n        BindableZoom.Value = BindableZoom.Default = ZoomableScrollContainerUtils.GetZoomLevelForVisibleMilliseconds(editorClock, 40000);\r\n\r\n        Add(new FixedSectionsContainer<Drawable>\r\n        {\r\n            FixedHeader = new PageScreenHeader(),\r\n            RelativeSizeAxes = Axes.Both,\r\n            Child = new GridContainer\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n                ColumnDimensions = new[]\r\n                {\r\n                    new Dimension(),\r\n                    new Dimension(GridSizeMode.Absolute, 250),\r\n                },\r\n                Content = new[]\r\n                {\r\n                    new Drawable[]\r\n                    {\r\n                        new PageEditor\r\n                        {\r\n                            RelativeSizeAxes = Axes.Both,\r\n                        },\r\n                        new PageSettings(),\r\n                    },\r\n                },\r\n            },\r\n        });\r\n    }\r\n\r\n    public void Select(Page item)\r\n    {\r\n        SelectedItems.Clear();\r\n        SelectedItems.Add(item);\r\n    }\r\n\r\n    private partial class FixedSectionsContainer<T> : SectionsContainer<T> where T : Drawable\r\n    {\r\n        // todo: check what this shit doing.\r\n        protected override Container<T> Content { get; }\r\n\r\n        public FixedSectionsContainer()\r\n        {\r\n            AddInternal(Content = new Container<T>\r\n            {\r\n                Masking = true,\r\n                RelativeSizeAxes = Axes.Both,\r\n                Padding = new MarginPadding { Top = 55 },\r\n            });\r\n        }\r\n    }\r\n\r\n    private partial class PageScreenHeader : OverlayHeader\r\n    {\r\n        protected override OverlayTitle CreateTitle() => new PageScreenTitle();\r\n\r\n        private partial class PageScreenTitle : OverlayTitle\r\n        {\r\n            public PageScreenTitle()\r\n            {\r\n                Title = \"page\";\r\n                Description = \"create page of your beatmap\";\r\n                Icon = OsuIcon.FeaturedArtistCircle;\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Pages/Settings/ConfirmReGeneratePageDialog.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Overlays.Dialog;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Pages.Settings;\r\n\r\npublic partial class ConfirmReGeneratePageDialog : PopupDialog\r\n{\r\n    public ConfirmReGeneratePageDialog(Action<bool> okAction)\r\n    {\r\n        Icon = FontAwesome.Solid.Trash;\r\n        HeaderText = \"Are you sure re-generate the pages? It will delete all the pages in the beatmap.\";\r\n        BodyText = \"page\";\r\n        Buttons = new PopupDialogButton[]\r\n        {\r\n            new PopupDialogOkButton\r\n            {\r\n                Text = \"Yes. Go for it.\",\r\n                Action = () => okAction(true),\r\n            },\r\n            new PopupDialogCancelButton\r\n            {\r\n                Text = \"No! Abort mission!\",\r\n                Action = () => okAction(false),\r\n            },\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Pages/Settings/PageAutoGenerateSection.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Beatmaps.Pages;\r\nusing osu.Game.Rulesets.Karaoke.Overlays.Dialog;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Markdown;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Pages.Settings;\r\n\r\npublic partial class PageAutoGenerateSection : AutoGenerateSection\r\n{\r\n    protected override AutoGenerateSubsection CreateAutoGenerateSubsection()\r\n        => new PageAutoGenerateSubsection();\r\n\r\n    private partial class PageAutoGenerateSubsection : AutoGenerateSubsection\r\n    {\r\n        protected override EditorSectionButton CreateGenerateButton()\r\n            => new PageAutoGenerateButton();\r\n\r\n        protected override DescriptionFormat CreateInvalidDescriptionFormat()\r\n            => new()\r\n            {\r\n                Text = \"Seems have some time-related issues in the lyrics. Go to lyric editor to fix them.\",\r\n            };\r\n\r\n        protected override ConfigButton CreateConfigButton()\r\n            => new PageAutoGenerateConfigButton();\r\n\r\n        protected partial class PageAutoGenerateConfigButton : ConfigButton\r\n        {\r\n            public override Popover GetPopover()\r\n                => new GeneratorConfigPopover(KaraokeRulesetEditGeneratorSetting.BeatmapPageGeneratorConfig);\r\n        }\r\n\r\n        private partial class PageAutoGenerateButton : EditorSectionButton\r\n        {\r\n            [Resolved]\r\n            private KaraokeRulesetEditGeneratorConfigManager generatorConfigManager { get; set; } = null!;\r\n\r\n            [Resolved]\r\n            private IDialogOverlay dialogOverlay { get; set; } = null!;\r\n\r\n            [Resolved]\r\n            private IBeatmapPagesChangeHandler beatmapPagesChangeHandler { get; set; } = null!;\r\n\r\n            public PageAutoGenerateButton()\r\n            {\r\n                Text = \"Generate\";\r\n                Action = () =>\r\n                {\r\n                    bool canGenerate = beatmapPagesChangeHandler.CanGenerate();\r\n\r\n                    if (!canGenerate)\r\n                    {\r\n                        dialogOverlay.Push(new OkPopupDialog\r\n                        {\r\n                            Icon = FontAwesome.Solid.ExclamationTriangle,\r\n                            HeaderText = \"Seems still have some issues need to be fixed.\",\r\n                            BodyText = beatmapPagesChangeHandler.GetGeneratorNotSupportedMessage()!.Value,\r\n                        });\r\n                        return;\r\n                    }\r\n\r\n                    bool clearPagesAfterGenerated = clearExistPagesAfterGenerated();\r\n\r\n                    if (clearPagesAfterGenerated)\r\n                    {\r\n                        dialogOverlay.Push(new ConfirmReGeneratePageDialog(isOk =>\r\n                        {\r\n                            if (isOk)\r\n                                beatmapPagesChangeHandler.AutoGenerate();\r\n                        }));\r\n                    }\r\n                    else\r\n                    {\r\n                        beatmapPagesChangeHandler.AutoGenerate();\r\n                    }\r\n                };\r\n            }\r\n\r\n            private bool clearExistPagesAfterGenerated()\r\n                => generatorConfigManager.Get<PageGeneratorConfig>().ClearExistPages.Value;\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Pages/Settings/PageEditorIssueSection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Extensions;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Issues;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Pages.Settings;\r\n\r\npublic partial class PageEditorIssueSection : IssueSection\r\n{\r\n    protected override EmptyIssue CreateEmptyIssue() => new PageEditorEmptyIssue();\r\n\r\n    protected override IssueNavigator CreateIssueNavigator() => new PageEditorIssueNavigator();\r\n\r\n    protected override IssueTable CreateIssueTable() => new PageEditorIssueTable();\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(IPageEditorVerifier pageEditorVerifier)\r\n    {\r\n        Issues.BindTo(pageEditorVerifier.Issues);\r\n    }\r\n\r\n    private partial class PageEditorEmptyIssue : EmptyIssue\r\n    {\r\n        [Resolved]\r\n        private IPageEditorVerifier pageEditorVerifier { get; set; } = null!;\r\n\r\n        protected override void OnRefreshButtonClicked()\r\n            => pageEditorVerifier.Refresh();\r\n    }\r\n\r\n    private partial class PageEditorIssueNavigator : IssueNavigator\r\n    {\r\n        [Resolved]\r\n        private IPageEditorVerifier pageEditorVerifier { get; set; } = null!;\r\n\r\n        protected override void OnRefreshButtonClicked()\r\n            => pageEditorVerifier.Refresh();\r\n    }\r\n\r\n    public partial class PageEditorIssueTable : IssueTable\r\n    {\r\n        [Resolved]\r\n        private IPageEditorVerifier pageEditorVerifier { get; set; } = null!;\r\n\r\n        protected override void OnIssueClicked(Issue issue)\r\n            => pageEditorVerifier.Navigate(issue);\r\n\r\n        protected override TableColumn[] CreateHeaders() => new[]\r\n        {\r\n            new TableColumn(string.Empty, Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize, minSize: 30)),\r\n            new TableColumn(\"Time\", Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize, minSize: 40)),\r\n            new TableColumn(\"Message\", Anchor.CentreLeft),\r\n        };\r\n\r\n        protected override Drawable[] CreateContent(Issue issue)\r\n        {\r\n            return new Drawable[]\r\n            {\r\n                new IssueIcon\r\n                {\r\n                    Origin = Anchor.Centre,\r\n                    Size = new Vector2(10),\r\n                    Margin = new MarginPadding { Left = 10 },\r\n                    Issue = issue,\r\n                },\r\n                new OsuSpriteText\r\n                {\r\n                    Text = getInvalidObjectTimeByIssue(issue),\r\n                    Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold),\r\n                    Margin = new MarginPadding { Right = 10 },\r\n                },\r\n                new TruncatingSpriteText\r\n                {\r\n                    Text = issue.ToString(),\r\n                    RelativeSizeAxes = Axes.X,\r\n                    Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Medium),\r\n                },\r\n            };\r\n        }\r\n\r\n        private static string getInvalidObjectTimeByIssue(Issue issue) => issue.Time?.ToEditorFormattedString() ?? string.Empty;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Pages/Settings/PageEditorSettingsHeader.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Allocation;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Markdown;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Pages.Settings;\r\n\r\npublic partial class PageEditorSettingsHeader : EditorSettingsHeader<PageEditorEditMode>\r\n{\r\n    protected override OverlayColourScheme CreateColourScheme()\r\n        => OverlayColourScheme.Green;\r\n\r\n    protected override EditStepTabControl CreateTabControl()\r\n        => new PageEditStepTabControl();\r\n\r\n    protected override DescriptionFormat GetSelectionDescription(PageEditorEditMode step) =>\r\n        step switch\r\n        {\r\n            PageEditorEditMode.Generate => \"Generate the pages by lyric.\",\r\n            PageEditorEditMode.Edit => \"Batch edit page in here.\",\r\n            PageEditorEditMode.Verify => \"Check if have any page issues.\",\r\n            _ => throw new ArgumentOutOfRangeException(nameof(step), step, null),\r\n        };\r\n\r\n    private partial class PageEditStepTabControl : EditStepTabControl\r\n    {\r\n        protected override StepTabButton CreateStepButton(OsuColour colours, PageEditorEditMode value)\r\n        {\r\n            return value switch\r\n            {\r\n                PageEditorEditMode.Generate => new StepTabButton(value)\r\n                {\r\n                    Text = \"Generate\",\r\n                    SelectedColour = colours.Blue,\r\n                    UnSelectedColour = colours.BlueDarker,\r\n                },\r\n                PageEditorEditMode.Edit => new StepTabButton(value)\r\n                {\r\n                    Text = \"Edit\",\r\n                    SelectedColour = colours.Red,\r\n                    UnSelectedColour = colours.RedDarker,\r\n                },\r\n                PageEditorEditMode.Verify => new VerifyStepTabButton(value)\r\n                {\r\n                    Text = \"Verify\",\r\n                    SelectedColour = colours.Yellow,\r\n                    UnSelectedColour = colours.YellowDarker,\r\n                },\r\n                _ => throw new ArgumentOutOfRangeException(nameof(value), value, null),\r\n            };\r\n        }\r\n    }\r\n\r\n    private partial class VerifyStepTabButton : IssueStepTabButton\r\n    {\r\n        public VerifyStepTabButton(PageEditorEditMode value)\r\n            : base(value)\r\n        {\r\n        }\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(IPageEditorVerifier pageEditorVerifier)\r\n        {\r\n            Issues.BindTo(pageEditorVerifier.Issues);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Pages/Settings/PageSettings.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Overlays;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Pages.Settings;\r\n\r\npublic partial class PageSettings : EditorSettings\r\n{\r\n    private readonly Bindable<PageEditorEditMode> bindableMode = new();\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OverlayColourProvider colourProvider, IPageStateProvider pageStateProvider)\r\n    {\r\n        bindableMode.BindTo(pageStateProvider.BindableEditMode);\r\n\r\n        // change the background colour to the lighter one.\r\n        ChangeBackgroundColour(colourProvider.Background3);\r\n    }\r\n\r\n    protected override EditorSettingsHeader CreateSettingHeader()\r\n        => new PageEditorSettingsHeader\r\n        {\r\n            Current = bindableMode,\r\n        };\r\n\r\n    protected override IReadOnlyList<EditorSection> CreateEditorSections() => bindableMode.Value switch\r\n    {\r\n        PageEditorEditMode.Generate => new[]\r\n        {\r\n            new PageAutoGenerateSection(),\r\n        },\r\n        PageEditorEditMode.Edit => new[]\r\n        {\r\n            new PagesSection(),\r\n        },\r\n        PageEditorEditMode.Verify => new[]\r\n        {\r\n            new PageEditorIssueSection(),\r\n        },\r\n        _ => throw new ArgumentOutOfRangeException(),\r\n    };\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Pages/Settings/PagesSection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Extensions;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Beatmaps;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Pages.Settings;\r\n\r\npublic partial class PagesSection : EditorSection\r\n{\r\n    protected override LocalisableString Title => \"Pages\";\r\n\r\n    public PagesSection()\r\n    {\r\n        Add(new SectionPageInfoEditor());\r\n    }\r\n\r\n    private partial class SectionPageInfoEditor : SectionTimingInfoItemsEditor<Page>\r\n    {\r\n        [BackgroundDependencyLoader]\r\n        private void load(IPageStateProvider pageStateProvider)\r\n        {\r\n            Items.BindTo(pageStateProvider.PageInfo.Pages);\r\n        }\r\n\r\n        protected override DrawableTimingInfoItem CreateTimingInfoDrawable(Page item) => new DrawablePage(item);\r\n\r\n        protected override EditorSectionButton CreateCreateNewItemButton() => new CreateNewPageButton();\r\n\r\n        private partial class DrawablePage : DrawableTimingInfoItem\r\n        {\r\n            private readonly IBindable<int> pagesVersion = new Bindable<int>();\r\n\r\n            [Resolved]\r\n            private IBeatmapPagesChangeHandler beatmapPagesChangeHandler { get; set; } = null!;\r\n\r\n            public DrawablePage(Page item)\r\n                : base(item)\r\n            {\r\n            }\r\n\r\n            protected override void RemoveItem(Page item)\r\n            {\r\n                beatmapPagesChangeHandler.Remove(item);\r\n            }\r\n\r\n            [BackgroundDependencyLoader]\r\n            private void load(IPageStateProvider pageStateProvider)\r\n            {\r\n                pagesVersion.BindTo(pageStateProvider.PageInfo.PagesVersion);\r\n                pagesVersion.BindValueChanged(_ =>\r\n                {\r\n                    int? order = pageStateProvider.PageInfo.GetPageOrder(Item);\r\n                    double time = Item.Time;\r\n\r\n                    ChangeDisplayOrder((int)time);\r\n                    Text = $\"#{order} {time.ToEditorFormattedString()}\";\r\n                }, true);\r\n            }\r\n        }\r\n\r\n        private partial class CreateNewPageButton : EditorSectionButton\r\n        {\r\n            [Resolved]\r\n            private IBeatmapPagesChangeHandler beatmapPagesChangeHandler { get; set; } = null!;\r\n\r\n            [Resolved]\r\n            private EditorClock clock { get; set; } = null!;\r\n\r\n            public CreateNewPageButton()\r\n            {\r\n                Text = \"Create new page\";\r\n                Action = () =>\r\n                {\r\n                    double currentTime = clock.CurrentTime;\r\n                    beatmapPagesChangeHandler.Add(new Page\r\n                    {\r\n                        Time = currentTime,\r\n                    });\r\n                };\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Singers/DeleteSingerDialog.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Overlays.Dialog;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Singers;\r\n\r\npublic partial class DeleteSingerDialog : PopupDialog\r\n{\r\n    public DeleteSingerDialog(Action<bool> okAction)\r\n    {\r\n        Icon = FontAwesome.Solid.Globe;\r\n        HeaderText = \"Confirm deletion of\";\r\n        BodyText = \"singer\"; //should change to singer name later\r\n        Buttons = new PopupDialogButton[]\r\n        {\r\n            new PopupDialogOkButton\r\n            {\r\n                Text = \"Yes. Go for it.\",\r\n                Action = () => okAction(true),\r\n            },\r\n            new PopupDialogCancelButton\r\n            {\r\n                Text = \"No! Abort mission!\",\r\n                Action = () => okAction(false),\r\n            },\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Singers/Detail/AvatarSection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.UserInterfaceV2;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Singers.Detail;\r\n\r\ninternal partial class AvatarSection : EditSingerSection\r\n{\r\n    protected override LocalisableString Title => \"Avatar\";\r\n\r\n    public AvatarSection(Singer singer)\r\n    {\r\n        Children = new Drawable[]\r\n        {\r\n            new LabelledHueSelector\r\n            {\r\n                Label = \"Colour\",\r\n                Description = \"Select singer colour.\",\r\n                Current = singer.HueBindable,\r\n            },\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Singers/Detail/EditSingerSection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Singers.Detail;\r\n\r\ninternal abstract partial class EditSingerSection : Section;\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Singers/Detail/MetadataSection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Graphics.UserInterfaceV2;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Singers.Detail;\r\n\r\ninternal partial class MetadataSection : EditSingerSection\r\n{\r\n    protected override LocalisableString Title => \"Metadata\";\r\n\r\n    public MetadataSection(Singer singer)\r\n    {\r\n        Children = new Drawable[]\r\n        {\r\n            new LabelledTextBox\r\n            {\r\n                Label = \"Singer\",\r\n                Current = singer.NameBindable,\r\n                TabbableContentContainer = this,\r\n            },\r\n            new LabelledTextBox\r\n            {\r\n                Label = \"Romanisation\",\r\n                Current = singer.RomanisationBindable,\r\n                TabbableContentContainer = this,\r\n            },\r\n            new LabelledTextBox\r\n            {\r\n                Label = \"English name\",\r\n                Current = singer.EnglishNameBindable,\r\n                TabbableContentContainer = this,\r\n            },\r\n            new LabelledTextBox\r\n            {\r\n                Label = \"Description\",\r\n                Current = singer.DescriptionBindable,\r\n                TabbableContentContainer = this,\r\n            },\r\n        };\r\n\r\n        // todo: see NoteEditPopover to implement the undo behavior.\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Singers/Detail/SingerEditPopover.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Graphics.Containers;\r\nusing osu.Game.Graphics.UserInterfaceV2;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Singers.Detail;\r\n\r\npublic partial class SingerEditPopover : OsuPopover\r\n{\r\n    public SingerEditPopover(Singer singer)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(singer);\r\n\r\n        Child = new OsuScrollContainer\r\n        {\r\n            Height = 500,\r\n            Width = 300,\r\n            Child = new FillFlowContainer<EditSingerSection>\r\n            {\r\n                Direction = FillDirection.Vertical,\r\n                RelativeSizeAxes = Axes.X,\r\n                AutoSizeAxes = Axes.Y,\r\n                Children = new EditSingerSection[]\r\n                {\r\n                    new AvatarSection(singer),\r\n                    new MetadataSection(singer),\r\n                },\r\n            },\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Singers/ISingerScreenScrollingInfoProvider.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Bindables;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Singers;\r\n\r\npublic interface ISingerScreenScrollingInfoProvider\r\n{\r\n    BindableFloat BindableZoom { get; }\r\n\r\n    BindableFloat BindableCurrent { get; }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Singers/Rows/Components/SingerAvatar.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.IO;\r\nusing System.Linq;\r\nusing System.Threading.Tasks;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Extensions;\r\nusing osu.Framework.Extensions.ObjectExtensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Cursor;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Game.Database;\r\nusing osu.Game.Graphics.UserInterfaceV2;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.Drawables;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Singers.Rows.Components;\r\n\r\npublic partial class SingerAvatar : CompositeDrawable, ICanAcceptFiles, IHasPopover\r\n{\r\n    private readonly string[] handledExtensions = { \".jpg\", \".jpeg\", \".png\" };\r\n\r\n    public IEnumerable<string> HandledExtensions => handledExtensions;\r\n\r\n    private readonly Bindable<FileInfo?> currentFile = new();\r\n\r\n    [Resolved]\r\n    private OsuGameBase game { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private IBeatmapSingersChangeHandler beatmapSingersChangeHandler { get; set; } = null!;\r\n\r\n    private readonly Singer singer;\r\n\r\n    public SingerAvatar(Singer singer)\r\n    {\r\n        this.singer = singer;\r\n\r\n        InternalChildren = new[]\r\n        {\r\n            new DrawableSingerAvatar\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n                Singer = singer,\r\n            },\r\n        };\r\n    }\r\n\r\n    protected override bool OnClick(ClickEvent e)\r\n    {\r\n        this.ShowPopover();\r\n        return true;\r\n    }\r\n\r\n    protected override void LoadComplete()\r\n    {\r\n        base.LoadComplete();\r\n\r\n        game.RegisterImportHandler(this);\r\n        currentFile.BindValueChanged(onFileSelected);\r\n    }\r\n\r\n    private void onFileSelected(ValueChangedEvent<FileInfo?> file)\r\n    {\r\n        if (file.NewValue == null)\r\n            return;\r\n\r\n        this.HidePopover();\r\n\r\n        beatmapSingersChangeHandler.ChangeSingerAvatar(singer, file.NewValue);\r\n    }\r\n\r\n    Task ICanAcceptFiles.Import(params string[] paths)\r\n    {\r\n        Schedule(() => currentFile.Value = new FileInfo(paths.First()));\r\n        return Task.CompletedTask;\r\n    }\r\n\r\n    Task ICanAcceptFiles.Import(ImportTask[] tasks, ImportParameters parameters) => throw new NotImplementedException();\r\n\r\n    protected override void Dispose(bool isDisposing)\r\n    {\r\n        base.Dispose(isDisposing);\r\n\r\n        if (game.IsNotNull())\r\n            game.UnregisterImportHandler(this);\r\n    }\r\n\r\n    public Popover GetPopover() => new FileChooserPopover(handledExtensions, currentFile);\r\n\r\n    private partial class FileChooserPopover : OsuPopover\r\n    {\r\n        public FileChooserPopover(string[] handledExtensions, Bindable<FileInfo?> currentFile)\r\n        {\r\n            Child = new Container\r\n            {\r\n                Size = new Vector2(600, 400),\r\n                Child = new OsuFileSelector(currentFile.Value?.DirectoryName, handledExtensions)\r\n                {\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    CurrentFile = { BindTarget = currentFile },\r\n                },\r\n            };\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Singers/Rows/Components/Timeline/LyricTimelineSelectionBlueprint.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Timeline;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Singers.Rows.Components.Timeline;\r\n\r\npublic partial class LyricTimelineSelectionBlueprint : EditableLyricTimelineSelectionBlueprint\r\n{\r\n    private readonly IBindableList<ElementId> singersBindable;\r\n\r\n    public LyricTimelineSelectionBlueprint(Lyric item)\r\n        : base(item)\r\n    {\r\n        singersBindable = Item.SingerIdsBindable.GetBoundCopy();\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(SingerLyricTimeline timeline)\r\n    {\r\n        singersBindable.BindCollectionChanged((_, _) =>\r\n        {\r\n            // Check is lyric contains this singer, or default singer\r\n            Selectable = lyricInCurrentSinger(Item, timeline.Singer);\r\n        }, true);\r\n\r\n        static bool lyricInCurrentSinger(Lyric lyric, Singer singer)\r\n        {\r\n            if (singer == DefaultLyricPlacementColumn.DefaultSinger)\r\n                return !lyric.SingerIds.Any();\r\n\r\n            return LyricUtils.ContainsSinger(lyric, singer);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Singers/Rows/Components/Timeline/SingerLyricEditorBlueprintContainer.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Game.Rulesets.Edit;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Components.ContextMenu;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Timeline;\r\nusing osu.Game.Screens.Edit;\r\nusing osu.Game.Screens.Edit.Compose.Components;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Singers.Rows.Components.Timeline;\r\n\r\npublic partial class SingerLyricEditorBlueprintContainer : EditableTimelineBlueprintContainer<Lyric>\r\n{\r\n    [BackgroundDependencyLoader]\r\n    private void load(ILyricsProvider lyricsProvider)\r\n    {\r\n        Items.BindTo(lyricsProvider.BindableLyrics);\r\n    }\r\n\r\n    protected override IEnumerable<SelectionBlueprint<Lyric>> SortForMovement(IReadOnlyList<SelectionBlueprint<Lyric>> blueprints)\r\n        => blueprints.OrderBy(b => b.Item.StartTime);\r\n\r\n    protected override SelectionHandler<Lyric> CreateSelectionHandler()\r\n        => new SingerLyricSelectionHandler();\r\n\r\n    protected override SelectionBlueprint<Lyric> CreateBlueprintFor(Lyric item)\r\n        => new LyricTimelineSelectionBlueprint(item);\r\n\r\n    protected partial class SingerLyricSelectionHandler : EditableTimelineSelectionHandler\r\n    {\r\n        [Resolved]\r\n        private EditorBeatmap beatmap { get; set; } = null!;\r\n\r\n        [Resolved]\r\n        private ILyricSingerChangeHandler lyricSingerChangeHandler { get; set; } = null!;\r\n\r\n        [Resolved]\r\n        private BindableList<Lyric> selectedLyrics { get; set; } = null!;\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load()\r\n        {\r\n            SelectedItems.BindTo(selectedLyrics);\r\n        }\r\n\r\n        protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint<Lyric>> selection)\r\n        {\r\n            var contextMenu = new SingerContextMenu(beatmap, lyricSingerChangeHandler, string.Empty, () =>\r\n            {\r\n                selectedLyrics.Clear();\r\n            });\r\n            return contextMenu.Items;\r\n        }\r\n\r\n        protected override void DeleteItems(IEnumerable<Lyric> items)\r\n        {\r\n            // todo : remove all in the same time.\r\n            foreach (var item in items)\r\n            {\r\n                lyricSingerChangeHandler.Clear();\r\n            }\r\n\r\n            selectedLyrics.Clear();\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Singers/Rows/Components/Timeline/SingerLyricTimeline.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Timeline;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Singers.Rows.Components.Timeline;\r\n\r\n[Cached]\r\npublic partial class SingerLyricTimeline : EditableTimeline\r\n{\r\n    private const float timeline_height = 38;\r\n\r\n    [Resolved]\r\n    private EditorBeatmap beatmap { get; set; } = null!;\r\n\r\n    public readonly Singer Singer;\r\n\r\n    public SingerLyricTimeline(Singer singer)\r\n    {\r\n        Singer = singer;\r\n\r\n        RelativeSizeAxes = Axes.Both;\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(ISingerScreenScrollingInfoProvider scrollingInfoProvider, OsuColour colours)\r\n    {\r\n        AddInternal(new Box\r\n        {\r\n            Name = \"Background\",\r\n            Depth = 1,\r\n            RelativeSizeAxes = Axes.X,\r\n            Height = timeline_height,\r\n            Anchor = Anchor.CentreLeft,\r\n            Origin = Anchor.CentreLeft,\r\n            Colour = colours.Gray3,\r\n        });\r\n\r\n        BindableZoom.BindTo(scrollingInfoProvider.BindableZoom);\r\n        BindableCurrent.BindTo(scrollingInfoProvider.BindableCurrent);\r\n    }\r\n\r\n    protected override Container CreateMainContainer()\r\n    {\r\n        return base.CreateMainContainer().With(c => c.Height = timeline_height);\r\n    }\r\n\r\n    protected override IEnumerable<Drawable> CreateBlueprintContainer()\r\n    {\r\n        yield return new SingerLyricEditorBlueprintContainer();\r\n    }\r\n\r\n    protected override void LoadComplete()\r\n    {\r\n        base.LoadComplete();\r\n\r\n        const float preempt_time = 1000;\r\n\r\n        var firstLyric = beatmap.HitObjects.OfType<Lyric>().FirstOrDefault(x => x.StartTime > 0);\r\n        double? lyricStartTime = firstLyric?.StartTime;\r\n        if (lyricStartTime == null)\r\n            return;\r\n\r\n        float position = PositionAtTime(lyricStartTime.Value - preempt_time);\r\n        ScrollTo(position, false);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Singers/Rows/CreateNewLyricPlacementRow.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Beatmaps;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Singers.Rows;\r\n\r\npublic partial class CreateNewLyricPlacementColumn : LyricPlacementColumn\r\n{\r\n    [Resolved]\r\n    private IBeatmapSingersChangeHandler beatmapSingersChangeHandler { get; set; } = null!;\r\n\r\n    public CreateNewLyricPlacementColumn()\r\n        : base(new Singer { Name = \"Press to create new singer\" })\r\n    {\r\n    }\r\n\r\n    protected override Drawable CreateSingerInfo(Singer singer)\r\n    {\r\n        return new Container\r\n        {\r\n            Child = new IconButton\r\n            {\r\n                Anchor = Anchor.Centre,\r\n                Origin = Anchor.Centre,\r\n                Icon = FontAwesome.Solid.PlusCircle,\r\n                Size = new Vector2(32),\r\n                TooltipText = \"Click to add new singer\",\r\n                Action = () =>\r\n                {\r\n                    beatmapSingersChangeHandler.Add();\r\n                },\r\n            },\r\n        };\r\n    }\r\n\r\n    protected override Drawable CreateTimeLinePart(Singer singer) => Empty();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Singers/Rows/DefaultLyricPlacementRow.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Extensions.Color4Extensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Singers.Rows.Components.Timeline;\r\nusing osu.Game.Screens.Edit.Compose.Components.Timeline;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Singers.Rows;\r\n\r\npublic partial class DefaultLyricPlacementColumn : LyricPlacementColumn\r\n{\r\n    protected const int LEFT_MARGIN = 22;\r\n\r\n    public static Singer DefaultSinger { get; } = new() { Name = \"Default\" };\r\n\r\n    [Resolved]\r\n    private ISingerScreenScrollingInfoProvider scrollingInfoProvider { get; set; } = null!;\r\n\r\n    public DefaultLyricPlacementColumn()\r\n        : base(DefaultSinger)\r\n    {\r\n    }\r\n\r\n    // should add extra width because this component is not draggable, which will have extra spacing.\r\n    protected override float SingerInfoSize => INFO_SIZE + LEFT_MARGIN;\r\n\r\n    // todo : might display song info?\r\n    protected override Drawable CreateSingerInfo(Singer singer) => new Container\r\n    {\r\n        Children = new Drawable[]\r\n        {\r\n            new Box\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n                Colour = Color4Extensions.FromHex(\"333\"),\r\n            },\r\n            new Container<TimelineButton>\r\n            {\r\n                Anchor = Anchor.CentreRight,\r\n                Origin = Anchor.CentreRight,\r\n                RelativeSizeAxes = Axes.Y,\r\n                AutoSizeAxes = Axes.X,\r\n                Masking = true,\r\n                Children = new[]\r\n                {\r\n                    new TimelineButton\r\n                    {\r\n                        RelativeSizeAxes = Axes.Y,\r\n                        Height = 0.5f,\r\n                        Icon = FontAwesome.Solid.SearchPlus,\r\n                        Action = () => changeZoom(1),\r\n                    },\r\n                    new TimelineButton\r\n                    {\r\n                        Anchor = Anchor.BottomLeft,\r\n                        Origin = Anchor.BottomLeft,\r\n                        RelativeSizeAxes = Axes.Y,\r\n                        Height = 0.5f,\r\n                        Icon = FontAwesome.Solid.SearchMinus,\r\n                        Action = () => changeZoom(-1),\r\n                    },\r\n                },\r\n            },\r\n        },\r\n    };\r\n\r\n    protected override Drawable CreateTimeLinePart(Singer singer)\r\n        => new SingerLyricTimeline(singer);\r\n\r\n    private void changeZoom(float change) => scrollingInfoProvider.BindableZoom.Value += change;\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Singers/Rows/LyricPlacementRow.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Singers.Rows;\r\n\r\npublic abstract partial class LyricPlacementColumn : CompositeDrawable\r\n{\r\n    protected const int INFO_SIZE = 178;\r\n\r\n    private readonly Singer singer;\r\n\r\n    protected LyricPlacementColumn(Singer singer)\r\n    {\r\n        this.singer = singer;\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OverlayColourProvider colourProvider)\r\n    {\r\n        InternalChildren = new Drawable[]\r\n        {\r\n            new Box\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n                Alpha = 0.3f,\r\n                Colour = colourProvider.Background1,\r\n            },\r\n            new GridContainer\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n                ColumnDimensions = new[]\r\n                {\r\n                    new Dimension(GridSizeMode.Absolute, SingerInfoSize),\r\n                    new Dimension(GridSizeMode.Absolute, 5),\r\n                    new Dimension(),\r\n                },\r\n                Content = new[]\r\n                {\r\n                    new[]\r\n                    {\r\n                        CreateSingerInfo(singer).With(x => { x.RelativeSizeAxes = Axes.Both; }),\r\n                        new Box\r\n                        {\r\n                            Name = \"Separator\",\r\n                            RelativeSizeAxes = Axes.Both,\r\n                            Colour = colourProvider.Dark1,\r\n                        },\r\n                        CreateTimeLinePart(singer).With(x => { x.RelativeSizeAxes = Axes.Both; }),\r\n                    },\r\n                },\r\n            },\r\n        };\r\n    }\r\n\r\n    protected virtual float SingerInfoSize => INFO_SIZE;\r\n\r\n    protected abstract Drawable CreateSingerInfo(Singer singer);\r\n\r\n    protected abstract Drawable CreateTimeLinePart(Singer singer);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Singers/Rows/SingerLyricPlacementRow.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Extensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Cursor;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.Cursor;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Singers.Detail;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Singers.Rows.Components;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Singers.Rows.Components.Timeline;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Singers.Rows;\r\n\r\npublic partial class SingerLyricPlacementColumn : LyricPlacementColumn\r\n{\r\n    public SingerLyricPlacementColumn(Singer singer)\r\n        : base(singer)\r\n    {\r\n    }\r\n\r\n    protected override Drawable CreateSingerInfo(Singer singer)\r\n        => new DrawableSingerInfo(singer);\r\n\r\n    protected override Drawable CreateTimeLinePart(Singer singer)\r\n        => new SingerLyricTimeline(singer);\r\n\r\n    internal partial class DrawableSingerInfo : CompositeDrawable, IHasCustomTooltip<Singer>, IHasContextMenu, IHasPopover\r\n    {\r\n        private const int avatar_size = 48;\r\n        private const int main_text_size = 24;\r\n        private const int sub_text_size = 12;\r\n\r\n        [Resolved]\r\n        private IBeatmapSingersChangeHandler beatmapSingersChangeHandler { get; set; } = null!;\r\n\r\n        [Resolved]\r\n        private IDialogOverlay dialogOverlay { get; set; } = null!;\r\n\r\n        private readonly IBindable<int> bindableOrder = new Bindable<int>();\r\n        private readonly IBindable<float> bindableHue = new Bindable<float>();\r\n        private readonly IBindable<string> bindableSingerName = new Bindable<string>();\r\n        private readonly IBindable<string> bindableEnglishName = new Bindable<string>();\r\n\r\n        private readonly Singer singer;\r\n\r\n        public DrawableSingerInfo(Singer singer)\r\n        {\r\n            this.singer = singer;\r\n\r\n            bindableOrder.BindTo(singer.OrderBindable);\r\n            bindableHue.BindTo(singer.HueBindable);\r\n            bindableSingerName.BindTo(singer.NameBindable);\r\n            bindableEnglishName.BindTo(singer.EnglishNameBindable);\r\n\r\n            Box background;\r\n            OsuSpriteText singerName;\r\n            OsuSpriteText singerEnglishName;\r\n\r\n            InternalChildren = new Drawable[]\r\n            {\r\n                background = new Box\r\n                {\r\n                    Name = \"Background\",\r\n                    RelativeSizeAxes = Axes.Both,\r\n                },\r\n                new Container\r\n                {\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    Padding = new MarginPadding(10) { Right = 0 },\r\n                    Child = new GridContainer\r\n                    {\r\n                        Name = \"Basic info\",\r\n                        RelativeSizeAxes = Axes.X,\r\n                        Height = avatar_size,\r\n                        ColumnDimensions = new[]\r\n                        {\r\n                            new Dimension(GridSizeMode.Absolute, avatar_size),\r\n                            new Dimension(),\r\n                        },\r\n                        Content = new[]\r\n                        {\r\n                            new Drawable[]\r\n                            {\r\n                                new SingerAvatar(singer)\r\n                                {\r\n                                    Name = \"Avatar\",\r\n                                    Size = new Vector2(avatar_size),\r\n                                },\r\n                                new FillFlowContainer\r\n                                {\r\n                                    Name = \"Singer name\",\r\n                                    RelativeSizeAxes = Axes.X,\r\n                                    AutoSizeAxes = Axes.Y,\r\n                                    Direction = FillDirection.Vertical,\r\n                                    Padding = new MarginPadding { Left = 5 },\r\n                                    Spacing = new Vector2(1),\r\n                                    Children = new Drawable[]\r\n                                    {\r\n                                        singerName = new TruncatingSpriteText\r\n                                        {\r\n                                            Name = \"Singer name\",\r\n                                            Font = OsuFont.GetFont(weight: FontWeight.Bold, size: main_text_size),\r\n                                            RelativeSizeAxes = Axes.X,\r\n                                        },\r\n                                        singerEnglishName = new TruncatingSpriteText\r\n                                        {\r\n                                            Name = \"English name\",\r\n                                            Font = OsuFont.GetFont(weight: FontWeight.Bold, size: sub_text_size),\r\n                                            RelativeSizeAxes = Axes.X,\r\n                                        },\r\n                                    },\r\n                                },\r\n                            },\r\n                        },\r\n                    },\r\n                },\r\n            };\r\n\r\n            bindableOrder.BindValueChanged(_ =>\r\n            {\r\n                singerName.Text = $\"#{singer.Order} {singer.Name}\";\r\n            }, true);\r\n\r\n            bindableHue.BindValueChanged(_ =>\r\n            {\r\n                // background\r\n                background.Colour = SingerUtils.GetBackgroundColour(singer);\r\n            }, true);\r\n\r\n            bindableSingerName.BindValueChanged(_ =>\r\n            {\r\n                singerName.Text = $\"#{singer.Order} {singer.Name}\";\r\n            }, true);\r\n\r\n            bindableEnglishName.BindValueChanged(_ =>\r\n            {\r\n                singerEnglishName.Text = singer.EnglishName;\r\n            }, true);\r\n        }\r\n\r\n        public ITooltip<Singer> GetCustomTooltip() => new SingerToolTip();\r\n\r\n        public Singer TooltipContent => singer;\r\n\r\n        public MenuItem[] ContextMenuItems => new MenuItem[]\r\n        {\r\n            new OsuMenuItem(\"Edit singer info\", MenuItemType.Standard, this.ShowPopover),\r\n            new OsuMenuItem(\"Delete\", MenuItemType.Destructive, () =>\r\n            {\r\n                dialogOverlay.Push(new DeleteSingerDialog(isOk =>\r\n                {\r\n                    if (isOk)\r\n                        beatmapSingersChangeHandler.Remove(singer);\r\n                }));\r\n            }),\r\n        };\r\n\r\n        protected override bool OnClick(ClickEvent e)\r\n        {\r\n            this.ShowPopover();\r\n            return base.OnClick(e);\r\n        }\r\n\r\n        public Popover GetPopover()\r\n            => new SingerEditPopover(singer);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Singers/SingerEditSection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Singers.Rows;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Singers;\r\n\r\npublic partial class SingerEditSection : CompositeDrawable\r\n{\r\n    private SingerRearrangeableList singerContainers = null!;\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(IBeatmapSingersChangeHandler beatmapSingersChangeHandler)\r\n    {\r\n        InternalChild = new GridContainer\r\n        {\r\n            RelativeSizeAxes = Axes.Both,\r\n            RowDimensions = new[]\r\n            {\r\n                new Dimension(GridSizeMode.Absolute, 100),\r\n                new Dimension(),\r\n            },\r\n            Content = new[]\r\n            {\r\n                new Drawable[]\r\n                {\r\n                    new DefaultLyricPlacementColumn\r\n                    {\r\n                        Name = \"Default\",\r\n                        RelativeSizeAxes = Axes.Both,\r\n                    },\r\n                },\r\n                new Drawable[]\r\n                {\r\n                    singerContainers = new SingerRearrangeableList\r\n                    {\r\n                        Name = \"List of singer\",\r\n                        RelativeSizeAxes = Axes.Both,\r\n                        DisplayBottomDrawable = true,\r\n                    },\r\n                },\r\n            },\r\n        };\r\n\r\n        singerContainers.Items.BindTo(beatmapSingersChangeHandler.Singers);\r\n        singerContainers.OnOrderChanged += beatmapSingersChangeHandler.ChangeOrder;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Singers/SingerRearrangeableList.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Graphics.Containers;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.Containers;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Singers.Rows;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Singers;\r\n\r\npublic partial class SingerRearrangeableList : OrderRearrangeableListContainer<Singer>\r\n{\r\n    protected override Vector2 Spacing => new(0, 5);\r\n\r\n    public SingerRearrangeableList()\r\n    {\r\n        Padding = new MarginPadding\r\n        {\r\n            Top = Spacing.Y,\r\n            Bottom = Spacing.Y,\r\n        };\r\n    }\r\n\r\n    protected override OsuRearrangeableListItem<Singer> CreateOsuDrawable(Singer item)\r\n        => new SingerRearrangeableListItem(item);\r\n\r\n    protected override Drawable CreateBottomDrawable()\r\n    {\r\n        return new Container\r\n        {\r\n            RelativeSizeAxes = Axes.X,\r\n            Height = 64,\r\n            Padding = new MarginPadding { Left = 22 },\r\n            Child = new Container\r\n            {\r\n                Masking = true,\r\n                CornerRadius = 5,\r\n                RelativeSizeAxes = Axes.Both,\r\n                Child = new CreateNewLyricPlacementColumn\r\n                {\r\n                    RelativeSizeAxes = Axes.Both,\r\n                },\r\n            },\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Singers/SingerRearrangeableListItem.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Containers;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Singers.Rows;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Singers;\r\n\r\npublic partial class SingerRearrangeableListItem : OsuRearrangeableListItem<Singer>\r\n{\r\n    private Box dragAlert = null!;\r\n\r\n    public SingerRearrangeableListItem(Singer item)\r\n        : base(item)\r\n    {\r\n    }\r\n\r\n    protected override Drawable CreateContent()\r\n    {\r\n        return new Container\r\n        {\r\n            Masking = true,\r\n            CornerRadius = 5,\r\n            RelativeSizeAxes = Axes.X,\r\n            Height = 90,\r\n            Children = new Drawable[]\r\n            {\r\n                dragAlert = new Box\r\n                {\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    Alpha = 0,\r\n                },\r\n                new SingerLyricPlacementColumn(Model)\r\n                {\r\n                    RelativeSizeAxes = Axes.Both,\r\n                },\r\n            },\r\n        };\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OsuColour colours)\r\n    {\r\n        dragAlert.Colour = colours.YellowDarker;\r\n    }\r\n\r\n    protected override bool OnDragStart(DragStartEvent e)\r\n    {\r\n        if (!base.OnDragStart(e))\r\n            return false;\r\n\r\n        dragAlert.Show();\r\n        return true;\r\n    }\r\n\r\n    protected override void OnDragEnd(DragEndEvent e)\r\n    {\r\n        dragAlert.Hide();\r\n        base.OnDragEnd(e);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Singers/SingerScreen.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Containers;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Singers;\r\n\r\n[Cached(typeof(ISingerScreenScrollingInfoProvider))]\r\npublic partial class SingerScreen : BeatmapEditorRoundedScreen, ISingerScreenScrollingInfoProvider\r\n{\r\n    [Cached(typeof(IBeatmapSingersChangeHandler))]\r\n    private readonly BeatmapSingersChangeHandler beatmapSingersChangeHandler;\r\n\r\n    [Cached(typeof(ILyricSingerChangeHandler))]\r\n    private readonly LyricSingerChangeHandler lyricSingerChangeHandler;\r\n\r\n    [Cached]\r\n    private readonly BindableList<Lyric> selectedLyrics = new();\r\n\r\n    [Resolved]\r\n    private EditorBeatmap editorBeatmap { get; set; } = null!;\r\n\r\n    public BindableFloat BindableZoom { get; } = new();\r\n\r\n    public BindableFloat BindableCurrent { get; } = new();\r\n\r\n    public SingerScreen()\r\n        : base(KaraokeBeatmapEditorScreenMode.Singer)\r\n    {\r\n        AddInternal(beatmapSingersChangeHandler = new BeatmapSingersChangeHandler());\r\n        AddInternal(lyricSingerChangeHandler = new LyricSingerChangeHandler());\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(EditorClock editorClock)\r\n    {\r\n        // initialize scroll zone.\r\n        BindableZoom.MaxValue = ZoomableScrollContainerUtils.GetZoomLevelForVisibleMilliseconds(editorClock, 8000);\r\n        BindableZoom.MinValue = ZoomableScrollContainerUtils.GetZoomLevelForVisibleMilliseconds(editorClock, 80000);\r\n        BindableZoom.Value = BindableZoom.Default = ZoomableScrollContainerUtils.GetZoomLevelForVisibleMilliseconds(editorClock, 40000);\r\n\r\n        Add(new FixedSectionsContainer<Drawable>\r\n        {\r\n            FixedHeader = new SingerScreenHeader(),\r\n            RelativeSizeAxes = Axes.Both,\r\n            Children = new[]\r\n            {\r\n                new SingerEditSection\r\n                {\r\n                    RelativeSizeAxes = Axes.Both,\r\n                },\r\n            },\r\n        });\r\n    }\r\n\r\n    protected override void LoadComplete()\r\n    {\r\n        base.LoadComplete();\r\n\r\n        // sync the selected lyrics in here for prevent the invalid thread for mutation exception.\r\n        BindablesUtils.Sync(selectedLyrics, editorBeatmap.SelectedHitObjects);\r\n    }\r\n\r\n    protected override void PopOut()\r\n    {\r\n        base.PopOut();\r\n\r\n        // should clear the selected lyrics because other place might not support multi select.\r\n        selectedLyrics.Clear();\r\n    }\r\n\r\n    private partial class FixedSectionsContainer<T> : SectionsContainer<T> where T : Drawable\r\n    {\r\n        // todo: check what this shit doing.\r\n        protected override Container<T> Content { get; }\r\n\r\n        public FixedSectionsContainer()\r\n        {\r\n            AddInternal(Content = new Container<T>\r\n            {\r\n                Masking = true,\r\n                RelativeSizeAxes = Axes.Both,\r\n                Padding = new MarginPadding { Top = 55 },\r\n            });\r\n        }\r\n    }\r\n\r\n    private partial class SingerScreenHeader : OverlayHeader\r\n    {\r\n        protected override OverlayTitle CreateTitle() => new SingerScreenTitle();\r\n\r\n        private partial class SingerScreenTitle : OverlayTitle\r\n        {\r\n            public SingerScreenTitle()\r\n            {\r\n                Title = \"singer\";\r\n                Description = \"create singer of your beatmap\";\r\n                Icon = OsuIcon.Player;\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Translations/Components/CreateNewTranslationButton.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Globalization;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Extensions;\r\nusing osu.Framework.Graphics.Cursor;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.UserInterfaceV2;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Translations.Components;\r\n\r\npublic partial class CreateNewTranslationButton : IconButton, IHasPopover\r\n{\r\n    [Resolved]\r\n    private IBeatmapTranslationsChangeHandler beatmapTranslationsChangeHandler { get; set; } = null!;\r\n\r\n    private readonly Bindable<CultureInfo?> currentLanguage = new();\r\n\r\n    public CreateNewTranslationButton()\r\n    {\r\n        Icon = FontAwesome.Solid.Plus;\r\n        Action = this.ShowPopover;\r\n\r\n        currentLanguage.BindValueChanged(e =>\r\n        {\r\n            var newLanguage = e.NewValue;\r\n            if (newLanguage == null)\r\n                return;\r\n\r\n            if (!beatmapTranslationsChangeHandler.Languages.Contains(newLanguage))\r\n            {\r\n                beatmapTranslationsChangeHandler.Add(newLanguage);\r\n            }\r\n\r\n            // after selected the language, should always hide the popover.\r\n            this.HidePopover();\r\n\r\n            // Should clear the bindable after selected.\r\n            currentLanguage.Value = null;\r\n        });\r\n    }\r\n\r\n    public Popover GetPopover()\r\n        => new LanguageSelectorPopover(currentLanguage);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Translations/Components/LanguageDropdown.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Globalization;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Translations.Components;\r\n\r\npublic partial class LanguageDropdown : OsuDropdown<CultureInfo>\r\n{\r\n    protected override LocalisableString GenerateItemText(CultureInfo item)\r\n        => CultureInfoUtils.GetLanguageDisplayText(item);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Translations/Components/LyricTranslationTextBox.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Globalization;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Translations.Components;\r\n\r\npublic partial class LyricTranslationTextBox : OsuTextBox\r\n{\r\n    [Resolved]\r\n    private EditorBeatmap beatmap { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private ILyricTranslationChangeHandler lyricTranslationChangeHandler { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private ITranslationInfoProvider translationInfoProvider { get; set; } = null!;\r\n\r\n    private readonly IBindable<CultureInfo?> currentLanguage = new Bindable<CultureInfo?>();\r\n\r\n    private readonly Lyric lyric;\r\n\r\n    public LyricTranslationTextBox(Lyric lyric)\r\n    {\r\n        this.lyric = lyric;\r\n\r\n        currentLanguage.BindValueChanged(v =>\r\n        {\r\n            var cultureInfo = v.NewValue;\r\n\r\n            // disable and clear text box if contains no language in language list.\r\n            Text = cultureInfo != null ? translationInfoProvider.GetLyricTranslation(lyric, cultureInfo) : null;\r\n            ScheduleAfterChildren(() =>\r\n            {\r\n                Current.Disabled = cultureInfo == null;\r\n            });\r\n        }, true);\r\n\r\n        OnCommit += (t, newText) =>\r\n        {\r\n            if (!newText)\r\n                return;\r\n\r\n            string text = t.Text.Trim();\r\n\r\n            var cultureInfo = currentLanguage.Value;\r\n            if (cultureInfo == null)\r\n                return;\r\n\r\n            lyricTranslationChangeHandler.UpdateTranslation(cultureInfo, text);\r\n        };\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(IBindable<CultureInfo?> currentLanguage)\r\n    {\r\n        this.currentLanguage.BindTo(currentLanguage);\r\n    }\r\n\r\n    protected override void OnFocus(FocusEvent e)\r\n    {\r\n        base.OnFocus(e);\r\n        beatmap.SelectedHitObjects.Add(lyric);\r\n    }\r\n\r\n    protected override void OnFocusLost(FocusLostEvent e)\r\n    {\r\n        base.OnFocusLost(e);\r\n        Schedule(() =>\r\n        {\r\n            // should remove lyric until commit finished.\r\n            beatmap.SelectedHitObjects.Remove(lyric);\r\n        });\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Translations/Components/PreviewLyricSpriteText.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics.Cursor;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.Cursor;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Translations.Components;\r\n\r\npublic partial class PreviewLyricSpriteText : DrawableLyricSpriteText, IHasCustomTooltip<Lyric>\r\n{\r\n    public PreviewLyricSpriteText(Lyric hitObject)\r\n        : base(hitObject)\r\n    {\r\n        TooltipContent = hitObject;\r\n    }\r\n\r\n    public ITooltip<Lyric> GetCustomTooltip() => new LyricTooltip();\r\n\r\n    public Lyric TooltipContent { get; }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Translations/Components/RemoveTranslationButton.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Globalization;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Beatmaps;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Translations.Components;\r\n\r\npublic partial class RemoveTranslationButton : IconButton\r\n{\r\n    [Resolved]\r\n    private IBeatmapTranslationsChangeHandler beatmapTranslationsChangeHandler { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private IDialogOverlay dialogOverlay { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private IBindable<CultureInfo> currentLanguage { get; set; } = null!;\r\n\r\n    public RemoveTranslationButton()\r\n    {\r\n        Icon = FontAwesome.Solid.Trash;\r\n        Action = () =>\r\n        {\r\n            if (beatmapTranslationsChangeHandler.IsLanguageContainsTranslation(currentLanguage.Value))\r\n            {\r\n                dialogOverlay.Push(new DeleteLanguagePopupDialog(currentLanguage.Value, isOk =>\r\n                {\r\n                    if (isOk)\r\n                        beatmapTranslationsChangeHandler.Remove(currentLanguage.Value);\r\n                }));\r\n            }\r\n            else\r\n            {\r\n                beatmapTranslationsChangeHandler.Remove(currentLanguage.Value);\r\n            }\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Translations/DeleteLanguagePopupDialog.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Globalization;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Overlays.Dialog;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Translations;\r\n\r\npublic partial class DeleteLanguagePopupDialog : PopupDialog\r\n{\r\n    public DeleteLanguagePopupDialog(CultureInfo currentLanguage, Action<bool> okAction)\r\n    {\r\n        Icon = FontAwesome.Regular.TrashAlt;\r\n        HeaderText = $\"Confirm deletion of language {CultureInfoUtils.GetLanguageDisplayText(currentLanguage)}?\";\r\n        BodyText = \"It will also remove the translations.\";\r\n        Buttons = new PopupDialogButton[]\r\n        {\r\n            new PopupDialogOkButton\r\n            {\r\n                Text = \"Yes. Go for it.\",\r\n                Action = () => okAction(true),\r\n            },\r\n            new PopupDialogCancelButton\r\n            {\r\n                Text = \"No! Abort mission!\",\r\n                Action = () => okAction(false),\r\n            },\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Translations/ITranslationInfoProvider.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Globalization;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Translations;\r\n\r\npublic interface ITranslationInfoProvider\r\n{\r\n    string? GetLyricTranslation(Lyric lyric, CultureInfo cultureInfo);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Translations/TranslationEditSection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Globalization;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.Shapes;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Translations.Components;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Translations;\r\n\r\n[Cached(typeof(ITranslationInfoProvider))]\r\npublic partial class TranslationEditSection : Container, ITranslationInfoProvider\r\n{\r\n    private const int row_height = 50;\r\n    private const int column_spacing = 10;\r\n    private const int row_inner_spacing = 10;\r\n\r\n    private readonly CornerBackground timeSectionBackground;\r\n    private readonly CornerBackground lyricSectionBackground;\r\n    private readonly LanguageDropdown languageDropdown;\r\n\r\n    [Cached(typeof(IBindable<CultureInfo?>))]\r\n    private readonly Bindable<CultureInfo?> currentLanguage = new();\r\n\r\n    [Resolved]\r\n    private IBeatmapTranslationsChangeHandler beatmapTranslationsChangeHandler { get; set; } = null!;\r\n\r\n    private readonly IBindableList<Lyric> bindableLyrics = new BindableList<Lyric>();\r\n\r\n    public TranslationEditSection()\r\n    {\r\n        Padding = new MarginPadding(10);\r\n\r\n        var columnDimensions = new[]\r\n        {\r\n            new Dimension(GridSizeMode.Absolute, 200),\r\n            new Dimension(GridSizeMode.Absolute, column_spacing),\r\n            new Dimension(GridSizeMode.Absolute, 400),\r\n            new Dimension(GridSizeMode.Absolute, column_spacing),\r\n            new Dimension(),\r\n        };\r\n        GridContainer translationsGrid;\r\n\r\n        Child = new FillFlowContainer\r\n        {\r\n            RelativeSizeAxes = Axes.X,\r\n            AutoSizeAxes = Axes.Y,\r\n            Children = new Drawable[]\r\n            {\r\n                new GridContainer\r\n                {\r\n                    Name = \"LanguageSelection\",\r\n                    RowDimensions = new[]\r\n                    {\r\n                        new Dimension(GridSizeMode.AutoSize),\r\n                    },\r\n                    ColumnDimensions = columnDimensions,\r\n                    RelativeSizeAxes = Axes.X,\r\n                    AutoSizeAxes = Axes.Y,\r\n                    Content = new[]\r\n                    {\r\n                        new[]\r\n                        {\r\n                            Empty(),\r\n                            Empty(),\r\n                            Empty(),\r\n                            Empty(),\r\n                            new GridContainer\r\n                            {\r\n                                RelativeSizeAxes = Axes.X,\r\n                                AutoSizeAxes = Axes.Y,\r\n                                ColumnDimensions = new[]\r\n                                {\r\n                                    new Dimension(),\r\n                                    new Dimension(GridSizeMode.Absolute, column_spacing),\r\n                                    new Dimension(GridSizeMode.Absolute, 50),\r\n                                    new Dimension(GridSizeMode.Absolute, column_spacing),\r\n                                    new Dimension(GridSizeMode.Absolute, 50),\r\n                                },\r\n                                RowDimensions = new[]\r\n                                {\r\n                                    new Dimension(GridSizeMode.AutoSize),\r\n                                },\r\n                                Content = new[]\r\n                                {\r\n                                    new[]\r\n                                    {\r\n                                        languageDropdown = new LanguageDropdown\r\n                                        {\r\n                                            RelativeSizeAxes = Axes.X,\r\n                                        },\r\n                                        Empty(),\r\n                                        new CreateNewTranslationButton\r\n                                        {\r\n                                            Y = 5,\r\n                                        },\r\n                                        Empty(),\r\n                                        new RemoveTranslationButton\r\n                                        {\r\n                                            Y = 5,\r\n                                        },\r\n                                    },\r\n                                },\r\n                            },\r\n                        },\r\n                    },\r\n                },\r\n                new Container\r\n                {\r\n                    RelativeSizeAxes = Axes.X,\r\n                    AutoSizeAxes = Axes.Y,\r\n                    Children = new[]\r\n                    {\r\n                        new GridContainer\r\n                        {\r\n                            Name = \"Background\",\r\n                            RowDimensions = new[]\r\n                            {\r\n                                new Dimension(GridSizeMode.AutoSize),\r\n                            },\r\n                            ColumnDimensions = columnDimensions,\r\n                            RelativeSizeAxes = Axes.Both,\r\n                            Content = new[]\r\n                            {\r\n                                new[]\r\n                                {\r\n                                    new CornerBackground\r\n                                    {\r\n                                        Alpha = 0,\r\n                                    },\r\n                                    null,\r\n                                    null,\r\n                                    null,\r\n                                    null,\r\n                                },\r\n                                new[]\r\n                                {\r\n                                    timeSectionBackground = new CornerBackground\r\n                                    {\r\n                                        RelativeSizeAxes = Axes.Both,\r\n                                    },\r\n                                    null,\r\n                                    lyricSectionBackground = new CornerBackground\r\n                                    {\r\n                                        RelativeSizeAxes = Axes.Both,\r\n                                    },\r\n                                    null,\r\n                                    null,\r\n                                },\r\n                            },\r\n                        },\r\n                        translationsGrid = new GridContainer\r\n                        {\r\n                            Name = \"Translations\",\r\n                            ColumnDimensions = columnDimensions,\r\n                            RelativeSizeAxes = Axes.X,\r\n                            AutoSizeAxes = Axes.Y,\r\n                        },\r\n                    },\r\n                },\r\n            },\r\n        };\r\n\r\n        languageDropdown.Current.BindValueChanged(x =>\r\n        {\r\n            // should use currentLanguage.BindTo(languageDropdown.Current); once bindable is not nullable again.\r\n            currentLanguage.Value = x.NewValue;\r\n        });\r\n\r\n        bindableLyrics.BindCollectionChanged((_, _) =>\r\n        {\r\n            // just re-create all the view, lazy to save the performance in here.\r\n            translationsGrid.RowDimensions = bindableLyrics.Select(_ => new Dimension(GridSizeMode.Absolute, row_height)).ToArray();\r\n            translationsGrid.Content = createContent();\r\n        });\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(ILyricsProvider lyricsProvider, OverlayColourProvider colourProvider)\r\n    {\r\n        languageDropdown.ItemSource = beatmapTranslationsChangeHandler.Languages;\r\n\r\n        bindableLyrics.BindTo(lyricsProvider.BindableLyrics);\r\n\r\n        timeSectionBackground.Colour = colourProvider.Background6;\r\n        lyricSectionBackground.Colour = colourProvider.Dark6;\r\n    }\r\n\r\n    private Drawable[][] createContent()\r\n    {\r\n        return bindableLyrics.Select(x =>\r\n        {\r\n            return new[]\r\n            {\r\n                createTimeDrawable(x),\r\n                Empty(),\r\n                createPreviewSpriteText(x),\r\n                Empty(),\r\n                createTranslationTextBox(x),\r\n            };\r\n        }).ToArray();\r\n    }\r\n\r\n    private Drawable createTimeDrawable(Lyric lyric)\r\n    {\r\n        return new OsuSpriteText\r\n        {\r\n            Text = LyricUtils.LyricTimeFormattedString(lyric),\r\n            Anchor = Anchor.CentreLeft,\r\n            Origin = Anchor.CentreLeft,\r\n            Margin = new MarginPadding { Left = row_inner_spacing },\r\n            Font = OsuFont.GetFont(size: 18, fixedWidth: true),\r\n            AllowMultiline = false,\r\n        };\r\n    }\r\n\r\n    private Drawable createPreviewSpriteText(Lyric lyric) =>\r\n        new PreviewLyricSpriteText(lyric)\r\n        {\r\n            Anchor = Anchor.CentreLeft,\r\n            Origin = Anchor.CentreLeft,\r\n            RelativeSizeAxes = Axes.X,\r\n            AllowMultiline = false,\r\n            Truncate = true,\r\n            Padding = new MarginPadding { Left = row_inner_spacing },\r\n            Font = new FontUsage(size: 25),\r\n            TopTextFont = new FontUsage(size: 10),\r\n            BottomTextFont = new FontUsage(size: 10),\r\n        };\r\n\r\n    private Drawable createTranslationTextBox(Lyric lyric) =>\r\n        new LyricTranslationTextBox(lyric)\r\n        {\r\n            Anchor = Anchor.CentreLeft,\r\n            Origin = Anchor.CentreLeft,\r\n            RelativeSizeAxes = Axes.X,\r\n            TabbableContentContainer = this,\r\n            CommitOnFocusLost = true,\r\n        };\r\n\r\n    public string? GetLyricTranslation(Lyric lyric, CultureInfo cultureInfo)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(cultureInfo);\r\n\r\n        return lyric.Translations.TryGetValue(cultureInfo, out string? translation) ? translation : null;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Beatmaps/Translations/TranslationScreen.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Containers;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Translations;\r\n\r\npublic partial class TranslationScreen : BeatmapEditorRoundedScreen\r\n{\r\n    [Cached(typeof(IBeatmapTranslationsChangeHandler))]\r\n    private readonly BeatmapTranslationsChangeHandler beatmapTranslationsChangeHandler;\r\n\r\n    [Cached(typeof(ILyricTranslationChangeHandler))]\r\n    private readonly LyricTranslationChangeHandler lyricTranslationChangeHandler;\r\n\r\n    public TranslationScreen()\r\n        : base(KaraokeBeatmapEditorScreenMode.Translation)\r\n    {\r\n        AddInternal(beatmapTranslationsChangeHandler = new BeatmapTranslationsChangeHandler());\r\n        AddInternal(lyricTranslationChangeHandler = new LyricTranslationChangeHandler());\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        Add(new SectionsContainer<Container>\r\n        {\r\n            FixedHeader = new TranslationScreenHeader(),\r\n            RelativeSizeAxes = Axes.Both,\r\n            Children = new Container[]\r\n            {\r\n                new TranslationEditSection\r\n                {\r\n                    RelativeSizeAxes = Axes.X,\r\n                    AutoSizeAxes = Axes.Y,\r\n                },\r\n            },\r\n        });\r\n    }\r\n\r\n    internal partial class TranslationScreenHeader : OverlayHeader\r\n    {\r\n        protected override OverlayTitle CreateTitle() => new TranslationScreenTitle();\r\n\r\n        private partial class TranslationScreenTitle : OverlayTitle\r\n        {\r\n            public TranslationScreenTitle()\r\n            {\r\n                Title = \"translation\";\r\n                Description = \"create translation of your beatmap\";\r\n                Icon = OsuIcon.Online;\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/BottomBar.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Extensions.Color4Extensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Effects;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Screens.Edit.Components;\r\nusing osu.Game.Screens.Edit.Components.Timelines.Summary;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit;\r\n\r\n/// <summary>\r\n/// Copy the Component from the <see cref=\"Game.Screens.Edit\"/>\r\n/// </summary>\r\npublic partial class BottomBar : CompositeDrawable\r\n{\r\n    private readonly Box background;\r\n\r\n    public BottomBar()\r\n    {\r\n        Anchor = Anchor.BottomLeft;\r\n        Origin = Anchor.BottomLeft;\r\n\r\n        RelativeSizeAxes = Axes.X;\r\n\r\n        Height = 60;\r\n\r\n        Masking = true;\r\n        EdgeEffect = new EdgeEffectParameters\r\n        {\r\n            Colour = Color4.Black.Opacity(0.2f),\r\n            Type = EdgeEffectType.Shadow,\r\n            Radius = 10f,\r\n        };\r\n\r\n        InternalChildren = new Drawable[]\r\n        {\r\n            background = new Box\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n            },\r\n            new GridContainer\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n                ColumnDimensions = new[]\r\n                {\r\n                    new Dimension(GridSizeMode.Absolute, 170),\r\n                    new Dimension(),\r\n                    new Dimension(GridSizeMode.Absolute, 220),\r\n                },\r\n                Content = new[]\r\n                {\r\n                    new Drawable[]\r\n                    {\r\n                        new TimeInfoContainer { RelativeSizeAxes = Axes.Both },\r\n                        new SummaryTimeline { RelativeSizeAxes = Axes.Both },\r\n                        new PlaybackControl { RelativeSizeAxes = Axes.Both },\r\n                    },\r\n                },\r\n            },\r\n        };\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OverlayColourProvider colourProvider)\r\n    {\r\n        background.Colour = colourProvider.Background4;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Components/Containers/BindableScrollContainer.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Game.Screens.Edit.Compose.Components.Timeline;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Containers;\r\n\r\npublic abstract partial class BindableScrollContainer : ZoomableScrollContainer\r\n{\r\n    protected readonly BindableFloat BindableZoom = new();\r\n    protected readonly BindableFloat BindableCurrent = new();\r\n\r\n    protected BindableScrollContainer()\r\n    {\r\n        ZoomDuration = 200;\r\n        ZoomEasing = Easing.OutQuint;\r\n        ScrollbarVisible = false;\r\n\r\n        BindableZoom.MaxValueChanged += assignZoomRange;\r\n        BindableZoom.MinValueChanged += assignZoomRange;\r\n\r\n        BindableZoom.BindValueChanged(e =>\r\n        {\r\n            if (e.NewValue == Zoom)\r\n                return;\r\n\r\n            Zoom = e.NewValue;\r\n        }, true);\r\n\r\n        BindableCurrent.BindValueChanged(e =>\r\n        {\r\n            ScrollTo(e.NewValue);\r\n        }, true);\r\n\r\n        void assignZoomRange(float _)\r\n        {\r\n            // we should make sure that will not cause error while assigning the size.\r\n            float initial = Math.Clamp(BindableZoom.Value, BindableZoom.MinValue, BindableZoom.MaxValue);\r\n            float minimum = BindableZoom.MinValue;\r\n            float maximum = BindableZoom.MaxValue;\r\n            SetupZoom(initial, minimum, maximum);\r\n        }\r\n    }\r\n\r\n    protected override bool OnScroll(ScrollEvent e)\r\n    {\r\n        bool zoneChanged = base.OnScroll(e);\r\n        if (!zoneChanged)\r\n            return false;\r\n\r\n        if (e.AltPressed)\r\n        {\r\n            // todo : this event not working while zooming, because zooming will also call scroll to.\r\n            // bindableCurrent.Value = getCurrentPosition();\r\n\r\n            // Update zoom to target, ignore easing value.\r\n            BindableZoom.Value = Zoom;\r\n        }\r\n\r\n        return true;\r\n\r\n        /*\r\n        float getCurrentPosition()\r\n        {\r\n            // params\r\n            var zoomedContent = Content;\r\n            var focusPoint = zoomedContent.ToLocalSpace(e.ScreenSpaceMousePosition).X;\r\n            var contentSize = zoomedContent.DrawWidth;\r\n            var scrollOffset = Current;\r\n\r\n            // calculation\r\n            float focusOffset = focusPoint - scrollOffset;\r\n            float expectedWidth = DrawWidth * Zoom;\r\n            float targetOffset = expectedWidth * (focusPoint / contentSize) - focusOffset;\r\n\r\n            return targetOffset;\r\n        }\r\n        */\r\n    }\r\n\r\n    protected override void OnUserScroll(double value, bool animated = true, double? distanceDecay = null)\r\n    {\r\n        base.OnUserScroll(value, animated, distanceDecay);\r\n\r\n        // update current value if user scroll to.\r\n        BindableCurrent.Value = (float)value;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Components/Issues/IssueIcon.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks.Issues;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Components.Sprites;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Issues;\r\n\r\npublic partial class IssueIcon : CompositeDrawable\r\n{\r\n    private Issue? issue;\r\n\r\n    public virtual Issue? Issue\r\n    {\r\n        get => issue;\r\n        set\r\n        {\r\n            issue = value;\r\n            updateIssue();\r\n        }\r\n    }\r\n\r\n    private void updateIssue()\r\n    {\r\n        InternalChild = Issue != null ? createDrawableByIssue(Issue) : null;\r\n    }\r\n\r\n    private static Drawable createDrawableByIssue(Issue issue)\r\n    {\r\n        return createDrawable(issue).With(x =>\r\n        {\r\n            x.Colour = issue.Template.Colour;\r\n            x.RelativeSizeAxes = Axes.Both;\r\n        });\r\n\r\n        static Drawable createDrawable(Issue issue) =>\r\n            issue switch\r\n            {\r\n                LyricTimeTagIssue lyricTimeTagIssue => new DrawableTextIndex { State = lyricTimeTagIssue.TimeTag.Index.State },\r\n                _ => new SpriteIcon\r\n                {\r\n                    Icon = getIconByIssue(issue),\r\n                },\r\n            };\r\n    }\r\n\r\n    private static IconUsage getIconByIssue(Issue issue)\r\n        => getIconByIssueTemplate(issue.Template);\r\n\r\n    private static IconUsage getIconByIssueTemplate(IssueTemplate issueTemplate)\r\n        => getIconUsageByIssueTemplate(issueTemplate) ?? getIconUsageByCheck(issueTemplate.Check);\r\n\r\n    private static IconUsage? getIconUsageByIssueTemplate(IssueTemplate issueTemplate)\r\n    {\r\n        // will override the icon if needed.\r\n        return null;\r\n    }\r\n\r\n    private static IconUsage getIconUsageByCheck(ICheck check) =>\r\n        check switch\r\n        {\r\n            CheckBeatmapAvailableTranslations => FontAwesome.Solid.Language,\r\n            CheckClassicStageInfo => FontAwesome.Solid.AlignLeft,\r\n            CheckBeatmapNoteInfo => FontAwesome.Solid.Microphone,\r\n            CheckBeatmapPageInfo => FontAwesome.Solid.Pager,\r\n            CheckLyricLanguage => FontAwesome.Solid.Globe,\r\n            CheckLyricReferenceLyric => FontAwesome.Solid.Link,\r\n            CheckLyricRubyTag => FontAwesome.Solid.Tag,\r\n            CheckLyricSinger => FontAwesome.Solid.Music,\r\n            CheckLyricText => FontAwesome.Solid.TextHeight,\r\n            CheckLyricTimeTag => FontAwesome.Solid.Tag,\r\n            CheckLyricTranslations => FontAwesome.Solid.Language,\r\n            CheckNoteReferenceLyric => FontAwesome.Solid.Link,\r\n            CheckNoteText => FontAwesome.Solid.Link,\r\n            CheckNoteTime => FontAwesome.Solid.Times,\r\n            _ => throw new ArgumentOutOfRangeException(nameof(check), check, null),\r\n        };\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Components/Issues/IssuesToolTip.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Extensions.Color4Extensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Containers;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.Cursor;\r\nusing osuTK;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Issues;\r\n\r\npublic partial class IssuesToolTip : BackgroundToolTip<Issue[]>\r\n{\r\n    private readonly MessageTextFlowContainer invalidMessage;\r\n\r\n    public IssuesToolTip()\r\n    {\r\n        Child = invalidMessage = new MessageTextFlowContainer\r\n        {\r\n            Width = 300,\r\n            AutoSizeAxes = Axes.Y,\r\n            Colour = Color4.White.Opacity(0.75f),\r\n            Spacing = new Vector2(0, 5),\r\n            Name = \"Invalid message\",\r\n        };\r\n    }\r\n\r\n    private Issue[]? lastIssues;\r\n\r\n    public override void SetContent(Issue[] issues)\r\n    {\r\n        if (issues == lastIssues)\r\n            return;\r\n\r\n        lastIssues = issues;\r\n\r\n        // clear exist warning.\r\n        invalidMessage.Clear();\r\n\r\n        // show no problem message\r\n        if (issues.Length == 0)\r\n        {\r\n            invalidMessage.AddSuccessParagraph(\"Seems no issue in this lyric.\");\r\n        }\r\n        else\r\n        {\r\n            foreach (var issue in issues)\r\n            {\r\n                invalidMessage.AddIssueParagraph(issue);\r\n            }\r\n        }\r\n    }\r\n\r\n    private partial class MessageTextFlowContainer : OsuTextFlowContainer\r\n    {\r\n        private const int font_size = 14;\r\n\r\n        [Resolved]\r\n        private OsuColour colours { get; set; } = null!;\r\n\r\n        public MessageTextFlowContainer()\r\n            : base(s => s.Font = s.Font.With(size: font_size))\r\n        {\r\n        }\r\n\r\n        public void AddIssueParagraph(Issue issue)\r\n        {\r\n            NewParagraph();\r\n\r\n            AddPart(new IssueIconTextPart(issue));\r\n            AddText($\" {issue}\");\r\n        }\r\n\r\n        public void AddSuccessParagraph(string text)\r\n        {\r\n            NewParagraph();\r\n            AddIcon(FontAwesome.Solid.Check, icon =>\r\n            {\r\n                icon.Colour = colours.Green;\r\n            });\r\n            AddText($\" {text}\");\r\n        }\r\n\r\n        private class IssueIconTextPart : TextPart\r\n        {\r\n            private readonly Issue issue;\r\n\r\n            public IssueIconTextPart(Issue issue)\r\n            {\r\n                this.issue = issue;\r\n            }\r\n\r\n            protected override IEnumerable<Drawable> CreateDrawablesFor(TextFlowContainer textFlowContainer)\r\n            {\r\n                yield return new IssueIcon\r\n                {\r\n                    Size = new Vector2(font_size),\r\n                    Issue = issue,\r\n                };\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Components/Markdown/DescriptionFormat.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Localisation;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Markdown;\r\n\r\npublic struct DescriptionFormat\r\n{\r\n    public const string LINK_KEY_ACTION = \"action\";\r\n\r\n    public LocalisableString Text { get; set; }\r\n\r\n    public IDictionary<string, IDescriptionAction> Actions { get; set; }\r\n\r\n    // todo: will be removed eventually.\r\n    public static implicit operator DescriptionFormat(string text)\r\n        => (LocalisableString)text;\r\n\r\n    public static implicit operator DescriptionFormat(LocalisableString text) => new()\r\n    {\r\n        Text = text,\r\n    };\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Components/Markdown/DescriptionTextFlowContainer.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing Markdig.Syntax;\r\nusing Markdig.Syntax.Inlines;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Containers.Markdown;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Containers.Markdown;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Components.Markdown;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Markdown;\r\n\r\npublic partial class DescriptionTextFlowContainer : Container, IMarkdownTextComponent\r\n{\r\n    private readonly DescriptionMarkdownTextFlowContainer description;\r\n\r\n    public DescriptionTextFlowContainer()\r\n    {\r\n        AddInternal(description = new DescriptionMarkdownTextFlowContainer\r\n        {\r\n            RelativeSizeAxes = Axes.X,\r\n            AutoSizeAxes = Axes.Y,\r\n            AddTextAction = processLinkText,\r\n        });\r\n    }\r\n\r\n    private DescriptionFormat descriptionFormat;\r\n\r\n    public DescriptionFormat Description\r\n    {\r\n        get => descriptionFormat;\r\n        set\r\n        {\r\n            descriptionFormat = value;\r\n\r\n            var markdownDocument = Markdig.Markdown.Parse(descriptionFormat.Text.ToString());\r\n            description.Clear();\r\n\r\n            if (markdownDocument.FirstOrDefault() is ParagraphBlock paragraphBlock)\r\n                description.AddInlineText(paragraphBlock.Inline);\r\n        }\r\n    }\r\n\r\n    public SpriteText CreateSpriteText() => new OsuSpriteText\r\n    {\r\n        Font = OsuFont.GetFont(size: 14, weight: FontWeight.Regular),\r\n    };\r\n\r\n    private OsuMarkdownLinkText? processLinkText(string text, string? url)\r\n    {\r\n        if (text != DescriptionFormat.LINK_KEY_ACTION)\r\n            return null;\r\n\r\n        var keys = Description.Actions;\r\n        if (url == null || !keys.TryGetValue(url, out var inputKey))\r\n            throw new ArgumentNullException(nameof(keys));\r\n\r\n        return GetLinkTextByDescriptionAction(inputKey);\r\n    }\r\n\r\n    protected virtual OsuMarkdownLinkText GetLinkTextByDescriptionAction(IDescriptionAction descriptionAction) =>\r\n        descriptionAction switch\r\n        {\r\n            InputKeyDescriptionAction inputKey => new InputKeyText(inputKey),\r\n            SwitchModeDescriptionAction switchMode => new SwitchMoteText(switchMode),\r\n            _ => throw new InvalidCastException(),\r\n        };\r\n\r\n    internal partial class DescriptionMarkdownTextFlowContainer : OsuMarkdownTextFlowContainer\r\n    {\r\n        public Func<string, string?, OsuMarkdownLinkText?>? AddTextAction;\r\n\r\n        protected override void AddLinkText(string text, LinkInline linkInline)\r\n        {\r\n            var linkText = AddTextAction?.Invoke(text, linkInline.Url);\r\n\r\n            if (linkText != null)\r\n            {\r\n                AddDrawable(linkText);\r\n            }\r\n            else\r\n            {\r\n                base.AddLinkText(text, linkInline);\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Components/Markdown/IDescriptionAction.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Localisation;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Markdown;\r\n\r\npublic interface IDescriptionAction\r\n{\r\n    public LocalisableString Text { get; set; }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Components/Markdown/InputKeyDescriptionAction.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Localisation;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Markdown;\r\n\r\npublic struct InputKeyDescriptionAction : IDescriptionAction\r\n{\r\n    public LocalisableString Text { get; set; }\r\n\r\n    public IList<KaraokeEditAction> AdjustableActions { get; set; }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Components/Markdown/InputKeyText.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Diagnostics;\r\nusing System.Linq;\r\nusing Markdig.Syntax.Inlines;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Extensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Cursor;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Framework.Input;\r\nusing osu.Game.Database;\r\nusing osu.Game.Graphics.Containers.Markdown;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Graphics.UserInterfaceV2;\r\nusing osu.Game.Input.Bindings;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Overlays.Settings.Sections.Input;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Markdown;\r\n\r\n/// <summary>\r\n/// For showing the key and adjust the key binding.\r\n/// </summary>\r\npublic partial class InputKeyText : OsuMarkdownLinkText, IHasPopover\r\n{\r\n    private readonly InputKeyDescriptionAction inputKeyDescriptionAction;\r\n\r\n    [Resolved]\r\n    private ReadableKeyCombinationProvider keyCombinationProvider { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private RealmAccess realm { get; set; } = null!;\r\n\r\n    public InputKeyText(InputKeyDescriptionAction inputKeyDescriptionAction)\r\n        : base(inputKeyDescriptionAction.Text.ToString(), new LinkInline { Title = \"Click to change the key.\" })\r\n    {\r\n        this.inputKeyDescriptionAction = inputKeyDescriptionAction;\r\n\r\n        CornerRadius = 4;\r\n        Masking = true;\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OverlayColourProvider colourProvider)\r\n    {\r\n        AddInternal(new Box\r\n        {\r\n            Name = \"Background\",\r\n            Depth = 1,\r\n            RelativeSizeAxes = Axes.Both,\r\n            Colour = colourProvider.Background6,\r\n        });\r\n\r\n        updateDisplayText();\r\n\r\n        // todo: IDK why not being triggered.\r\n        keyCombinationProvider.KeymapChanged += updateDisplayText;\r\n    }\r\n\r\n    private void updateDisplayText()\r\n    {\r\n        string text = string.IsNullOrEmpty(inputKeyDescriptionAction.Text.ToString())\r\n            ? getKeyName(inputKeyDescriptionAction.AdjustableActions.FirstOrDefault())\r\n            : inputKeyDescriptionAction.Text.ToString();\r\n\r\n        var spriteText = InternalChildren.OfType<OsuSpriteText>().FirstOrDefault();\r\n        Debug.Assert(spriteText != null);\r\n\r\n        spriteText.Text = text;\r\n        spriteText.Padding = new MarginPadding { Horizontal = 4 };\r\n\r\n        string getKeyName(KaraokeEditAction action)\r\n        {\r\n            var ruleset = new KaraokeRuleset();\r\n            string rulesetName = ruleset.ShortName;\r\n            const int edit_input_variant = KaraokeRuleset.EDIT_INPUT_VARIANT;\r\n\r\n            var keyBinding = realm.Run(r => r.All<RealmKeyBinding>()\r\n                                             .Where(b => b.RulesetName == rulesetName && b.Variant == edit_input_variant)\r\n                                             .Detach()).FirstOrDefault(x => (int)x.Action == (int)action);\r\n\r\n            if (keyBinding == null)\r\n                throw new ArgumentNullException(nameof(keyBinding));\r\n\r\n            return keyCombinationProvider.GetReadableString(keyBinding.KeyCombination);\r\n        }\r\n    }\r\n\r\n    protected override void OnLinkPressed()\r\n    {\r\n        // open the popover\r\n        this.ShowPopover();\r\n    }\r\n\r\n    public Popover GetPopover()\r\n    {\r\n        var popover = new OsuPopover\r\n        {\r\n            Child = new PopoverKeyBindingsSubsection(inputKeyDescriptionAction.AdjustableActions)\r\n            {\r\n                Width = 300,\r\n                RelativeSizeAxes = Axes.None,\r\n            },\r\n        };\r\n\r\n        // because it's not possible to get the key change event, so at least update the key after popover closed.\r\n        popover.State.BindValueChanged(x =>\r\n        {\r\n            if (x.NewValue == Visibility.Hidden)\r\n                updateDisplayText();\r\n        });\r\n\r\n        return popover;\r\n    }\r\n\r\n    protected override void Dispose(bool isDisposing)\r\n    {\r\n        base.Dispose(isDisposing);\r\n\r\n        keyCombinationProvider.KeymapChanged -= updateDisplayText;\r\n    }\r\n\r\n    private partial class PopoverKeyBindingsSubsection : VariantBindingsSubsection\r\n    {\r\n        public PopoverKeyBindingsSubsection(IEnumerable<KaraokeEditAction> actions)\r\n            : base(new KaraokeRuleset().RulesetInfo, KaraokeRuleset.EDIT_INPUT_VARIANT)\r\n        {\r\n            // should only show the keys in the list.\r\n            Defaults = Defaults.Where(x => x.Action is KaraokeEditAction action && actions.Contains(action));\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Components/Menus/GenericScreenSelectionTabControl.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Screens.Edit.Components.Menus;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Menus;\r\n\r\n/// <summary>\r\n/// Copied from <see cref=\"EditorScreenSwitcherControl\"/>\r\n/// </summary>\r\n/// <typeparam name=\"TScreenMode\"></typeparam>\r\npublic partial class GenericScreenSelectionTabControl<TScreenMode> : OsuTabControl<TScreenMode>\r\n{\r\n    public GenericScreenSelectionTabControl()\r\n    {\r\n        AutoSizeAxes = Axes.X;\r\n        RelativeSizeAxes = Axes.Y;\r\n\r\n        TabContainer.RelativeSizeAxes &= ~Axes.X;\r\n        TabContainer.AutoSizeAxes = Axes.X;\r\n        TabContainer.Padding = new MarginPadding(10);\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OverlayColourProvider colourProvider)\r\n    {\r\n        AccentColour = colourProvider.Light3;\r\n\r\n        AddInternal(new Box\r\n        {\r\n            RelativeSizeAxes = Axes.Both,\r\n            Colour = colourProvider.Background2,\r\n        });\r\n    }\r\n\r\n    protected override Dropdown<TScreenMode>? CreateDropdown() => null;\r\n\r\n    protected override TabItem<TScreenMode> CreateTabItem(TScreenMode value) => new TabItem(value);\r\n\r\n    private partial class TabItem : OsuTabItem\r\n    {\r\n        private const float transition_length = 250;\r\n\r\n        public TabItem(TScreenMode value)\r\n            : base(value)\r\n        {\r\n            Text.Margin = new MarginPadding();\r\n            Text.Anchor = Anchor.CentreLeft;\r\n            Text.Origin = Anchor.CentreLeft;\r\n\r\n            Text.Font = OsuFont.TorusAlternate;\r\n\r\n            Bar.Expire();\r\n        }\r\n\r\n        protected override void OnActivated()\r\n        {\r\n            base.OnActivated();\r\n            Bar.ScaleTo(new Vector2(1, 5), transition_length, Easing.OutQuint);\r\n        }\r\n\r\n        protected override void OnDeactivated()\r\n        {\r\n            base.OnDeactivated();\r\n            Bar.ScaleTo(Vector2.One, transition_length, Easing.OutQuint);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Components/Timeline/EditableLyricTimelineSelectionBlueprint.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Cursor;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.Cursor;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Timeline;\r\n\r\npublic partial class EditableLyricTimelineSelectionBlueprint : EditableTimelineSelectionBlueprint<Lyric>, IHasCustomTooltip<Lyric>\r\n{\r\n    private const double default_time = 0;\r\n    private const double default_duration = 1000;\r\n\r\n    private const float lyric_size = 20;\r\n\r\n    public EditableLyricTimelineSelectionBlueprint(Lyric item)\r\n        : base(item)\r\n    {\r\n        X = Item.TimeValid ? (float)Item.StartTime : (float)default_time;\r\n\r\n        RelativeSizeAxes = Axes.X;\r\n        Height = lyric_size;\r\n\r\n        AddInternal(new Container\r\n        {\r\n            Anchor = Anchor.CentreLeft,\r\n            Origin = Anchor.CentreLeft,\r\n            RelativeSizeAxes = Axes.Both,\r\n            Masking = true,\r\n            CornerRadius = 5,\r\n            Children = new Drawable[]\r\n            {\r\n                new Box\r\n                {\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    Colour = Color4.Gray,\r\n                },\r\n                new TruncatingSpriteText\r\n                {\r\n                    Anchor = Anchor.CentreLeft,\r\n                    Origin = Anchor.CentreLeft,\r\n                    Margin = new MarginPadding { Left = 5 },\r\n                    RelativeSizeAxes = Axes.X,\r\n                    Text = item.Text,\r\n                    ShowTooltip = false,\r\n                },\r\n            },\r\n        });\r\n    }\r\n\r\n    protected override void Update()\r\n    {\r\n        base.Update();\r\n\r\n        // no bindable so we perform this every update\r\n        double duration = Item.TimeValid ? Item.Duration : default_duration;\r\n        float durationWidth = (float)duration;\r\n\r\n        if (Width != durationWidth)\r\n        {\r\n            Width = durationWidth;\r\n        }\r\n    }\r\n\r\n    public virtual ITooltip<Lyric> GetCustomTooltip() => new LyricTooltip();\r\n\r\n    public Lyric TooltipContent => Item;\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Components/Timeline/EditableTimeline.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Rulesets.Edit;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Containers;\r\nusing osu.Game.Screens.Edit;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Timeline;\r\n\r\n[Cached]\r\npublic partial class EditableTimeline : BindableScrollContainer\r\n{\r\n    [Resolved]\r\n    private EditorClock editorClock { get; set; } = null!;\r\n\r\n    public EditableTimeline()\r\n    {\r\n        ZoomDuration = 200;\r\n        ZoomEasing = Easing.OutQuint;\r\n        ScrollbarVisible = false;\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        AddRange(new Drawable[]\r\n        {\r\n            CreateMainContainer().With(c =>\r\n            {\r\n                c.RelativeSizeAxes = Axes.X;\r\n                c.Depth = float.MaxValue;\r\n                c.Children = CreateBlueprintContainer().ToList();\r\n            }),\r\n        });\r\n    }\r\n\r\n    protected virtual Container CreateMainContainer() =>\r\n        new()\r\n        {\r\n            Anchor = Anchor.CentreLeft,\r\n            Origin = Anchor.CentreLeft,\r\n        };\r\n\r\n    protected virtual IEnumerable<Drawable> CreateBlueprintContainer()\r\n    {\r\n        yield return new EditableTimelineBlueprintContainer<Lyric>();\r\n    }\r\n\r\n    public double TimeAtPosition(float x)\r\n    {\r\n        return x / Content.DrawWidth * editorClock.TrackLength;\r\n    }\r\n\r\n    public float PositionAtTime(double time)\r\n    {\r\n        return (float)(time / editorClock.TrackLength * Content.DrawWidth);\r\n    }\r\n\r\n    public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition)\r\n    {\r\n        double time = TimeAtPosition(Content.ToLocalSpace(screenSpacePosition).X);\r\n        return new SnapResult(screenSpacePosition, time);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Components/Timeline/EditableTimelineBlueprintContainer.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Game.Rulesets.Edit;\r\nusing osu.Game.Screens.Edit.Components.Timelines.Summary.Parts;\r\nusing osu.Game.Screens.Edit.Compose.Components;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Timeline;\r\n\r\npublic partial class EditableTimelineBlueprintContainer<TItem> : BlueprintContainer<TItem> where TItem : class\r\n{\r\n    [Resolved]\r\n    private EditableTimeline timeline { get; set; } = null!;\r\n\r\n    protected readonly IBindableList<TItem> Items = new BindableList<TItem>();\r\n\r\n    public EditableTimelineBlueprintContainer()\r\n    {\r\n        Items.BindCollectionChanged((_, b) =>\r\n        {\r\n            var removedItems = b.OldItems?.OfType<TItem>().ToArray();\r\n            var createdItems = b.NewItems?.OfType<TItem>().ToArray();\r\n\r\n            if (removedItems != null)\r\n            {\r\n                foreach (var item in removedItems)\r\n                    RemoveBlueprintFor(item);\r\n            }\r\n\r\n            if (createdItems != null)\r\n            {\r\n                foreach (var item in createdItems)\r\n                    AddBlueprintFor(item);\r\n            }\r\n        });\r\n    }\r\n\r\n    protected override void SelectAll()\r\n    {\r\n        SelectedItems.AddRange(Items);\r\n    }\r\n\r\n    protected override bool TryMoveBlueprints(DragEvent e, IList<(SelectionBlueprint<TItem> blueprint, Vector2[] originalSnapPositions)> blueprints)\r\n    {\r\n        var result = timeline.FindSnappedPositionAndTime(e.ScreenSpaceMousePosition);\r\n\r\n        if (result.Time == null)\r\n            return false;\r\n\r\n        var items = blueprints.Select(x => x.blueprint.Item).ToArray();\r\n        double time = result.Time.Value;\r\n        return ApplyOffsetResult(items, time);\r\n    }\r\n\r\n    protected virtual bool ApplyOffsetResult(TItem[] items, double time) => false;\r\n\r\n    protected override SelectionBlueprintContainer CreateSelectionBlueprintContainer()\r\n        => new EditableTimelineSelectionBlueprintContainer { RelativeSizeAxes = Axes.Both };\r\n\r\n    protected override SelectionHandler<TItem> CreateSelectionHandler()\r\n        => new EditableTimelineSelectionHandler();\r\n\r\n    protected override DragBox CreateDragBox() => new EditableTimelineDragBox();\r\n\r\n    protected partial class EditableTimelineSelectionHandler : SelectionHandler<TItem>\r\n    {\r\n        protected override void OnSelectionChanged()\r\n        {\r\n            base.OnSelectionChanged();\r\n\r\n            // should hide selection box if not dragging at current row.\r\n            bool dragging = Parent.IsDragged;\r\n            SelectionBox.FadeTo(dragging ? 1f : 0.0f);\r\n        }\r\n\r\n        protected override void DeleteItems(IEnumerable<TItem> items)\r\n        {\r\n            // implement in the child class.\r\n        }\r\n    }\r\n\r\n    private partial class EditableTimelineDragBox : DragBox\r\n    {\r\n        public double MinTime { get; private set; }\r\n\r\n        public double MaxTime { get; private set; }\r\n\r\n        private double? startTime;\r\n\r\n        [Resolved]\r\n        private EditableTimeline timeline { get; set; } = null!;\r\n\r\n        protected override Drawable CreateBox() => new Box\r\n        {\r\n            RelativeSizeAxes = Axes.Y,\r\n            Alpha = 0.3f,\r\n        };\r\n\r\n        public override void HandleDrag(MouseButtonEvent e)\r\n        {\r\n            startTime ??= timeline.TimeAtPosition(e.MouseDownPosition.X);\r\n            double endTime = timeline.TimeAtPosition(e.MousePosition.X);\r\n\r\n            MinTime = Math.Min(startTime.Value, endTime);\r\n            MaxTime = Math.Max(startTime.Value, endTime);\r\n\r\n            Box.X = timeline.PositionAtTime(MinTime);\r\n            Box.Width = timeline.PositionAtTime(MaxTime) - Box.X;\r\n        }\r\n\r\n        public override void Hide()\r\n        {\r\n            base.Hide();\r\n            startTime = null;\r\n        }\r\n    }\r\n\r\n    protected partial class EditableTimelineSelectionBlueprintContainer : SelectionBlueprintContainer\r\n    {\r\n        protected override Container<SelectionBlueprint<TItem>> Content { get; }\r\n\r\n        public EditableTimelineSelectionBlueprintContainer()\r\n        {\r\n            AddInternal(new TimelinePart<SelectionBlueprint<TItem>>(Content = new Container<SelectionBlueprint<TItem>> { RelativeSizeAxes = Axes.Both }) { RelativeSizeAxes = Axes.Both });\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Components/Timeline/EditableTimelineSelectionBlueprint.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Primitives;\r\nusing osu.Game.Rulesets.Edit;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Timeline;\r\n\r\npublic abstract partial class EditableTimelineSelectionBlueprint<TItem> : SelectionBlueprint<TItem>\r\n{\r\n    private bool selectable = true;\r\n\r\n    protected EditableTimelineSelectionBlueprint(TItem item)\r\n        : base(item)\r\n    {\r\n        Anchor = Anchor.CentreLeft;\r\n        Origin = Anchor.CentreLeft;\r\n\r\n        RelativePositionAxes = Axes.X;\r\n    }\r\n\r\n    protected bool Selectable\r\n    {\r\n        get => selectable;\r\n        set\r\n        {\r\n            if (selectable == value)\r\n                return;\r\n\r\n            selectable = value;\r\n            OnSelectableStatusChanged(selectable);\r\n        }\r\n    }\r\n\r\n    protected virtual void OnSelectableStatusChanged(bool selectable)\r\n    {\r\n        if (selectable)\r\n        {\r\n            Show();\r\n        }\r\n        else\r\n        {\r\n            this.FadeTo(0.1f, 200);\r\n        }\r\n    }\r\n\r\n    protected sealed override void OnSelected()\r\n    {\r\n        // base logic hides selected blueprints when not selected, but timeline doesn't do that.\r\n    }\r\n\r\n    protected sealed override void OnDeselected()\r\n    {\r\n        // base logic hides selected blueprints when not selected, but timeline doesn't do that.\r\n    }\r\n\r\n    public sealed override bool ReceivePositionalInputAt(Vector2 screenSpacePos)\r\n    {\r\n        var drawable = GetInteractDrawable();\r\n        if (drawable == this)\r\n            return base.ReceivePositionalInputAt(screenSpacePos);\r\n\r\n        return drawable.ReceivePositionalInputAt(screenSpacePos);\r\n    }\r\n\r\n    // prevent selection.\r\n    public sealed override Vector2 ScreenSpaceSelectionPoint => selectable ? GetInteractDrawable().ScreenSpaceDrawQuad.TopLeft : new Vector2(int.MinValue);\r\n\r\n    // prevent single select.\r\n    public sealed override Quad SelectionQuad => selectable ? GetInteractDrawable().ScreenSpaceDrawQuad : new Quad();\r\n\r\n    protected virtual Drawable GetInteractDrawable() => this;\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Components/UserInterface/DeleteIconButton.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.UserInterface;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Components.UserInterface;\r\n\r\npublic partial class DeleteIconButton : IconButton\r\n{\r\n    [Resolved]\r\n    protected OsuColour Colours { get; private set; } = null!;\r\n\r\n    public Action<bool>? Hover;\r\n\r\n    public DeleteIconButton()\r\n    {\r\n        Icon = FontAwesome.Solid.Trash;\r\n    }\r\n\r\n    protected override bool OnHover(HoverEvent e)\r\n    {\r\n        Colour = Colours.Yellow;\r\n        Hover?.Invoke(true);\r\n        return base.OnHover(e);\r\n    }\r\n\r\n    protected override void OnHoverLost(HoverLostEvent e)\r\n    {\r\n        Colour = Colours.GrayF;\r\n        Hover?.Invoke(false);\r\n        base.OnHoverLost(e);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Components/UserInterface/LanguagesSelector.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Globalization;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Extensions.Color4Extensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.UserInterfaceV2;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Components.UserInterface;\r\n\r\npublic partial class LanguagesSelector : FillFlowContainer, IHasCurrentValue<CultureInfo[]>\r\n{\r\n    private readonly Bindable<CultureInfo[]> current = new();\r\n\r\n    public Bindable<CultureInfo[]> Current\r\n    {\r\n        get => current;\r\n        set\r\n        {\r\n            ArgumentNullException.ThrowIfNull(value);\r\n\r\n            current.UnbindBindings();\r\n            current.BindTo(value);\r\n        }\r\n    }\r\n\r\n    public LanguagesSelector()\r\n    {\r\n        RelativeSizeAxes = Axes.X;\r\n        AutoSizeAxes = Axes.Y;\r\n        Spacing = new Vector2(10);\r\n\r\n        current.BindValueChanged(e =>\r\n        {\r\n            RemoveAll(x => x is SelectedLanguage, true);\r\n\r\n            for (int i = 0; i < e.NewValue.Length; i++)\r\n            {\r\n                var cultureInfo = e.NewValue[i];\r\n\r\n                var bindable = new Bindable<CultureInfo>(cultureInfo);\r\n                bindable.BindValueChanged(c =>\r\n                {\r\n                    removeCultureInfo(c.OldValue);\r\n                    addCultureInfo(c.NewValue);\r\n                });\r\n\r\n                Add(new SelectedLanguage\r\n                {\r\n                    RelativeSizeAxes = Axes.X,\r\n                    Text = $\"#{i} - {CultureInfoUtils.GetLanguageDisplayText(cultureInfo)}\",\r\n                    OnDeleteButtonClick = () => removeCultureInfo(cultureInfo),\r\n                });\r\n            }\r\n        });\r\n\r\n        var fillFlowContainer = Content as FillFlowContainer;\r\n        fillFlowContainer?.Insert(int.MaxValue, new CreateLanguageSubsection\r\n        {\r\n            RelativeSizeAxes = Axes.X,\r\n            Height = 300,\r\n            LanguageSelected = addCultureInfo,\r\n        });\r\n    }\r\n\r\n    private void removeCultureInfo(CultureInfo cultureInfo)\r\n    {\r\n        var languageList = current.Value?.ToList() ?? new List<CultureInfo>();\r\n        if (!languageList.Contains(cultureInfo))\r\n            return;\r\n\r\n        languageList.Remove(cultureInfo);\r\n        current.Value = languageList.ToArray();\r\n    }\r\n\r\n    private void addCultureInfo(CultureInfo cultureInfo)\r\n    {\r\n        var languageList = current.Value?.ToList() ?? new List<CultureInfo>();\r\n        if (languageList.Contains(cultureInfo))\r\n            return;\r\n\r\n        languageList.Add(cultureInfo);\r\n        current.Value = languageList.ToArray();\r\n    }\r\n\r\n    // todo: will use rearrangeable list view for able to change the order.\r\n    private partial class SelectedLanguage : CompositeDrawable\r\n    {\r\n        private const float delete_button_size = 20f;\r\n        private const int padding = 15;\r\n\r\n        public Action? OnDeleteButtonClick;\r\n\r\n        private readonly OsuSpriteText languageSpriteText;\r\n        private readonly Box background;\r\n\r\n        public SelectedLanguage()\r\n        {\r\n            Height = 32;\r\n            Masking = true;\r\n            CornerRadius = 15;\r\n\r\n            InternalChildren = new Drawable[]\r\n            {\r\n                background = new Box\r\n                {\r\n                    RelativeSizeAxes = Axes.Both,\r\n                },\r\n                languageSpriteText = new TruncatingSpriteText\r\n                {\r\n                    Anchor = Anchor.CentreLeft,\r\n                    Origin = Anchor.CentreLeft,\r\n                    RelativeSizeAxes = Axes.X,\r\n                    Padding = new MarginPadding\r\n                    {\r\n                        Left = padding,\r\n                        Right = padding * 2 + delete_button_size,\r\n                    },\r\n                },\r\n                new DeleteIconButton\r\n                {\r\n                    Anchor = Anchor.CentreRight,\r\n                    Origin = Anchor.CentreRight,\r\n                    Margin = new MarginPadding\r\n                    {\r\n                        Right = padding,\r\n                    },\r\n                    Size = new Vector2(delete_button_size),\r\n                    Action = () => OnDeleteButtonClick?.Invoke(),\r\n                },\r\n            };\r\n        }\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(OverlayColourProvider? colourProvider)\r\n        {\r\n            background.Colour = colourProvider?.Background5 ?? Color4Extensions.FromHex(\"1c2125\");\r\n        }\r\n\r\n        public string Text\r\n        {\r\n            set => languageSpriteText.Text = value;\r\n        }\r\n    }\r\n\r\n    private partial class CreateLanguageSubsection : CompositeDrawable\r\n    {\r\n        private const int padding = 15;\r\n\r\n        private readonly Bindable<CultureInfo?> current = new();\r\n\r\n        public Action<CultureInfo>? LanguageSelected;\r\n\r\n        public CreateLanguageSubsection()\r\n        {\r\n            current.BindValueChanged(e =>\r\n            {\r\n                var newLanguage = e.NewValue;\r\n                if (newLanguage == null)\r\n                    return;\r\n\r\n                LanguageSelected?.Invoke(newLanguage);\r\n                current.Value = null;\r\n            });\r\n        }\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(OverlayColourProvider? colourProvider)\r\n        {\r\n            Masking = true;\r\n            CornerRadius = 15;\r\n            InternalChildren = new Drawable[]\r\n            {\r\n                new Box\r\n                {\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    Colour = colourProvider?.Background5 ?? Color4Extensions.FromHex(\"1c2125\"),\r\n                },\r\n                new Container\r\n                {\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    Padding = new MarginPadding(padding),\r\n                    Child = new LanguageSelector\r\n                    {\r\n                        RelativeSizeAxes = Axes.Both,\r\n                        Current = current,\r\n                    },\r\n                },\r\n            };\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/EditorSection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit;\r\n\r\n/// <summary>\r\n/// Base section class for lyric editor.\r\n/// todo: should inherit the EditorRoundedScreenSettingsSection eventually, but seems that class haven't ready.\r\n/// </summary>\r\npublic abstract partial class EditorSection : Section\r\n{\r\n    protected EditorSection()\r\n    {\r\n        Padding = new MarginPadding(0);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/EditorSectionButton.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Graphics.UserInterface;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit;\r\n\r\npublic abstract partial class EditorSectionButton : OsuButton\r\n{\r\n    protected EditorSectionButton()\r\n    {\r\n        RelativeSizeAxes = Axes.X;\r\n        Content.CornerRadius = 15;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/EditorSettings.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Testing;\r\nusing osu.Game.Graphics.Containers;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit;\r\n\r\n[Cached]\r\npublic abstract partial class EditorSettings : EditorRoundedScreenSettings\r\n{\r\n    protected override void LoadComplete()\r\n    {\r\n        base.LoadComplete();\r\n\r\n        var newSettingHeader = CreateSettingHeader();\r\n        if (newSettingHeader == null)\r\n            return;\r\n\r\n        AddInternal(newSettingHeader);\r\n    }\r\n\r\n    public void ReloadSections()\r\n    {\r\n        // reload section after header ready.\r\n        Schedule(() =>\r\n        {\r\n            // adjust the scroll position.\r\n            var settingsHeader = this.ChildrenOfType<EditorSettingsHeader>().FirstOrDefault();\r\n\r\n            if (settingsHeader != null)\r\n            {\r\n                var scrollContainer = this.ChildrenOfType<OsuScrollContainer>().First();\r\n                scrollContainer.Padding = new MarginPadding { Top = settingsHeader.DrawHeight };\r\n            }\r\n\r\n            // re-create the content.\r\n            var content = this.ChildrenOfType<FillFlowContainer>().First();\r\n            content.Children = CreateSections();\r\n        });\r\n    }\r\n\r\n    protected void ChangeBackgroundColour(Colour4 colour4)\r\n    {\r\n        this.ChildrenOfType<Box>().First().Colour = colour4;\r\n\r\n        // apply colour after header ready.\r\n        Schedule(() =>\r\n        {\r\n            var settingsHeader = this.ChildrenOfType<EditorSettingsHeader>().FirstOrDefault();\r\n            if (settingsHeader != null)\r\n                settingsHeader.BackgroundColour = colour4.Darken(0.4f);\r\n        });\r\n    }\r\n\r\n    protected sealed override IReadOnlyList<Drawable> CreateSections()\r\n    {\r\n        return CreateEditorSections();\r\n    }\r\n\r\n    protected virtual EditorSettingsHeader? CreateSettingHeader() => null;\r\n\r\n    protected abstract IReadOnlyList<EditorSection> CreateEditorSections();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/EditorSettingsHeader.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Extensions.IEnumerableExtensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Overlays.Toolbar;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Markdown;\r\nusing osuTK;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit;\r\n\r\npublic abstract partial class EditorSettingsHeader<TEditStep> : EditorSettingsHeader, IHasCurrentValue<TEditStep> where TEditStep : struct, Enum\r\n{\r\n    private const int border_margin = 10;\r\n\r\n    private const int tab_height = 40;\r\n    private const int description_padding = 10;\r\n\r\n    public Bindable<TEditStep> Current\r\n    {\r\n        get => tabControl.Current;\r\n        set => tabControl.Current = value;\r\n    }\r\n\r\n    [Resolved]\r\n    private EditorSettings editorSettings { get; set; } = null!;\r\n\r\n    // for the DescriptionMarkdownTextFlowContainer.\r\n    [Cached]\r\n    private readonly OverlayColourProvider overlayColourProvider;\r\n\r\n    private readonly EditStepTabControl tabControl;\r\n    private readonly DescriptionTextFlowContainer lyricEditorDescription;\r\n\r\n    protected EditorSettingsHeader()\r\n    {\r\n        overlayColourProvider = new OverlayColourProvider(CreateColourScheme());\r\n\r\n        AddInternal(new FillFlowContainer\r\n        {\r\n            RelativeSizeAxes = Axes.X,\r\n            AutoSizeAxes = Axes.Y,\r\n            Direction = FillDirection.Vertical,\r\n            Padding = new MarginPadding(border_margin),\r\n            Children = new Drawable[]\r\n            {\r\n                tabControl = CreateTabControl().With(x =>\r\n                {\r\n                    x.RelativeSizeAxes = Axes.X;\r\n                    x.Height = tab_height;\r\n                }),\r\n                lyricEditorDescription = CreateDescriptionTextFlowContainer().With(x =>\r\n                {\r\n                    x.RelativeSizeAxes = Axes.X;\r\n                    x.AutoSizeAxes = Axes.Y;\r\n                    x.Padding = new MarginPadding(description_padding);\r\n                }),\r\n            },\r\n        });\r\n    }\r\n\r\n    protected override void LoadComplete()\r\n    {\r\n        base.LoadComplete();\r\n\r\n        tabControl.Items = Enum.GetValues<TEditStep>();\r\n        tabControl.Current.BindValueChanged(x =>\r\n        {\r\n            var step = x.NewValue;\r\n\r\n            // update description text.\r\n            lyricEditorDescription.Description = GetSelectionDescription(step);\r\n\r\n            // wait until description text ready.\r\n            Schedule(() =>\r\n            {\r\n                editorSettings.ReloadSections();\r\n                UpdateEditStep(step);\r\n            });\r\n        }, true);\r\n    }\r\n\r\n    protected virtual DescriptionTextFlowContainer CreateDescriptionTextFlowContainer() => new();\r\n\r\n    protected abstract OverlayColourScheme CreateColourScheme();\r\n\r\n    protected abstract EditStepTabControl CreateTabControl();\r\n\r\n    protected abstract DescriptionFormat GetSelectionDescription(TEditStep step);\r\n\r\n    protected virtual void UpdateEditStep(TEditStep step)\r\n    {\r\n    }\r\n\r\n    protected abstract partial class EditStepTabControl : TabControl<TEditStep>\r\n    {\r\n        public const int SPACING = 10;\r\n\r\n        protected override TabFillFlowContainer CreateTabFlow() => new()\r\n        {\r\n            RelativeSizeAxes = Axes.Y,\r\n            AutoSizeAxes = Axes.X,\r\n            Direction = FillDirection.Horizontal,\r\n            Spacing = new Vector2(SPACING, 0),\r\n        };\r\n\r\n        protected override Dropdown<TEditStep>? CreateDropdown() => null;\r\n\r\n        protected sealed override TabItem<TEditStep> CreateTabItem(TEditStep value) => CreateStepButton(new OsuColour(), value);\r\n\r\n        protected abstract StepTabButton CreateStepButton(OsuColour colour, TEditStep step);\r\n    }\r\n\r\n    protected abstract partial class IssueStepTabButton : StepTabButton\r\n    {\r\n        protected readonly IBindableList<Issue> Issues = new BindableList<Issue>();\r\n\r\n        protected IssueStepTabButton(TEditStep value)\r\n            : base(value)\r\n        {\r\n            CountCircle countCircle;\r\n\r\n            AddInternal(countCircle = new CountCircle\r\n            {\r\n                Anchor = Anchor.TopRight,\r\n                Origin = Anchor.Centre,\r\n                X = -5,\r\n            });\r\n\r\n            Issues.BindCollectionChanged((_, _) =>\r\n            {\r\n                int count = Issues.Count;\r\n                countCircle.Alpha = count == 0 ? 0 : 1;\r\n                countCircle.Count = count;\r\n            });\r\n        }\r\n    }\r\n\r\n    protected partial class StepTabButton : TabItem<TEditStep>\r\n    {\r\n        private readonly Box background;\r\n        private readonly OsuSpriteText text;\r\n\r\n        public StepTabButton(TEditStep value)\r\n            : base(value)\r\n        {\r\n            RelativeSizeAxes = Axes.Y;\r\n\r\n            Child = new Container\r\n            {\r\n                Masking = true,\r\n                CornerRadius = 15,\r\n                RelativeSizeAxes = Axes.Both,\r\n                Children = new Drawable[]\r\n                {\r\n                    background = new Box\r\n                    {\r\n                        RelativeSizeAxes = Axes.Both,\r\n                    },\r\n                    text = new OsuSpriteText\r\n                    {\r\n                        Anchor = Anchor.Centre,\r\n                        Origin = Anchor.Centre,\r\n                        Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold),\r\n                    },\r\n                },\r\n            };\r\n        }\r\n\r\n        protected override void LoadComplete()\r\n        {\r\n            base.LoadComplete();\r\n\r\n            updateState();\r\n            updateTabSize();\r\n        }\r\n\r\n        private void updateTabSize()\r\n        {\r\n            if (Parent?.Parent is not EditStepTabControl control)\r\n                throw new InvalidOperationException();\r\n\r\n            int tabAmount = Enum.GetValues<TEditStep>().Length;\r\n            Width = (control.DrawWidth - (tabAmount - 1) * EditStepTabControl.SPACING) / tabAmount;\r\n        }\r\n\r\n        public LocalisableString Text\r\n        {\r\n            get => text.Text;\r\n            set => text.Text = value;\r\n        }\r\n\r\n        public Color4 SelectedColour { get; init; }\r\n\r\n        public Color4 UnSelectedColour { get; init; }\r\n\r\n        protected sealed override void OnActivated() => updateState();\r\n\r\n        protected sealed override void OnDeactivated() => updateState();\r\n\r\n        private void updateState()\r\n        {\r\n            background.Colour = Active.Value ? SelectedColour : UnSelectedColour;\r\n            Children.ForEach(x => x.Alpha = Active.Value ? 1.0f : 0.6f);\r\n        }\r\n    }\r\n\r\n    /// <summary>\r\n    /// Copied from <see cref=\"ToolbarNotificationButton\"/>\r\n    /// </summary>\r\n    private partial class CountCircle : CompositeDrawable\r\n    {\r\n        private readonly OsuSpriteText countText;\r\n        private readonly Circle circle;\r\n\r\n        private int count;\r\n\r\n        public int Count\r\n        {\r\n            get => count;\r\n            set\r\n            {\r\n                if (count == value)\r\n                    return;\r\n\r\n                if (value != count)\r\n                {\r\n                    circle.FlashColour(Color4.White, 600, Easing.OutQuint);\r\n                    this.ScaleTo(1.1f).Then().ScaleTo(1, 600, Easing.OutElastic);\r\n                }\r\n\r\n                count = value;\r\n                countText.Text = value.ToString(\"#,0\");\r\n            }\r\n        }\r\n\r\n        public CountCircle()\r\n        {\r\n            AutoSizeAxes = Axes.X;\r\n            Height = 20;\r\n\r\n            InternalChildren = new Drawable[]\r\n            {\r\n                circle = new Circle\r\n                {\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    Colour = Color4.Red,\r\n                },\r\n                countText = new OsuSpriteText\r\n                {\r\n                    Anchor = Anchor.Centre,\r\n                    Origin = Anchor.Centre,\r\n                    Y = -1,\r\n                    Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold),\r\n                    Padding = new MarginPadding(5),\r\n                    Colour = Color4.White,\r\n                    UseFullGlyphHeight = true,\r\n                },\r\n            };\r\n        }\r\n    }\r\n}\r\n\r\npublic abstract partial class EditorSettingsHeader : CompositeDrawable\r\n{\r\n    private readonly Box box;\r\n\r\n    protected EditorSettingsHeader()\r\n    {\r\n        RelativeSizeAxes = Axes.X;\r\n        AutoSizeAxes = Axes.Y;\r\n\r\n        InternalChildren = new Drawable[]\r\n        {\r\n            box = new Box\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n            },\r\n        };\r\n    }\r\n\r\n    public Colour4 BackgroundColour\r\n    {\r\n        get => box.Colour;\r\n        set => box.Colour = value;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/EditorTable.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Diagnostics;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Extensions.LocalisationExtensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Containers;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Overlays;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit;\r\n\r\npublic abstract partial class EditorTable : TableContainer\r\n{\r\n    public event Action<Drawable>? OnRowSelected;\r\n\r\n    private const float horizontal_inset = 20;\r\n\r\n    protected const float ROW_HEIGHT = 25;\r\n\r\n    public const int TEXT_SIZE = 14;\r\n\r\n    protected readonly FillFlowContainer<RowBackground> BackgroundFlow;\r\n\r\n    // We can avoid potentially thousands of objects being added to the input sub-tree since item selection is being handled by the BackgroundFlow\r\n    // and no items in the underlying table are clickable.\r\n    protected override bool ShouldBeConsideredForInput(Drawable child) => child == BackgroundFlow && base.ShouldBeConsideredForInput(child);\r\n\r\n    protected EditorTable()\r\n    {\r\n        RelativeSizeAxes = Axes.X;\r\n        AutoSizeAxes = Axes.Y;\r\n\r\n        Padding = new MarginPadding { Horizontal = horizontal_inset };\r\n        RowSize = new Dimension(GridSizeMode.Absolute, ROW_HEIGHT);\r\n\r\n        AddInternal(BackgroundFlow = new FillFlowContainer<RowBackground>\r\n        {\r\n            RelativeSizeAxes = Axes.Both,\r\n            Depth = 1f,\r\n            Padding = new MarginPadding { Horizontal = -horizontal_inset },\r\n            Margin = new MarginPadding { Top = ROW_HEIGHT },\r\n        });\r\n    }\r\n\r\n    protected int GetIndexForObject(object? item)\r\n    {\r\n        for (int i = 0; i < BackgroundFlow.Count; i++)\r\n        {\r\n            if (BackgroundFlow[i].Item == item)\r\n                return i;\r\n        }\r\n\r\n        return -1;\r\n    }\r\n\r\n    protected virtual bool SetSelectedRow(object? item)\r\n    {\r\n        bool foundSelection = false;\r\n\r\n        foreach (var b in BackgroundFlow)\r\n        {\r\n            b.Selected = ReferenceEquals(b.Item, item);\r\n\r\n            if (b.Selected)\r\n            {\r\n                Debug.Assert(!foundSelection);\r\n                OnRowSelected?.Invoke(b);\r\n                foundSelection = true;\r\n            }\r\n        }\r\n\r\n        return foundSelection;\r\n    }\r\n\r\n    protected object? GetObjectAtIndex(int index)\r\n    {\r\n        if (index < 0 || index > BackgroundFlow.Count - 1)\r\n            return null;\r\n\r\n        return BackgroundFlow[index].Item;\r\n    }\r\n\r\n    protected override Drawable CreateHeader(int index, TableColumn? column) => new HeaderText(column?.Header ?? default);\r\n\r\n    private partial class HeaderText : OsuSpriteText\r\n    {\r\n        public HeaderText(LocalisableString text)\r\n        {\r\n            Text = text.ToUpper();\r\n            Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold);\r\n        }\r\n    }\r\n\r\n    public partial class RowBackground : OsuClickableContainer\r\n    {\r\n        public readonly object Item;\r\n\r\n        private const int fade_duration = 100;\r\n\r\n        private readonly Box hoveredBackground;\r\n\r\n        public RowBackground(object item)\r\n        {\r\n            Item = item;\r\n\r\n            RelativeSizeAxes = Axes.X;\r\n            Height = 25;\r\n\r\n            AlwaysPresent = true;\r\n\r\n            CornerRadius = 3;\r\n            Masking = true;\r\n\r\n            Children = new Drawable[]\r\n            {\r\n                hoveredBackground = new Box\r\n                {\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    Alpha = 0,\r\n                },\r\n            };\r\n        }\r\n\r\n        private Color4 colourHover;\r\n        private Color4 colourSelected;\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(OverlayColourProvider colours)\r\n        {\r\n            colourHover = colours.Background1;\r\n            colourSelected = colours.Colour3;\r\n        }\r\n\r\n        protected override void LoadComplete()\r\n        {\r\n            base.LoadComplete();\r\n\r\n            updateState();\r\n            FinishTransforms(true);\r\n        }\r\n\r\n        private bool selected;\r\n\r\n        public bool Selected\r\n        {\r\n            get => selected;\r\n            set\r\n            {\r\n                if (value == selected)\r\n                    return;\r\n\r\n                selected = value;\r\n                updateState();\r\n            }\r\n        }\r\n\r\n        protected override bool OnHover(HoverEvent e)\r\n        {\r\n            updateState();\r\n            return base.OnHover(e);\r\n        }\r\n\r\n        protected override void OnHoverLost(HoverLostEvent e)\r\n        {\r\n            updateState();\r\n            base.OnHoverLost(e);\r\n        }\r\n\r\n        private void updateState()\r\n        {\r\n            hoveredBackground.FadeColour(selected ? colourSelected : colourHover, 450, Easing.OutQuint);\r\n\r\n            if (selected || IsHovered)\r\n                hoveredBackground.FadeIn(fade_duration, Easing.OutQuint);\r\n            else\r\n                hoveredBackground.FadeOut(fade_duration, Easing.OutQuint);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/EditorVerifier.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Rulesets.Edit;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit;\r\n\r\n/// <summary>\r\n/// This class is focus on mange the list of <see cref=\"ICheck\"/> and save/load list of <see cref=\"Issue\"/>.\r\n/// </summary>\r\n/// <typeparam name=\"TEnum\"></typeparam>\r\npublic abstract partial class EditorVerifier<TEnum> : Component, IEditorVerifier<TEnum> where TEnum : struct, Enum\r\n{\r\n    [Resolved]\r\n    private EditorBeatmap beatmap { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private IBindable<WorkingBeatmap> workingBeatmap { get; set; } = null!;\r\n\r\n    private readonly IDictionary<TEnum, ICheck[]> checkMappings = new Dictionary<TEnum, ICheck[]>();\r\n    private readonly IDictionary<TEnum, BindableList<Issue>> issues = new Dictionary<TEnum, BindableList<Issue>>();\r\n\r\n    protected EditorVerifier()\r\n    {\r\n        initializeCheckMappings();\r\n        initializeIssues();\r\n    }\r\n\r\n    private void initializeCheckMappings()\r\n    {\r\n        foreach (var mode in Enum.GetValues<TEnum>())\r\n        {\r\n            checkMappings.Add(mode, CreateChecks(mode).ToArray());\r\n        }\r\n    }\r\n\r\n    private void initializeIssues()\r\n    {\r\n        foreach (var mode in Enum.GetValues<TEnum>())\r\n        {\r\n            issues.Add(mode, new BindableList<Issue>());\r\n        }\r\n    }\r\n\r\n    public IBindableList<Issue> GetIssueByType(TEnum type)\r\n        => issues[type];\r\n\r\n    public abstract void Refresh();\r\n\r\n    #region Checks\r\n\r\n    protected abstract IEnumerable<ICheck> CreateChecks(TEnum type);\r\n\r\n    protected void ClearChecks(TEnum type)\r\n    {\r\n        issues[type].Clear();\r\n    }\r\n\r\n    protected void AddChecks(TEnum type, IEnumerable<Issue> newIssues)\r\n    {\r\n        issues[type].AddRange(newIssues);\r\n    }\r\n\r\n    protected virtual TEnum ClassifyIssue(Issue issue)\r\n    {\r\n        foreach (var (type, checks) in checkMappings)\r\n        {\r\n            if (checks.Contains(issue.Check))\r\n                return type;\r\n        }\r\n\r\n        throw new ArgumentOutOfRangeException();\r\n    }\r\n\r\n    protected virtual BeatmapVerifierContext CreateBeatmapVerifierContext(IBeatmap beatmap, WorkingBeatmap workingBeatmap) => new(beatmap, workingBeatmap);\r\n\r\n    protected IEnumerable<Issue> CreateIssues(Action<BeatmapVerifierContext>? action = null)\r\n    {\r\n        var context = CreateBeatmapVerifierContext(beatmap, workingBeatmap.Value);\r\n        action?.Invoke(context);\r\n        return new EditorBeatmapVerifier(checkMappings.Values.SelectMany(x => x)).Run(context);\r\n    }\r\n\r\n    protected IEnumerable<Issue> CreateIssuesByType(TEnum type, BeatmapVerifierContext context)\r\n        => new EditorBeatmapVerifier(checkMappings[type]).Run(context);\r\n\r\n    #endregion\r\n\r\n    private class EditorBeatmapVerifier : IBeatmapVerifier\r\n    {\r\n        private readonly IEnumerable<ICheck> checks;\r\n\r\n        public EditorBeatmapVerifier(IEnumerable<ICheck> checks)\r\n        {\r\n            this.checks = checks;\r\n        }\r\n\r\n        public IEnumerable<Issue> Run(BeatmapVerifierContext context)\r\n        {\r\n            return checks.SelectMany(check => check.Run(context));\r\n        }\r\n    }\r\n}\r\n\r\n/// <summary>\r\n/// This class is focus on mange the list of <see cref=\"ICheck\"/> and save/load list of <see cref=\"Issue\"/>.\r\n/// </summary>\r\npublic abstract partial class EditorVerifier : Component, IEditorVerifier\r\n{\r\n    [Resolved]\r\n    private EditorBeatmap beatmap { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private IBindable<WorkingBeatmap> workingBeatmap { get; set; } = null!;\r\n\r\n    private readonly List<ICheck> checks = new();\r\n    private readonly BindableList<Issue> issues = new();\r\n\r\n    public IBindableList<Issue> Issues => issues;\r\n\r\n    protected EditorVerifier()\r\n    {\r\n        checks.AddRange(CreateChecks().ToList());\r\n    }\r\n\r\n    public abstract void Refresh();\r\n\r\n    #region Checks\r\n\r\n    protected abstract IEnumerable<ICheck> CreateChecks();\r\n\r\n    protected IBindableList<Issue> GetIssues()\r\n        => issues;\r\n\r\n    protected void ClearChecks()\r\n    {\r\n        issues.Clear();\r\n    }\r\n\r\n    protected void AddChecks(IEnumerable<Issue> newIssues)\r\n    {\r\n        issues.AddRange(newIssues);\r\n    }\r\n\r\n    protected virtual BeatmapVerifierContext CreateBeatmapVerifierContext(IBeatmap beatmap, WorkingBeatmap workingBeatmap) => new(beatmap, workingBeatmap);\r\n\r\n    protected IEnumerable<Issue> CreateIssues(Action<BeatmapVerifierContext>? action = null)\r\n    {\r\n        var context = CreateBeatmapVerifierContext(beatmap, workingBeatmap.Value);\r\n        action?.Invoke(context);\r\n        return new EditorBeatmapVerifier(checks).Run(context);\r\n    }\r\n\r\n    #endregion\r\n\r\n    private class EditorBeatmapVerifier : IBeatmapVerifier\r\n    {\r\n        private readonly IEnumerable<ICheck> checks;\r\n\r\n        public EditorBeatmapVerifier(IEnumerable<ICheck> checks)\r\n        {\r\n            this.checks = checks;\r\n        }\r\n\r\n        public IEnumerable<Issue> Run(BeatmapVerifierContext context)\r\n        {\r\n            return checks.SelectMany(check => check.Run(context));\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/GeneratorConfigPopover.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Globalization;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Configuration;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Containers;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Graphics.UserInterfaceV2;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.UserInterface;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit;\r\n\r\npublic partial class GeneratorConfigPopover : OsuPopover\r\n{\r\n    private const string default_category_name = \"General\";\r\n\r\n    private readonly KaraokeRulesetEditGeneratorSetting setting;\r\n\r\n    private readonly FillFlowContainer<GeneratorConfigSection> sections;\r\n\r\n    public GeneratorConfigPopover(KaraokeRulesetEditGeneratorSetting setting)\r\n    {\r\n        this.setting = setting;\r\n\r\n        Child = new OsuScrollContainer\r\n        {\r\n            Height = 500,\r\n            Width = 300,\r\n            Child = sections = new FillFlowContainer<GeneratorConfigSection>\r\n            {\r\n                Direction = FillDirection.Vertical,\r\n                RelativeSizeAxes = Axes.X,\r\n                AutoSizeAxes = Axes.Y,\r\n            },\r\n        };\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(KaraokeRulesetEditGeneratorConfigManager config)\r\n    {\r\n        var generatorConfig = config.GetGeneratorConfig(setting);\r\n        sections.Children = createConfigsSections(generatorConfig).ToArray();\r\n    }\r\n\r\n    private static IEnumerable<GeneratorConfigSection> createConfigsSections(GeneratorConfig config)\r\n    {\r\n        var defaultCategory = new ConfigCategoryAttribute(default_category_name);\r\n\r\n        foreach (var (category, properties) in config.GetOrderedConfigsSourceDictionary(defaultCategory))\r\n        {\r\n            yield return new GeneratorConfigSection\r\n            {\r\n                Title = category.Category,\r\n                Children = properties.Select(x =>\r\n                {\r\n                    object value = x.Item2.GetValue(config)!;\r\n                    return createControl(value, x.Item1);\r\n                }).ToArray(),\r\n            };\r\n        }\r\n    }\r\n\r\n    private static Drawable createControl(object value, ConfigSourceAttribute attribute)\r\n    {\r\n        return value switch\r\n        {\r\n            BindableNumber<int> bInt => new LabelledSliderBar<int>\r\n            {\r\n                Label = attribute.Label,\r\n                Description = attribute.Description,\r\n                Current = bInt,\r\n            },\r\n            BindableNumber<float> bFloat => new LabelledSliderBar<float>\r\n            {\r\n                Label = attribute.Label,\r\n                Description = attribute.Description,\r\n                Current = bFloat,\r\n            },\r\n            BindableNumber<double> bDouble => new LabelledSliderBar<double>\r\n            {\r\n                Label = attribute.Label,\r\n                Description = attribute.Description,\r\n                Current = bDouble,\r\n            },\r\n            Bindable<bool> bBool => new LabelledSwitchButton\r\n            {\r\n                Label = attribute.Label,\r\n                Description = attribute.Description,\r\n                Current = bBool,\r\n            },\r\n            Bindable<CultureInfo[]> bCultureInfos => new LanguagesSelector\r\n            {\r\n                Current = bCultureInfos,\r\n            },\r\n            _ => throw new InvalidOperationException($\"{nameof(SettingSourceAttribute)} was attached to an unsupported type ({value})\"),\r\n        };\r\n    }\r\n\r\n    public partial class GeneratorConfigSection : Container\r\n    {\r\n        private readonly FillFlowContainer flow;\r\n        private readonly OsuSpriteText title;\r\n\r\n        protected override Container<Drawable> Content => flow;\r\n\r\n        public LocalisableString Title\r\n        {\r\n            get => title.Text;\r\n            set => title.Text = value;\r\n        }\r\n\r\n        public GeneratorConfigSection()\r\n        {\r\n            RelativeSizeAxes = Axes.X;\r\n            AutoSizeAxes = Axes.Y;\r\n\r\n            Padding = new MarginPadding(10);\r\n\r\n            InternalChildren = new Drawable[]\r\n            {\r\n                title = new OsuSpriteText\r\n                {\r\n                    Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 18),\r\n                },\r\n                flow = new FillFlowContainer\r\n                {\r\n                    RelativeSizeAxes = Axes.X,\r\n                    AutoSizeAxes = Axes.Y,\r\n                    Spacing = new Vector2(10),\r\n                    Direction = FillDirection.Vertical,\r\n                    Margin = new MarginPadding { Top = 30 },\r\n                },\r\n            };\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/GenericEditor.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Framework.Input.Bindings;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Framework.Screens;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Cursor;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Input.Bindings;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Menus;\r\nusing osu.Game.Screens.Edit;\r\nusing osu.Game.Screens.Edit.Components.Menus;\r\nusing osu.Game.Screens.Play;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit;\r\n\r\npublic abstract partial class GenericEditor<TScreenMode> : ScreenWithBeatmapBackground, IKeyBindingHandler<GlobalAction> where TScreenMode : Enum\r\n{\r\n    public override float BackgroundParallaxAmount => 0.1f;\r\n\r\n    public override bool AllowUserExit => false;\r\n\r\n    public override bool HideOverlaysOnEnter => true;\r\n\r\n    public override bool DisallowExternalBeatmapRulesetChanges => true;\r\n\r\n    public override bool? ApplyModTrackAdjustments => false;\r\n\r\n    public readonly Bindable<TScreenMode> Mode = new();\r\n\r\n    private Container<GenericEditorScreen<TScreenMode>> screenContainer = null!;\r\n\r\n    private GenericEditorScreen<TScreenMode>? currentScreen;\r\n\r\n    private EditorMenuBar menuBar = null!;\r\n\r\n    private DependencyContainer dependencies = null!;\r\n\r\n    protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)\r\n        => dependencies = new DependencyContainer(base.CreateChildDependencies(parent));\r\n\r\n    [BackgroundDependencyLoader(true)]\r\n    private void load(OsuColour colours, EditorBeatmap editorBeatmap, BindableBeatDivisor beatDivisor)\r\n    {\r\n        // todo: should re-inject editor clock because it will let track cannot change time because it's in another screen.\r\n        var clock = new EditorClock(editorBeatmap, beatDivisor);\r\n\r\n        var loadableBeatmap = Beatmap.Value;\r\n        clock.ChangeSource(loadableBeatmap.Track);\r\n\r\n        dependencies.CacheAs(clock);\r\n        AddInternal(clock);\r\n\r\n        AddInternal(new OsuContextMenuContainer\r\n        {\r\n            RelativeSizeAxes = Axes.Both,\r\n            Children = new Drawable[]\r\n            {\r\n                new Container\r\n                {\r\n                    Name = \"Screen container\",\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    Padding = new MarginPadding { Top = 40, Bottom = 60 },\r\n                    Child = screenContainer = new Container<GenericEditorScreen<TScreenMode>>\r\n                    {\r\n                        RelativeSizeAxes = Axes.Both,\r\n                        Masking = true,\r\n                    },\r\n                },\r\n                new Container\r\n                {\r\n                    Name = \"Top bar\",\r\n                    RelativeSizeAxes = Axes.X,\r\n                    Height = 40,\r\n                    Children = new Drawable[]\r\n                    {\r\n                        menuBar = new EditorMenuBar\r\n                        {\r\n                            Anchor = Anchor.CentreLeft,\r\n                            Origin = Anchor.CentreLeft,\r\n                            RelativeSizeAxes = Axes.Both,\r\n                        },\r\n                        new GenericScreenSelectionTabControl<TScreenMode>\r\n                        {\r\n                            Anchor = Anchor.BottomRight,\r\n                            Origin = Anchor.BottomRight,\r\n                            X = -15,\r\n                            Current = Mode,\r\n                        },\r\n                    },\r\n                },\r\n                new BottomBar(),\r\n            },\r\n        });\r\n\r\n        Mode.BindValueChanged(onModeChanged, true);\r\n    }\r\n\r\n    private void onModeChanged(ValueChangedEvent<TScreenMode> e)\r\n    {\r\n        var lastScreen = currentScreen;\r\n\r\n        lastScreen?.Hide();\r\n\r\n        try\r\n        {\r\n            if ((currentScreen = screenContainer.SingleOrDefault(s => EqualityComparer<TScreenMode>.Default.Equals(s.Type, e.NewValue))) != null)\r\n            {\r\n                screenContainer.ChangeChildDepth(currentScreen, lastScreen?.Depth + 1 ?? 0);\r\n                currentScreen.Show();\r\n                return;\r\n            }\r\n\r\n            currentScreen = GenerateScreen(e.NewValue);\r\n\r\n            LoadComponentAsync(currentScreen, newScreen =>\r\n            {\r\n                if (newScreen != currentScreen)\r\n                    return;\r\n\r\n                screenContainer.Add(newScreen);\r\n                newScreen.Show();\r\n            });\r\n        }\r\n        finally\r\n        {\r\n            updateMenuItems(e.NewValue);\r\n        }\r\n    }\r\n\r\n    private void updateMenuItems(TScreenMode screenMode)\r\n    {\r\n        var menuItems = new List<MenuItem>\r\n        {\r\n            new(\"Menu\")\r\n            {\r\n                Items = new[]\r\n                {\r\n                    new EditorMenuItem(\"Save\"),\r\n                    new EditorMenuItem(\"Back\", MenuItemType.Standard, this.Exit),\r\n                },\r\n            },\r\n        };\r\n\r\n        menuItems.AddRange(GenerateMenuItems(screenMode));\r\n\r\n        menuBar.Items = menuItems;\r\n    }\r\n\r\n    protected abstract GenericEditorScreen<TScreenMode> GenerateScreen(TScreenMode screenMode);\r\n\r\n    protected abstract MenuItem[] GenerateMenuItems(TScreenMode screenMode);\r\n\r\n    public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)\r\n    {\r\n        if (e.Repeat)\r\n            return false;\r\n\r\n        switch (e.Action)\r\n        {\r\n            case GlobalAction.Back:\r\n                // as we don't want to display the back button, manual handling of exit action is required.\r\n                // follow how editor.cs does.\r\n                this.Exit();\r\n                return true;\r\n\r\n            default:\r\n                return false;\r\n        }\r\n    }\r\n\r\n    public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)\r\n    {\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/GenericEditorScreen.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit;\r\n\r\n/// <summary>\r\n/// TODO: eventually make this inherit Screen and add a local screen stack inside the Editor.\r\n/// </summary>\r\npublic abstract partial class GenericEditorScreen<TType> : EditorScreen\r\n{\r\n    public new readonly TType Type;\r\n\r\n    protected GenericEditorScreen(TType type)\r\n        : base(EditorScreenMode.Compose)\r\n    {\r\n        Type = type;\r\n    }\r\n\r\n    protected override void PopIn()\r\n    {\r\n        this.ScaleTo(1f, 200, Easing.OutQuint)\r\n            .FadeIn(200, Easing.OutQuint);\r\n    }\r\n\r\n    protected override void PopOut()\r\n    {\r\n        this.ScaleTo(0.98f, 200, Easing.OutQuint)\r\n            .FadeOut(200, Easing.OutQuint);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/IEditorVerifier.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit;\r\n\r\npublic interface IEditorVerifier<in TEnum> where TEnum : struct, Enum\r\n{\r\n    IBindableList<Issue> GetIssueByType(TEnum type);\r\n\r\n    void Refresh();\r\n}\r\n\r\npublic interface IEditorVerifier\r\n{\r\n    IBindableList<Issue> Issues { get; }\r\n\r\n    void Refresh();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/ISectionItemsEditorProvider.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit;\r\n\r\npublic interface ISectionItemsEditorProvider\r\n{\r\n    void UpdateDisplayOrder(Drawable drawable, int order);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Import/Lyrics/AssignLanguage/AssignLanguageNavigation.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Import.Lyrics.AssignLanguage;\r\n\r\npublic partial class AssignLanguageNavigation : TopNavigation<AssignLanguageStepScreen>\r\n{\r\n    private const string auto_assign_language = \"AUTO_ASSIGN_LANGUAGE\";\r\n\r\n    public AssignLanguageNavigation(AssignLanguageStepScreen screen)\r\n        : base(screen)\r\n    {\r\n    }\r\n\r\n    protected override NavigationTextContainer CreateTextContainer()\r\n        => new AssignLanguageTextFlowContainer(Screen);\r\n\r\n    protected override NavigationState GetState(Lyric[] lyrics)\r\n    {\r\n        if (lyrics.All(x => x.Language != null))\r\n            return NavigationState.Done;\r\n\r\n        if (lyrics.Any(x => x.Language != null))\r\n            return NavigationState.Working;\r\n\r\n        return NavigationState.Initial;\r\n    }\r\n\r\n    protected override LocalisableString GetNavigationText(NavigationState value) =>\r\n        value switch\r\n        {\r\n            NavigationState.Initial => $\"Try to select left side to mark lyric's language, or click [{auto_assign_language}] to let system auto detect lyric language.\",\r\n            NavigationState.Working => $\"Almost there, you can still click [{auto_assign_language}] to re-detect each lyric's language.\",\r\n            NavigationState.Done => \"Cool! Seems all lyric has it's own language. Go to next step to generate ruby.\",\r\n            NavigationState.Error => \"Oops, seems cause some error in here.\",\r\n            _ => throw new ArgumentOutOfRangeException(nameof(value)),\r\n        };\r\n\r\n    private partial class AssignLanguageTextFlowContainer : NavigationTextContainer\r\n    {\r\n        public AssignLanguageTextFlowContainer(AssignLanguageStepScreen screen)\r\n        {\r\n            AddLinkFactory(auto_assign_language, \"language detector\", screen.AskForAutoAssignLanguage);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Import/Lyrics/AssignLanguage/AssignLanguageStepScreen.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Import.Lyrics.AssignLanguage;\r\n\r\npublic partial class AssignLanguageStepScreen : LyricImporterStepScreenWithLyricEditor, IHasTopNavigation\r\n{\r\n    public override string Title => \"Language\";\r\n\r\n    public override IconUsage Icon => FontAwesome.Solid.Globe;\r\n\r\n    [Cached(typeof(ILyricPropertyAutoGenerateChangeHandler))]\r\n    private readonly LyricPropertyAutoGenerateChangeHandler lyricPropertyAutoGenerateChangeHandler;\r\n\r\n    [Cached(typeof(ILyricLanguageChangeHandler))]\r\n    private readonly LyricLanguageChangeHandler lyricLanguageChangeHandler;\r\n\r\n    public AssignLanguageStepScreen()\r\n    {\r\n        AddInternal(lyricPropertyAutoGenerateChangeHandler = new LyricPropertyAutoGenerateChangeHandler());\r\n        AddInternal(lyricLanguageChangeHandler = new LyricLanguageChangeHandler());\r\n    }\r\n\r\n    public TopNavigation CreateNavigation()\r\n        => new AssignLanguageNavigation(this);\r\n\r\n    protected override Drawable CreateContent()\r\n        => base.CreateContent().With(_ =>\r\n        {\r\n            SwitchLyricEditorMode(LyricEditorMode.EditLanguage);\r\n        });\r\n\r\n    protected override void LoadComplete()\r\n    {\r\n        base.LoadComplete();\r\n        AskForAutoAssignLanguage();\r\n    }\r\n\r\n    public override void Complete()\r\n    {\r\n        // Check is need to go to generate ruby step or just skip.\r\n        var nextStep = lyricPropertyAutoGenerateChangeHandler.CanGenerate(AutoGenerateType.AutoGenerateRubyTags)\r\n            ? LyricImporterStep.GenerateRuby\r\n            : LyricImporterStep.GenerateTimeTag;\r\n\r\n        ScreenStack.Push(nextStep);\r\n    }\r\n\r\n    internal void AskForAutoAssignLanguage()\r\n    {\r\n        DialogOverlay.Push(new UseLanguageDetectorPopupDialog(ok =>\r\n        {\r\n            if (!ok)\r\n                return;\r\n\r\n            PrepareAutoGenerate();\r\n        }));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Import/Lyrics/AssignLanguage/UseLanguageDetectorPopupDialog.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Overlays.Dialog;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Import.Lyrics.AssignLanguage;\r\n\r\npublic partial class UseLanguageDetectorPopupDialog : PopupDialog\r\n{\r\n    public UseLanguageDetectorPopupDialog(Action<bool> okAction)\r\n    {\r\n        Icon = FontAwesome.Solid.Globe;\r\n        HeaderText = \"Language detector\";\r\n        BodyText = \"Would you like to use language detector to auto assign each lyric's language?\";\r\n        Buttons = new PopupDialogButton[]\r\n        {\r\n            new PopupDialogOkButton\r\n            {\r\n                Text = \"OK\",\r\n                Action = () => okAction(true),\r\n            },\r\n            new PopupDialogCancelButton\r\n            {\r\n                Text = \"Cancel\",\r\n                Action = () => okAction(false),\r\n            },\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Import/Lyrics/DragFile/Components/DrawableDragFile.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.IO;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Containers;\r\nusing osu.Game.Graphics.UserInterfaceV2;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Import.Lyrics.DragFile.Components;\r\n\r\npublic partial class DrawableDragFile : Container\r\n{\r\n    private const float button_height = 50;\r\n    private const float button_vertical_margin = 15;\r\n\r\n    private OsuFileSelector fileSelector = null!;\r\n    private TextFlowContainer currentFileText = null!;\r\n\r\n    private RoundedButton importButton = null!;\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OsuColour colours)\r\n    {\r\n        Masking = true;\r\n        CornerRadius = 10;\r\n        Children = new Drawable[]\r\n        {\r\n            new Box\r\n            {\r\n                Colour = colours.GreySeaFoamDark,\r\n                RelativeSizeAxes = Axes.Both,\r\n            },\r\n            fileSelector = new OsuFileSelector(validFileExtensions: ImportLyricManager.LyricFormatExtensions)\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n                Width = 0.6f,\r\n            },\r\n            new Container\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n                Width = 0.4f,\r\n                Anchor = Anchor.TopRight,\r\n                Origin = Anchor.TopRight,\r\n                Children = new Drawable[]\r\n                {\r\n                    new Box\r\n                    {\r\n                        Colour = colours.GreySeaFoamDarker,\r\n                        RelativeSizeAxes = Axes.Both,\r\n                    },\r\n                    new Container\r\n                    {\r\n                        RelativeSizeAxes = Axes.Both,\r\n                        Padding = new MarginPadding { Bottom = button_height + button_vertical_margin * 2 },\r\n                        Child = new OsuScrollContainer\r\n                        {\r\n                            RelativeSizeAxes = Axes.Both,\r\n                            Anchor = Anchor.TopCentre,\r\n                            Origin = Anchor.TopCentre,\r\n                            Child = currentFileText = new TextFlowContainer(t => t.Font = OsuFont.Default.With(size: 30))\r\n                            {\r\n                                AutoSizeAxes = Axes.Y,\r\n                                RelativeSizeAxes = Axes.X,\r\n                                Anchor = Anchor.Centre,\r\n                                Origin = Anchor.Centre,\r\n                                TextAnchor = Anchor.Centre,\r\n                            },\r\n                            ScrollContent =\r\n                            {\r\n                                Anchor = Anchor.Centre,\r\n                                Origin = Anchor.Centre,\r\n                            },\r\n                        },\r\n                    },\r\n                    importButton = new RoundedButton\r\n                    {\r\n                        Text = \"Import\",\r\n                        Anchor = Anchor.BottomCentre,\r\n                        Origin = Anchor.BottomCentre,\r\n                        RelativeSizeAxes = Axes.X,\r\n                        Height = button_height,\r\n                        Width = 0.9f,\r\n                        Margin = new MarginPadding { Vertical = button_vertical_margin },\r\n                        Action = () =>\r\n                        {\r\n                            string? fileName = fileSelector.CurrentFile.Value?.FullName;\r\n                            if (string.IsNullOrEmpty(fileName))\r\n                                return;\r\n\r\n                            Import?.Invoke(fileName);\r\n                        },\r\n                    },\r\n                },\r\n            },\r\n        };\r\n\r\n        fileSelector.CurrentFile.BindValueChanged(fileChanged, true);\r\n        fileSelector.CurrentPath.BindValueChanged(directoryChanged);\r\n    }\r\n\r\n    private void fileChanged(ValueChangedEvent<FileInfo?> selectedFile)\r\n    {\r\n        importButton.Enabled.Value = selectedFile.NewValue != null;\r\n        currentFileText.Text = selectedFile.NewValue?.Name ?? \"Select a file or drag to import.\";\r\n    }\r\n\r\n    private void directoryChanged(ValueChangedEvent<DirectoryInfo> _)\r\n    {\r\n        // this should probably be done by the selector itself, but let's do it here for now.\r\n        fileSelector.CurrentFile.Value = null;\r\n    }\r\n\r\n    public Action<string>? Import { get; set; }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Import/Lyrics/DragFile/DragFileStepScreen.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.IO;\r\nusing System.Linq;\r\nusing System.Threading.Tasks;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Screens;\r\nusing osu.Game.Database;\r\nusing osu.Game.Overlays.Dialog;\r\nusing osu.Game.Rulesets.Karaoke.Overlays.Dialog;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Import.Lyrics.DragFile.Components;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Import.Lyrics.DragFile;\r\n\r\npublic partial class DragFileStepScreen : LyricImporterStepScreen, ICanAcceptFiles\r\n{\r\n    public override string Title => \"Import\";\r\n    public override IconUsage Icon => FontAwesome.Solid.Upload;\r\n\r\n    public IEnumerable<string> HandledExtensions => ImportLyricManager.LyricFormatExtensions;\r\n\r\n    [Resolved]\r\n    private ImportLyricManager importManager { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private OsuGameBase game { get; set; } = null!;\r\n\r\n    public DragFileStepScreen()\r\n    {\r\n        InternalChild = new DrawableDragFile\r\n        {\r\n            Anchor = Anchor.Centre,\r\n            Origin = Anchor.Centre,\r\n            RelativeSizeAxes = Axes.Both,\r\n            Padding = new MarginPadding(64),\r\n            Import = path =>\r\n            {\r\n                Task.Factory.StartNew(async () =>\r\n                {\r\n                    await Import(path).ConfigureAwait(false);\r\n                }, TaskCreationOptions.LongRunning);\r\n            },\r\n        };\r\n    }\r\n\r\n    public Task Import(params string[] paths)\r\n    {\r\n        var fileInfo = new FileInfo(paths.First());\r\n        ImportLyricFile(fileInfo);\r\n        return Task.CompletedTask;\r\n    }\r\n\r\n    public Task Import(ImportTask[] tasks, ImportParameters parameters = default)\r\n    {\r\n        // todo : wail until really implement needed.\r\n        throw new NotImplementedException(\"Report to https://github.com/karaoke-dev/karaoke and i will implement it.\");\r\n    }\r\n\r\n    public void ImportLyricFile(FileInfo fileInfo)\r\n    {\r\n        Schedule(() =>\r\n        {\r\n            // Check file is exist\r\n            if (!fileInfo.Exists)\r\n            {\r\n                DialogOverlay.Push(createFileNotFoundDialog());\r\n                return;\r\n            }\r\n\r\n            // Check format is match\r\n            if (!ImportLyricManager.LyricFormatExtensions.Contains(fileInfo.Extension))\r\n            {\r\n                DialogOverlay.Push(createFormatNotMatchDialog());\r\n                return;\r\n            }\r\n\r\n            DialogOverlay.Push(new ImportLyricDialog(execute =>\r\n            {\r\n                if (!execute)\r\n                    return;\r\n\r\n                try\r\n                {\r\n                    importManager.ImportFile(fileInfo);\r\n                    DialogOverlay.Push(createCompleteDialog());\r\n                }\r\n                catch (Exception ex)\r\n                {\r\n                    switch (ex)\r\n                    {\r\n                        case FileNotFoundException:\r\n                            DialogOverlay.Push(createFileNotFoundDialog());\r\n                            break;\r\n\r\n                        case FileLoadException loadException:\r\n                            DialogOverlay.Push(createLoadExceptionDialog(loadException));\r\n                            break;\r\n\r\n                        default:\r\n                            DialogOverlay.Push(createUnknownExceptionDialog());\r\n                            break;\r\n                    }\r\n                }\r\n            }));\r\n        });\r\n    }\r\n\r\n    public override void Complete()\r\n    {\r\n        ScreenStack.Push(LyricImporterStep.EditLyric);\r\n    }\r\n\r\n    public override void ConfirmRollBackFromStep(ILyricImporterStepScreen fromScreen, Action<bool> callBack)\r\n    {\r\n        base.ConfirmRollBackFromStep(fromScreen, ok =>\r\n        {\r\n            DialogOverlay.Push(new RollBackResetPopupDialog(fromScreen, reset =>\r\n            {\r\n                if (reset)\r\n                {\r\n                    // Should be better to clear all the lyrics before roll-back to the current page.\r\n                    importManager.AbortImport();\r\n                }\r\n\r\n                callBack(ok);\r\n            }));\r\n        });\r\n    }\r\n\r\n    public override void OnEntering(ScreenTransitionEvent e)\r\n    {\r\n        game.RegisterImportHandler(this);\r\n        base.OnEntering(e);\r\n    }\r\n\r\n    public override void OnResuming(ScreenTransitionEvent e)\r\n    {\r\n        game.RegisterImportHandler(this);\r\n        base.OnResuming(e);\r\n    }\r\n\r\n    public override void OnSuspending(ScreenTransitionEvent e)\r\n    {\r\n        game.UnregisterImportHandler(this);\r\n        base.OnSuspending(e);\r\n    }\r\n\r\n    public override bool OnExiting(ScreenExitEvent e)\r\n    {\r\n        game.UnregisterImportHandler(this);\r\n        return base.OnExiting(e);\r\n    }\r\n\r\n    private PopupDialog createFileNotFoundDialog()\r\n        => new OkPopupDialog\r\n        {\r\n            Icon = FontAwesome.Regular.QuestionCircle,\r\n            HeaderText = \"Seems file is not exist\",\r\n            BodyText = \"Drag the file then drop again.\",\r\n        };\r\n\r\n    private PopupDialog createFormatNotMatchDialog()\r\n        => new OkPopupDialog\r\n        {\r\n            Icon = FontAwesome.Solid.ExclamationTriangle,\r\n            HeaderText = \"This type of file is not supported\",\r\n            BodyText = \"May sure this type of file is supported.\",\r\n        };\r\n\r\n    private PopupDialog createLoadExceptionDialog(FileLoadException loadException)\r\n        => new OkPopupDialog\r\n        {\r\n            Icon = FontAwesome.Solid.Bug,\r\n            HeaderText = \"File loading error\",\r\n            BodyText = loadException.Message,\r\n        };\r\n\r\n    private PopupDialog createUnknownExceptionDialog()\r\n        => new OkPopupDialog\r\n        {\r\n            Icon = FontAwesome.Solid.Bug,\r\n            HeaderText = \"Unknown error\",\r\n            BodyText = \"Unknown error QAQa.\",\r\n        };\r\n\r\n    private PopupDialog createCompleteDialog()\r\n        => new OkPopupDialog(_ => { Complete(); })\r\n        {\r\n            Icon = FontAwesome.Regular.CheckCircle,\r\n            HeaderText = \"Import success\",\r\n            BodyText = \"Lyrics has been imported.\",\r\n        };\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Import/Lyrics/DragFile/ImportLyricDialog.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Overlays.Dialog;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Import.Lyrics.DragFile;\r\n\r\npublic partial class ImportLyricDialog : PopupDialog\r\n{\r\n    public ImportLyricDialog(Action<bool> resetAction)\r\n    {\r\n        Icon = FontAwesome.Regular.TrashAlt;\r\n        HeaderText = \"Confirm import lyric file?\";\r\n        BodyText = \"Import lyric file will clean-up all exist lyric.\";\r\n\r\n        Buttons = new PopupDialogButton[]\r\n        {\r\n            new PopupDialogOkButton\r\n            {\r\n                Text = \"Yes. Go for it.\",\r\n                Action = () => resetAction(true),\r\n            },\r\n            new PopupDialogCancelButton\r\n            {\r\n                Text = \"No! Abort mission!\",\r\n                Action = () => resetAction(false),\r\n            },\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Import/Lyrics/EditLyric/EditLyricNavigation.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.ComponentModel;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Import.Lyrics.EditLyric;\r\n\r\npublic partial class EditLyricNavigation : TopNavigation<EditLyricStepScreen>\r\n{\r\n    private const string typing_mode = \"TYPING_MODE\";\r\n    private const string split_mode = \"SPLIT_MODE\";\r\n\r\n    private readonly IBindable<LyricEditorMode> bindableMode = new Bindable<LyricEditorMode>();\r\n\r\n    public EditLyricNavigation(EditLyricStepScreen screen)\r\n        : base(screen)\r\n    {\r\n        bindableMode.BindValueChanged(_ =>\r\n        {\r\n            // should update the display text in navigation bar if mode change.\r\n            TriggerStateChange();\r\n        });\r\n    }\r\n\r\n    protected override void LoadComplete()\r\n    {\r\n        base.LoadComplete();\r\n\r\n        var state = GetDependency<ILyricEditorState>();\r\n        bindableMode.BindTo(state.BindableMode);\r\n    }\r\n\r\n    protected override NavigationTextContainer CreateTextContainer()\r\n        => new EditLyricTextFlowContainer(Screen);\r\n\r\n    protected override NavigationState GetState(Lyric[] lyrics)\r\n        => NavigationState.Working;\r\n\r\n    protected override LocalisableString GetNavigationText(NavigationState value)\r\n    {\r\n        switch (value)\r\n        {\r\n            case NavigationState.Initial:\r\n                return $\"Does something looks weird? Try switching [{typing_mode}] or [{split_mode}] to edit your lyric.\";\r\n\r\n            case NavigationState.Working:\r\n            case NavigationState.Done:\r\n                var step = Screen.GetLyricEditorModeState<TextEditStep>();\r\n\r\n                return step switch\r\n                {\r\n                    TextEditStep.Typing => $\"Cool! Try switching to [{split_mode}] if you want to cut or combine lyric.\",\r\n                    TextEditStep.Split => $\"Cool! Try switching to [{typing_mode}] if you want to edit lyric.\",\r\n                    TextEditStep.Verify => $\"Cool! Try switching to [{split_mode}] or [{typing_mode}] if you want to fix the issue.\",\r\n                    _ => throw new InvalidEnumArgumentException(nameof(step)),\r\n                };\r\n\r\n            case NavigationState.Error:\r\n                return \"Oops, seems cause some error in here.\";\r\n\r\n            default:\r\n                throw new InvalidEnumArgumentException(nameof(value));\r\n        }\r\n    }\r\n\r\n    protected override bool AbleToNextStep(NavigationState value)\r\n        => true;\r\n\r\n    private partial class EditLyricTextFlowContainer : NavigationTextContainer\r\n    {\r\n        public EditLyricTextFlowContainer(EditLyricStepScreen screen)\r\n        {\r\n            AddLinkFactory(typing_mode, \"typing mode\", () => screen.SwitchToEditModeState(TextEditStep.Typing));\r\n            AddLinkFactory(split_mode, \"split mode\", () => screen.SwitchToEditModeState(TextEditStep.Split));\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Import/Lyrics/EditLyric/EditLyricStepScreen.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Import.Lyrics.EditLyric;\r\n\r\npublic partial class EditLyricStepScreen : LyricImporterStepScreenWithLyricEditor, IHasTopNavigation\r\n{\r\n    public override string Title => \"Edit lyric\";\r\n\r\n    public override IconUsage Icon => FontAwesome.Solid.Globe;\r\n\r\n    [Cached(typeof(ILyricsChangeHandler))]\r\n    private readonly LyricsChangeHandler lyricsChangeHandler;\r\n\r\n    [Cached(typeof(ILyricTextChangeHandler))]\r\n    private readonly LyricTextChangeHandler lyricTextChangeHandler;\r\n\r\n    public EditLyricStepScreen()\r\n    {\r\n        AddInternal(lyricsChangeHandler = new LyricsChangeHandler());\r\n        AddInternal(lyricTextChangeHandler = new LyricTextChangeHandler());\r\n    }\r\n\r\n    public TopNavigation CreateNavigation()\r\n        => new EditLyricNavigation(this);\r\n\r\n    protected override Drawable CreateContent()\r\n        => base.CreateContent().With(_ =>\r\n        {\r\n            // todo : will cause text update because has ScheduleAfterChildren in lyric editor.\r\n            SwitchLyricEditorMode(LyricEditorMode.EditText);\r\n        });\r\n\r\n    public override void Complete()\r\n    {\r\n        ScreenStack.Push(LyricImporterStep.AssignLanguage);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Import/Lyrics/GenerateRuby/GenerateRubyNavigation.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Import.Lyrics.GenerateRuby;\r\n\r\npublic partial class GenerateRubyNavigation : TopNavigation<GenerateRubyStepScreen>\r\n{\r\n    private const string auto_generate_ruby = \"AUTO_GENERATE_RUBY\";\r\n\r\n    public GenerateRubyNavigation(GenerateRubyStepScreen screen)\r\n        : base(screen)\r\n    {\r\n    }\r\n\r\n    protected override NavigationTextContainer CreateTextContainer()\r\n        => new GenerateRubyTextFlowContainer(Screen);\r\n\r\n    protected override NavigationState GetState(Lyric[] lyrics)\r\n    {\r\n        // technically, all non-english lyric should have romanisation.\r\n        if (lyrics.All(hasRomanisation))\r\n            return NavigationState.Done;\r\n\r\n        // not all (japanese) lyric contains ruby, so it's ok with that.\r\n        if (lyrics.Any(hasRuby) || lyrics.Any(hasRomanisation))\r\n            return NavigationState.Working;\r\n\r\n        return NavigationState.Initial;\r\n\r\n        static bool hasRuby(Lyric lyric)\r\n            => lyric.RubyTags.Any();\r\n\r\n        static bool hasRomanisation(Lyric lyric)\r\n            => lyric.TimeTags.Any(x => !string.IsNullOrEmpty(x.RomanisedSyllable));\r\n    }\r\n\r\n    protected override LocalisableString GetNavigationText(NavigationState value) =>\r\n        value switch\r\n        {\r\n            NavigationState.Initial => $\"Lazy to typing ruby? Press [{auto_generate_ruby}].\",\r\n            NavigationState.Working => \"Go to next step to generate time-tag.\",\r\n            NavigationState.Done => \"Go to next step to generate time-tag.\",\r\n            NavigationState.Error => \"Oops, seems cause some error in here.\",\r\n            _ => throw new ArgumentOutOfRangeException(nameof(value)),\r\n        };\r\n\r\n    protected override bool AbleToNextStep(NavigationState value)\r\n        => value is NavigationState.Initial or NavigationState.Working or NavigationState.Done;\r\n\r\n    private partial class GenerateRubyTextFlowContainer : NavigationTextContainer\r\n    {\r\n        public GenerateRubyTextFlowContainer(GenerateRubyStepScreen screen)\r\n        {\r\n            AddLinkFactory(auto_generate_ruby, \"auto generate ruby\", screen.AskForAutoGenerateRuby);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Import/Lyrics/GenerateRuby/GenerateRubyStepScreen.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Import.Lyrics.GenerateRuby;\r\n\r\npublic partial class GenerateRubyStepScreen : LyricImporterStepScreenWithLyricEditor, IHasTopNavigation\r\n{\r\n    public override string Title => \"Generate ruby\";\r\n\r\n    public override IconUsage Icon => FontAwesome.Solid.Gem;\r\n\r\n    [Cached(typeof(ILyricPropertyAutoGenerateChangeHandler))]\r\n    private readonly LyricPropertyAutoGenerateChangeHandler lyricPropertyAutoGenerateChangeHandler;\r\n\r\n    public GenerateRubyStepScreen()\r\n    {\r\n        AddInternal(lyricPropertyAutoGenerateChangeHandler = new LyricPropertyAutoGenerateChangeHandler());\r\n    }\r\n\r\n    public TopNavigation CreateNavigation()\r\n        => new GenerateRubyNavigation(this);\r\n\r\n    protected override Drawable CreateContent()\r\n        => base.CreateContent().With(_ =>\r\n        {\r\n            SwitchLyricEditorMode(LyricEditorMode.EditRomanisation);\r\n        });\r\n\r\n    protected override void LoadComplete()\r\n    {\r\n        base.LoadComplete();\r\n\r\n        // Asking auto-generate ruby.\r\n        if (lyricPropertyAutoGenerateChangeHandler.CanGenerate(AutoGenerateType.AutoGenerateRubyTags))\r\n            AskForAutoGenerateRuby();\r\n    }\r\n\r\n    public override void Complete()\r\n    {\r\n        ScreenStack.Push(LyricImporterStep.GenerateTimeTag);\r\n    }\r\n\r\n    internal void AskForAutoGenerateRuby()\r\n    {\r\n        SwitchLyricEditorMode(LyricEditorMode.EditRuby);\r\n\r\n        DialogOverlay.Push(new UseAutoGenerateRubyPopupDialog(ok =>\r\n        {\r\n            if (!ok)\r\n                return;\r\n\r\n            PrepareAutoGenerate();\r\n        }));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Import/Lyrics/GenerateRuby/UseAutoGenerateRubyPopupDialog.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Overlays.Dialog;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Import.Lyrics.GenerateRuby;\r\n\r\npublic partial class UseAutoGenerateRubyPopupDialog : PopupDialog\r\n{\r\n    public UseAutoGenerateRubyPopupDialog(Action<bool> okAction)\r\n    {\r\n        Icon = FontAwesome.Solid.Globe;\r\n        HeaderText = \"Auto generate ruby\";\r\n        BodyText = \"Would you like to use ruby generator to auto generate each lyric's ruby?\";\r\n        Buttons = new PopupDialogButton[]\r\n        {\r\n            new PopupDialogOkButton\r\n            {\r\n                Text = \"OK\",\r\n                Action = () => okAction(true),\r\n            },\r\n            new PopupDialogCancelButton\r\n            {\r\n                Text = \"Cancel\",\r\n                Action = () => okAction(false),\r\n            },\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Import/Lyrics/GenerateTimeTag/AlreadyContainTimeTagPopupDialog.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Overlays.Dialog;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Import.Lyrics.GenerateTimeTag;\r\n\r\npublic partial class AlreadyContainTimeTagPopupDialog : PopupDialog\r\n{\r\n    public AlreadyContainTimeTagPopupDialog(Action<bool> okAction)\r\n    {\r\n        Icon = FontAwesome.Solid.Globe;\r\n        HeaderText = \"Already contains time-tag.\";\r\n        BodyText = \"Seems this karaoke file already contains valid time-tag.\";\r\n        Buttons = new PopupDialogButton[]\r\n        {\r\n            new PopupDialogOkButton\r\n            {\r\n                Text = \"OK\",\r\n                Action = () => okAction(true),\r\n            },\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Import/Lyrics/GenerateTimeTag/GenerateTimeTagNavigation.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Import.Lyrics.GenerateTimeTag;\r\n\r\npublic partial class GenerateTimeTagNavigation : TopNavigation<GenerateTimeTagStepScreen>\r\n{\r\n    private const string auto_generate_time_tag = \"AUTO_GENERATE_TIME_TAG\";\r\n\r\n    public GenerateTimeTagNavigation(GenerateTimeTagStepScreen screen)\r\n        : base(screen)\r\n    {\r\n    }\r\n\r\n    protected override NavigationTextContainer CreateTextContainer()\r\n        => new GenerateTimeTagTextFlowContainer(Screen);\r\n\r\n    protected override NavigationState GetState(Lyric[] lyrics)\r\n    {\r\n        if (lyrics.All(hasTimeTag))\r\n            return NavigationState.Done;\r\n\r\n        if (lyrics.Any(hasTimeTag))\r\n            return NavigationState.Working;\r\n\r\n        return NavigationState.Initial;\r\n\r\n        static bool hasTimeTag(Lyric lyric)\r\n            => lyric.TimeTags.Any();\r\n    }\r\n\r\n    protected override LocalisableString GetNavigationText(NavigationState value) =>\r\n        value switch\r\n        {\r\n            NavigationState.Initial => $\"Press [{auto_generate_time_tag}] to auto-generate time tag. It's very easy.\",\r\n            NavigationState.Working => $\"Cool, you can reset your time-tag by pressing [{auto_generate_time_tag}]\",\r\n            NavigationState.Done => $\"Cool, you can reset your time-tag by pressing [{auto_generate_time_tag}]\",\r\n            NavigationState.Error => \"Oops, seems cause some error in here.\",\r\n            _ => throw new ArgumentOutOfRangeException(nameof(value)),\r\n        };\r\n\r\n    protected override bool AbleToNextStep(NavigationState value)\r\n        => value is NavigationState.Working or NavigationState.Done;\r\n\r\n    private partial class GenerateTimeTagTextFlowContainer : NavigationTextContainer\r\n    {\r\n        public GenerateTimeTagTextFlowContainer(GenerateTimeTagStepScreen screen)\r\n        {\r\n            AddLinkFactory(auto_generate_time_tag, \"auto generate time tag\", screen.AskForAutoGenerateTimeTag);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Import/Lyrics/GenerateTimeTag/GenerateTimeTagStepScreen.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Import.Lyrics.GenerateTimeTag;\r\n\r\npublic partial class GenerateTimeTagStepScreen : LyricImporterStepScreenWithLyricEditor, IHasTopNavigation\r\n{\r\n    public override string Title => \"Generate time tag\";\r\n\r\n    public override IconUsage Icon => FontAwesome.Solid.Tag;\r\n\r\n    [Cached(typeof(ILyricPropertyAutoGenerateChangeHandler))]\r\n    private readonly LyricPropertyAutoGenerateChangeHandler lyricPropertyAutoGenerateChangeHandler;\r\n\r\n    [Cached(typeof(ILyricTimeTagsChangeHandler))]\r\n    private readonly LyricTimeTagsChangeHandler lyricTimeTagsChangeHandler;\r\n\r\n    public GenerateTimeTagStepScreen()\r\n    {\r\n        AddInternal(lyricPropertyAutoGenerateChangeHandler = new LyricPropertyAutoGenerateChangeHandler());\r\n        AddInternal(lyricTimeTagsChangeHandler = new LyricTimeTagsChangeHandler());\r\n    }\r\n\r\n    public TopNavigation CreateNavigation()\r\n        => new GenerateTimeTagNavigation(this);\r\n\r\n    protected override Drawable CreateContent()\r\n        => base.CreateContent().With(_ =>\r\n        {\r\n            SwitchLyricEditorMode(LyricEditorMode.EditTimeTag);\r\n        });\r\n\r\n    protected override void LoadComplete()\r\n    {\r\n        base.LoadComplete();\r\n\r\n        // todo: we should better way to switch between time-tag mode or romanisation mode.\r\n        // or even create new step for it.\r\n        if (lyricPropertyAutoGenerateChangeHandler.CanGenerate(AutoGenerateType.AutoGenerateRomanisation))\r\n        {\r\n            AskForAutoGenerateRomanisation();\r\n        }\r\n        else\r\n        {\r\n            AskForAutoGenerateTimeTag();\r\n        }\r\n    }\r\n\r\n    public override void Complete()\r\n    {\r\n        ScreenStack.Push(LyricImporterStep.Success);\r\n    }\r\n\r\n    internal void AskForAutoGenerateTimeTag()\r\n    {\r\n        var lyrics = Beatmap.Value.Beatmap.HitObjects.OfType<Lyric>();\r\n\r\n        if (LyricsUtils.HasTimedTimeTags(lyrics))\r\n        {\r\n            // do not touch user's lyric if already contains valid time-tag with time.\r\n            DialogOverlay.Push(new AlreadyContainTimeTagPopupDialog(ok =>\r\n            {\r\n                // do nothing if already contains valid tags.\r\n            }));\r\n        }\r\n        else\r\n        {\r\n            DialogOverlay.Push(new UseAutoGenerateTimeTagPopupDialog(ok =>\r\n            {\r\n                if (!ok)\r\n                    return;\r\n\r\n                PrepareAutoGenerate();\r\n            }));\r\n        }\r\n    }\r\n\r\n    internal void AskForAutoGenerateRomanisation()\r\n    {\r\n        SwitchLyricEditorMode(LyricEditorMode.EditRomanisation);\r\n\r\n        DialogOverlay.Push(new UseAutoGenerateRomanisationPopupDialog(ok =>\r\n        {\r\n            if (!ok)\r\n                return;\r\n\r\n            PrepareAutoGenerate();\r\n        }));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Import/Lyrics/GenerateTimeTag/UseAutoGenerateRomanisationPopupDialog.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Overlays.Dialog;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Import.Lyrics.GenerateTimeTag;\r\n\r\npublic partial class UseAutoGenerateRomanisationPopupDialog : PopupDialog\r\n{\r\n    public UseAutoGenerateRomanisationPopupDialog(Action<bool> okAction)\r\n    {\r\n        Icon = FontAwesome.Solid.Globe;\r\n        HeaderText = \"Auto generate romanisation\";\r\n        BodyText = \"Would you like to use generator to auto generate each lyric's romanisation?\";\r\n        Buttons = new PopupDialogButton[]\r\n        {\r\n            new PopupDialogOkButton\r\n            {\r\n                Text = \"OK\",\r\n                Action = () => okAction(true),\r\n            },\r\n            new PopupDialogCancelButton\r\n            {\r\n                Text = \"Cancel\",\r\n                Action = () => okAction(false),\r\n            },\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Import/Lyrics/GenerateTimeTag/UseAutoGenerateTimeTagPopupDialog.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Overlays.Dialog;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Import.Lyrics.GenerateTimeTag;\r\n\r\npublic partial class UseAutoGenerateTimeTagPopupDialog : PopupDialog\r\n{\r\n    public UseAutoGenerateTimeTagPopupDialog(Action<bool> okAction)\r\n    {\r\n        Icon = FontAwesome.Solid.Globe;\r\n        HeaderText = \"Auto generate time tag\";\r\n        BodyText = \"Would you like to use time-tag to auto generate each lyric's time tag?\";\r\n        Buttons = new PopupDialogButton[]\r\n        {\r\n            new PopupDialogOkButton\r\n            {\r\n                Text = \"OK\",\r\n                Action = () => okAction(true),\r\n            },\r\n            new PopupDialogCancelButton\r\n            {\r\n                Text = \"Cancel\",\r\n                Action = () => okAction(false),\r\n            },\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Import/Lyrics/IHasTopNavigation.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Import.Lyrics;\r\n\r\npublic interface IHasTopNavigation\r\n{\r\n    public TopNavigation CreateNavigation();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Import/Lyrics/IImportStateResolver.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Import.Lyrics;\r\n\r\npublic interface IImportStateResolver\r\n{\r\n    void Cancel();\r\n\r\n    void Finish();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Import/Lyrics/ILyricImporterStepScreen.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Screens;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Import.Lyrics;\r\n\r\npublic interface ILyricImporterStepScreen : IScreen\r\n{\r\n    string Title { get; }\r\n\r\n    IconUsage Icon { get; }\r\n\r\n    void ConfirmRollBackFromStep(ILyricImporterStepScreen fromScreen, Action<bool> callBack);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Import/Lyrics/ImportLyricHeader.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Extensions.IEnumerableExtensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Framework.Screens;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Overlays;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Import.Lyrics;\r\n\r\npublic partial class ImportLyricHeader : TabControlOverlayHeader<ILyricImporterStepScreen>\r\n{\r\n    protected override OverlayTitle CreateTitle() => new ImportLyricHeaderTitle();\r\n\r\n    protected override OsuTabControl<ILyricImporterStepScreen> CreateTabControl() => new ImportStepTabControl();\r\n\r\n    protected override Drawable CreateContent() => new ImportLyricHeaderContent();\r\n\r\n    private LyricImporterSubScreenStack screenStack = null!;\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(LyricImporterSubScreenStack screenStack)\r\n    {\r\n        this.screenStack = screenStack;\r\n\r\n        screenStack.ScreenPushed += onPushed;\r\n        screenStack.ScreenExited += onExited;\r\n\r\n        Current.ValueChanged += e =>\r\n        {\r\n            var newScreen = e.NewValue;\r\n            if (newScreen == null)\r\n                throw new InvalidOperationException();\r\n\r\n            onClickTabItem(newScreen);\r\n        };\r\n    }\r\n\r\n    private void onPushed(IScreen _, IScreen newScreen)\r\n    {\r\n        if (newScreen is not ILyricImporterStepScreen lyricSubScreen)\r\n            throw new NotImportStepScreenException();\r\n\r\n        TabControl.AddItem(lyricSubScreen);\r\n        Current.Value = lyricSubScreen;\r\n    }\r\n\r\n    private void onExited(IScreen _, IScreen newScreen)\r\n    {\r\n        if (newScreen is not ILyricImporterStepScreen lyricSubScreen)\r\n            throw new NotImportStepScreenException();\r\n\r\n        TabControl.Items.ToList().SkipWhile(s => s != lyricSubScreen).Skip(1).ForEach(TabControl.RemoveItem);\r\n        Current.Value = lyricSubScreen;\r\n    }\r\n\r\n    private void onClickTabItem(ILyricImporterStepScreen screen)\r\n    {\r\n        screenStack.Pop(screen);\r\n    }\r\n\r\n    private partial class ImportLyricHeaderTitle : OverlayTitle\r\n    {\r\n        public ImportLyricHeaderTitle()\r\n        {\r\n            Title = \"Import lyric\";\r\n            Description = \"Import the lyric from the file.\";\r\n            Icon = OsuIcon.News;\r\n        }\r\n    }\r\n\r\n    public partial class ImportStepTabControl : BreadcrumbControl<ILyricImporterStepScreen>\r\n    {\r\n        public ImportStepTabControl()\r\n        {\r\n            RelativeSizeAxes = Axes.X;\r\n            Height = 47;\r\n        }\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(OverlayColourProvider colourProvider)\r\n        {\r\n            AccentColour = colourProvider.Light2;\r\n        }\r\n\r\n        protected override TabItem<ILyricImporterStepScreen> CreateTabItem(ILyricImporterStepScreen value) => new ControlTabItem(value)\r\n        {\r\n            AccentColour = AccentColour,\r\n        };\r\n\r\n        private partial class ControlTabItem : BreadcrumbTabItem\r\n        {\r\n            protected override float ChevronSize => 8;\r\n\r\n            public ControlTabItem(ILyricImporterStepScreen value)\r\n                : base(value)\r\n            {\r\n                RelativeSizeAxes = Axes.Y;\r\n                Text.Font = Text.Font.With(size: 14);\r\n                Text.Text = value.Title;\r\n                Text.Anchor = Anchor.CentreLeft;\r\n                Text.Origin = Anchor.CentreLeft;\r\n                Chevron.Y = 1;\r\n                Bar.Height = 0;\r\n            }\r\n\r\n            // base OsuTabItem makes font bold on activation, we don't want that here\r\n            protected override void OnActivated() => FadeHovered();\r\n\r\n            protected override void OnDeactivated() => FadeUnhovered();\r\n        }\r\n    }\r\n\r\n    public partial class ImportLyricHeaderContent : CompositeDrawable\r\n    {\r\n        public ImportLyricHeaderContent()\r\n        {\r\n            Height = 32;\r\n            RelativeSizeAxes = Axes.X;\r\n        }\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(LyricImporterSubScreenStack screenStack)\r\n        {\r\n            screenStack.ScreenPushed += onScreenChanged;\r\n            screenStack.ScreenExited += onScreenChanged;\r\n        }\r\n\r\n        private void onScreenChanged(IScreen _, IScreen newScreen)\r\n        {\r\n            ClearInternal();\r\n\r\n            if (newScreen is not IHasTopNavigation screenWithNavigation)\r\n                return;\r\n\r\n            Schedule(() =>\r\n            {\r\n                // Should wait until DI loaded inside.\r\n                AddInternal(screenWithNavigation.CreateNavigation());\r\n            });\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Import/Lyrics/ImportLyricManager.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.IO;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.IO;\r\nusing osu.Game.Rulesets.Karaoke.Integration.Formats;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Screens.Edit;\r\nusing FileInfo = System.IO.FileInfo;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Import.Lyrics;\r\n\r\npublic partial class ImportLyricManager : Component\r\n{\r\n    public static string[] LyricFormatExtensions { get; } = { \".lrc\", \".kar\", \".txt\" };\r\n\r\n    private const string backup_file_name = \"backup\";\r\n\r\n    [Resolved]\r\n    private EditorBeatmap editorBeatmap { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;\r\n\r\n    public void ImportFile(FileInfo info)\r\n    {\r\n        if (!info.Exists)\r\n            throw new FileNotFoundException(\"Lyric file does not found!\");\r\n\r\n        bool isFormatMatch = LyricFormatExtensions.Contains(info.Extension);\r\n        if (!isFormatMatch)\r\n            throw new FileLoadException(\"Only .lrc or .kar karaoke file is supported now\");\r\n\r\n        var set = beatmap.Value.BeatmapSetInfo;\r\n        var oldFile = set.Files.FirstOrDefault(f => f.Filename == backup_file_name);\r\n\r\n        using var stream = info.OpenRead();\r\n\r\n        // todo : make a backup if has new lyric file.\r\n        /*\r\n        if (oldFile != null)\r\n            beatmaps.ReplaceFile(set, oldFile, stream, backup_file_name);\r\n        else\r\n            beatmaps.AddFile(set, stream, backup_file_name);\r\n        */\r\n\r\n        // Import and replace all the file.\r\n        using var reader = new LineBufferedReader(stream);\r\n        string content = reader.ReadToEnd();\r\n        var lyrics = decodeLyrics(content, info.Extension);\r\n\r\n        // remove all hit objects (note and lyric) from beatmap\r\n        editorBeatmap.Clear();\r\n\r\n        // then re-add the lyric.\r\n        editorBeatmap.AddRange(lyrics);\r\n    }\r\n\r\n    private static Lyric[] decodeLyrics(string content, string extension)\r\n    {\r\n        IDecoder<Lyric[]> decoder = extension switch\r\n        {\r\n            \".lrc\" => new LrcDecoder(),\r\n            \".kar\" => new KarDecoder(),\r\n            \".txt\" => new LyricTextDecoder(),\r\n            _ => throw new NotSupportedException(\"Unsupported lyric file format\"),\r\n        };\r\n\r\n        return decoder.Decode(content);\r\n    }\r\n\r\n    public void AbortImport()\r\n    {\r\n        editorBeatmap.Clear();\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Import/Lyrics/ImportLyricOverlay.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Cursor;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Input.Bindings;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Import.Lyrics;\r\n\r\n[Cached(typeof(IImportStateResolver))]\r\npublic partial class ImportLyricOverlay : FullscreenOverlay<ImportLyricHeader>, IImportStateResolver\r\n{\r\n    public Action<IBeatmap>? OnImportFinished;\r\n\r\n    public Action? OverlayClosed;\r\n\r\n    [Cached]\r\n    protected LyricImporterSubScreenStack ScreenStack { get; private set; }\r\n\r\n    private readonly BindableBeatDivisor beatDivisor = new();\r\n\r\n    private EditorBeatmap editorBeatmap = null!;\r\n\r\n    private ImportLyricManager importManager = null!;\r\n\r\n    private LyricsProvider lyricsProvider = null!;\r\n\r\n    private DependencyContainer dependencies = null!;\r\n\r\n    protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)\r\n        => dependencies = new DependencyContainer(base.CreateChildDependencies(parent));\r\n\r\n    public bool IsFirstStep() => ScreenStack.IsFirstStep();\r\n\r\n    protected override bool DimMainContent => false;\r\n\r\n    public ImportLyricOverlay()\r\n        : base(OverlayColourScheme.Pink)\r\n    {\r\n        Width = 1;\r\n\r\n        Child = new GridContainer\r\n        {\r\n            RelativeSizeAxes = Axes.Both,\r\n            RowDimensions = new[]\r\n            {\r\n                new Dimension(GridSizeMode.AutoSize),\r\n                new Dimension(),\r\n            },\r\n            Content = new[]\r\n            {\r\n                new Drawable[]\r\n                {\r\n                    Header,\r\n                },\r\n                new Drawable[]\r\n                {\r\n                    new PopoverContainer\r\n                    {\r\n                        RelativeSizeAxes = Axes.Both,\r\n                        Child = ScreenStack = new LyricImporterSubScreenStack\r\n                        {\r\n                            RelativeSizeAxes = Axes.Both,\r\n                        },\r\n                    },\r\n                },\r\n            },\r\n        };\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        // todo: remove caching of this and consume via editorBeatmap?\r\n        // follow how editor.cs do.\r\n        dependencies.Cache(beatDivisor);\r\n\r\n        // inject local editor beatmap handler because should not affect global beatmap data.\r\n        var playableBeatmap = new KaraokeBeatmap\r\n        {\r\n            BeatmapInfo =\r\n            {\r\n                Ruleset = new KaraokeRuleset().RulesetInfo,\r\n            },\r\n        };\r\n        AddInternal(editorBeatmap = new EditorBeatmap(playableBeatmap));\r\n        dependencies.CacheAs(editorBeatmap);\r\n\r\n        AddInternal(importManager = new ImportLyricManager());\r\n        dependencies.Cache(importManager);\r\n\r\n        AddInternal(lyricsProvider = new LyricsProvider());\r\n        dependencies.CacheAs<ILyricsProvider>(lyricsProvider);\r\n\r\n        dependencies.Cache(new KaraokeRulesetEditGeneratorConfigManager());\r\n\r\n        // Load the screen until everything ready.\r\n        ScreenStack.Push(LyricImporterStep.ImportLyric);\r\n    }\r\n\r\n    protected override ImportLyricHeader CreateHeader() => new();\r\n\r\n    public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)\r\n    {\r\n        if (e.Repeat)\r\n            return false;\r\n\r\n        switch (e.Action)\r\n        {\r\n            case GlobalAction.Back:\r\n                if (!IsFirstStep())\r\n                {\r\n                    // go to previous step.\r\n                    ScreenStack.Pop();\r\n                }\r\n                else\r\n                {\r\n                    Cancel();\r\n                }\r\n\r\n                return true;\r\n\r\n            default:\r\n                return base.OnPressed(e);\r\n        }\r\n    }\r\n\r\n    public void Cancel()\r\n    {\r\n        Hide();\r\n    }\r\n\r\n    public void Finish()\r\n    {\r\n        OnImportFinished?.Invoke(editorBeatmap);\r\n        Hide();\r\n    }\r\n\r\n    protected override void PopOutComplete()\r\n    {\r\n        if (LoadState < LoadState.Ready)\r\n            return;\r\n\r\n        OverlayClosed?.Invoke();\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Import/Lyrics/LyricImporter.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Screens;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Screens.Play;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Import.Lyrics;\r\n\r\npublic partial class LyricImporter : ScreenWithBeatmapBackground\r\n{\r\n    private readonly ImportLyricOverlay importLyricOverlay;\r\n\r\n    public override bool AllowUserExit => false;\r\n\r\n    public override bool HideOverlaysOnEnter => true;\r\n\r\n    public override bool DisallowExternalBeatmapRulesetChanges => true;\r\n\r\n    public override bool? ApplyModTrackAdjustments => false;\r\n\r\n    public Action<IBeatmap>? OnImportFinished;\r\n\r\n    public LyricImporter()\r\n    {\r\n        InternalChild = importLyricOverlay = new ImportLyricOverlay\r\n        {\r\n            OnImportFinished = b =>\r\n            {\r\n                OnImportFinished?.Invoke(b);\r\n            },\r\n            OverlayClosed = this.Exit,\r\n        };\r\n    }\r\n\r\n    protected override void LoadComplete()\r\n    {\r\n        base.LoadComplete();\r\n        importLyricOverlay.Show();\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Import/Lyrics/LyricImporterStep.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Import.Lyrics;\r\n\r\npublic enum LyricImporterStep\r\n{\r\n    ImportLyric,\r\n\r\n    EditLyric,\r\n\r\n    AssignLanguage,\r\n\r\n    GenerateRuby,\r\n\r\n    GenerateTimeTag,\r\n\r\n    Success,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Import/Lyrics/LyricImporterStepScreen.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Screens;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Screens;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Import.Lyrics;\r\n\r\npublic abstract partial class LyricImporterStepScreen : OsuScreen, ILyricImporterStepScreen\r\n{\r\n    public const float X_SHIFT = 200;\r\n    public const double X_MOVE_DURATION = 800;\r\n    public const double RESUME_TRANSITION_DELAY = DISAPPEAR_DURATION / 2;\r\n    public const double APPEAR_DURATION = 800;\r\n    public const double DISAPPEAR_DURATION = 500;\r\n\r\n    [Resolved]\r\n    protected LyricImporterSubScreenStack ScreenStack { get; private set; } = null!;\r\n\r\n    [Resolved]\r\n    protected IDialogOverlay DialogOverlay { get; private set; } = null!;\r\n\r\n    public abstract IconUsage Icon { get; }\r\n\r\n    protected LyricImporterStepScreen()\r\n    {\r\n        Anchor = Anchor.Centre;\r\n        Origin = Anchor.Centre;\r\n        RelativeSizeAxes = Axes.Both;\r\n\r\n        InternalChildren = new Drawable[]\r\n        {\r\n            new LyricImporterStepButton\r\n            {\r\n                Anchor = Anchor.Centre,\r\n                Origin = Anchor.Centre,\r\n                Width = 240,\r\n                Text = $\"{Title}, Click to next step.\",\r\n                Action = Complete,\r\n            },\r\n        };\r\n    }\r\n\r\n    public override void OnEntering(ScreenTransitionEvent e)\r\n    {\r\n        base.OnEntering(e);\r\n        this.FadeInFromZero(APPEAR_DURATION, Easing.OutQuint);\r\n    }\r\n\r\n    public override bool OnExiting(ScreenExitEvent e)\r\n    {\r\n        base.OnExiting(e);\r\n        this.FadeOut(DISAPPEAR_DURATION, Easing.OutQuint);\r\n        return false;\r\n    }\r\n\r\n    public override void OnResuming(ScreenTransitionEvent e)\r\n    {\r\n        base.OnResuming(e);\r\n        this.FadeIn(APPEAR_DURATION, Easing.OutQuint);\r\n    }\r\n\r\n    public override void OnSuspending(ScreenTransitionEvent e)\r\n    {\r\n        base.OnSuspending(e);\r\n        this.FadeOut(DISAPPEAR_DURATION, Easing.OutQuint);\r\n    }\r\n\r\n    public abstract void Complete();\r\n\r\n    public virtual void ConfirmRollBackFromStep(ILyricImporterStepScreen fromScreen, Action<bool> callBack)\r\n    {\r\n        DialogOverlay.Push(new RollBackPopupDialog(fromScreen, callBack));\r\n    }\r\n\r\n    public override string ToString() => Title;\r\n\r\n    private partial class LyricImporterStepButton : OsuButton;\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Import/Lyrics/LyricImporterStepScreenWithLyricEditor.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Import.Lyrics;\r\n\r\npublic abstract partial class LyricImporterStepScreenWithLyricEditor : LyricImporterStepScreen\r\n{\r\n    // it's a tricky way to let navigation bar able to get the lyric state.\r\n    // not a good solution, but have no better way now.\r\n    [Cached(typeof(ILyricEditorState))]\r\n    private ImportLyricEditor lyricEditor { get; set; } = null!;\r\n\r\n    [Cached(typeof(ILockChangeHandler))]\r\n    private readonly LockChangeHandler lockChangeHandler;\r\n\r\n    protected LyricImporterStepScreenWithLyricEditor()\r\n    {\r\n        InternalChildren = new[]\r\n        {\r\n            lockChangeHandler = new LockChangeHandler(),\r\n            CreateContent(),\r\n        };\r\n    }\r\n\r\n    protected virtual Drawable CreateContent()\r\n        => lyricEditor = new ImportLyricEditor\r\n        {\r\n            RelativeSizeAxes = Axes.Both,\r\n        };\r\n\r\n    public LyricEditorMode LyricEditorMode\r\n        => lyricEditor.Mode;\r\n\r\n    public T GetLyricEditorModeState<T>() where T : Enum\r\n        => lyricEditor.GetLyricEditorModeState<T>();\r\n\r\n    public virtual void SwitchLyricEditorMode(LyricEditorMode mode)\r\n        => lyricEditor.SwitchMode(mode);\r\n\r\n    public void SwitchToEditModeState<T>(T mode) where T : Enum\r\n        => lyricEditor.SwitchEditStep(mode);\r\n\r\n    protected void PrepareAutoGenerate()\r\n    {\r\n        lyricEditor.PrepareAutoGenerate();\r\n    }\r\n\r\n    private partial class ImportLyricEditor : LyricEditor\r\n    {\r\n        [Resolved]\r\n        private LyricImporterSubScreenStack screenStack { get; set; } = null!;\r\n\r\n        private ILyricSelectionState lyricSelectionState { get; set; } = null!;\r\n\r\n        public void PrepareAutoGenerate()\r\n        {\r\n            // then open the selecting mode and select all lyrics.\r\n            lyricSelectionState.StartSelecting();\r\n            lyricSelectionState.SelectAll();\r\n\r\n            // for some mode, we need to switch to generate section.\r\n            SwitchEditStep(LanguageEditStep.Generate);\r\n            SwitchEditStep(RubyTagEditStep.Generate);\r\n            SwitchEditStep(RomanisationTagEditStep.Generate);\r\n        }\r\n\r\n        protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)\r\n        {\r\n            var dependencies = base.CreateChildDependencies(parent);\r\n            lyricSelectionState = dependencies.Get<ILyricSelectionState>();\r\n            return dependencies;\r\n        }\r\n\r\n        public T GetLyricEditorModeState<T>() where T : Enum =>\r\n            BindableModeWithEditStep.Value.GetEditStep<T>();\r\n\r\n        public override void NavigateToFix(LyricEditorMode mode)\r\n        {\r\n            switch (mode)\r\n            {\r\n                case LyricEditorMode.EditText:\r\n                    screenStack.Pop(LyricImporterStep.EditLyric);\r\n                    break;\r\n\r\n                case LyricEditorMode.EditLanguage:\r\n                    screenStack.Pop(LyricImporterStep.AssignLanguage);\r\n                    break;\r\n\r\n                case LyricEditorMode.EditTimeTag:\r\n                    screenStack.Pop(LyricImporterStep.GenerateTimeTag);\r\n                    break;\r\n\r\n                default:\r\n                    throw new ArgumentOutOfRangeException(nameof(mode));\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Import/Lyrics/LyricImporterSubScreenStack.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Screens;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Import.Lyrics.AssignLanguage;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Import.Lyrics.DragFile;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Import.Lyrics.EditLyric;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Import.Lyrics.GenerateRuby;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Import.Lyrics.GenerateTimeTag;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Import.Lyrics.Success;\r\nusing osu.Game.Screens;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Import.Lyrics;\r\n\r\npublic partial class LyricImporterSubScreenStack : OsuScreenStack\r\n{\r\n    private readonly Stack<ILyricImporterStepScreen> stack = new();\r\n\r\n    public LyricImporterStep CurrentStep => getStepByScreen(stack.Peek());\r\n\r\n    public void Push(LyricImporterStep step)\r\n    {\r\n        Push(getScreenByStep(step));\r\n    }\r\n\r\n    public new void Push(IScreen screen)\r\n    {\r\n        if (screen is not ILyricImporterStepScreen newStepScreen)\r\n            throw new NotImportStepScreenException();\r\n\r\n        if (CurrentScreen is ILyricImporterStepScreen currentScreen)\r\n        {\r\n            var currentStep = getStepByScreen(currentScreen);\r\n            var newStep = getStepByScreen(newStepScreen);\r\n\r\n            checkStep(currentStep, newStep);\r\n        }\r\n\r\n        stack.Push(newStepScreen);\r\n        base.Push(screen);\r\n        return;\r\n\r\n        static void checkStep(LyricImporterStep currentStep, LyricImporterStep newStep)\r\n        {\r\n            if (newStep == currentStep)\r\n                throw new ScreenNotCurrentException(\"Cannot push same screen.\");\r\n\r\n            if (newStep <= currentStep)\r\n                throw new ScreenNotCurrentException(\"Cannot push previous then current screen.\");\r\n\r\n            if (currentStep == LyricImporterStep.AssignLanguage && newStep > LyricImporterStep.GenerateTimeTag)\r\n                throw new ScreenNotCurrentException(\"Only generate ruby step can be skipped.\");\r\n        }\r\n    }\r\n\r\n    public void Pop(LyricImporterStep step)\r\n    {\r\n        Pop(getScreenByStep(step));\r\n    }\r\n\r\n    public void Pop(IScreen screen)\r\n    {\r\n        if (screen is not ILyricImporterStepScreen newStepScreen)\r\n            throw new NotImportStepScreenException();\r\n\r\n        var targetScreen = stack.FirstOrDefault(x => x == newStepScreen);\r\n        if (targetScreen == null)\r\n            throw new ScreenNotCurrentException(\"Screen is not in the lyric import step.\");\r\n\r\n        targetScreen.MakeCurrent();\r\n\r\n        // pop to target stack.\r\n        while (stack.Peek() != targetScreen)\r\n        {\r\n            stack.Pop();\r\n        }\r\n    }\r\n\r\n    public void Pop()\r\n    {\r\n        stack.Pop();\r\n        stack.Peek().MakeCurrent();\r\n    }\r\n\r\n    public bool IsFirstStep()\r\n    {\r\n        return CurrentStep == LyricImporterStep.ImportLyric;\r\n    }\r\n\r\n    private static ILyricImporterStepScreen getScreenByStep(LyricImporterStep step) =>\r\n        step switch\r\n        {\r\n            LyricImporterStep.ImportLyric => new DragFileStepScreen(),\r\n            LyricImporterStep.EditLyric => new EditLyricStepScreen(),\r\n            LyricImporterStep.AssignLanguage => new AssignLanguageStepScreen(),\r\n            LyricImporterStep.GenerateRuby => new GenerateRubyStepScreen(),\r\n            LyricImporterStep.GenerateTimeTag => new GenerateTimeTagStepScreen(),\r\n            LyricImporterStep.Success => new SuccessStepScreen(),\r\n            _ => throw new ScreenNotCurrentException(\"Screen is not in the lyric import step.\"),\r\n        };\r\n\r\n    private static LyricImporterStep getStepByScreen(ILyricImporterStepScreen screen) =>\r\n        screen switch\r\n        {\r\n            DragFileStepScreen => LyricImporterStep.ImportLyric,\r\n            EditLyricStepScreen => LyricImporterStep.EditLyric,\r\n            AssignLanguageStepScreen => LyricImporterStep.AssignLanguage,\r\n            GenerateRubyStepScreen => LyricImporterStep.GenerateRuby,\r\n            GenerateTimeTagStepScreen => LyricImporterStep.GenerateTimeTag,\r\n            SuccessStepScreen => LyricImporterStep.Success,\r\n            _ => throw new ScreenNotCurrentException(\"Screen is not in the lyric import step.\"),\r\n        };\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Import/Lyrics/NotImportStepScreenException.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Import.Lyrics;\r\n\r\npublic class NotImportStepScreenException : InvalidOperationException\r\n{\r\n    public NotImportStepScreenException()\r\n        : base(\"Screen stack should only contains step screen\")\r\n    {\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Import/Lyrics/RollBackPopupDialog.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Game.Overlays.Dialog;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Import.Lyrics;\r\n\r\npublic partial class RollBackPopupDialog : PopupDialog\r\n{\r\n    public RollBackPopupDialog(ILyricImporterStepScreen screen, Action<bool> okAction)\r\n    {\r\n        Icon = screen.Icon;\r\n        HeaderText = \"Rollback?\";\r\n        BodyText = $\"Will roll-back to step '{screen.Title}'\";\r\n        Buttons = new PopupDialogButton[]\r\n        {\r\n            new PopupDialogOkButton\r\n            {\r\n                Text = \"OK\",\r\n                Action = () => okAction(true),\r\n            },\r\n            new PopupDialogCancelButton\r\n            {\r\n                Text = \"Cancel\",\r\n                Action = () => okAction(false),\r\n            },\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Import/Lyrics/RollBackResetPopupDialog.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Game.Overlays.Dialog;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Import.Lyrics;\r\n\r\npublic partial class RollBackResetPopupDialog : PopupDialog\r\n{\r\n    public RollBackResetPopupDialog(ILyricImporterStepScreen screen, Action<bool> okAction)\r\n    {\r\n        Icon = screen.Icon;\r\n        HeaderText = \"Really sure?\";\r\n        BodyText = $\"Are you really sure you want to roll-back to step '{screen.Title}'? You might lost every change you made.\";\r\n        Buttons = new PopupDialogButton[]\r\n        {\r\n            new PopupDialogDangerousButton\r\n            {\r\n                Text = \"Forget all changes\",\r\n                Action = () => okAction(true),\r\n            },\r\n            new PopupDialogCancelButton\r\n            {\r\n                Text = \"Let me think about it\",\r\n                Action = () => okAction(false),\r\n            },\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Import/Lyrics/Success/SuccessStepScreen.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics.Sprites;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Import.Lyrics.Success;\r\n\r\npublic partial class SuccessStepScreen : LyricImporterStepScreen\r\n{\r\n    public override string Title => \"Success\";\r\n\r\n    public override IconUsage Icon => FontAwesome.Regular.CheckCircle;\r\n\r\n    [Resolved]\r\n    private IImportStateResolver importStateResolver { get; set; } = null!;\r\n\r\n    public override void Complete()\r\n    {\r\n        importStateResolver.Finish();\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Import/Lyrics/TopNavigation.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Import.Lyrics;\r\n\r\npublic abstract partial class TopNavigation<T> : TopNavigation where T : LyricImporterStepScreen, IHasTopNavigation\r\n{\r\n    protected new T Screen => (T)base.Screen;\r\n\r\n    protected TopNavigation(T screen)\r\n        : base(screen)\r\n    {\r\n    }\r\n}\r\n\r\npublic abstract partial class TopNavigation : CompositeDrawable\r\n{\r\n    [Resolved]\r\n    private OsuColour colours { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private EditorBeatmap editorBeatmap { get; set; } = null!;\r\n\r\n    protected LyricImporterStepScreen Screen { get; }\r\n\r\n    private readonly Box background;\r\n    private readonly NavigationTextContainer text;\r\n    private readonly IconButton button;\r\n\r\n    private NavigationState state;\r\n\r\n    protected TopNavigation(LyricImporterStepScreen screen)\r\n    {\r\n        Screen = screen;\r\n\r\n        RelativeSizeAxes = Axes.Both;\r\n        InternalChildren = new Drawable[]\r\n        {\r\n            background = new Box\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n            },\r\n            text = CreateTextContainer().With(t =>\r\n            {\r\n                t.Anchor = Anchor.CentreLeft;\r\n                t.Origin = Anchor.CentreLeft;\r\n                t.RelativeSizeAxes = Axes.X;\r\n                t.AutoSizeAxes = Axes.Y;\r\n                t.Margin = new MarginPadding { Left = WaveOverlayContainer.HORIZONTAL_PADDING };\r\n            }),\r\n            button = new IconButton\r\n            {\r\n                Anchor = Anchor.CentreRight,\r\n                Origin = Anchor.CentreRight,\r\n                Margin = new MarginPadding { Right = 5 },\r\n                Action = () =>\r\n                {\r\n                    if (AbleToNextStep(state))\r\n                    {\r\n                        CompleteClicked();\r\n                    }\r\n                },\r\n            },\r\n        };\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        // use transaction ended for some reason.\r\n        // 1. seems customized beatmap cannot get hit object updated event(not really sure why).\r\n        // 2. object updated event will trigger hit object updated event lots of time.\r\n        editorBeatmap.TransactionEnded += TriggerStateChange;\r\n\r\n        TriggerStateChange();\r\n    }\r\n\r\n    protected void TriggerStateChange()\r\n    {\r\n        // wait for a bit until lyric editor's all property loaded.\r\n        ScheduleAfterChildren(() =>\r\n        {\r\n            state = GetState(editorBeatmap.HitObjects.OfType<Lyric>().ToArray());\r\n            updateNavigationDisplayInfo(state);\r\n        });\r\n    }\r\n\r\n    private void updateNavigationDisplayInfo(NavigationState value)\r\n    {\r\n        switch (value)\r\n        {\r\n            case NavigationState.Initial:\r\n                background.Colour = colours.Gray2;\r\n                text.Colour = colours.GrayF;\r\n                button.Colour = colours.Gray6;\r\n                button.Icon = FontAwesome.Regular.QuestionCircle;\r\n                break;\r\n\r\n            case NavigationState.Working:\r\n                background.Colour = colours.Gray2;\r\n                text.Colour = colours.GrayF;\r\n                button.Colour = colours.Gray6;\r\n                button.Icon = FontAwesome.Solid.InfoCircle;\r\n                break;\r\n\r\n            case NavigationState.Done:\r\n                background.Colour = colours.Gray6;\r\n                text.Colour = colours.GrayF;\r\n                button.Colour = colours.Yellow;\r\n                button.Icon = FontAwesome.Regular.ArrowAltCircleRight;\r\n                break;\r\n\r\n            case NavigationState.Error:\r\n                background.Colour = colours.Gray2;\r\n                text.Colour = colours.GrayF;\r\n                button.Colour = colours.Yellow;\r\n                button.Icon = FontAwesome.Solid.ExclamationTriangle;\r\n                break;\r\n\r\n            default:\r\n                throw new ArgumentOutOfRangeException(nameof(value));\r\n        }\r\n\r\n        // Force change style if this step is able to go to next step.\r\n        if (AbleToNextStep(value))\r\n        {\r\n            button.Icon = FontAwesome.Regular.ArrowAltCircleRight;\r\n        }\r\n\r\n        text.Text = GetNavigationText(value);\r\n    }\r\n\r\n    protected abstract NavigationTextContainer CreateTextContainer();\r\n\r\n    protected abstract NavigationState GetState(Lyric[] lyrics);\r\n\r\n    protected abstract LocalisableString GetNavigationText(NavigationState value);\r\n\r\n    protected virtual bool AbleToNextStep(NavigationState value)\r\n        => value == NavigationState.Done;\r\n\r\n    protected virtual void CompleteClicked() => Screen.Complete();\r\n\r\n    protected override void Dispose(bool isDisposing)\r\n    {\r\n        base.Dispose(isDisposing);\r\n\r\n        editorBeatmap.TransactionEnded -= TriggerStateChange;\r\n    }\r\n\r\n    public partial class NavigationTextContainer : CustomizableTextContainer\r\n    {\r\n        private static readonly FontUsage default_font = new(size: 14);\r\n\r\n        protected override SpriteText CreateSpriteText() => base.CreateSpriteText().With(x => x.Font = default_font);\r\n\r\n        protected void AddLinkFactory(string name, string text, Action action)\r\n        {\r\n            AddIconFactory(name, () => new ClickableSpriteText\r\n            {\r\n                Font = default_font,\r\n                Text = text,\r\n                Action = action,\r\n            });\r\n        }\r\n\r\n        internal partial class ClickableSpriteText : OsuSpriteText\r\n        {\r\n            public Action? Action { get; set; }\r\n\r\n            protected override bool OnClick(ClickEvent e)\r\n            {\r\n                Action?.Invoke();\r\n                return base.OnClick(e);\r\n            }\r\n\r\n            [BackgroundDependencyLoader]\r\n            private void load(OsuColour colours)\r\n            {\r\n                Colour = colours.Yellow;\r\n            }\r\n        }\r\n    }\r\n\r\n    /// <summary>\r\n    /// Get the dependency from the screen instead of <see cref=\"ImportLyricHeader\"/>\r\n    /// </summary>\r\n    /// <typeparam name=\"TInject\"></typeparam>\r\n    /// <returns></returns>\r\n    protected TInject GetDependency<TInject>() where TInject : class\r\n        => Screen.Dependencies.Get<TInject>();\r\n}\r\n\r\npublic enum NavigationState\r\n{\r\n    Initial,\r\n\r\n    Working,\r\n\r\n    Done,\r\n\r\n    Error,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/IssueSection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Extensions.Color4Extensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Colour;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Edit;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit;\r\n\r\npublic abstract partial class IssueSection : EditorSection\r\n{\r\n    protected sealed override LocalisableString Title => \"Issues\";\r\n\r\n    protected readonly IBindableList<Issue> Issues = new BindableList<Issue>();\r\n\r\n    protected IssueSection()\r\n    {\r\n        EmptyIssue emptyIssue;\r\n\r\n        IssueNavigator issueNavigator;\r\n        IssueTable issueTable;\r\n\r\n        Children = new Drawable[]\r\n        {\r\n            emptyIssue = CreateEmptyIssue().With(x =>\r\n            {\r\n                x.Anchor = Anchor.Centre;\r\n                x.Origin = Anchor.Centre;\r\n                x.RelativeSizeAxes = Axes.X;\r\n                x.AutoSizeAxes = Axes.Y;\r\n                x.Padding = new MarginPadding(10);\r\n            }),\r\n\r\n            issueNavigator = CreateIssueNavigator(),\r\n            issueTable = CreateIssueTable(),\r\n        };\r\n\r\n        Issues.BindCollectionChanged((_, _) =>\r\n        {\r\n            bool hasIssue = Issues.Any();\r\n\r\n            emptyIssue.Alpha = hasIssue ? 0 : 1;\r\n\r\n            issueNavigator.Alpha = hasIssue ? 1 : 0;\r\n            issueNavigator.Issues = Issues;\r\n\r\n            issueTable.Alpha = hasIssue ? 1 : 0;\r\n            issueTable.Issues = Issues.Take(100);\r\n        }, true);\r\n    }\r\n\r\n    protected abstract EmptyIssue CreateEmptyIssue();\r\n\r\n    protected abstract IssueNavigator CreateIssueNavigator();\r\n\r\n    protected abstract IssueTable CreateIssueTable();\r\n\r\n    protected abstract partial class EmptyIssue : ClickableContainer\r\n    {\r\n        private readonly SpriteIcon icon;\r\n\r\n        protected readonly Box Background;\r\n        protected readonly OsuSpriteText Text;\r\n\r\n        protected EmptyIssue()\r\n        {\r\n            Action = OnRefreshButtonClicked;\r\n\r\n            InternalChild = new Container\r\n            {\r\n                CornerRadius = 20,\r\n                Masking = true,\r\n                Anchor = Anchor.Centre,\r\n                Origin = Anchor.Centre,\r\n                RelativeSizeAxes = Axes.X,\r\n                AutoSizeAxes = Axes.Y,\r\n                Children = new Drawable[]\r\n                {\r\n                    Background = new Box\r\n                    {\r\n                        RelativeSizeAxes = Axes.Both,\r\n                        Alpha = 0.8f,\r\n                    },\r\n                    new FillFlowContainer\r\n                    {\r\n                        RelativeSizeAxes = Axes.X,\r\n                        AutoSizeAxes = Axes.Y,\r\n                        Anchor = Anchor.Centre,\r\n                        Origin = Anchor.Centre,\r\n                        Padding = new MarginPadding(20),\r\n                        Direction = FillDirection.Vertical,\r\n                        Children = new Drawable[]\r\n                        {\r\n                            icon = new SpriteIcon\r\n                            {\r\n                                Icon = FontAwesome.Solid.CheckCircle,\r\n                                Anchor = Anchor.TopCentre,\r\n                                Origin = Anchor.TopCentre,\r\n                                Size = new Vector2(50),\r\n                            },\r\n                            Text = new OsuSpriteText\r\n                            {\r\n                                Anchor = Anchor.TopCentre,\r\n                                Origin = Anchor.TopCentre,\r\n                                Text = \"No issue here!\",\r\n                                Font = OsuFont.GetFont(size: 28),\r\n                            },\r\n                            new OsuSpriteText\r\n                            {\r\n                                Anchor = Anchor.TopCentre,\r\n                                Origin = Anchor.TopCentre,\r\n                                Text = \"Click this area to re-check again.\",\r\n                                Font = OsuFont.GetFont(size: 14),\r\n                            },\r\n                        },\r\n                    },\r\n                },\r\n            };\r\n\r\n            AddInternal(new HoverClickSounds(HoverSampleSet.Button));\r\n        }\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(OverlayColourProvider colourProvider, OsuColour colours)\r\n        {\r\n            Background.Colour = colourProvider.Background5;\r\n            icon.Colour = colours.Green;\r\n            Text.Colour = colourProvider.Colour1;\r\n        }\r\n\r\n        protected abstract void OnRefreshButtonClicked();\r\n\r\n        protected override bool OnMouseDown(MouseDownEvent e)\r\n        {\r\n            Content.ScaleTo(0.9f, 4000, Easing.OutQuint);\r\n            return base.OnMouseDown(e);\r\n        }\r\n\r\n        protected override void OnMouseUp(MouseUpEvent e)\r\n        {\r\n            Content.ScaleTo(1, 1000, Easing.OutElastic);\r\n            base.OnMouseUp(e);\r\n        }\r\n    }\r\n\r\n    protected abstract partial class IssueNavigator : CompositeDrawable\r\n    {\r\n        private readonly FillFlowContainer<IssueCategory> categoryList;\r\n        private readonly IconButton reloadButton;\r\n\r\n        protected readonly Box Background;\r\n        protected readonly Box BlockBox;\r\n\r\n        protected IssueNavigator()\r\n        {\r\n            RelativeSizeAxes = Axes.X;\r\n            AutoSizeAxes = Axes.Y;\r\n\r\n            InternalChild = new Container\r\n            {\r\n                CornerRadius = 5,\r\n                Masking = true,\r\n                RelativeSizeAxes = Axes.X,\r\n                AutoSizeAxes = Axes.Y,\r\n                Children = new Drawable[]\r\n                {\r\n                    Background = new Box\r\n                    {\r\n                        RelativeSizeAxes = Axes.Both,\r\n                        Alpha = 0.8f,\r\n                    },\r\n                    categoryList = new FillFlowContainer<IssueCategory>\r\n                    {\r\n                        RelativeSizeAxes = Axes.X,\r\n                        AutoSizeAxes = Axes.Y,\r\n                        Anchor = Anchor.Centre,\r\n                        Origin = Anchor.Centre,\r\n                        Padding = new MarginPadding(10),\r\n                        Direction = FillDirection.Horizontal,\r\n                        Spacing = new Vector2(5),\r\n                        Children = createCategory(),\r\n                    },\r\n                    BlockBox = new Box\r\n                    {\r\n                        RelativeSizeAxes = Axes.Both,\r\n                        RelativePositionAxes = Axes.Both,\r\n                        X = 0.5f,\r\n                        Size = new Vector2(0.5f, 1f),\r\n                    },\r\n                    reloadButton = new IconButton\r\n                    {\r\n                        Anchor = Anchor.CentreRight,\r\n                        Origin = Anchor.CentreRight,\r\n                        X = -5,\r\n                        Icon = FontAwesome.Solid.Redo,\r\n                    },\r\n                },\r\n            };\r\n        }\r\n\r\n        private IssueCategory[] createCategory()\r\n            => Enum.GetValues<IssueType>().Select(type => new IssueCategory\r\n            {\r\n                Type = type,\r\n                Text = getTextByIssueType(type),\r\n                IssueColour = getColourByIssueType(type),\r\n            }).ToArray();\r\n\r\n        private LocalisableString getTextByIssueType(IssueType issueType) =>\r\n            issueType switch\r\n            {\r\n                IssueType.Problem => \"Problem\",\r\n                IssueType.Warning => \"Warning\",\r\n                IssueType.Error => \"Internal error\",\r\n                IssueType.Negligible => \"Suggestion\",\r\n                _ => throw new ArgumentOutOfRangeException(nameof(issueType), issueType, null),\r\n            };\r\n\r\n        private Colour4 getColourByIssueType(IssueType issueType) =>\r\n            new IssueTemplate(new EmptyCheck(), issueType, string.Empty).Colour;\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(OverlayColourProvider colourProvider)\r\n        {\r\n            var colour = colourProvider.Background5;\r\n            Background.Colour = colour;\r\n            BlockBox.Colour = ColourInfo.GradientHorizontal(colour.Opacity(0), colour);\r\n            reloadButton.Action = OnRefreshButtonClicked;\r\n        }\r\n\r\n        protected abstract void OnRefreshButtonClicked();\r\n\r\n        public IReadOnlyList<Issue> Issues\r\n        {\r\n            set\r\n            {\r\n                foreach (var category in categoryList.Children)\r\n                {\r\n                    int count = value.Count(x => x.Template.Type == category.Type);\r\n\r\n                    category.Alpha = count == 0 ? 0 : 1;\r\n                    category.Count = count;\r\n                }\r\n            }\r\n        }\r\n\r\n        private partial class IssueCategory : CompositeDrawable\r\n        {\r\n            private const int text_size = 14;\r\n\r\n            private readonly Box background;\r\n            private readonly OsuSpriteText issueName;\r\n            private readonly OsuSpriteText countSpriteText;\r\n\r\n            public IssueCategory()\r\n            {\r\n                AutoSizeAxes = Axes.X;\r\n                Height = 20;\r\n\r\n                InternalChild = new Container\r\n                {\r\n                    CornerRadius = 5,\r\n                    Masking = true,\r\n                    RelativeSizeAxes = Axes.Y,\r\n                    AutoSizeAxes = Axes.X,\r\n                    Children = new Drawable[]\r\n                    {\r\n                        background = new Box\r\n                        {\r\n                            RelativeSizeAxes = Axes.Both,\r\n                        },\r\n                        new FillFlowContainer\r\n                        {\r\n                            RelativeSizeAxes = Axes.Y,\r\n                            AutoSizeAxes = Axes.X,\r\n                            Direction = FillDirection.Horizontal,\r\n                            Spacing = new Vector2(5),\r\n                            Padding = new MarginPadding\r\n                            {\r\n                                Horizontal = 5,\r\n                            },\r\n                            Children = new[]\r\n                            {\r\n                                issueName = new OsuSpriteText\r\n                                {\r\n                                    Anchor = Anchor.CentreLeft,\r\n                                    Origin = Anchor.CentreLeft,\r\n                                    Font = OsuFont.GetFont(size: text_size, weight: FontWeight.Bold),\r\n                                },\r\n                                countSpriteText = new OsuSpriteText\r\n                                {\r\n                                    Anchor = Anchor.CentreLeft,\r\n                                    Origin = Anchor.CentreLeft,\r\n                                    Font = OsuFont.GetFont(size: text_size, weight: FontWeight.Bold),\r\n                                },\r\n                            },\r\n                        },\r\n                    },\r\n                };\r\n            }\r\n\r\n            public IssueType Type { get; init; }\r\n\r\n            public LocalisableString Text\r\n            {\r\n                get => issueName.Text;\r\n                set => issueName.Text = value;\r\n            }\r\n\r\n            private Colour4 issueColour;\r\n\r\n            public Colour4 IssueColour\r\n            {\r\n                get => issueColour;\r\n                set\r\n                {\r\n                    issueColour = value;\r\n\r\n                    background.Colour = value;\r\n                    issueName.Colour = value.Darken(0.7f);\r\n                    countSpriteText.Colour = value.Lighten(1f);\r\n                }\r\n            }\r\n\r\n            private int count;\r\n\r\n            public int Count\r\n            {\r\n                get => count;\r\n                set\r\n                {\r\n                    count = value;\r\n                    countSpriteText.Text = value.ToString(\"#,0\");\r\n                }\r\n            }\r\n        }\r\n    }\r\n\r\n    private class EmptyCheck : ICheck\r\n    {\r\n        public IEnumerable<Issue> Run(BeatmapVerifierContext context)\r\n        {\r\n            throw new NotImplementedException();\r\n        }\r\n\r\n        public CheckMetadata Metadata => new(CheckCategory.Metadata, string.Empty);\r\n\r\n        public IEnumerable<IssueTemplate> PossibleTemplates => Array.Empty<IssueTemplate>();\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/IssueTable.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Extensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit;\r\n\r\npublic abstract partial class IssueTable : EditorTable\r\n{\r\n    private const float horizontal_inset = 0;\r\n\r\n    protected IssueTable()\r\n    {\r\n        Padding = new MarginPadding { Horizontal = horizontal_inset };\r\n        BackgroundFlow.Padding = new MarginPadding { Horizontal = -horizontal_inset };\r\n\r\n        Columns = CreateHeaders();\r\n    }\r\n\r\n    public IEnumerable<Issue> Issues\r\n    {\r\n        set\r\n        {\r\n            Content = null;\r\n            BackgroundFlow.Clear();\r\n\r\n            foreach (var issue in value)\r\n            {\r\n                BackgroundFlow.Add(CreateRowBackground(issue).With(x =>\r\n                {\r\n                    x.Action = () =>\r\n                    {\r\n                        OnIssueClicked(issue);\r\n                    };\r\n                }));\r\n            }\r\n\r\n            Content = value.Select(CreateContent).ToArray().ToRectangular();\r\n        }\r\n    }\r\n\r\n    protected abstract void OnIssueClicked(Issue issue);\r\n\r\n    protected virtual RowBackground CreateRowBackground(Issue issue) => new(issue);\r\n\r\n    protected abstract TableColumn[] CreateHeaders();\r\n\r\n    protected abstract Drawable[] CreateContent(Issue issue);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/SectionItemsEditor.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections;\r\nusing System.Collections.Generic;\r\nusing System.Collections.Specialized;\r\nusing System.Diagnostics;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit;\r\n\r\n/// <summary>\r\n/// This section handle display the list of <typeparamref name=\"TModel\"/>.\r\n/// And able to create or remove the <typeparamref name=\"TModel\"/> in here.\r\n/// </summary>\r\n/// <typeparam name=\"TModel\"></typeparam>\r\n[Cached(typeof(ISectionItemsEditorProvider))]\r\npublic abstract partial class SectionItemsEditor<TModel> : CompositeDrawable, ISectionItemsEditorProvider where TModel : class\r\n{\r\n    protected readonly IBindableList<TModel> Items = new BindableList<TModel>();\r\n\r\n    private readonly Dictionary<TModel, Drawable> itemMap = new();\r\n\r\n    private readonly FillFlowContainer content;\r\n\r\n    protected SectionItemsEditor()\r\n    {\r\n        RelativeSizeAxes = Axes.X;\r\n        AutoSizeAxes = Axes.Y;\r\n\r\n        InternalChild = content = new FillFlowContainer\r\n        {\r\n            RelativeSizeAxes = Axes.X,\r\n            AutoSizeAxes = Axes.Y,\r\n            Spacing = new Vector2(10),\r\n            LayoutEasing = Easing.Out,\r\n        };\r\n\r\n        Items.BindCollectionChanged((_, args) =>\r\n        {\r\n            switch (args.Action)\r\n            {\r\n                case NotifyCollectionChangedAction.Add:\r\n                    Debug.Assert(args.NewItems != null);\r\n                    addItems(args.NewItems);\r\n\r\n                    break;\r\n\r\n                case NotifyCollectionChangedAction.Remove:\r\n                    Debug.Assert(args.OldItems != null);\r\n                    removeItems(args.OldItems);\r\n\r\n                    break;\r\n            }\r\n        });\r\n\r\n        addCreateButton();\r\n    }\r\n\r\n    public void RedrewContent()\r\n    {\r\n        clearDrawable();\r\n\r\n        addItems(Items.ToList());\r\n\r\n        addCreateButton();\r\n    }\r\n\r\n    private void clearDrawable()\r\n    {\r\n        content.Clear();\r\n        itemMap.Clear();\r\n    }\r\n\r\n    private void addItems(IList items)\r\n    {\r\n        bool enableAddAnimation = items.Count == 1;\r\n        content.LayoutDuration = enableAddAnimation ? 100 : 0;\r\n\r\n        foreach (var item in items.Cast<TModel>())\r\n        {\r\n            var drawable = CreateDrawable(item);\r\n            if (drawable == null)\r\n                continue;\r\n\r\n            content.Add(drawable);\r\n            itemMap.Add(item, drawable);\r\n        }\r\n    }\r\n\r\n    private void removeItems(IList items)\r\n    {\r\n        foreach (var item in items.Cast<TModel>())\r\n        {\r\n            if (!itemMap.TryGetValue(item, out var drawable))\r\n                continue;\r\n\r\n            content.Remove(drawable, true);\r\n            itemMap.Remove(item);\r\n        }\r\n    }\r\n\r\n    private void addCreateButton()\r\n    {\r\n        var button = CreateCreateNewItemButton();\r\n        if (button == null)\r\n            return;\r\n\r\n        content.Insert(int.MaxValue, button);\r\n    }\r\n\r\n    public void UpdateDisplayOrder(Drawable drawable, int order)\r\n    {\r\n        float newPosition = order;\r\n        content.SetLayoutPosition(drawable, newPosition);\r\n    }\r\n\r\n    /// <summary>\r\n    /// Create editable drawable for the item.\r\n    /// Return null if the item is not editable.\r\n    /// </summary>\r\n    /// <param name=\"item\"></param>\r\n    /// <returns></returns>\r\n    protected abstract Drawable? CreateDrawable(TModel item);\r\n\r\n    protected abstract EditorSectionButton? CreateCreateNewItemButton();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/SectionTimingInfoItemsEditor.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.UserInterface;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit;\r\n\r\npublic abstract partial class SectionTimingInfoItemsEditor<TItem> : SectionItemsEditor<TItem> where TItem : class\r\n{\r\n    protected sealed override Drawable CreateDrawable(TItem item)\r\n        => CreateTimingInfoDrawable(item);\r\n\r\n    protected abstract DrawableTimingInfoItem CreateTimingInfoDrawable(TItem item);\r\n\r\n    protected abstract partial class DrawableTimingInfoItem : CompositeDrawable\r\n    {\r\n        [Resolved]\r\n        private ISectionItemsEditorProvider sectionItemsEditorProvider { get; set; } = null!;\r\n\r\n        public readonly TItem Item;\r\n\r\n        private readonly Box background;\r\n        private readonly OsuSpriteText spriteText;\r\n        private readonly DeleteIconButton deleteIconButton;\r\n\r\n        protected DrawableTimingInfoItem(TItem item)\r\n        {\r\n            Item = item;\r\n\r\n            Masking = true;\r\n            CornerRadius = 5;\r\n            RelativeSizeAxes = Axes.X;\r\n            Height = 28;\r\n            InternalChildren = new Drawable[]\r\n            {\r\n                background = new Box\r\n                {\r\n                    RelativeSizeAxes = Axes.Both,\r\n                },\r\n                spriteText = new OsuSpriteText\r\n                {\r\n                    Anchor = Anchor.CentreLeft,\r\n                    Origin = Anchor.CentreLeft,\r\n                    Padding = new MarginPadding\r\n                    {\r\n                        Horizontal = 5,\r\n                    },\r\n                },\r\n                deleteIconButton = new DeleteIconButton\r\n                {\r\n                    Anchor = Anchor.CentreRight,\r\n                    Origin = Anchor.CentreRight,\r\n                    X = -5,\r\n                    Size = new Vector2(20),\r\n                    Action = () =>\r\n                    {\r\n                        RemoveItem(item);\r\n                    },\r\n                },\r\n            };\r\n        }\r\n\r\n        protected abstract void RemoveItem(TItem item);\r\n\r\n        protected string Text\r\n        {\r\n            set => spriteText.Text = value;\r\n        }\r\n\r\n        protected void ChangeDisplayOrder(int order)\r\n        {\r\n            Schedule(() =>\r\n            {\r\n                sectionItemsEditorProvider.UpdateDisplayOrder(this, order);\r\n            });\r\n        }\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(OsuColour colours)\r\n        {\r\n            background.Colour = colours.YellowLight;\r\n            spriteText.Colour = colours.YellowDarker;\r\n            deleteIconButton.IconColour = colours.YellowDarker;\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Stages/Classic/ClassicStageEditor.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Stages.Classic.Config;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Stages.Classic.Stage;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Stages.Classic;\r\n\r\npublic partial class ClassicStageEditor : GenericEditor<ClassicStageEditorScreenMode>\r\n{\r\n    [Cached]\r\n    private readonly OverlayColourProvider colourProvider = new(OverlayColourScheme.Green);\r\n\r\n    protected override GenericEditorScreen<ClassicStageEditorScreenMode> GenerateScreen(ClassicStageEditorScreenMode screenMode) =>\r\n        screenMode switch\r\n        {\r\n            ClassicStageEditorScreenMode.Stage => new StageScreen(),\r\n            ClassicStageEditorScreenMode.Config => new ConfigScreen(),\r\n            _ => throw new InvalidOperationException(\"Editor menu bar switched to an unsupported mode\"),\r\n        };\r\n\r\n    protected override MenuItem[] GenerateMenuItems(ClassicStageEditorScreenMode screenMode)\r\n    {\r\n        return screenMode switch\r\n        {\r\n            _ => Array.Empty<MenuItem>(),\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Stages/Classic/ClassicStageEditorScreenMode.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.ComponentModel;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Stages.Classic;\r\n\r\npublic enum ClassicStageEditorScreenMode\r\n{\r\n    [Description(\"Stage\")]\r\n    Stage,\r\n\r\n    [Description(\"Config\")]\r\n    Config,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Stages/Classic/ClassicStageScreen.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Stages.Classic;\r\n\r\npublic partial class ClassicStageScreen : GenericEditorScreen<ClassicStageEditorScreenMode>\r\n{\r\n    public ClassicStageScreen(ClassicStageEditorScreenMode type)\r\n        : base(type)\r\n    {\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Stages/Classic/Config/ConfigScreen.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Stages.Classic.Config;\r\n\r\npublic partial class ConfigScreen : ClassicStageScreen\r\n{\r\n    public ConfigScreen()\r\n        : base(ClassicStageEditorScreenMode.Config)\r\n    {\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Stages/Classic/Stage/IStageEditorStateProvider.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos.Classic;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Stages.Classic.Stage;\r\n\r\npublic interface IStageEditorStateProvider\r\n{\r\n    IBindable<StageEditorEditCategory> BindableEditCategory { get; }\r\n\r\n    StageEditorEditCategory EditCategory => BindableEditCategory.Value;\r\n\r\n    void ChangeEditCategory(StageEditorEditCategory mode);\r\n\r\n    Bindable<StageEditorEditMode> BindableEditMode { get; }\r\n\r\n    StageEditorEditMode EditMode => BindableEditMode.Value;\r\n\r\n    ClassicStageInfo StageInfo { get; }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Stages/Classic/Stage/IStageEditorVerifier.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Stages.Classic.Stage;\r\n\r\npublic interface IStageEditorVerifier : IEditorVerifier<StageEditorEditCategory>\r\n{\r\n    void Navigate(Issue issue);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Stages/Classic/Stage/Settings/StageEditorIssueSection.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Extensions;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Issues;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Stages.Classic.Stage.Settings;\r\n\r\npublic partial class StageEditorIssueSection : IssueSection\r\n{\r\n    private readonly StageEditorEditCategory category;\r\n\r\n    public StageEditorIssueSection(StageEditorEditCategory category)\r\n    {\r\n        this.category = category;\r\n    }\r\n\r\n    protected override EmptyIssue CreateEmptyIssue() => new StageEditorEmptyIssue();\r\n\r\n    protected override IssueNavigator CreateIssueNavigator() => new StageEditorIssueNavigator();\r\n\r\n    protected override IssueTable CreateIssueTable() => new StageEditorIssueTable(category);\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(IStageEditorVerifier stageEditorVerifier)\r\n    {\r\n        Issues.BindTo(stageEditorVerifier.GetIssueByType(category));\r\n    }\r\n\r\n    private partial class StageEditorEmptyIssue : EmptyIssue\r\n    {\r\n        [Resolved]\r\n        private IStageEditorVerifier stageEditorVerifier { get; set; } = null!;\r\n\r\n        protected override void OnRefreshButtonClicked()\r\n            => stageEditorVerifier.Refresh();\r\n    }\r\n\r\n    private partial class StageEditorIssueNavigator : IssueNavigator\r\n    {\r\n        [Resolved]\r\n        private IStageEditorVerifier stageEditorVerifier { get; set; } = null!;\r\n\r\n        protected override void OnRefreshButtonClicked()\r\n            => stageEditorVerifier.Refresh();\r\n    }\r\n\r\n    public partial class StageEditorIssueTable : IssueTable\r\n    {\r\n        [Resolved]\r\n        private IStageEditorVerifier stageEditorVerifier { get; set; } = null!;\r\n\r\n        private readonly StageEditorEditCategory category;\r\n\r\n        public StageEditorIssueTable(StageEditorEditCategory category)\r\n        {\r\n            this.category = category;\r\n        }\r\n\r\n        protected override void OnIssueClicked(Issue issue)\r\n            => stageEditorVerifier.Navigate(issue);\r\n\r\n        protected override TableColumn[] CreateHeaders()\r\n        {\r\n            if (category == StageEditorEditCategory.Timing)\r\n            {\r\n                return new[]\r\n                {\r\n                    new TableColumn(string.Empty, Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize, minSize: 30)),\r\n                    new TableColumn(\"Time\", Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize, minSize: 40)),\r\n                    new TableColumn(\"Message\", Anchor.CentreLeft),\r\n                };\r\n            }\r\n\r\n            return new[]\r\n            {\r\n                new TableColumn(string.Empty, Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize, minSize: 30)),\r\n                new TableColumn(\"Message\", Anchor.CentreLeft),\r\n            };\r\n        }\r\n\r\n        protected override Drawable[] CreateContent(Issue issue)\r\n        {\r\n            if (category == StageEditorEditCategory.Timing)\r\n            {\r\n                return new Drawable[]\r\n                {\r\n                    new IssueIcon\r\n                    {\r\n                        Origin = Anchor.Centre,\r\n                        Size = new Vector2(10),\r\n                        Margin = new MarginPadding { Left = 10 },\r\n                        Issue = issue,\r\n                    },\r\n                    new OsuSpriteText\r\n                    {\r\n                        Text = getInvalidObjectTimeByIssue(issue),\r\n                        Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold),\r\n                        Margin = new MarginPadding { Right = 10 },\r\n                    },\r\n                    new TruncatingSpriteText\r\n                    {\r\n                        Text = issue.ToString(),\r\n                        RelativeSizeAxes = Axes.X,\r\n                        Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Medium),\r\n                    },\r\n                };\r\n            }\r\n\r\n            return new Drawable[]\r\n            {\r\n                new IssueIcon\r\n                {\r\n                    Origin = Anchor.Centre,\r\n                    Size = new Vector2(10),\r\n                    Margin = new MarginPadding { Left = 10 },\r\n                    Issue = issue,\r\n                },\r\n                new TruncatingSpriteText\r\n                {\r\n                    Text = issue.ToString(),\r\n                    RelativeSizeAxes = Axes.X,\r\n                    Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Medium),\r\n                },\r\n            };\r\n        }\r\n\r\n        private static string getInvalidObjectTimeByIssue(Issue issue) => issue.Time?.ToEditorFormattedString() ?? string.Empty;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Stages/Classic/Stage/Settings/StageEditorSettingsHeader.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Allocation;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Markdown;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Stages.Classic.Stage.Settings;\r\n\r\npublic partial class StageEditorSettingsHeader : EditorSettingsHeader<StageEditorEditMode>\r\n{\r\n    private readonly StageEditorEditCategory category;\r\n\r\n    public StageEditorSettingsHeader(StageEditorEditCategory category)\r\n    {\r\n        this.category = category;\r\n    }\r\n\r\n    protected override OverlayColourScheme CreateColourScheme()\r\n        => OverlayColourScheme.Green;\r\n\r\n    protected override EditStepTabControl CreateTabControl()\r\n        => new StageEditStepTabControl(category);\r\n\r\n    protected override DescriptionFormat GetSelectionDescription(StageEditorEditMode step) =>\r\n        step switch\r\n        {\r\n            StageEditorEditMode.Edit => \"Edit the stage property in here.\",\r\n            StageEditorEditMode.Verify => \"Check if have any stage issues.\",\r\n            _ => throw new ArgumentOutOfRangeException(nameof(step), step, null),\r\n        };\r\n\r\n    private partial class StageEditStepTabControl : EditStepTabControl\r\n    {\r\n        private readonly StageEditorEditCategory category;\r\n\r\n        public StageEditStepTabControl(StageEditorEditCategory category)\r\n        {\r\n            this.category = category;\r\n        }\r\n\r\n        protected override StepTabButton CreateStepButton(OsuColour colours, StageEditorEditMode value)\r\n        {\r\n            return value switch\r\n            {\r\n                StageEditorEditMode.Edit => new StepTabButton(value)\r\n                {\r\n                    Text = \"Edit\",\r\n                    SelectedColour = colours.Red,\r\n                    UnSelectedColour = colours.RedDarker,\r\n                },\r\n                StageEditorEditMode.Verify => new VerifyStepTabButton(value, category)\r\n                {\r\n                    Text = \"Verify\",\r\n                    SelectedColour = colours.Yellow,\r\n                    UnSelectedColour = colours.YellowDarker,\r\n                },\r\n                _ => throw new ArgumentOutOfRangeException(nameof(value), value, null),\r\n            };\r\n        }\r\n    }\r\n\r\n    private partial class VerifyStepTabButton : IssueStepTabButton\r\n    {\r\n        private readonly StageEditorEditCategory category;\r\n\r\n        public VerifyStepTabButton(StageEditorEditMode value, StageEditorEditCategory category)\r\n            : base(value)\r\n        {\r\n            this.category = category;\r\n        }\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(IStageEditorVerifier stageEditorVerifier)\r\n        {\r\n            Issues.BindTo(stageEditorVerifier.GetIssueByType(category));\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Stages/Classic/Stage/Settings/StageSettings.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Overlays;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Stages.Classic.Stage.Settings;\r\n\r\npublic partial class StageSettings : EditorSettings\r\n{\r\n    private readonly IBindable<StageEditorEditCategory> bindableCategory = new Bindable<StageEditorEditCategory>();\r\n    private readonly Bindable<StageEditorEditMode> bindableMode = new();\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OverlayColourProvider colourProvider, IStageEditorStateProvider stageEditorStateProvider)\r\n    {\r\n        bindableCategory.BindTo(stageEditorStateProvider.BindableEditCategory);\r\n        bindableCategory.BindValueChanged(e =>\r\n        {\r\n            ReloadSections();\r\n        }, true);\r\n\r\n        bindableMode.BindTo(stageEditorStateProvider.BindableEditMode);\r\n\r\n        // change the background colour to the lighter one.\r\n        ChangeBackgroundColour(colourProvider.Background3);\r\n    }\r\n\r\n    protected override EditorSettingsHeader CreateSettingHeader()\r\n        => new StageEditorSettingsHeader(bindableCategory.Value)\r\n        {\r\n            Current = bindableMode,\r\n        };\r\n\r\n    protected override IReadOnlyList<EditorSection> CreateEditorSections() => bindableCategory.Value switch\r\n    {\r\n        StageEditorEditCategory.Layout => createSectionsForLayoutCategory(bindableMode.Value),\r\n        StageEditorEditCategory.Timing => createSectionsForTimingCategory(bindableMode.Value),\r\n        StageEditorEditCategory.Style => createSectionsForStyleCategory(bindableMode.Value),\r\n        _ => throw new ArgumentOutOfRangeException(),\r\n    };\r\n\r\n    private static IReadOnlyList<EditorSection> createSectionsForLayoutCategory(StageEditorEditMode editMode) => editMode switch\r\n    {\r\n        StageEditorEditMode.Edit => Array.Empty<EditorSection>(),\r\n        StageEditorEditMode.Verify => new EditorSection[]\r\n        {\r\n            new StageEditorIssueSection(StageEditorEditCategory.Layout),\r\n        },\r\n        _ => throw new ArgumentOutOfRangeException(),\r\n    };\r\n\r\n    private static IReadOnlyList<EditorSection> createSectionsForTimingCategory(StageEditorEditMode editMode) => editMode switch\r\n    {\r\n        StageEditorEditMode.Edit => new[]\r\n        {\r\n            new TimingPointsSection(),\r\n        },\r\n        StageEditorEditMode.Verify => new[]\r\n        {\r\n            new StageEditorIssueSection(StageEditorEditCategory.Timing),\r\n        },\r\n        _ => throw new ArgumentOutOfRangeException(),\r\n    };\r\n\r\n    private static IReadOnlyList<EditorSection> createSectionsForStyleCategory(StageEditorEditMode editMode) => editMode switch\r\n    {\r\n        StageEditorEditMode.Edit => Array.Empty<EditorSection>(),\r\n        StageEditorEditMode.Verify => new[]\r\n        {\r\n            new StageEditorIssueSection(StageEditorEditCategory.Style),\r\n        },\r\n        _ => throw new ArgumentOutOfRangeException(),\r\n    };\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Stages/Classic/Stage/Settings/TimingPointsSection.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Extensions;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Stages;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos.Classic;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Stages.Classic.Stage.Settings;\r\n\r\npublic partial class TimingPointsSection : EditorSection\r\n{\r\n    protected override LocalisableString Title => \"Timings\";\r\n\r\n    public TimingPointsSection()\r\n    {\r\n        Add(new SectionPageInfoEditor());\r\n    }\r\n\r\n    private partial class SectionPageInfoEditor : SectionTimingInfoItemsEditor<ClassicLyricTimingPoint>\r\n    {\r\n        [BackgroundDependencyLoader]\r\n        private void load(IStageEditorStateProvider stageEditorStateProvider)\r\n        {\r\n            Items.BindTo(stageEditorStateProvider.StageInfo.LyricTimingInfo.Timings);\r\n        }\r\n\r\n        protected override DrawableTimingInfoItem CreateTimingInfoDrawable(ClassicLyricTimingPoint item) => new DrawableTimingPoint(item);\r\n\r\n        protected override EditorSectionButton CreateCreateNewItemButton() => new CreateNewTimingPointButton();\r\n\r\n        private partial class DrawableTimingPoint : DrawableTimingInfoItem\r\n        {\r\n            private readonly IBindable<int> timingPointsVersion = new Bindable<int>();\r\n\r\n            [Resolved]\r\n            private IClassicStageChangeHandler classicStageChangeHandler { get; set; } = null!;\r\n\r\n            public DrawableTimingPoint(ClassicLyricTimingPoint item)\r\n                : base(item)\r\n            {\r\n            }\r\n\r\n            protected override void RemoveItem(ClassicLyricTimingPoint item)\r\n            {\r\n                classicStageChangeHandler.RemoveTimingPoint(item);\r\n            }\r\n\r\n            [BackgroundDependencyLoader]\r\n            private void load(IStageEditorStateProvider stageEditorStateProvider)\r\n            {\r\n                timingPointsVersion.BindTo(stageEditorStateProvider.StageInfo.LyricTimingInfo.TimingVersion);\r\n                timingPointsVersion.BindValueChanged(_ =>\r\n                {\r\n                    int? order = stageEditorStateProvider.StageInfo.LyricTimingInfo.GetTimingPointOrder(Item);\r\n                    double time = Item.Time;\r\n\r\n                    ChangeDisplayOrder((int)time);\r\n                    Text = $\"#{order} {time.ToEditorFormattedString()}\";\r\n                }, true);\r\n            }\r\n        }\r\n\r\n        private partial class CreateNewTimingPointButton : EditorSectionButton\r\n        {\r\n            [Resolved]\r\n            private IClassicStageChangeHandler classicStageChangeHandler { get; set; } = null!;\r\n\r\n            [Resolved]\r\n            private EditorClock clock { get; set; } = null!;\r\n\r\n            public CreateNewTimingPointButton()\r\n            {\r\n                Text = \"Create new timing\";\r\n                Action = () =>\r\n                {\r\n                    double currentTime = clock.CurrentTime;\r\n                    classicStageChangeHandler.AddTimingPoint(x => x.Time = currentTime);\r\n                };\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Stages/Classic/Stage/StageCategoryScreenStack.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Game.Overlays;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Stages.Classic.Stage;\r\n\r\npublic partial class StageCategoryScreenStack : WorkspaceScreenStack<StageEditorEditCategory>\r\n{\r\n    public const float LEFT_SIDE_PADDING = 200;\r\n\r\n    private readonly Box background;\r\n\r\n    public StageCategoryScreenStack()\r\n    {\r\n        AddInternal(background = new Box\r\n        {\r\n            RelativeSizeAxes = Axes.Both,\r\n            Depth = float.MaxValue,\r\n        });\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OverlayColourProvider colourProvider)\r\n    {\r\n        background.Colour = colourProvider.Background5;\r\n    }\r\n\r\n    protected override WorkspaceScreen<StageEditorEditCategory>? CreateWorkspaceScreen(StageEditorEditCategory item) =>\r\n        item switch\r\n        {\r\n            StageEditorEditCategory.Layout => null,\r\n            StageEditorEditCategory.Timing => null,\r\n            StageEditorEditCategory.Style => null,\r\n            _ => throw new InvalidOperationException(\"Editor menu bar switched to an unsupported mode\"),\r\n        };\r\n\r\n    protected override WorkspaceScreenStackTabControl CreateTabControl() => new StageCategoriesTabControl();\r\n\r\n    private partial class StageCategoriesTabControl : WorkspaceScreenStackTabControl\r\n    {\r\n        public StageCategoriesTabControl()\r\n        {\r\n            TabContainer.Margin = new MarginPadding { Horizontal = LEFT_SIDE_PADDING };\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Stages/Classic/Stage/StageEditor.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Game.Overlays;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Stages.Classic.Stage;\r\n\r\npublic partial class StageEditor : CompositeDrawable\r\n{\r\n    private readonly Box background;\r\n\r\n    public StageEditor()\r\n    {\r\n        InternalChildren = new Drawable[]\r\n        {\r\n            background = new Box\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n            },\r\n            new StageCategoryScreenStack\r\n            {\r\n                Anchor = Anchor.BottomCentre,\r\n                Origin = Anchor.BottomCentre,\r\n                RelativeSizeAxes = Axes.X,\r\n                Height = 250,\r\n            },\r\n        };\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OverlayColourProvider colourProvider)\r\n    {\r\n        background.Colour = colourProvider.Background4;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Stages/Classic/Stage/StageEditorEditCategory.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.ComponentModel;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Stages.Classic.Stage;\r\n\r\npublic enum StageEditorEditCategory\r\n{\r\n    [Description(\"Layout\")]\r\n    Layout,\r\n\r\n    [Description(\"Timing\")]\r\n    Timing,\r\n\r\n    [Description(\"Style\")]\r\n    Style,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Stages/Classic/Stage/StageEditorEditMode.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Stages.Classic.Stage;\r\n\r\npublic enum StageEditorEditMode\r\n{\r\n    Edit,\r\n\r\n    Verify,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Stages/Classic/Stage/StageEditorVerifier.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks;\r\nusing static osu.Game.Rulesets.Karaoke.Edit.Checks.CheckClassicStageInfo;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Stages.Classic.Stage;\r\n\r\npublic partial class StageEditorVerifier : EditorVerifier<StageEditorEditCategory>, IStageEditorVerifier\r\n{\r\n    protected override IEnumerable<ICheck> CreateChecks(StageEditorEditCategory type) =>\r\n        type switch\r\n        {\r\n            StageEditorEditCategory.Layout => new ICheck[] { new CheckClassicStageInfo() },\r\n            StageEditorEditCategory.Timing => new ICheck[] { new CheckClassicStageInfo() },\r\n            StageEditorEditCategory.Style => new ICheck[] { new CheckClassicStageInfo() },\r\n            _ => throw new ArgumentOutOfRangeException(nameof(type), type, null),\r\n        };\r\n\r\n    protected override StageEditorEditCategory ClassifyIssue(Issue issue)\r\n    {\r\n        switch (issue.Template)\r\n        {\r\n            case IssueTemplateInvalidRowHeight:\r\n                // just showing this issue on this section. will go to another screen if click this issue.\r\n                return StageEditorEditCategory.Layout;\r\n\r\n            case IssueTemplateLessThanTwoTimingPoints:\r\n            case IssueTemplateTimingIntervalTooShort:\r\n            case IssueTemplateTimingIntervalTooLong:\r\n            case IssueTemplateTimingInfoHitObjectNotExist:\r\n            case IssueTemplateTimingInfoMappingHasNoTiming:\r\n            case IssueTemplateTimingInfoTimingNotExist:\r\n            case IssueTemplateTimingInfoLyricNotHaveTwoTiming:\r\n                return StageEditorEditCategory.Timing;\r\n\r\n            case IssueTemplateLyricLayoutInvalidLineNumber:\r\n                return StageEditorEditCategory.Layout;\r\n\r\n            default:\r\n                throw new NotSupportedException();\r\n        }\r\n    }\r\n\r\n    public override void Refresh()\r\n    {\r\n        var allIssues = CreateIssues();\r\n        var groupByEditModeIssues = allIssues.GroupBy(ClassifyIssue).ToDictionary(x => x.Key, x => x.ToArray());\r\n\r\n        foreach (var editorMode in Enum.GetValues<StageEditorEditCategory>())\r\n        {\r\n            ClearChecks(editorMode);\r\n\r\n            if (groupByEditModeIssues.TryGetValue(editorMode, out var issues))\r\n                AddChecks(editorMode, issues);\r\n        }\r\n    }\r\n\r\n    public void Navigate(Issue issue)\r\n    {\r\n        // todo: should switch to another screen if got the IssueTemplateInvalidRowHeight issue.\r\n        // todo: doing something.\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/Stages/Classic/Stage/StageScreen.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Stages;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Stages.Classic.Stage.Settings;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos.Classic;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit.Stages.Classic.Stage;\r\n\r\n[Cached(typeof(IStageEditorStateProvider))]\r\npublic partial class StageScreen : ClassicStageScreen, IStageEditorStateProvider\r\n{\r\n    public IBindable<StageEditorEditCategory> BindableEditCategory => bindableCategory;\r\n    public Bindable<StageEditorEditMode> BindableEditMode { get; } = new();\r\n\r\n    private readonly Bindable<StageEditorEditCategory> bindableCategory = new();\r\n\r\n    [Cached(typeof(IClassicStageChangeHandler))]\r\n    private readonly ClassicStageChangeHandler classicStageChangeHandler;\r\n\r\n    [Cached(typeof(IStageEditorVerifier))]\r\n    private readonly StageEditorVerifier stageEditorVerifier;\r\n\r\n    public ClassicStageInfo StageInfo\r\n    {\r\n        get\r\n        {\r\n            // todo: should be able to read the stage info from the beatmap.\r\n            throw new NotImplementedException();\r\n        }\r\n    }\r\n\r\n    public StageScreen()\r\n        : base(ClassicStageEditorScreenMode.Stage)\r\n    {\r\n        AddInternal(classicStageChangeHandler = new ClassicStageChangeHandler());\r\n        AddInternal(stageEditorVerifier = new StageEditorVerifier());\r\n\r\n        Child = new GridContainer\r\n        {\r\n            RelativeSizeAxes = Axes.Both,\r\n            ColumnDimensions = new[]\r\n            {\r\n                new Dimension(),\r\n                new Dimension(GridSizeMode.Absolute, 250),\r\n            },\r\n            Content = new[]\r\n            {\r\n                new Drawable[]\r\n                {\r\n                    new StageEditor\r\n                    {\r\n                        RelativeSizeAxes = Axes.Both,\r\n                    },\r\n                    new StageSettings(),\r\n                },\r\n            },\r\n        };\r\n    }\r\n\r\n    public void ChangeEditCategory(StageEditorEditCategory mode)\r\n    {\r\n        bindableCategory.Value = mode;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/WorkspaceScreen.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics.Containers;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit;\r\n\r\n/// <summary>\r\n/// It's a switchable screen for able to switch workspace inside the <see cref=\"WorkspaceScreenStack{TItem}\"/>\r\n/// TODO: eventually make this inherit Screen and add a local screen stack inside the Editor.\r\n/// </summary>\r\npublic abstract partial class WorkspaceScreen<TItem> : VisibilityContainer\r\n{\r\n    public readonly TItem Item;\r\n\r\n    protected WorkspaceScreen(TItem item)\r\n    {\r\n        Item = item;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Edit/WorkspaceScreenStack.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Extensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Overlays;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Edit;\r\n\r\n/// <summary>\r\n/// A component which provides functionality for displaying and handling transitions between multiple <see cref=\"WorkspaceScreen{TItem}\"/>s.\r\n/// </summary>\r\npublic abstract partial class WorkspaceScreenStack<TItem> : CompositeDrawable\r\n{\r\n    private readonly Bindable<TItem> item = new();\r\n    private readonly Container<WorkspaceScreen<TItem>> screenContainer;\r\n\r\n    private WorkspaceScreen<TItem>? currentScreen;\r\n\r\n    protected WorkspaceScreenStack()\r\n    {\r\n        InternalChildren = new Drawable[]\r\n        {\r\n            new Container\r\n            {\r\n                Name = \"Screen container\",\r\n                RelativeSizeAxes = Axes.Both,\r\n                Padding = new MarginPadding { Top = 40, Bottom = 60 },\r\n                Child = screenContainer = new Container<WorkspaceScreen<TItem>>\r\n                {\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    Masking = true,\r\n                },\r\n            },\r\n            new Container\r\n            {\r\n                Name = \"Top bar\",\r\n                RelativeSizeAxes = Axes.X,\r\n                Height = 36,\r\n                Children = new Drawable[]\r\n                {\r\n                    CreateTabControl().With(x =>\r\n                    {\r\n                        x.RelativeSizeAxes = Axes.X;\r\n                        x.Height = 36;\r\n                    }),\r\n                },\r\n            },\r\n        };\r\n\r\n        item.BindValueChanged(onItemChanged, true);\r\n    }\r\n\r\n    private void onItemChanged(ValueChangedEvent<TItem> e)\r\n    {\r\n        var lastScreen = currentScreen;\r\n\r\n        lastScreen?.Hide();\r\n\r\n        try\r\n        {\r\n            if ((currentScreen = screenContainer.SingleOrDefault(s => EqualityComparer<TItem>.Default.Equals(s.Item, e.NewValue))) != null)\r\n            {\r\n                screenContainer.ChangeChildDepth(currentScreen, lastScreen?.Depth + 1 ?? 0);\r\n\r\n                currentScreen.Show();\r\n                return;\r\n            }\r\n\r\n            currentScreen = CreateWorkspaceScreen(e.NewValue);\r\n\r\n            LoadComponentAsync(currentScreen, newScreen =>\r\n            {\r\n                if (newScreen != currentScreen || newScreen == null)\r\n                    return;\r\n\r\n                screenContainer.Add(newScreen);\r\n                newScreen.Show();\r\n            });\r\n        }\r\n        catch\r\n        {\r\n        }\r\n    }\r\n\r\n    protected abstract WorkspaceScreen<TItem>? CreateWorkspaceScreen(TItem item);\r\n\r\n    protected virtual WorkspaceScreenStackTabControl CreateTabControl() => new();\r\n\r\n    public partial class WorkspaceScreenStackTabControl : OverlayTabControl<TItem>\r\n    {\r\n        public WorkspaceScreenStackTabControl()\r\n        {\r\n            TabContainer.Margin = new MarginPadding { Horizontal = 10 };\r\n        }\r\n\r\n        protected override TabItem<TItem> CreateTabItem(TItem value) => new WorkspaceScreenStackTabItem(value)\r\n        {\r\n            AccentColour = AccentColour,\r\n        };\r\n\r\n        private partial class WorkspaceScreenStackTabItem : OverlayTabItem\r\n        {\r\n            public WorkspaceScreenStackTabItem(TItem value)\r\n                : base(value)\r\n            {\r\n                // todo: copied from OsuTabItem.\r\n                Text.Text = value switch\r\n                {\r\n                    IHasDescription hasDescription => hasDescription.GetDescription(),\r\n                    Enum e => e.GetLocalisableDescription(),\r\n                    LocalisableString l => l,\r\n                    _ => value?.ToString() ?? string.Empty,\r\n                };\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Section.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens;\r\n\r\npublic abstract partial class Section : Container\r\n{\r\n    protected const float SECTION_PADDING = 10;\r\n\r\n    protected const float SECTION_SPACING = 10;\r\n\r\n    private readonly FillFlowContainer flow;\r\n\r\n    protected override Container<Drawable> Content => flow;\r\n\r\n    protected abstract LocalisableString Title { get; }\r\n\r\n    protected Section()\r\n    {\r\n        RelativeSizeAxes = Axes.X;\r\n        AutoSizeAxes = Axes.Y;\r\n\r\n        Padding = new MarginPadding(SECTION_PADDING);\r\n\r\n        InternalChildren = new Drawable[]\r\n        {\r\n            new OsuSpriteText\r\n            {\r\n                Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 18),\r\n                Text = Title,\r\n            },\r\n            flow = new FillFlowContainer\r\n            {\r\n                RelativeSizeAxes = Axes.X,\r\n                AutoSizeAxes = Axes.Y,\r\n                Spacing = new Vector2(SECTION_SPACING),\r\n                Direction = FillDirection.Vertical,\r\n                Margin = new MarginPadding { Top = 30 },\r\n            },\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Settings/Header.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Extensions.Color4Extensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Overlays.Settings;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Settings;\r\n\r\npublic partial class Header : Container\r\n{\r\n    public const float HEIGHT = 75;\r\n\r\n    [Resolved]\r\n    private KaraokeSettingsColourProvider colourProvider { get; set; } = null!;\r\n\r\n    private readonly Box background;\r\n    private readonly KaraokeConfigHeaderTitle title;\r\n    private readonly KaraokeConfigPageTabControl tabs;\r\n\r\n    public Header()\r\n    {\r\n        RelativeSizeAxes = Axes.X;\r\n        Height = HEIGHT;\r\n\r\n        Children = new Drawable[]\r\n        {\r\n            background = new Box\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n                Colour = Color4Extensions.FromHex(\"#1f1921\"),\r\n            },\r\n            new Container\r\n            {\r\n                Anchor = Anchor.CentreLeft,\r\n                Origin = Anchor.CentreLeft,\r\n                RelativeSizeAxes = Axes.Both,\r\n                Padding = new MarginPadding { Left = 10 },\r\n                Children = new Drawable[]\r\n                {\r\n                    title = new KaraokeConfigHeaderTitle\r\n                    {\r\n                        Anchor = Anchor.CentreLeft,\r\n                        Origin = Anchor.BottomLeft,\r\n                    },\r\n                    tabs = new KaraokeConfigPageTabControl\r\n                    {\r\n                        Anchor = Anchor.BottomLeft,\r\n                        Origin = Anchor.BottomLeft,\r\n                        RelativeSizeAxes = Axes.X,\r\n                        Scale = new Vector2(1.5f),\r\n                    },\r\n                },\r\n            },\r\n        };\r\n\r\n        tabs.Current.BindValueChanged(x =>\r\n        {\r\n            background.Delay(200).Then().FadeColour(colourProvider.GetBackground2Colour(x.NewValue), 500);\r\n\r\n            tabs.Colour = colourProvider.GetContent2Colour(x.NewValue);\r\n            tabs.StripColour = colourProvider.GetContentColour(x.NewValue);\r\n        });\r\n    }\r\n\r\n    public IReadOnlyList<SettingsSection> TabItems\r\n    {\r\n        get => tabs.Items;\r\n        set => tabs.Items = value;\r\n    }\r\n\r\n    public Bindable<SettingsSection> SelectedSection\r\n    {\r\n        get => tabs.Current;\r\n        set => tabs.Current = value;\r\n    }\r\n\r\n    private partial class KaraokeConfigHeaderTitle : CompositeDrawable\r\n    {\r\n        private const float spacing = 6;\r\n\r\n        private readonly OsuSpriteText dot;\r\n        private readonly OsuSpriteText pageTitle;\r\n\r\n        public KaraokeConfigHeaderTitle()\r\n        {\r\n            AutoSizeAxes = Axes.Both;\r\n\r\n            InternalChildren = new Drawable[]\r\n            {\r\n                new FillFlowContainer\r\n                {\r\n                    AutoSizeAxes = Axes.Both,\r\n                    Spacing = new Vector2(spacing, 0),\r\n                    Direction = FillDirection.Horizontal,\r\n                    Children = new Drawable[]\r\n                    {\r\n                        new OsuSpriteText\r\n                        {\r\n                            Anchor = Anchor.CentreLeft,\r\n                            Origin = Anchor.CentreLeft,\r\n                            Font = OsuFont.GetFont(size: 24),\r\n                            Text = \"Karaoke\",\r\n                        },\r\n                        dot = new OsuSpriteText\r\n                        {\r\n                            Anchor = Anchor.CentreLeft,\r\n                            Origin = Anchor.CentreLeft,\r\n                            Font = OsuFont.GetFont(size: 24),\r\n                            Text = \"·\",\r\n                        },\r\n                        pageTitle = new OsuSpriteText\r\n                        {\r\n                            Anchor = Anchor.CentreLeft,\r\n                            Origin = Anchor.CentreLeft,\r\n                            Font = OsuFont.GetFont(size: 24),\r\n                        },\r\n                    },\r\n                },\r\n            };\r\n        }\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(KaraokeSettingsColourProvider colourProvider, Bindable<SettingsSection> selectedSection)\r\n        {\r\n            selectedSection.BindValueChanged(x =>\r\n            {\r\n                var colour = colourProvider.GetContentColour(x.NewValue);\r\n\r\n                pageTitle.Text = x.NewValue?.Header ?? \"404 Not found\";\r\n                pageTitle.FadeColour(colour, 200);\r\n            });\r\n        }\r\n    }\r\n\r\n    private partial class KaraokeConfigPageTabControl : PageTabControl<SettingsSection>\r\n    {\r\n        protected override TabItem<SettingsSection> CreateTabItem(SettingsSection value)\r\n            => new KaraokeConfigPageTabItem(value);\r\n\r\n        internal partial class KaraokeConfigPageTabItem : PageTabItem\r\n        {\r\n            public KaraokeConfigPageTabItem(SettingsSection value)\r\n                : base(value)\r\n            {\r\n            }\r\n\r\n            protected override LocalisableString CreateText() => Value.Header;\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Settings/KaraokeSettings.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Extensions.Color4Extensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Cursor;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Framework.Platform;\r\nusing osu.Game.Graphics.Containers;\r\nusing osu.Game.Overlays.Settings;\r\nusing osu.Game.Rulesets.Karaoke.Extensions;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Settings.Previews;\r\nusing osu.Game.Rulesets.Karaoke.Skinning.Fonts;\r\nusing osu.Game.Rulesets.UI;\r\nusing osu.Game.Screens;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Settings;\r\n\r\npublic partial class KaraokeSettings : OsuScreen\r\n{\r\n    [Cached]\r\n    private KaraokeSettingsColourProvider colourProvider = new();\r\n\r\n    [Cached]\r\n    private Bindable<SettingsSection> selectedSection = new();\r\n\r\n    [Cached]\r\n    private Bindable<SettingsSubsection?> selectedSubsection = new();\r\n\r\n    private readonly KaraokeConfigWaveContainer waves;\r\n    private readonly Box background;\r\n    private readonly KaraokeSettingsPanel settingsPanel;\r\n    private readonly Header header;\r\n    private readonly Container previewArea;\r\n\r\n    public KaraokeSettings()\r\n    {\r\n        var backgroundColour = Color4Extensions.FromHex(\"3e3a44\");\r\n\r\n        InternalChild = waves = new KaraokeConfigWaveContainer\r\n        {\r\n            RelativeSizeAxes = Axes.Both,\r\n            Child = new PopoverContainer\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n                Children = new Drawable[]\r\n                {\r\n                    background = new Box\r\n                    {\r\n                        RelativeSizeAxes = Axes.Both,\r\n                        Colour = backgroundColour,\r\n                    },\r\n                    previewArea = new Container\r\n                    {\r\n                        RelativeSizeAxes = Axes.Both,\r\n                        Padding = new MarginPadding { Top = Header.HEIGHT, Left = KaraokeSettingsPanel.WIDTH },\r\n                    },\r\n                    settingsPanel = new KaraokeSettingsPanel(),\r\n                    header = new Header\r\n                    {\r\n                        Padding = new MarginPadding { Left = KaraokeSettingsPanel.WIDTH },\r\n                    },\r\n                    new KaraokeVersionManager().With(x => x.Show()),\r\n                },\r\n            },\r\n        };\r\n\r\n        // wait for a period until all children loaded.\r\n        // todo : should have a better way to do this.\r\n        Scheduler.AddDelayed(() =>\r\n        {\r\n            header.TabItems = settingsPanel.Sections;\r\n            header.SelectedSection = selectedSection;\r\n        }, 2000);\r\n\r\n        selectedSection.BindValueChanged(e =>\r\n        {\r\n            var newSection = e.NewValue;\r\n            background.Delay(200).Then().FadeColour(colourProvider.GetBackgroundColour(newSection), 500);\r\n\r\n            settingsPanel.ScrollToSection(newSection);\r\n        });\r\n\r\n        selectedSubsection.BindValueChanged(e =>\r\n        {\r\n            var preview = e.NewValue is KaraokeSettingsSubsection settingsSubsection\r\n                ? settingsSubsection.CreatePreview()\r\n                : new DefaultPreview();\r\n\r\n            previewArea.Child = new DelayedLoadWrapper(preview)\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n            };\r\n        }, true);\r\n    }\r\n\r\n    protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)\r\n    {\r\n        var baseDependencies = new DependencyContainer(base.CreateChildDependencies(parent));\r\n\r\n        return new OsuScreenDependencies(false, new DrawableRulesetDependencies(baseDependencies.GetRuleset(), baseDependencies));\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(GameHost host)\r\n    {\r\n        // todo : not really sure how to clean-up cached manager\r\n        if (host.Dependencies.Get<FontManager>() != null)\r\n            return;\r\n\r\n        // because not possible to remove cache from host, so only inject once.\r\n        var manager = new FontManager();\r\n        AddInternal(manager);\r\n        host.Dependencies.Cache(manager);\r\n    }\r\n\r\n    protected override void LoadComplete()\r\n    {\r\n        base.LoadComplete();\r\n        waves.Show();\r\n    }\r\n\r\n    protected override bool OnScroll(ScrollEvent e)\r\n    {\r\n        // Prevent scroll event cause volume control appear.\r\n        return true;\r\n    }\r\n\r\n    private partial class KaraokeConfigWaveContainer : WaveContainer\r\n    {\r\n        protected override bool StartHidden => true;\r\n\r\n        public KaraokeConfigWaveContainer()\r\n        {\r\n            FirstWaveColour = Color4Extensions.FromHex(\"654d8c\");\r\n            SecondWaveColour = Color4Extensions.FromHex(\"554075\");\r\n            ThirdWaveColour = Color4Extensions.FromHex(\"44325e\");\r\n            FourthWaveColour = Color4Extensions.FromHex(\"392850\");\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Settings/KaraokeSettingsColourProvider.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Game.Overlays.Settings;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Settings.Sections;\r\nusing osuTK;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Settings;\r\n\r\npublic class KaraokeSettingsColourProvider\r\n{\r\n    public Color4 GetContentColour(SettingsSection section) => getColour(section, 0.4f, 0.6f);\r\n\r\n    public Color4 GetContent2Colour(SettingsSection section) => getColour(section, 0.4f, 0.9f);\r\n\r\n    public Color4 GetBackgroundColour(SettingsSection section) => getColour(section, 0.1f, 0.4f);\r\n\r\n    public Color4 GetBackground2Colour(SettingsSection section) => getColour(section, 0.1f, 0.2f);\r\n\r\n    public Color4 GetBackground3Colour(SettingsSection section) => getColour(section, 0.1f, 0.15f);\r\n\r\n    private Color4 getColour(SettingsSection section, float saturation, float lightness) => Color4.FromHsl(new Vector4(getBaseHue(section), saturation, lightness, 1));\r\n\r\n    private static float getBaseHue(SettingsSection section)\r\n    {\r\n        return section switch\r\n        {\r\n            ConfigSection => 200 / 360f, // Blue\r\n            StyleSection => 333 / 360f, // Pink\r\n            ScoringSection => 45 / 360f, // Orange\r\n            null => 320 / 360f, // Plum\r\n            _ => throw new ArgumentException($\"{section} colour scheme does not provide a hue value in {nameof(getBaseHue)}.\"),\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Settings/KaraokeSettingsPanel.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Game.Graphics.Containers;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Input.Bindings;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Overlays.Settings;\r\nusing osu.Game.Rulesets.Karaoke.Extensions;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Settings.Sections;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Settings;\r\n\r\npublic partial class KaraokeSettingsPanel : SettingsPanel\r\n{\r\n    public new const float WIDTH = 300;\r\n\r\n    private Box hoverBackground = null!;\r\n\r\n    protected override IEnumerable<SettingsSection> CreateSections() => new SettingsSection[]\r\n    {\r\n        new ConfigSection(),\r\n        new StyleSection(),\r\n        new ScoringSection(),\r\n    };\r\n\r\n    protected override Drawable CreateFooter() => new Container\r\n    {\r\n        Height = 130,\r\n    };\r\n\r\n    public KaraokeSettingsPanel()\r\n        : base(false)\r\n    {\r\n    }\r\n\r\n    // prevent click outside to hide the overlay\r\n    protected override bool BlockPositionalInput => false;\r\n\r\n    // prevent handle back key event every time, should call onPressed() only once.\r\n    protected override bool BlockNonPositionalInput => false;\r\n\r\n    // on press should return false to prevent handle the back key action.\r\n    public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)\r\n        => false;\r\n\r\n    // prevent let main content darker.\r\n    protected override bool DimMainContent => false;\r\n\r\n    protected override void PopIn()\r\n    {\r\n        base.PopIn();\r\n\r\n        // We use our implementation of section display, thus not needed.\r\n        Sidebar.FinishTransforms();\r\n        Sidebar.Hide();\r\n        Sidebar.MoveToX(-PANEL_WIDTH);\r\n    }\r\n\r\n    protected override void UpdateAfterChildren()\r\n    {\r\n        base.UpdateAfterChildren();\r\n\r\n        // Reset margin\r\n        ContentContainer.Margin = new MarginPadding();\r\n    }\r\n\r\n    // prevent hide the overlay.\r\n    public override void Hide() { }\r\n\r\n    public void ScrollToSection(SettingsSection settingsSection)\r\n    {\r\n        // prevent trigger scroll by config section.\r\n        if (SectionsContainer.SelectedSection.Value == settingsSection)\r\n            return;\r\n\r\n        // instead of base scroll to method, using customized method to prevent weird spacing.\r\n        // SectionsContainer.ScrollTo(settingsSection);\r\n        var scrollContainer = SectionsContainer.GetInternalChildren()?.OfType<UserTrackingScrollContainer>().FirstOrDefault();\r\n        scrollContainer?.ScrollTo(scrollContainer.GetChildPosInContent(settingsSection) - (SectionsContainer.FixedHeader?.BoundingBox.Height ?? 0));\r\n    }\r\n\r\n    public IReadOnlyList<SettingsSection> Sections => SectionsContainer.Children;\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(KaraokeSettingsColourProvider colourProvider, Bindable<SettingsSection> selectedSection, Bindable<SettingsSubsection?> selectedSubsection)\r\n    {\r\n        initialSelectionContainer();\r\n        initialContentContainer();\r\n        initialSearchTextBox();\r\n        initialBackground();\r\n\r\n        Show();\r\n\r\n        void initialSelectionContainer() =>\r\n            SectionsContainer.SelectedSection.ValueChanged += section =>\r\n            {\r\n                var newSection = section.NewValue;\r\n                hoverBackground.FadeTo(newSection != null ? 0.6f : 0, 200, Easing.OutQuint);\r\n\r\n                // Would happen when no result matches the search query.\r\n                if (newSection == null)\r\n                    return;\r\n\r\n                selectedSection.Value = newSection;\r\n            };\r\n\r\n        void initialContentContainer()\r\n        {\r\n            ContentContainer.Width = WIDTH;\r\n\r\n            selectedSection.BindValueChanged(x =>\r\n            {\r\n                var background = ContentContainer.Children.OfType<Box>().FirstOrDefault();\r\n                if (background == null)\r\n                    return;\r\n\r\n                var colour = colourProvider.GetBackground3Colour(x.NewValue);\r\n                background.Delay(200).Then().FadeColour(colour, 500);\r\n            });\r\n        }\r\n\r\n        void initialSearchTextBox()\r\n        {\r\n            if (SectionsContainer.FixedHeader is SeekLimitedSearchTextBox searchTextBox)\r\n            {\r\n                searchTextBox.Current.ValueChanged += term =>\r\n                {\r\n                    // should clear selected sub-section if change search text.\r\n                    selectedSubsection.Value = null;\r\n                };\r\n            }\r\n        }\r\n\r\n        void initialBackground()\r\n        {\r\n            var scrollContainer = SectionsContainer.GetInternalChildren()?.OfType<UserTrackingScrollContainer>().FirstOrDefault();\r\n            if (scrollContainer == null)\r\n                return;\r\n\r\n            // create hove background.\r\n            scrollContainer.Add(hoverBackground = new Box\r\n            {\r\n                Name = \"Hover highlight\",\r\n                RelativeSizeAxes = Axes.X,\r\n                Depth = 1,\r\n            });\r\n\r\n            // change background color if section changed.\r\n            selectedSection.BindValueChanged(x =>\r\n            {\r\n                var colour = colourProvider.GetBackgroundColour(x.NewValue);\r\n                hoverBackground.Delay(200).Then().FadeColour(colour, 500);\r\n            });\r\n\r\n            // move background to target sub-section if user hover to it.\r\n            selectedSubsection.BindValueChanged(x =>\r\n            {\r\n                float alpha = x.NewValue != null ? 0.6f : 0f;\r\n                hoverBackground.FadeTo(alpha, 200);\r\n\r\n                if (x.NewValue == null)\r\n                    return;\r\n\r\n                // Position adjustments\r\n                float position = (float)scrollContainer.GetChildPosInContent(x.NewValue);\r\n                hoverBackground.MoveToY(position, 300, Easing.OutQuint);\r\n                hoverBackground.ResizeHeightTo(x.NewValue.DrawHeight + 15, 300, Easing.OutQuint);\r\n            });\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Settings/KaraokeSettingsSection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Testing;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Overlays.Settings;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Settings;\r\n\r\npublic abstract partial class KaraokeSettingsSection : SettingsSection\r\n{\r\n    private const int margin = 20;\r\n\r\n    protected KaraokeSettingsSection()\r\n    {\r\n        Margin = new MarginPadding { Bottom = margin };\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(KaraokeSettingsColourProvider colourProvider)\r\n    {\r\n        var colour = colourProvider.GetContentColour(this);\r\n\r\n        // set header box and text to target color.\r\n        var headerBox = FlowContent.ChildrenOfType<Box>().FirstOrDefault(x => x.Name == \"separator\");\r\n        var title = FlowContent.ChildrenOfType<OsuSpriteText>().FirstOrDefault(x => x.Text == Header);\r\n        if (headerBox == null || title == null)\r\n            return;\r\n\r\n        headerBox.Colour = colour;\r\n        title.Colour = colour;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Settings/KaraokeSettingsSubsection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Game.Overlays.Settings;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Settings.Previews;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Settings;\r\n\r\npublic abstract partial class KaraokeSettingsSubsection : SettingsSubsection\r\n{\r\n    [Resolved]\r\n    protected KaraokeRulesetConfigManager Config { get; private set; } = null!;\r\n\r\n    [Resolved]\r\n    private Bindable<SettingsSubsection?> selectedSubsection { get; set; } = null!;\r\n\r\n    public virtual SettingsSubsectionPreview CreatePreview() => new UnderConstructionPreview();\r\n\r\n    protected override bool OnHover(HoverEvent e)\r\n    {\r\n        selectedSubsection.Value = this;\r\n        return base.OnHover(e);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Settings/KaraokeVersionManager.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Development;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Graphics.Textures;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\nusing osuTK;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Settings;\r\n\r\npublic partial class KaraokeVersionManager : VisibilityContainer\r\n{\r\n    [BackgroundDependencyLoader]\r\n    private void load(OsuColour colours, TextureStore textures)\r\n    {\r\n        AutoSizeAxes = Axes.Both;\r\n        Anchor = Anchor.BottomCentre;\r\n        Origin = Anchor.BottomCentre;\r\n\r\n        Alpha = 0;\r\n\r\n        FillFlowContainer mainFill;\r\n\r\n        Children = new Drawable[]\r\n        {\r\n            mainFill = new FillFlowContainer\r\n            {\r\n                AutoSizeAxes = Axes.Both,\r\n                Direction = FillDirection.Vertical,\r\n                Children = new Drawable[]\r\n                {\r\n                    new FillFlowContainer\r\n                    {\r\n                        AutoSizeAxes = Axes.Both,\r\n                        Direction = FillDirection.Horizontal,\r\n                        Spacing = new Vector2(5),\r\n                        Anchor = Anchor.TopCentre,\r\n                        Origin = Anchor.TopCentre,\r\n                        Children = new Drawable[]\r\n                        {\r\n                            new OsuSpriteText\r\n                            {\r\n                                Font = OsuFont.GetFont(weight: FontWeight.Bold),\r\n                                Text = new KaraokeRuleset().ShortName,\r\n                            },\r\n                            new OsuSpriteText\r\n                            {\r\n                                Colour = DebugUtils.IsDebugBuild ? colours.Red : Color4.White,\r\n                                Text = VersionUtils.DisplayVersion,\r\n                            },\r\n                        },\r\n                    },\r\n                },\r\n            },\r\n        };\r\n\r\n        if (!VersionUtils.IsDeployedBuild)\r\n        {\r\n            mainFill.AddRange(new Drawable[]\r\n            {\r\n                new OsuSpriteText\r\n                {\r\n                    Anchor = Anchor.TopCentre,\r\n                    Origin = Anchor.TopCentre,\r\n                    Font = OsuFont.Numeric.With(size: 12),\r\n                    Colour = colours.Yellow,\r\n                    Text = \"Development Build\",\r\n                },\r\n                new Sprite\r\n                {\r\n                    Anchor = Anchor.TopCentre,\r\n                    Origin = Anchor.TopCentre,\r\n                    Texture = textures.Get(\"Menu/dev-build-footer\"),\r\n                },\r\n            });\r\n        }\r\n    }\r\n\r\n    protected override void PopIn()\r\n    {\r\n        this.FadeIn(1400, Easing.OutQuint);\r\n    }\r\n\r\n    protected override void PopOut()\r\n    {\r\n        this.FadeOut(500, Easing.OutQuint);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Settings/Previews/DefaultPreview.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Extensions.Color4Extensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Settings.Previews;\r\n\r\npublic partial class DefaultPreview : SettingsSubsectionPreview\r\n{\r\n    private const double transition_time = 1000;\r\n\r\n    public FillFlowContainer TextContainer { get; }\r\n\r\n    public DefaultPreview()\r\n    {\r\n        Size = new Vector2(0.3f);\r\n\r\n        Child = TextContainer = new FillFlowContainer\r\n        {\r\n            AutoSizeAxes = Axes.Both,\r\n            Anchor = Anchor.Centre,\r\n            Origin = Anchor.Centre,\r\n            Direction = FillDirection.Vertical,\r\n            Children = new Drawable[]\r\n            {\r\n                new SpriteIcon\r\n                {\r\n                    Icon = FontAwesome.Solid.Cog,\r\n                    Anchor = Anchor.TopCentre,\r\n                    Origin = Anchor.TopCentre,\r\n                    Size = new Vector2(50),\r\n                },\r\n                new OsuSpriteText\r\n                {\r\n                    Anchor = Anchor.TopCentre,\r\n                    Origin = Anchor.TopCentre,\r\n                    Text = \"Welcome to config!\",\r\n                    Colour = ThemeColor.Lighten(0.8f),\r\n                    Font = OsuFont.GetFont(size: 32),\r\n                },\r\n                new OsuSpriteText\r\n                {\r\n                    Anchor = Anchor.TopCentre,\r\n                    Origin = Anchor.TopCentre,\r\n                    Text = \"Select left size to adjust.\",\r\n                    Font = OsuFont.GetFont(size: 20),\r\n                },\r\n            },\r\n        };\r\n    }\r\n\r\n    protected override void LoadComplete()\r\n    {\r\n        base.LoadComplete();\r\n\r\n        TextContainer.Position = new Vector2(DrawSize.X / 16, 0);\r\n\r\n        using (BeginDelayedSequence(100))\r\n        {\r\n            TextContainer.MoveTo(Vector2.Zero, transition_time, Easing.OutExpo);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Settings/Previews/Gameplay/LyricPreview.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Globalization;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Rendering;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.IO.Stores;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.IO.Stores;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Scoring;\r\nusing osu.Game.Rulesets.Karaoke.Skinning.Fonts;\r\nusing osu.Game.Rulesets.Karaoke.UI;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Settings.Previews.Gameplay;\r\n\r\npublic partial class LyricPreview : SettingsSubsectionPreview\r\n{\r\n    private readonly Bindable<FontUsage> mainFont = new();\r\n    private readonly Bindable<FontUsage> rubyFont = new();\r\n    private readonly Bindable<FontUsage> romanisationFont = new();\r\n    private readonly Bindable<FontUsage> translationFont = new();\r\n\r\n    [Resolved]\r\n    private FontStore fontStore { get; set; } = null!;\r\n\r\n    private KaraokeLocalFontStore localFontStore = null!;\r\n\r\n    private readonly LyricPlayfield lyricPlayfield;\r\n    private readonly Lyric lyric;\r\n\r\n    public LyricPreview()\r\n    {\r\n        Size = new Vector2(0.7f, 0.5f);\r\n\r\n        Child = new Container\r\n        {\r\n            RelativeSizeAxes = Axes.Both,\r\n            Padding = new MarginPadding(30),\r\n            Child = lyricPlayfield = new LyricPlayfield\r\n            {\r\n                Anchor = Anchor.Centre,\r\n                Origin = Anchor.Centre,\r\n            },\r\n        };\r\n        lyricPlayfield.Add(lyric = createPreviewLyric());\r\n\r\n        mainFont.BindValueChanged(e =>\r\n        {\r\n            addFont(e.NewValue);\r\n        });\r\n        rubyFont.BindValueChanged(e =>\r\n        {\r\n            addFont(e.NewValue);\r\n        });\r\n        romanisationFont.BindValueChanged(e =>\r\n        {\r\n            addFont(e.NewValue);\r\n        });\r\n        translationFont.BindValueChanged(e =>\r\n        {\r\n            addFont(e.NewValue);\r\n        });\r\n\r\n        void addFont(FontUsage fontUsage)\r\n            => localFontStore.AddFont(fontUsage);\r\n    }\r\n\r\n    protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)\r\n    {\r\n        var baseDependencies = new DependencyContainer(base.CreateChildDependencies(parent));\r\n        baseDependencies.Cache(new KaraokeSessionStatics(baseDependencies.Get<KaraokeRulesetConfigManager>(), null));\r\n        return baseDependencies;\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(FontManager fontManager, IRenderer renderer, KaraokeRulesetConfigManager config)\r\n    {\r\n        // create local font store and import those files\r\n        localFontStore = new KaraokeLocalFontStore(fontManager, renderer);\r\n        fontStore.AddStore(localFontStore);\r\n\r\n        // fonts\r\n        config.BindWith(KaraokeRulesetSetting.MainFont, mainFont);\r\n        config.BindWith(KaraokeRulesetSetting.RubyFont, rubyFont);\r\n        config.BindWith(KaraokeRulesetSetting.RomanisationFont, romanisationFont);\r\n        config.BindWith(KaraokeRulesetSetting.TranslationFont, translationFont);\r\n    }\r\n\r\n    protected override void Dispose(bool isDisposing)\r\n    {\r\n        base.Dispose(isDisposing);\r\n\r\n        fontStore.RemoveStore(localFontStore);\r\n    }\r\n\r\n    private Lyric createPreviewLyric()\r\n        => new()\r\n        {\r\n            Text = \"カラオケ\",\r\n            RubyTags = new[]\r\n            {\r\n                new RubyTag\r\n                {\r\n                    StartIndex = 0,\r\n                    EndIndex = 0,\r\n                    Text = \"か\",\r\n                },\r\n                new RubyTag\r\n                {\r\n                    StartIndex = 2,\r\n                    EndIndex = 2,\r\n                    Text = \"お\",\r\n                },\r\n            },\r\n            TimeTags = new List<TimeTag>\r\n            {\r\n                new(new TextIndex(0), 500)\r\n                {\r\n                    FirstSyllable = true,\r\n                    RomanisedSyllable = \"karaoke\",\r\n                },\r\n                new(new TextIndex(1), 600),\r\n                new(new TextIndex(2), 1000),\r\n                new(new TextIndex(3), 1500),\r\n                new(new TextIndex(4), 2000),\r\n            },\r\n            HitWindows = new KaraokeLyricHitWindows(),\r\n        };\r\n\r\n    private IDictionary<CultureInfo, string> createPreviewTranslation(CultureInfo cultureInfo)\r\n    {\r\n        string translation = cultureInfo.Name switch\r\n        {\r\n            \"ja\" or \"Ja-jp\" => \"カラオケ\",\r\n            \"zh-Hans\" or \"zh-CN\" or \"zh-Hant\" or \"zh-TW\" => \"卡拉OK\",\r\n            _ => \"karaoke\",\r\n        };\r\n\r\n        return new Dictionary<CultureInfo, string>\r\n        {\r\n            { cultureInfo, translation },\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Settings/Previews/Gameplay/NotePlayfieldPreview.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Lists;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Scoring;\r\nusing osu.Game.Rulesets.Karaoke.UI;\r\nusing osu.Game.Rulesets.Karaoke.UI.Components;\r\nusing osu.Game.Rulesets.Karaoke.UI.Position;\r\nusing osu.Game.Rulesets.Karaoke.UI.Scrolling;\r\nusing osu.Game.Rulesets.Timing;\r\nusing osu.Game.Rulesets.UI.Scrolling;\r\nusing osu.Game.Rulesets.UI.Scrolling.Algorithms;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Settings.Previews.Gameplay;\r\n\r\npublic partial class NotePlayfieldPreview : SettingsSubsectionPreview\r\n{\r\n    private const int columns = 9;\r\n\r\n    [Cached(typeof(IScrollingInfo))]\r\n    private readonly LocalScrollingInfo scrollingInfo = new();\r\n\r\n    [Cached(typeof(INotePositionInfo))]\r\n    private readonly PreviewNotePositionInfo notePositionInfo = new();\r\n\r\n    private readonly NotePlayfield notePlayfield;\r\n\r\n    public NotePlayfieldPreview()\r\n    {\r\n        Size = new Vector2(0.7f, 0.5f);\r\n\r\n        Child = new Container\r\n        {\r\n            RelativeSizeAxes = Axes.Both,\r\n            Padding = new MarginPadding(30),\r\n            Child = notePlayfield = new NotePlayfield(columns)\r\n            {\r\n                Anchor = Anchor.Centre,\r\n                Origin = Anchor.Centre,\r\n            },\r\n        };\r\n    }\r\n\r\n    private double lastCreateSampleTime;\r\n\r\n    protected override void Update()\r\n    {\r\n        base.Update();\r\n\r\n        if (!(Time.Current > lastCreateSampleTime + 3000))\r\n            return;\r\n\r\n        lastCreateSampleTime = Time.Current;\r\n\r\n        double startTime = Time.Current + 3000;\r\n        const double duration = 1000;\r\n\r\n        var lyric = new Lyric\r\n        {\r\n            Text = \"Note\",\r\n            TimeTags = new List<TimeTag>\r\n            {\r\n                new(new TextIndex(0), startTime),\r\n                new(new TextIndex(4), startTime + duration),\r\n            },\r\n        };\r\n\r\n        notePlayfield.Add(new Note\r\n        {\r\n            Text = \"Note\",\r\n            ReferenceLyricId = lyric.ID,\r\n            ReferenceLyric = lyric,\r\n            HitWindows = new KaraokeNoteHitWindows(),\r\n        });\r\n\r\n        notePlayfield.Add(new BarLine\r\n        {\r\n            StartTime = startTime,\r\n            Major = true,\r\n        });\r\n    }\r\n\r\n    private readonly Bindable<KaraokeScrollingDirection> configDirection = new();\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(KaraokeRulesetConfigManager config)\r\n    {\r\n        config.BindWith(KaraokeRulesetSetting.ScrollDirection, configDirection);\r\n        configDirection.BindValueChanged(direction =>\r\n        {\r\n            if (scrollingInfo.Direction is Bindable<ScrollingDirection> bindableScrollingDirection)\r\n                bindableScrollingDirection.Value = (ScrollingDirection)direction.NewValue;\r\n        }, true);\r\n\r\n        config.BindWith(KaraokeRulesetSetting.ScrollTime, scrollingInfo.TimeRange as BindableDouble);\r\n    }\r\n\r\n    private class LocalScrollingInfo : IScrollingInfo\r\n    {\r\n        public IBindable<ScrollingDirection> Direction { get; } = new Bindable<ScrollingDirection>();\r\n\r\n        public IBindable<double> TimeRange { get; } = new BindableDouble(1500);\r\n\r\n        public IBindable<IScrollAlgorithm> Algorithm { get; } = new Bindable<IScrollAlgorithm>(new SequentialScrollAlgorithm(new SortedList<MultiplierControlPoint>(Comparer<MultiplierControlPoint>.Default)));\r\n    }\r\n\r\n    private class PreviewNotePositionInfo : INotePositionInfo\r\n    {\r\n        public IBindable<NotePositionCalculator> Position { get; } =\r\n            new Bindable<NotePositionCalculator>(new NotePositionCalculator(columns, DefaultColumnBackground.COLUMN_HEIGHT, ScrollingNotePlayfield.COLUMN_SPACING));\r\n\r\n        public NotePositionCalculator Calculator => Position.Value;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Settings/Previews/Gameplay/ShowCursorPreview.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Graphics.Cursor;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Settings.Previews.Gameplay;\r\n\r\npublic partial class ShowCursorPreview : SettingsSubsectionPreview\r\n{\r\n    private readonly Bindable<bool> bindableShowCursor = new();\r\n    private readonly MenuCursorContainer.Cursor cursor;\r\n\r\n    public ShowCursorPreview()\r\n    {\r\n        Size = new Vector2(0.3f);\r\n        Children = new Drawable[]\r\n        {\r\n            cursor = new MenuCursorContainer.Cursor\r\n            {\r\n                Anchor = Anchor.Centre,\r\n                Origin = Anchor.Centre,\r\n            },\r\n            new OsuSpriteText\r\n            {\r\n                Anchor = Anchor.Centre,\r\n                Origin = Anchor.Centre,\r\n                Y = 30,\r\n                Text = \"Wanna show this while gameplay?\",\r\n            },\r\n        };\r\n\r\n        bindableShowCursor.BindValueChanged(e =>\r\n        {\r\n            bool showCursor = e.NewValue;\r\n\r\n            if (showCursor)\r\n            {\r\n                cursor.FadeTo(1, 200);\r\n            }\r\n            else\r\n            {\r\n                cursor.FadeTo(0.5f, 200);\r\n            }\r\n        }, true);\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(KaraokeRulesetConfigManager config)\r\n    {\r\n        config.BindWith(KaraokeRulesetSetting.ShowCursor, bindableShowCursor);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Settings/Previews/Graphics/ManageFontPreview.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Audio.Track;\r\nusing osu.Framework.Extensions.Color4Extensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Graphics.Textures;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Framework.Utils;\r\nusing osu.Game.Beatmaps.ControlPoints;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Containers;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Settings.Previews.Graphics;\r\n\r\npublic partial class ManageFontPreview : SettingsSubsectionPreview\r\n{\r\n    private const float preview_width = 400;\r\n    private const float preview_height = 320;\r\n\r\n    private const float angle = 30;\r\n\r\n    public ManageFontPreview()\r\n    {\r\n        ShowBackground = false;\r\n    }\r\n\r\n    private EggContainer eggContainer = null!;\r\n    private FillFlowContainer<GenerateRowContainer> textContainer = null!;\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OsuColour colours)\r\n    {\r\n        Children = new Drawable[]\r\n        {\r\n            eggContainer = new EggContainer\r\n            {\r\n                Name = \"Egg container\",\r\n                Anchor = Anchor.Centre,\r\n                Origin = Anchor.Centre,\r\n                Size = new Vector2(preview_width, preview_height),\r\n            },\r\n            new Container\r\n            {\r\n                Anchor = Anchor.Centre,\r\n                Origin = Anchor.Centre,\r\n                Size = new Vector2(preview_width, preview_height),\r\n                Masking = true,\r\n                CornerRadius = 15,\r\n                BorderThickness = 10f,\r\n                BorderColour = colours.Gray6,\r\n                Children = new Drawable[]\r\n                {\r\n                    new Box\r\n                    {\r\n                        RelativeSizeAxes = Axes.Both,\r\n                        Colour = colours.Gray3,\r\n                    },\r\n                    textContainer = new FillFlowContainer<GenerateRowContainer>\r\n                    {\r\n                        Anchor = Anchor.Centre,\r\n                        Origin = Anchor.Centre,\r\n                        Width = preview_width / (float)Math.Cos(Math.PI * angle / 180.0),\r\n                        AutoSizeAxes = Axes.Y,\r\n                        Spacing = new Vector2(10),\r\n                        Rotation = -angle,\r\n                        Children = new[]\r\n                        {\r\n                            new GenerateRowContainer(GenerateDirection.LeftToRight),\r\n                            new GenerateRowContainer(GenerateDirection.RightToLeft),\r\n                            new GenerateRowContainer(GenerateDirection.LeftToRight),\r\n                            new GenerateRowContainer(GenerateDirection.RightToLeft),\r\n                            new GenerateRowContainer(GenerateDirection.LeftToRight),\r\n                            new GenerateRowContainer(GenerateDirection.RightToLeft),\r\n                            new GenerateRowContainer(GenerateDirection.LeftToRight),\r\n                            new GenerateRowContainer(GenerateDirection.RightToLeft),\r\n                        },\r\n                    },\r\n                },\r\n            },\r\n        };\r\n\r\n        foreach (var row in textContainer.Children)\r\n        {\r\n            row.ClickedText += text =>\r\n            {\r\n                (string textureName, float scale, float yOffset) = getTexture(text);\r\n                if (string.IsNullOrEmpty(textureName))\r\n                    return;\r\n\r\n                eggContainer.GenerateEgg(textureName, scale, yOffset);\r\n\r\n                static (string, float, float) getTexture(string text)\r\n                {\r\n                    switch (text)\r\n                    {\r\n                        case \"egg\":\r\n                            return (\"Eggs/blue-easter-egg\", 1, 30);\r\n\r\n                        case \"osu!\":\r\n                        case \"lazer!\":\r\n                            return (\"Eggs/pink-easter-egg\", 1, 30);\r\n\r\n                        case \"UWU\":\r\n                            return (\"Eggs/yellow-easter-egg\", 1, 50);\r\n\r\n                        case \"karaoke!\":\r\n                        case \"カラオケ！\":\r\n                            return (\"Eggs/golden-egg\", 0.6f, 80);\r\n\r\n                        case \"\\\\andy840119/\":\r\n                            return (\"Eggs/easter-egg-roll\", 0.3f, 30);\r\n\r\n                        case \"=U=\":\r\n                            return (\"Eggs/camp-tent\", 0.5f, 50);\r\n\r\n                        default:\r\n                            return (string.Empty, 0, 0);\r\n                    }\r\n                }\r\n            };\r\n        }\r\n    }\r\n\r\n    public partial class GenerateRowContainer : BeatSyncedContainer\r\n    {\r\n        private readonly IDictionary<string, int> words = new Dictionary<string, int>\r\n        {\r\n            { \"Font\", 10 },\r\n            { \"文字\", 10 },\r\n            { \"Moji\", 10 },\r\n            { \"もじ\", 10 },\r\n            { \"Config\", 10 },\r\n            { \"Style\", 7 },\r\n            { \"karaoke!\", 5 },\r\n            { \"カラオケ！\", 5 },\r\n            { \"Random\", 2 },\r\n            { \"osu!\", 2 },\r\n            { \"lazer!\", 2 },\r\n            { \"egg\", 1 },\r\n            { \"\\\\andy840119/\", 1 },\r\n            { \"UWU\", 1 },\r\n            { \"OwO\", 1 },\r\n            { \"=U=\", 1 },\r\n            { \"(*´▽`*)\", 1 },\r\n            { \"(」・ω・)」うー！\", 1 },\r\n            { \"(／・ω・)／\", 1 },\r\n            { \"(((ﾟдﾟ)))\", 1 },\r\n            { \"( • ̀ω•́ )\", 1 },\r\n            { \"┌(┌^o^)┐\", 1 },\r\n        };\r\n\r\n        private const float moving_speed = 60;\r\n        private const float max_text_amount = 10;\r\n        private const float spacing_between_text = 20;\r\n        private const float font_size = 48;\r\n\r\n        private readonly GenerateDirection direction;\r\n\r\n        public Action<string>? ClickedText;\r\n\r\n        public GenerateRowContainer(GenerateDirection direction)\r\n        {\r\n            this.direction = direction;\r\n\r\n            RelativeSizeAxes = Axes.X;\r\n            Height = font_size;\r\n        }\r\n\r\n        protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)\r\n        {\r\n            base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes);\r\n\r\n            foreach (var text in Children)\r\n            {\r\n                text.ScaleTo(new Vector2(1.1f), 30, Easing.OutBack)\r\n                    .Then()\r\n                    .ScaleTo(1, 20, Easing.OutBack);\r\n            }\r\n        }\r\n\r\n        protected override void Update()\r\n        {\r\n            base.Update();\r\n\r\n            var lastChild = Children.LastOrDefault();\r\n            bool generateNewObject = lastChild == null || isAllTextPartAppear(lastChild, direction);\r\n            if (generateNewObject && Children.Count < max_text_amount)\r\n                createNewText();\r\n\r\n            static bool isAllTextPartAppear(Drawable text, GenerateDirection direction)\r\n            {\r\n                bool startFromLeft = direction == GenerateDirection.LeftToRight;\r\n                float textEndPositionX = text.X + text.DrawWidth / 2 * (startFromLeft ? -1 : 1);\r\n                return startFromLeft ? textEndPositionX > spacing_between_text : textEndPositionX < -spacing_between_text;\r\n            }\r\n        }\r\n\r\n        protected override bool OnClick(ClickEvent e)\r\n        {\r\n            foreach (var spriteText in Children.OfType<OsuSpriteText>())\r\n            {\r\n                if (spriteText.ReceivePositionalInputAt(e.ScreenSpaceMousePosition))\r\n                    ClickedText?.Invoke(spriteText.Text.ToString());\r\n            }\r\n\r\n            return base.OnClick(e);\r\n        }\r\n\r\n        private void createNewText()\r\n        {\r\n            bool startFromLeft = direction == GenerateDirection.LeftToRight;\r\n            var text = new OsuSpriteText\r\n            {\r\n                Anchor = startFromLeft ? Anchor.CentreLeft : Anchor.CentreRight,\r\n                Origin = Anchor.Centre,\r\n                Text = getRandomText(),\r\n                Colour = getRandomColour(),\r\n                Font = OsuFont.Default.With(size: font_size),\r\n            };\r\n            Add(text);\r\n\r\n            // set start position\r\n            float fontWidth = text.DrawWidth;\r\n            text.X = fontWidth * (startFromLeft ? -0.5f : 0.5f);\r\n\r\n            // set moving transform.\r\n            float moveLength = DrawWidth + fontWidth + spacing_between_text;\r\n            float movePosition = startFromLeft ? moveLength : -moveLength;\r\n            float duration = moveLength / moving_speed * 1000;\r\n            text.MoveToOffset(new Vector2(movePosition, 0), duration).Then().Expire();\r\n\r\n            string getRandomText()\r\n            {\r\n                int maxNumber = words.Values.Sum();\r\n                int randomNumber = RNG.Next(maxNumber - 1);\r\n\r\n                foreach ((string key, int value) in words)\r\n                {\r\n                    if (value >= randomNumber)\r\n                        return key;\r\n\r\n                    randomNumber -= value;\r\n                }\r\n\r\n                return \":Bug:\";\r\n            }\r\n\r\n            static Colour4 getRandomColour()\r\n            {\r\n                int randomNumber = RNG.Next(1, 359);\r\n                return Color4Extensions.FromHSV(randomNumber, 0.2f, 0.7f);\r\n            }\r\n        }\r\n    }\r\n\r\n    public partial class EggContainer : BeatSyncedContainer\r\n    {\r\n        [Resolved]\r\n        private TextureStore textures { get; set; } = null!;\r\n\r\n        public void GenerateEgg(string textureName, float scale, float yOffset)\r\n        {\r\n            var texture = textures.Get(textureName);\r\n            if (texture == null)\r\n                return;\r\n\r\n            var drawableEgg = new Container\r\n            {\r\n                Scale = new Vector2(scale),\r\n                Child = new Sprite\r\n                {\r\n                    Origin = Anchor.BottomCentre,\r\n                    Y = yOffset,\r\n                    Texture = texture,\r\n                },\r\n            };\r\n            Add(drawableEgg);\r\n\r\n            // moving around the corner.\r\n            float width = DrawWidth;\r\n            float height = DrawHeight;\r\n            const int speed = 100;\r\n            drawableEgg.MoveToOffset(new Vector2(width, 0), width / speed * 1000).Then()\r\n                       .RotateTo(90, 300, Easing.In).MoveToOffset(new Vector2(0, height), height / speed * 1000).Then()\r\n                       .RotateTo(180, 300, Easing.In).MoveToOffset(new Vector2(-width, 0), width / speed * 1000).Then()\r\n                       .RotateTo(270, 300, Easing.In).MoveToOffset(new Vector2(0, -height), height / speed * 1000).Then()\r\n                       .RotateTo(520, 1000, Easing.In).ScaleTo(0, 1000, Easing.In).Expire();\r\n\r\n            // swing effect.\r\n            drawableEgg.Child.RotateTo(-15, 500, Easing.In).Then()\r\n                       .RotateTo(15, 500, Easing.In).Loop();\r\n        }\r\n\r\n        protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)\r\n        {\r\n            base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes);\r\n\r\n            foreach (var text in Children.OfType<Container>())\r\n            {\r\n                text.Child.MoveToOffset(new Vector2(0, -15), 100, Easing.OutBack)\r\n                    .Then()\r\n                    .MoveToOffset(new Vector2(0, 15), 100, Easing.OutBack);\r\n            }\r\n        }\r\n    }\r\n\r\n    public enum GenerateDirection\r\n    {\r\n        LeftToRight,\r\n\r\n        RightToLeft,\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Settings/Previews/Input/MicrophoneDevicePreview.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing ManagedBass;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Input;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Settings.Previews.Input;\r\n\r\npublic partial class MicrophoneDevicePreview : SettingsSubsectionPreview\r\n{\r\n    private readonly Bindable<string> bindableMicrophoneDeviceName = new();\r\n\r\n    public MicrophoneDevicePreview()\r\n    {\r\n        ShowBackground = false;\r\n        bindableMicrophoneDeviceName.BindValueChanged(x =>\r\n        {\r\n            // Find index by selection id\r\n            var microphoneList = new MicrophoneManager().MicrophoneDeviceNames.ToList();\r\n            int deviceIndex = microphoneList.IndexOf(x.NewValue);\r\n\r\n            bool hasDevice = microphoneList.Any();\r\n            string deviceName = deviceIndex == Bass.DefaultDevice ? \"Default microphone device\" : x.NewValue;\r\n\r\n            Child = new MicrophoneInputManager(deviceIndex)\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n                Child = new MicrophoneSoundVisualizer\r\n                {\r\n                    Anchor = Anchor.Centre,\r\n                    Origin = Anchor.Centre,\r\n                    HasDevice = hasDevice,\r\n                    DeviceName = deviceName,\r\n                },\r\n            };\r\n        }, true);\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(KaraokeRulesetConfigManager config)\r\n    {\r\n        config.BindWith(KaraokeRulesetSetting.MicrophoneDevice, bindableMicrophoneDeviceName);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Settings/Previews/Input/MicrophoneSoundVisualizer.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Extensions.Color4Extensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osuTK;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Settings.Previews.Input;\r\n\r\npublic partial class MicrophoneSoundVisualizer : CompositeDrawable\r\n{\r\n    private const float max_decibel = 100;\r\n    private const float max_pitch = 60;\r\n\r\n    private readonly Box background;\r\n    private readonly MicrophoneInfo microphoneInfo;\r\n    private readonly DecibelVisualizer decibelVisualizer;\r\n    private readonly PitchVisualizer pitchVisualizer;\r\n\r\n    public MicrophoneSoundVisualizer()\r\n    {\r\n        Width = 310;\r\n        Height = 100;\r\n        Masking = true;\r\n        CornerRadius = 5f;\r\n        InternalChildren = new Drawable[]\r\n        {\r\n            background = new Box\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n            },\r\n            new GridContainer\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n                RowDimensions = new[]\r\n                {\r\n                    new Dimension(GridSizeMode.Relative, 0.6f),\r\n                    new Dimension(GridSizeMode.Relative, 0.2f),\r\n                    new Dimension(GridSizeMode.Relative, 0.2f),\r\n                },\r\n                Content = new[]\r\n                {\r\n                    new Drawable[]\r\n                    {\r\n                        microphoneInfo = new MicrophoneInfo\r\n                        {\r\n                            RelativeSizeAxes = Axes.Both,\r\n                        },\r\n                    },\r\n                    new Drawable[]\r\n                    {\r\n                        decibelVisualizer = new DecibelVisualizer\r\n                        {\r\n                            Anchor = Anchor.Centre,\r\n                            Origin = Anchor.Centre,\r\n                            Y = 2,\r\n                        },\r\n                    },\r\n                    new Drawable[]\r\n                    {\r\n                        pitchVisualizer = new PitchVisualizer\r\n                        {\r\n                            Anchor = Anchor.Centre,\r\n                            Origin = Anchor.Centre,\r\n                        },\r\n                    },\r\n                },\r\n            },\r\n        };\r\n\r\n        updateDeviceInfo();\r\n    }\r\n\r\n    private string deviceName = string.Empty;\r\n\r\n    public string DeviceName\r\n    {\r\n        get => deviceName;\r\n        set\r\n        {\r\n            if (deviceName == value)\r\n                return;\r\n\r\n            deviceName = value;\r\n            updateDeviceInfo();\r\n        }\r\n    }\r\n\r\n    private bool hasDevice;\r\n\r\n    public bool HasDevice\r\n    {\r\n        get => hasDevice;\r\n        set\r\n        {\r\n            if (hasDevice == value)\r\n                return;\r\n\r\n            hasDevice = value;\r\n            updateDeviceInfo();\r\n        }\r\n    }\r\n\r\n    private void updateDeviceInfo()\r\n    {\r\n        microphoneInfo.DeviceName = HasDevice ? DeviceName : \"Seems no microphone device.\";\r\n        microphoneInfo.HasDevice = HasDevice;\r\n    }\r\n\r\n    protected override bool Handle(UIEvent e)\r\n    {\r\n        return e switch\r\n        {\r\n            MicrophoneStartPitchingEvent microphoneStartPitching => OnMicrophoneStartSinging(microphoneStartPitching),\r\n            MicrophoneEndPitchingEvent microphoneEndPitching => OnMicrophoneEndSinging(microphoneEndPitching),\r\n            MicrophonePitchingEvent microphonePitching => OnMicrophoneSinging(microphonePitching),\r\n            _ => base.Handle(e),\r\n        };\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OsuColour colours)\r\n    {\r\n        background.Colour = colours.Gray2;\r\n    }\r\n\r\n    protected virtual bool OnMicrophoneStartSinging(MicrophoneStartPitchingEvent e)\r\n    {\r\n        return false;\r\n    }\r\n\r\n    protected virtual bool OnMicrophoneEndSinging(MicrophoneEndPitchingEvent e)\r\n    {\r\n        decibelVisualizer.Decibel = 0;\r\n        pitchVisualizer.Pitch = 0;\r\n\r\n        return false;\r\n    }\r\n\r\n    protected virtual bool OnMicrophoneSinging(MicrophonePitchingEvent e)\r\n    {\r\n        var voice = e.CurrentState.Microphone.Voice;\r\n        float decibel = voice.Decibel;\r\n        float pitch = voice.Pitch;\r\n\r\n        // todo : should convert to better value.\r\n        decibelVisualizer.Decibel = decibel;\r\n        pitchVisualizer.Pitch = pitch / 8;\r\n\r\n        return false;\r\n    }\r\n\r\n    internal partial class MicrophoneInfo : CompositeDrawable\r\n    {\r\n        private readonly Box background;\r\n        private readonly SpriteIcon microphoneIcon;\r\n        private readonly OsuSpriteText deviceName;\r\n\r\n        [Resolved]\r\n        private OsuColour colours { get; set; } = null!;\r\n\r\n        public MicrophoneInfo()\r\n        {\r\n            InternalChildren = new Drawable[]\r\n            {\r\n                background = new Box\r\n                {\r\n                    RelativeSizeAxes = Axes.Both,\r\n                },\r\n                new FillFlowContainer\r\n                {\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    Padding = new MarginPadding\r\n                    {\r\n                        Top = 20,\r\n                        Left = 15,\r\n                    },\r\n                    Spacing = new Vector2(15),\r\n                    Direction = FillDirection.Horizontal,\r\n                    Children = new Drawable[]\r\n                    {\r\n                        microphoneIcon = new SpriteIcon\r\n                        {\r\n                            Size = new Vector2(24),\r\n                            Icon = FontAwesome.Solid.Microphone,\r\n                        },\r\n                        deviceName = new TruncatingSpriteText\r\n                        {\r\n                            Width = 250,\r\n                            Font = OsuFont.Default.With(size: 20),\r\n                            Text = \"Microphone name\",\r\n                        },\r\n                    },\r\n                },\r\n            };\r\n        }\r\n\r\n        public string DeviceName\r\n        {\r\n            set => deviceName.Text = value;\r\n        }\r\n\r\n        public bool HasDevice\r\n        {\r\n            set =>\r\n                Schedule(() =>\r\n                {\r\n                    microphoneIcon.Colour = value ? colours.GrayF : colours.RedLight;\r\n                });\r\n        }\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(OsuColour colours)\r\n        {\r\n            background.Colour = colours.Gray3;\r\n        }\r\n    }\r\n\r\n    internal partial class DecibelVisualizer : CompositeDrawable\r\n    {\r\n        private const float var_width = 294;\r\n\r\n        private readonly Box background;\r\n        private readonly Box decibelMarker;\r\n        private readonly Box decibelRippleMarker;\r\n        private readonly Box maxDecibelMarker;\r\n\r\n        public DecibelVisualizer()\r\n        {\r\n            Width = var_width;\r\n            Height = 8;\r\n            InternalChildren = new Drawable[]\r\n            {\r\n                background = new Box\r\n                {\r\n                    RelativeSizeAxes = Axes.Both,\r\n                },\r\n                decibelMarker = new Box\r\n                {\r\n                    RelativeSizeAxes = Axes.Y,\r\n                },\r\n                decibelRippleMarker = new Box\r\n                {\r\n                    RelativeSizeAxes = Axes.Y,\r\n                },\r\n                maxDecibelMarker = new Box\r\n                {\r\n                    RelativeSizeAxes = Axes.Y,\r\n                    Width = 5,\r\n                },\r\n            };\r\n        }\r\n\r\n        private float decibel;\r\n\r\n        private float maxDecibel;\r\n\r\n        public float Decibel\r\n        {\r\n            get => decibel;\r\n            set\r\n            {\r\n                if (decibel == value)\r\n                    return;\r\n\r\n                decibel = value;\r\n                if (decibel > maxDecibel)\r\n                    maxDecibel = decibel;\r\n\r\n                if (decibel > rippleDecibel)\r\n                    rippleDecibel = value;\r\n\r\n                decibelMarker.Width = calculatePosition(Decibel);\r\n                maxDecibelMarker.X = calculatePosition(maxDecibel);\r\n            }\r\n        }\r\n\r\n        private float rippleDecibel;\r\n\r\n        protected override void Update()\r\n        {\r\n            base.Update();\r\n\r\n            if (rippleDecibel <= 0)\r\n                return;\r\n\r\n            //1% of extra bar length to make it a little faster when bar is almost at it's minimum\r\n            rippleDecibel *= 0.99f;\r\n\r\n            // just make value to 0 if too small;\r\n            if (rippleDecibel < 0.5)\r\n                rippleDecibel = 0;\r\n\r\n            decibelRippleMarker.Width = rippleDecibel;\r\n        }\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(OsuColour colours)\r\n        {\r\n            background.Colour = colours.Gray5;\r\n            decibelMarker.Colour = colours.GrayD;\r\n            decibelRippleMarker.Colour = colours.GrayA;\r\n            maxDecibelMarker.Colour = colours.Red;\r\n        }\r\n\r\n        private static float calculatePosition(float decibel)\r\n            => decibel / max_decibel * var_width;\r\n    }\r\n\r\n    internal partial class PitchVisualizer : CompositeDrawable\r\n    {\r\n        private const int dot_width = 5;\r\n        private const int dot_height = 10;\r\n        private const int dot_amount = 30;\r\n        private const float spacing = 5;\r\n\r\n        private readonly PitchDot currentDot;\r\n\r\n        public PitchVisualizer()\r\n        {\r\n            AutoSizeAxes = Axes.Both;\r\n\r\n            // todo : draw that stupid shapes with progressive background color.\r\n            InternalChildren = new Drawable[]\r\n            {\r\n                new FillFlowContainer\r\n                {\r\n                    AutoSizeAxes = Axes.Both,\r\n                    Spacing = new Vector2(spacing),\r\n                    Children = Enumerable.Range(0, dot_amount).Select(i => new PitchDot\r\n                    {\r\n                        Colour = calculateDotColour(i, 0.8f),\r\n                    }).ToArray(),\r\n                },\r\n                currentDot = new PitchDot\r\n                {\r\n                    Alpha = 0,\r\n                },\r\n            };\r\n        }\r\n\r\n        private float pitch;\r\n\r\n        private bool showPitch;\r\n\r\n        public float Pitch\r\n        {\r\n            get => pitch;\r\n            set\r\n            {\r\n                if (EqualityComparer<float>.Default.Equals(pitch, value))\r\n                    return;\r\n\r\n                pitch = value;\r\n\r\n                // adjust dot position\r\n                currentDot.X = calculateDotPosition((int)pitch);\r\n\r\n                // adjust show / hide.\r\n                bool show = pitch != 0;\r\n                if (showPitch == show)\r\n                    return;\r\n\r\n                showPitch = show;\r\n\r\n                if (show)\r\n                {\r\n                    currentDot.FadeIn(200);\r\n                }\r\n                else\r\n                {\r\n                    currentDot.FadeOut(200);\r\n                }\r\n            }\r\n        }\r\n\r\n        private Color4 calculateDotColour(int index, float s)\r\n        {\r\n            const float start_v = 0.4f;\r\n            const float end_v = 0.7f;\r\n            float v = (end_v - start_v) / dot_amount * index + start_v;\r\n            return Color4Extensions.FromHSV(0, s, v);\r\n        }\r\n\r\n        private float calculateDotPosition(int index)\r\n            => (dot_width + spacing) * index;\r\n\r\n        public partial class PitchDot : CompositeDrawable\r\n        {\r\n            private readonly CircularContainer circle;\r\n\r\n            public PitchDot()\r\n            {\r\n                Size = new Vector2(dot_width, dot_height);\r\n                InternalChildren = new[]\r\n                {\r\n                    circle = new CircularContainer\r\n                    {\r\n                        Anchor = Anchor.Centre,\r\n                        Origin = Anchor.Centre,\r\n                        Masking = true,\r\n                        Alpha = 0,\r\n                        RelativeSizeAxes = Axes.Both,\r\n                        Size = new Vector2(0.8f, 0),\r\n                        Children = new[]\r\n                        {\r\n                            new Box\r\n                            {\r\n                                Colour = Color4.White,\r\n                                RelativeSizeAxes = Axes.Both,\r\n                            },\r\n                        },\r\n                    },\r\n                };\r\n            }\r\n\r\n            protected override void LoadComplete()\r\n            {\r\n                base.LoadComplete();\r\n                circle.FadeIn(500, Easing.OutQuint);\r\n                circle.ResizeTo(new Vector2(0.8f), 500, Easing.OutQuint);\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Settings/Previews/SettingsSubsectionPreview.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Settings.Previews;\r\n\r\npublic abstract partial class SettingsSubsectionPreview : Container\r\n{\r\n    private const double transition_time = 1000;\r\n\r\n    private readonly Container boxContainer;\r\n    private readonly Box background;\r\n    private readonly Container content;\r\n\r\n    protected override Container<Drawable> Content => content;\r\n\r\n    protected virtual Color4 ThemeColor => getColourFor(GetType());\r\n\r\n    protected SettingsSubsectionPreview()\r\n    {\r\n        RelativeSizeAxes = Axes.Both;\r\n        Anchor = Anchor.Centre;\r\n        Origin = Anchor.Centre;\r\n\r\n        InternalChildren = new Drawable[]\r\n        {\r\n            boxContainer = new Container\r\n            {\r\n                CornerRadius = 20,\r\n                Masking = true,\r\n                RelativeSizeAxes = Axes.Both,\r\n                Anchor = Anchor.Centre,\r\n                Origin = Anchor.Centre,\r\n                Children = new Drawable[]\r\n                {\r\n                    background = new Box\r\n                    {\r\n                        RelativeSizeAxes = Axes.Both,\r\n\r\n                        Colour = ThemeColor,\r\n                        Alpha = 0.2f,\r\n                        Blending = BlendingParameters.Additive,\r\n                    },\r\n                    content = new Container\r\n                    {\r\n                        RelativeSizeAxes = Axes.Both,\r\n                    },\r\n                },\r\n            },\r\n        };\r\n    }\r\n\r\n    protected override void LoadComplete()\r\n    {\r\n        base.LoadComplete();\r\n\r\n        boxContainer.Hide();\r\n        boxContainer.ScaleTo(0.2f);\r\n        boxContainer.RotateTo(-20);\r\n\r\n        using (BeginDelayedSequence(100))\r\n        {\r\n            boxContainer.ScaleTo(1, transition_time, Easing.OutElastic);\r\n            boxContainer.RotateTo(0, transition_time / 2, Easing.OutQuint);\r\n            boxContainer.FadeIn(transition_time, Easing.OutExpo);\r\n        }\r\n    }\r\n\r\n    private bool showBackground;\r\n\r\n    protected bool ShowBackground\r\n    {\r\n        get => showBackground;\r\n        set\r\n        {\r\n            showBackground = value;\r\n\r\n            if (showBackground)\r\n            {\r\n                background.Show();\r\n            }\r\n            else\r\n            {\r\n                background.Hide();\r\n            }\r\n        }\r\n    }\r\n\r\n    private static Color4 getColourFor(object type)\r\n    {\r\n        int hash = type.GetHashCode();\r\n        byte r = (byte)Math.Clamp(((hash & 0xFF0000) >> 16) * 0.8f, 20, 255);\r\n        byte g = (byte)Math.Clamp(((hash & 0x00FF00) >> 8) * 0.8f, 20, 255);\r\n        byte b = (byte)Math.Clamp((hash & 0x0000FF) * 0.8f, 20, 255);\r\n        return new Color4(r, g, b, 255);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Settings/Previews/UnderConstructionPreview.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Extensions.Color4Extensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Settings.Previews;\r\n\r\npublic partial class UnderConstructionPreview : SettingsSubsectionPreview\r\n{\r\n    private const double transition_time = 1000;\r\n\r\n    public FillFlowContainer TextContainer { get; }\r\n\r\n    public UnderConstructionPreview()\r\n    {\r\n        Size = new Vector2(0.3f);\r\n\r\n        Child = TextContainer = new FillFlowContainer\r\n        {\r\n            AutoSizeAxes = Axes.Both,\r\n            Anchor = Anchor.Centre,\r\n            Origin = Anchor.Centre,\r\n            Direction = FillDirection.Vertical,\r\n            Children = new Drawable[]\r\n            {\r\n                new SpriteIcon\r\n                {\r\n                    Icon = FontAwesome.Solid.UniversalAccess,\r\n                    Anchor = Anchor.TopCentre,\r\n                    Origin = Anchor.TopCentre,\r\n                    Size = new Vector2(50),\r\n                },\r\n                new OsuSpriteText\r\n                {\r\n                    Anchor = Anchor.TopCentre,\r\n                    Origin = Anchor.TopCentre,\r\n                    Text = \"Oops\",\r\n                    Colour = ThemeColor.Lighten(0.8f),\r\n                    Font = OsuFont.GetFont(size: 36),\r\n                },\r\n                new OsuSpriteText\r\n                {\r\n                    Anchor = Anchor.TopCentre,\r\n                    Origin = Anchor.TopCentre,\r\n                    Text = \"this preview is not yet ready for use!\",\r\n                    Font = OsuFont.GetFont(size: 20),\r\n                },\r\n                new OsuSpriteText\r\n                {\r\n                    Anchor = Anchor.TopCentre,\r\n                    Origin = Anchor.TopCentre,\r\n                    Text = \"please check back a bit later.\",\r\n                    Font = OsuFont.GetFont(size: 14),\r\n                },\r\n            },\r\n        };\r\n    }\r\n\r\n    protected override void LoadComplete()\r\n    {\r\n        base.LoadComplete();\r\n\r\n        TextContainer.Position = new Vector2(DrawSize.X / 16, 0);\r\n\r\n        using (BeginDelayedSequence(100))\r\n        {\r\n            TextContainer.MoveTo(Vector2.Zero, transition_time, Easing.OutExpo);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Settings/Sections/ConfigSection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Settings.Sections.Gameplay;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Settings.Sections;\r\n\r\npublic partial class ConfigSection : KaraokeSettingsSection\r\n{\r\n    public override LocalisableString Header => \"Config\";\r\n\r\n    public override Drawable CreateIcon() => new SpriteIcon\r\n    {\r\n        Icon = FontAwesome.Solid.Cog,\r\n    };\r\n\r\n    public ConfigSection()\r\n    {\r\n        Children = new Drawable[]\r\n        {\r\n            new GeneralSettings(),\r\n            new NoteSettings(),\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Settings/Sections/Gameplay/GeneralSettings.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Overlays.Settings;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Settings.Previews;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Settings.Previews.Gameplay;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Settings.Sections.Gameplay;\r\n\r\npublic partial class GeneralSettings : KaraokeSettingsSubsection\r\n{\r\n    protected override LocalisableString Header => \"General\";\r\n\r\n    public override SettingsSubsectionPreview CreatePreview() => new ShowCursorPreview();\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        Children = new[]\r\n        {\r\n            new SettingsCheckbox\r\n            {\r\n                LabelText = \"Show cursor while playing\",\r\n                Current = Config.GetBindable<bool>(KaraokeRulesetSetting.ShowCursor),\r\n            },\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Settings/Sections/Gameplay/NoteSettings.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Overlays.Settings;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Settings.Previews;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Settings.Previews.Gameplay;\r\nusing osu.Game.Rulesets.Karaoke.UI;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Settings.Sections.Gameplay;\r\n\r\npublic partial class NoteSettings : KaraokeSettingsSubsection\r\n{\r\n    protected override LocalisableString Header => \"Note\";\r\n\r\n    public override SettingsSubsectionPreview CreatePreview() => new NotePlayfieldPreview();\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        Children = new Drawable[]\r\n        {\r\n            new SettingsEnumDropdown<KaraokeScrollingDirection>\r\n            {\r\n                LabelText = \"Scrolling direction\",\r\n                Current = Config.GetBindable<KaraokeScrollingDirection>(KaraokeRulesetSetting.ScrollDirection),\r\n            },\r\n            new SettingsSlider<double, TimeSlider>\r\n            {\r\n                LabelText = \"Scroll speed\",\r\n                Current = Config.GetBindable<double>(KaraokeRulesetSetting.ScrollTime),\r\n            },\r\n            new SettingsCheckbox\r\n            {\r\n                LabelText = \"Display ruby text\",\r\n                Current = Config.GetBindable<bool>(KaraokeRulesetSetting.DisplayNoteRubyText),\r\n            },\r\n        };\r\n    }\r\n\r\n    private partial class TimeSlider : RoundedSliderBar<double>\r\n    {\r\n        public override LocalisableString TooltipText => Current.Value.ToString(\"N0\") + \"ms\";\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Settings/Sections/Gameplay/ScoringSettings.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Overlays.Settings;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Settings.Sections.Gameplay;\r\n\r\npublic partial class ScoringSettings : KaraokeSettingsSubsection\r\n{\r\n    protected override LocalisableString Header => \"Scoring\";\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        // todo : should separate scoring and pitch part?\r\n        Children = new Drawable[]\r\n        {\r\n            new SettingsCheckbox\r\n            {\r\n                LabelText = \"Override pitch at gameplay\",\r\n                Current = Config.GetBindable<bool>(KaraokeRulesetSetting.OverridePitchAtGameplay),\r\n            },\r\n            new SettingsSlider<int, PitchSlider>\r\n            {\r\n                LabelText = \"Pitch\",\r\n                Current = Config.GetBindable<int>(KaraokeRulesetSetting.Pitch),\r\n            },\r\n            new SettingsCheckbox\r\n            {\r\n                LabelText = \"Override vocal pitch at gameplay\",\r\n                Current = Config.GetBindable<bool>(KaraokeRulesetSetting.OverrideVocalPitchAtGameplay),\r\n            },\r\n            new SettingsSlider<int, PitchSlider>\r\n            {\r\n                LabelText = \"Vocal pitch\",\r\n                Current = Config.GetBindable<int>(KaraokeRulesetSetting.VocalPitch),\r\n            },\r\n            new SettingsCheckbox\r\n            {\r\n                LabelText = \"Override scoring pitch at gameplay\",\r\n                Current = Config.GetBindable<bool>(KaraokeRulesetSetting.OverrideScoringPitchAtGameplay),\r\n            },\r\n            new SettingsSlider<int, PitchSlider>\r\n            {\r\n                LabelText = \"scoring pitch\",\r\n                Current = Config.GetBindable<int>(KaraokeRulesetSetting.ScoringPitch),\r\n            },\r\n        };\r\n    }\r\n\r\n    private partial class PitchSlider : RoundedSliderBar<int>\r\n    {\r\n        public override LocalisableString TooltipText => (Current.Value >= 0 ? \"+\" : string.Empty) + Current.Value.ToString(\"N0\");\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Settings/Sections/Graphics/LyricFontSettings.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Overlays.Settings;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Settings.Previews;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Settings.Previews.Gameplay;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Settings.Sections.Graphics;\r\n\r\npublic partial class LyricFontSettings : KaraokeSettingsSubsection\r\n{\r\n    protected override LocalisableString Header => \"Lyric font\";\r\n\r\n    public override SettingsSubsectionPreview CreatePreview() => new LyricPreview();\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        Children = new Drawable[]\r\n        {\r\n            new SettingsFont\r\n            {\r\n                LabelText = \"Default main font\",\r\n                Current = Config.GetBindable<FontUsage>(KaraokeRulesetSetting.MainFont),\r\n            },\r\n            new SettingsFont\r\n            {\r\n                LabelText = \"Default ruby font\",\r\n                Current = Config.GetBindable<FontUsage>(KaraokeRulesetSetting.RubyFont),\r\n            },\r\n            new SettingsSlider<int>\r\n            {\r\n                LabelText = \"Ruby margin\",\r\n                Current = Config.GetBindable<int>(KaraokeRulesetSetting.RubyMargin),\r\n                KeyboardStep = 1,\r\n            },\r\n            new SettingsFont\r\n            {\r\n                LabelText = \"Default romanisation font\",\r\n                Current = Config.GetBindable<FontUsage>(KaraokeRulesetSetting.RomanisationFont),\r\n            },\r\n            new SettingsSlider<int>\r\n            {\r\n                LabelText = \"Romanisation margin\",\r\n                Current = Config.GetBindable<int>(KaraokeRulesetSetting.RomanisationMargin),\r\n                KeyboardStep = 1,\r\n            },\r\n            new SettingsCheckbox\r\n            {\r\n                LabelText = \"Force use default lyric font.\",\r\n                TooltipText = \"Force use default font even has customize font in skin or beatmap.\",\r\n                Current = Config.GetBindable<bool>(KaraokeRulesetSetting.ForceUseDefaultFont),\r\n            },\r\n            new SettingsFont\r\n            {\r\n                LabelText = \"Translation font\",\r\n                Current = Config.GetBindable<FontUsage>(KaraokeRulesetSetting.TranslationFont),\r\n            },\r\n            new SettingsCheckbox\r\n            {\r\n                LabelText = \"Force use default translation font.\",\r\n                TooltipText = \"Force use default font even has customize font in skin or beatmap.\",\r\n                Current = Config.GetBindable<bool>(KaraokeRulesetSetting.ForceUseDefaultTranslationFont),\r\n            },\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Settings/Sections/Graphics/ManageFontSettings.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Localisation;\r\nusing osu.Framework.Platform;\r\nusing osu.Game.Overlays.Settings;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Settings.Previews;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Settings.Previews.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Skinning.Fonts;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Settings.Sections.Graphics;\r\n\r\npublic partial class ManageFontSettings : KaraokeSettingsSubsection\r\n{\r\n    protected override LocalisableString Header => \"Font Management\";\r\n\r\n    public override SettingsSubsectionPreview CreatePreview() => new ManageFontPreview();\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(Storage storage)\r\n    {\r\n        Children = new Drawable[]\r\n        {\r\n            new SettingsButton\r\n            {\r\n                Text = \"Open import text folder\",\r\n                TooltipText = \"After open the folder, you can drag the font file to the folder you wants to import\",\r\n                Action = () => storage.GetStorageForDirectory(FontManager.FONT_BASE_PATH).PresentExternally(),\r\n            },\r\n            new SettingsButton\r\n            {\r\n                Text = \"Import file\",\r\n                TooltipText = \"If some font is placed into folder but not import, press here to try again.\",\r\n            },\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Settings/Sections/Graphics/NoteFontSettings.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Overlays.Settings;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Settings.Sections.Graphics;\r\n\r\npublic partial class NoteFontSettings : KaraokeSettingsSubsection\r\n{\r\n    protected override LocalisableString Header => \"Note font\";\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        Children = new Drawable[]\r\n        {\r\n            new SettingsFont\r\n            {\r\n                LabelText = \"Note font\",\r\n                Current = Config.GetBindable<FontUsage>(KaraokeRulesetSetting.NoteFont),\r\n            },\r\n            new SettingsCheckbox\r\n            {\r\n                LabelText = \"Force use default note font.\",\r\n                TooltipText = \"Override any custom font in skin or beatmap.\",\r\n                Current = Config.GetBindable<bool>(KaraokeRulesetSetting.ForceUseDefaultNoteFont),\r\n            },\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Settings/Sections/Graphics/TransparentSettings.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Overlays.Settings;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Settings.Sections.Graphics;\r\n\r\npublic partial class TransparentSettings : KaraokeSettingsSubsection\r\n{\r\n    protected override LocalisableString Header => \"Transparent\";\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        Children = new Drawable[]\r\n        {\r\n            new SettingsSlider<double>\r\n            {\r\n                LabelText = \"Lyric playfield alpha\",\r\n                Current = Config.GetBindable<double>(KaraokeRulesetSetting.LyricAlpha),\r\n                KeyboardStep = 0.01f,\r\n                DisplayAsPercentage = true,\r\n            },\r\n            new SettingsSlider<double>\r\n            {\r\n                LabelText = \"Note playfield alpha\",\r\n                Current = Config.GetBindable<double>(KaraokeRulesetSetting.NoteAlpha),\r\n                KeyboardStep = 0.01f,\r\n                DisplayAsPercentage = true,\r\n            },\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Settings/Sections/Input/MicrophoneSettings.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Settings.Previews;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Settings.Previews.Input;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Settings.Sections.Input;\r\n\r\npublic partial class MicrophoneSettings : KaraokeSettingsSubsection\r\n{\r\n    protected override LocalisableString Header => \"Microphone\";\r\n\r\n    public override SettingsSubsectionPreview CreatePreview() => new MicrophoneDevicePreview();\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        Children = new Drawable[]\r\n        {\r\n            new SettingsMicrophoneDeviceDropdown\r\n            {\r\n                LabelText = \"Microphone devices\",\r\n                Current = Config.GetBindable<string>(KaraokeRulesetSetting.MicrophoneDevice),\r\n            },\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Settings/Sections/ScoringSection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Settings.Sections.Gameplay;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Settings.Sections.Input;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Settings.Sections;\r\n\r\npublic partial class ScoringSection : KaraokeSettingsSection\r\n{\r\n    public override LocalisableString Header => \"Scoring\";\r\n\r\n    public override Drawable CreateIcon() => new SpriteIcon\r\n    {\r\n        Icon = FontAwesome.Solid.Gamepad,\r\n    };\r\n\r\n    public ScoringSection()\r\n    {\r\n        Children = new Drawable[]\r\n        {\r\n            new MicrophoneSettings(),\r\n            new ScoringSettings(),\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Settings/Sections/StyleSection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Settings.Sections.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Settings.Sections;\r\n\r\npublic partial class StyleSection : KaraokeSettingsSection\r\n{\r\n    public override LocalisableString Header => \"Style\";\r\n\r\n    public override Drawable CreateIcon() => new SpriteIcon\r\n    {\r\n        Icon = FontAwesome.Solid.PaintBrush,\r\n    };\r\n\r\n    public StyleSection()\r\n    {\r\n        Children = new Drawable[]\r\n        {\r\n            new TransparentSettings(),\r\n            new LyricFontSettings(),\r\n            new NoteFontSettings(),\r\n            new ManageFontSettings(),\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Settings/SettingsFont.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Extensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Cursor;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Game.Graphics.UserInterfaceV2;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Overlays.Settings;\r\nusing osu.Game.Rulesets.Karaoke.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.UserInterfaceV2;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Settings;\r\n\r\npublic partial class SettingsFont : SettingsItem<FontUsage>\r\n{\r\n    protected override Drawable CreateControl() => new FontSelectionButton\r\n    {\r\n        RelativeSizeAxes = Axes.X,\r\n    };\r\n\r\n    private partial class FontSelectionButton : CompositeDrawable, IHasCurrentValue<FontUsage>, IHasPopover\r\n    {\r\n        private const float height = 30;\r\n\r\n        private readonly BindableWithCurrent<FontUsage> current = new();\r\n        private BindableFontUsage? bindableFontUsage;\r\n\r\n        private readonly GridContainer grid;\r\n        private readonly SettingsButton fontButton;\r\n        private readonly SettingsButton decreaseFontSizeButton;\r\n        private readonly SettingsButton increaseFontSizeButton;\r\n\r\n        private float[] availableSizes = FontUtils.DefaultFontSize();\r\n\r\n        public Bindable<FontUsage> Current\r\n        {\r\n            get => current.Current;\r\n            set\r\n            {\r\n                current.Current = value;\r\n                bindableFontUsage = value as BindableFontUsage;\r\n\r\n                availableSizes = bindableFontUsage != null\r\n                    ? FontUtils.DefaultFontSize(bindableFontUsage.MinFontSize, bindableFontUsage.MaxFontSize)\r\n                    : FontUtils.DefaultFontSize();\r\n\r\n                bool showSizeButton = availableSizes.Length > 1;\r\n                decreaseFontSizeButton.Alpha = showSizeButton ? 1 : 0;\r\n                increaseFontSizeButton.Alpha = showSizeButton ? 1 : 0;\r\n\r\n                int spacing = showSizeButton ? 5 : 0;\r\n                float buttonWidth = showSizeButton ? height : 0;\r\n                grid.ColumnDimensions = new[]\r\n                {\r\n                    new Dimension(),\r\n                    new Dimension(GridSizeMode.Absolute, spacing),\r\n                    new Dimension(GridSizeMode.Absolute, buttonWidth),\r\n                    new Dimension(GridSizeMode.Absolute, spacing),\r\n                    new Dimension(GridSizeMode.Absolute, buttonWidth),\r\n                };\r\n            }\r\n        }\r\n\r\n        public FontSelectionButton()\r\n        {\r\n            AutoSizeAxes = Axes.Y;\r\n            InternalChild = grid = new GridContainer\r\n            {\r\n                RelativeSizeAxes = Axes.X,\r\n                AutoSizeAxes = Axes.Y,\r\n                RowDimensions = new[]\r\n                {\r\n                    new Dimension(GridSizeMode.AutoSize),\r\n                },\r\n                Content = new[]\r\n                {\r\n                    new[]\r\n                    {\r\n                        fontButton = new SettingsButton\r\n                        {\r\n                            RelativeSizeAxes = Axes.X,\r\n                            Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS },\r\n                            Height = height,\r\n                            Action = this.ShowPopover,\r\n                        },\r\n                        null,\r\n                        decreaseFontSizeButton = new SettingsButton\r\n                        {\r\n                            RelativeSizeAxes = Axes.X,\r\n                            Padding = new MarginPadding(),\r\n                            Height = height,\r\n                            Text = \"-\",\r\n                            Action = () =>\r\n                            {\r\n                                float currentSize = current.Value.Size;\r\n                                float nextSize = availableSizes.Reverse().FirstOrDefault(x => x < currentSize);\r\n                                if (nextSize == 0)\r\n                                    return;\r\n\r\n                                current.Value = current.Value.With(size: nextSize);\r\n                            },\r\n                        },\r\n                        null,\r\n                        increaseFontSizeButton = new SettingsButton\r\n                        {\r\n                            RelativeSizeAxes = Axes.X,\r\n                            Padding = new MarginPadding(),\r\n                            Height = height,\r\n                            Text = \"+\",\r\n                            Action = () =>\r\n                            {\r\n                                float currentSize = current.Value.Size;\r\n                                float nextSize = availableSizes.FirstOrDefault(x => x > currentSize);\r\n                                if (nextSize == 0)\r\n                                    return;\r\n\r\n                                current.Value = current.Value.With(size: nextSize);\r\n                            },\r\n                        },\r\n                    },\r\n                },\r\n            };\r\n\r\n            Current.BindValueChanged(e =>\r\n            {\r\n                var font = e.NewValue;\r\n                string fontName = font.FontName;\r\n                string size = FontUtils.GetText(font.Size);\r\n                string fixedWidthText = font.FixedWidth ? \"(fixed width)\" : string.Empty;\r\n                string displayText = $\"{fontName}, {size} {fixedWidthText}\";\r\n                fontButton.Text = displayText;\r\n            });\r\n        }\r\n\r\n        public Popover GetPopover()\r\n        {\r\n            // note: should return BindableFontUsage first for restrict the size range in the FontSelector\r\n            return new FontSelectorPopover(bindableFontUsage ?? Current);\r\n        }\r\n    }\r\n\r\n    private partial class FontSelectorPopover : OsuPopover\r\n    {\r\n        public FontSelectorPopover(Bindable<FontUsage> bindableFontUsage)\r\n        {\r\n            Child = new FontSelector\r\n            {\r\n                Width = 1000,\r\n                Height = 600,\r\n                Current = bindableFontUsage,\r\n            };\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Settings/SettingsMicrophoneDeviceDropdown.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Input;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Localisation;\r\nusing osu.Game.Overlays.Settings;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Settings;\r\n\r\npublic partial class SettingsMicrophoneDeviceDropdown : SettingsDropdown<string>\r\n{\r\n    protected override OsuDropdown<string> CreateDropdown() => new MicrophoneDeviceDropdownControl();\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        var microphoneManager = new MicrophoneManager();\r\n\r\n        var deviceItems = new List<string> { string.Empty };\r\n        deviceItems.AddRange(microphoneManager.MicrophoneDeviceNames);\r\n\r\n        Items = deviceItems.Distinct().ToList();\r\n    }\r\n\r\n    private partial class MicrophoneDeviceDropdownControl : DropdownControl\r\n    {\r\n        protected override LocalisableString GenerateItemText(string item)\r\n            => string.IsNullOrEmpty(item) ? CommonStrings.Default : base.GenerateItemText(item);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Skin/Config/ConfigScreen.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Skinning;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Skin.Config;\r\n\r\npublic partial class ConfigScreen : KaraokeSkinEditorScreen\r\n{\r\n    [Cached]\r\n    protected readonly LyricFontInfoManager LyricFontInfoManager;\r\n\r\n    public ConfigScreen(ISkin skin)\r\n        : base(skin, KaraokeSkinEditorScreenMode.Config)\r\n    {\r\n        AddInternal(LyricFontInfoManager = new LyricFontInfoManager());\r\n    }\r\n\r\n    protected override Section[] CreateSelectionContainer()\r\n        => Array.Empty<Section>();\r\n\r\n    protected override Section[] CreatePropertiesContainer()\r\n        => new Section[]\r\n        {\r\n            new IntervalSection(),\r\n            new PositionSection(),\r\n            new RubyAndRomanisationSection(),\r\n        };\r\n\r\n    protected override Container CreatePreviewArea()\r\n        => new();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Skin/Config/IntervalSection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.UserInterfaceV2;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Skin.Config;\r\n\r\ninternal partial class IntervalSection : LyricConfigSection\r\n{\r\n    private readonly LabelledRealTimeSliderBar<int> lyricIntervalSliderBar;\r\n    private readonly LabelledRealTimeSliderBar<int> rubyIntervalSliderBar;\r\n    private readonly LabelledRealTimeSliderBar<int> romanisationIntervalSliderBar;\r\n\r\n    protected override LocalisableString Title => \"Interval\";\r\n\r\n    public IntervalSection()\r\n    {\r\n        Children = new[]\r\n        {\r\n            lyricIntervalSliderBar = new LabelledRealTimeSliderBar<int>\r\n            {\r\n                Label = \"Lyrics interval\",\r\n                Description = \"Lyrics interval section\",\r\n                Current = new BindableNumber<int>\r\n                {\r\n                    MinValue = 0,\r\n                    MaxValue = 30,\r\n                    Value = 10,\r\n                    Default = 10,\r\n                },\r\n            },\r\n            rubyIntervalSliderBar = new LabelledRealTimeSliderBar<int>\r\n            {\r\n                Label = \"Ruby interval\",\r\n                Description = \"Ruby interval section\",\r\n                Current = new BindableNumber<int>\r\n                {\r\n                    MinValue = 0,\r\n                    MaxValue = 30,\r\n                    Value = 10,\r\n                    Default = 10,\r\n                },\r\n            },\r\n            romanisationIntervalSliderBar = new LabelledRealTimeSliderBar<int>\r\n            {\r\n                Label = \"Romanisation interval\",\r\n                Description = \"Romanisation interval section\",\r\n                Current = new BindableNumber<int>\r\n                {\r\n                    MinValue = 0,\r\n                    MaxValue = 30,\r\n                    Value = 10,\r\n                    Default = 10,\r\n                },\r\n            },\r\n        };\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(LyricFontInfoManager lyricFontInfoManager)\r\n    {\r\n        lyricFontInfoManager.LoadedLyricFontInfo.BindValueChanged(e =>\r\n        {\r\n            var lyricFontInfo = e.NewValue;\r\n            applyCurrent(lyricIntervalSliderBar.Current, lyricFontInfo.LyricsInterval);\r\n            applyCurrent(rubyIntervalSliderBar.Current, lyricFontInfo.RubyInterval);\r\n            applyCurrent(romanisationIntervalSliderBar.Current, lyricFontInfo.RomanisationInterval);\r\n\r\n            static void applyCurrent<T>(Bindable<T> bindable, T value)\r\n                => bindable.Value = bindable.Default = value;\r\n        }, true);\r\n\r\n        lyricIntervalSliderBar.Current.BindValueChanged(x => lyricFontInfoManager.ApplyCurrentLyricFontInfoChange(l => l.LyricsInterval = x.NewValue));\r\n        rubyIntervalSliderBar.Current.BindValueChanged(x => lyricFontInfoManager.ApplyCurrentLyricFontInfoChange(l => l.RubyInterval = x.NewValue));\r\n        romanisationIntervalSliderBar.Current.BindValueChanged(x => lyricFontInfoManager.ApplyCurrentLyricFontInfoChange(l => l.RomanisationInterval = x.NewValue));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Skin/Config/LyricConfigSection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Skin.Config;\r\n\r\ninternal abstract partial class LyricConfigSection : Section;\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Skin/Config/LyricFontInfoManager.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Skinning;\r\nusing osu.Game.Rulesets.Karaoke.Skinning.Elements;\r\nusing osu.Game.Skinning;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Skin.Config;\r\n\r\npublic partial class LyricFontInfoManager : Component\r\n{\r\n    public readonly BindableList<LyricFontInfo> Configs = new();\r\n\r\n    public readonly Bindable<LyricFontInfo> LoadedLyricFontInfo = new();\r\n\r\n    public readonly Bindable<LyricFontInfo> EditLyricFontInfo = new();\r\n\r\n    [Resolved]\r\n    private ISkinSource source { get; set; } = null!;\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        var lookup = new KaraokeSkinLookup(ElementType.LyricFontInfo);\r\n        var lyricFontInfo = source.GetConfig<KaraokeSkinLookup, LyricFontInfo>(lookup)?.Value;\r\n        if (lyricFontInfo == null)\r\n            return;\r\n\r\n        Configs.Add(lyricFontInfo);\r\n\r\n        LoadedLyricFontInfo.Value = Configs.First();\r\n        EditLyricFontInfo.Value = Configs.First();\r\n    }\r\n\r\n    public void ApplyCurrentLyricFontInfoChange(Action<LyricFontInfo> action)\r\n    {\r\n        action(LoadedLyricFontInfo.Value);\r\n        LoadedLyricFontInfo.TriggerChange();\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Skin/Config/PositionSection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Graphics.UserInterfaceV2;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Skin.Config;\r\n\r\ninternal partial class PositionSection : LyricConfigSection\r\n{\r\n    private readonly LabelledEnumDropdown<KaraokeTextSmartHorizon> smartHorizonDropdown;\r\n\r\n    protected override LocalisableString Title => \"Position\";\r\n\r\n    public PositionSection()\r\n    {\r\n        Children = new Drawable[]\r\n        {\r\n            smartHorizonDropdown = new LabelledEnumDropdown<KaraokeTextSmartHorizon>(true)\r\n            {\r\n                Label = \"Smart horizon\",\r\n                Description = \"Smart horizon section\",\r\n            },\r\n        };\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(LyricFontInfoManager lyricFontInfoManager)\r\n    {\r\n        lyricFontInfoManager.LoadedLyricFontInfo.BindValueChanged(e =>\r\n        {\r\n            var lyricFontInfo = e.NewValue;\r\n            applyCurrent(smartHorizonDropdown.Current, lyricFontInfo.SmartHorizon);\r\n\r\n            static void applyCurrent<T>(Bindable<T> bindable, T value)\r\n                => bindable.Value = bindable.Default = value;\r\n        }, true);\r\n\r\n        smartHorizonDropdown.Current.BindValueChanged(x => lyricFontInfoManager.ApplyCurrentLyricFontInfoChange(l => l.SmartHorizon = x.NewValue));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Skin/Config/RubyAndRomanisationSection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Graphics.UserInterfaceV2;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.UserInterfaceV2;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Skin.Config;\r\n\r\ninternal partial class RubyAndRomanisationSection : LyricConfigSection\r\n{\r\n    private readonly LabelledEnumDropdown<LyricTextAlignment> rubyAlignmentDropdown;\r\n    private readonly LabelledEnumDropdown<LyricTextAlignment> romanisationAlignmentDropdown;\r\n    private readonly LabelledRealTimeSliderBar<int> rubyMarginSliderBar;\r\n    private readonly LabelledRealTimeSliderBar<int> romanisationMarginSliderBar;\r\n\r\n    protected override LocalisableString Title => \"Ruby/Romanisation\";\r\n\r\n    public RubyAndRomanisationSection()\r\n    {\r\n        Children = new Drawable[]\r\n        {\r\n            rubyAlignmentDropdown = new LabelledEnumDropdown<LyricTextAlignment>(true)\r\n            {\r\n                Label = \"Ruby alignment\",\r\n                Description = \"Ruby alignment section\",\r\n            },\r\n            romanisationAlignmentDropdown = new LabelledEnumDropdown<LyricTextAlignment>(true)\r\n            {\r\n                Label = \"Romanisation alignment\",\r\n                Description = \"Romanisation alignment section\",\r\n            },\r\n            rubyMarginSliderBar = new LabelledRealTimeSliderBar<int>\r\n            {\r\n                Label = \"Ruby margin\",\r\n                Description = \"Ruby margin section\",\r\n                Current = new BindableNumber<int>\r\n                {\r\n                    MinValue = 0,\r\n                    MaxValue = 30,\r\n                    Value = 10,\r\n                    Default = 10,\r\n                },\r\n            },\r\n            romanisationMarginSliderBar = new LabelledRealTimeSliderBar<int>\r\n            {\r\n                Label = \"Romanisation margin\",\r\n                Description = \"Romanisation margin section\",\r\n                Current = new BindableNumber<int>\r\n                {\r\n                    MinValue = 0,\r\n                    MaxValue = 30,\r\n                    Value = 10,\r\n                    Default = 10,\r\n                },\r\n            },\r\n        };\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(LyricFontInfoManager lyricFontInfoManager)\r\n    {\r\n        lyricFontInfoManager.LoadedLyricFontInfo.BindValueChanged(e =>\r\n        {\r\n            var lyricFontInfo = e.NewValue;\r\n            applyCurrent(rubyAlignmentDropdown.Current, lyricFontInfo.RubyAlignment);\r\n            applyCurrent(romanisationAlignmentDropdown.Current, lyricFontInfo.RomanisationAlignment);\r\n            applyCurrent(rubyMarginSliderBar.Current, lyricFontInfo.RubyMargin);\r\n            applyCurrent(romanisationMarginSliderBar.Current, lyricFontInfo.RomanisationMargin);\r\n\r\n            static void applyCurrent<T>(Bindable<T> bindable, T value)\r\n                => bindable.Value = bindable.Default = value;\r\n        }, true);\r\n\r\n        rubyAlignmentDropdown.Current.BindValueChanged(x => lyricFontInfoManager.ApplyCurrentLyricFontInfoChange(l => l.RubyAlignment = x.NewValue));\r\n        romanisationAlignmentDropdown.Current.BindValueChanged(x => lyricFontInfoManager.ApplyCurrentLyricFontInfoChange(l => l.RomanisationAlignment = x.NewValue));\r\n        rubyMarginSliderBar.Current.BindValueChanged(x => lyricFontInfoManager.ApplyCurrentLyricFontInfoChange(l => l.RubyMargin = x.NewValue));\r\n        romanisationMarginSliderBar.Current.BindValueChanged(x => lyricFontInfoManager.ApplyCurrentLyricFontInfoChange(l => l.RomanisationMargin = x.NewValue));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Skin/KaraokeSkinEditor.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Skin.Config;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Skin.Style;\r\nusing osu.Game.Skinning;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Skin;\r\n\r\npublic partial class KaraokeSkinEditor : GenericEditor<KaraokeSkinEditorScreenMode>\r\n{\r\n    [Cached]\r\n    private readonly OverlayColourProvider colourProvider = new(OverlayColourScheme.Pink);\r\n\r\n    private readonly ISkin skin;\r\n\r\n    public KaraokeSkinEditor(ISkin skin)\r\n    {\r\n        this.skin = skin;\r\n    }\r\n\r\n    protected override GenericEditorScreen<KaraokeSkinEditorScreenMode> GenerateScreen(KaraokeSkinEditorScreenMode screenMode) =>\r\n        screenMode switch\r\n        {\r\n            KaraokeSkinEditorScreenMode.Config => new ConfigScreen(skin),\r\n            KaraokeSkinEditorScreenMode.Style => new StyleScreen(skin),\r\n            _ => throw new InvalidOperationException(\"Editor menu bar switched to an unsupported mode\"),\r\n        };\r\n\r\n    protected override MenuItem[] GenerateMenuItems(KaraokeSkinEditorScreenMode screenMode)\r\n    {\r\n        // todo: waiting for implementation.\r\n        return Array.Empty<MenuItem>();\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Skin/KaraokeSkinEditorScreen.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Game.Graphics.Containers;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit;\r\nusing osu.Game.Skinning;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Skin;\r\n\r\npublic abstract partial class KaraokeSkinEditorScreen : GenericEditorScreen<KaraokeSkinEditorScreenMode>\r\n{\r\n    private const float section_scale = 0.75f;\r\n    private const float left_column_width = 200;\r\n    private const float right_column_width = 300;\r\n\r\n    private readonly ISkin skin;\r\n\r\n    protected KaraokeSkinEditorScreen(ISkin skin, KaraokeSkinEditorScreenMode type)\r\n        : base(type)\r\n    {\r\n        this.skin = skin;\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OverlayColourProvider colourProvider)\r\n    {\r\n        AddInternal(new SkinProvidingContainer(skin)\r\n        {\r\n            Children = new[]\r\n            {\r\n                new Container\r\n                {\r\n                    Anchor = Anchor.CentreLeft,\r\n                    Origin = Anchor.CentreLeft,\r\n                    Width = left_column_width,\r\n                    RelativeSizeAxes = Axes.Y,\r\n                    Children = new Drawable[]\r\n                    {\r\n                        new Box\r\n                        {\r\n                            Name = \"Background\",\r\n                            Colour = colourProvider.Background2,\r\n                            RelativeSizeAxes = Axes.Both,\r\n                        },\r\n                        new OsuScrollContainer\r\n                        {\r\n                            RelativeSizeAxes = Axes.Both,\r\n                            Scale = new Vector2(section_scale),\r\n                            Size = new Vector2(1 / section_scale),\r\n                            Child = new FillFlowContainer<Section>\r\n                            {\r\n                                RelativeSizeAxes = Axes.X,\r\n                                AutoSizeAxes = Axes.Y,\r\n                                Children = CreateSelectionContainer(),\r\n                            },\r\n                        },\r\n                    },\r\n                },\r\n                new Container\r\n                {\r\n                    Anchor = Anchor.CentreRight,\r\n                    Origin = Anchor.CentreRight,\r\n                    Width = right_column_width,\r\n                    RelativeSizeAxes = Axes.Y,\r\n                    Children = new Drawable[]\r\n                    {\r\n                        new Box\r\n                        {\r\n                            Name = \"Background\",\r\n                            Colour = colourProvider.Background2,\r\n                            RelativeSizeAxes = Axes.Both,\r\n                        },\r\n                        new OsuScrollContainer\r\n                        {\r\n                            RelativeSizeAxes = Axes.Both,\r\n                            Scale = new Vector2(section_scale),\r\n                            Size = new Vector2(1 / section_scale),\r\n                            Child = new FillFlowContainer<Section>\r\n                            {\r\n                                RelativeSizeAxes = Axes.X,\r\n                                AutoSizeAxes = Axes.Y,\r\n                                Children = CreatePropertiesContainer(),\r\n                            },\r\n                        },\r\n                    },\r\n                },\r\n                new Container\r\n                {\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    Padding = new MarginPadding { Left = left_column_width, Right = right_column_width },\r\n                    Child = CreatePreviewArea(),\r\n                },\r\n            },\r\n        });\r\n    }\r\n\r\n    /// <summary>\r\n    /// Create all sections with selectable options.\r\n    /// </summary>\r\n    /// <returns></returns>\r\n    protected abstract Section[] CreateSelectionContainer();\r\n\r\n    /// <summary>\r\n    /// Create properties for the skin part.\r\n    /// </summary>\r\n    /// <returns></returns>\r\n    protected abstract Section[] CreatePropertiesContainer();\r\n\r\n    /// <summary>\r\n    /// Create preview container.\r\n    /// </summary>\r\n    /// <returns></returns>\r\n    protected abstract Drawable CreatePreviewArea();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Skin/KaraokeSkinEditorScreenMode.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.ComponentModel;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Skin;\r\n\r\npublic enum KaraokeSkinEditorScreenMode\r\n{\r\n    [Description(\"Config\")]\r\n    Config,\r\n\r\n    [Description(\"Style\")]\r\n    Style,\r\n\r\n    [Description(\"Layout\")]\r\n    Layout,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Skin/Style/LyricColorSection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Graphics.UserInterfaceV2;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.UserInterfaceV2;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Skin.Style;\r\n\r\ninternal partial class LyricColorSection : StyleSection\r\n{\r\n    private readonly LabelledEnumDropdown<ColorArea> colorAreaDropdown;\r\n    private readonly LabelledColourSelector colorPicker;\r\n\r\n    protected override LocalisableString Title => \"Color\";\r\n\r\n    public LyricColorSection()\r\n    {\r\n        Children = new Drawable[]\r\n        {\r\n            colorAreaDropdown = new LabelledEnumDropdown<ColorArea>(true)\r\n            {\r\n                Label = \"Color area\",\r\n                Description = \"Select the area you wish to adjust.\",\r\n            },\r\n            colorPicker = new LabelledColourSelector\r\n            {\r\n                Label = \"Color\",\r\n                Description = \"Select color.\",\r\n            },\r\n        };\r\n    }\r\n}\r\n\r\npublic enum ColorArea\r\n{\r\n    Front_Text,\r\n\r\n    Front_Border,\r\n\r\n    Front_Shadow,\r\n\r\n    Back_Text,\r\n\r\n    Back_Border,\r\n\r\n    Back_Shadow,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Skin/Style/LyricFontSection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Graphics.UserInterfaceV2;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.UserInterfaceV2;\r\nusing osu.Game.Skinning;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Skin.Style;\r\n\r\ninternal partial class LyricFontSection : StyleSection\r\n{\r\n    private readonly LabelledEnumDropdown<Font> fontDropdown;\r\n    private readonly LabelledSwitchButton boldSwitchButton;\r\n    private readonly LabelledRealTimeSliderBar<float> fontSizeSliderBar;\r\n    private readonly LabelledRealTimeSliderBar<int> borderSliderBar;\r\n\r\n    protected override LocalisableString Title => \"Font\";\r\n\r\n    public LyricFontSection()\r\n    {\r\n        Children = new Drawable[]\r\n        {\r\n            fontDropdown = new LabelledEnumDropdown<Font>(true)\r\n            {\r\n                Label = \"Font\",\r\n                Description = \"Select display font.\",\r\n            },\r\n            boldSwitchButton = new LabelledSwitchButton\r\n            {\r\n                Label = \"Bold\",\r\n                Description = \"Select bold or not.\",\r\n            },\r\n            fontSizeSliderBar = new LabelledRealTimeSliderBar<float>\r\n            {\r\n                Label = \"Font size\",\r\n                Description = \"Adjust font size.\",\r\n                Current = new BindableFloat\r\n                {\r\n                    Value = 30,\r\n                    MinValue = 10,\r\n                    MaxValue = 70,\r\n                },\r\n            },\r\n            borderSliderBar = new LabelledRealTimeSliderBar<int>\r\n            {\r\n                Label = \"Border size\",\r\n                Description = \"Adjust border size.\",\r\n                Current = new BindableInt\r\n                {\r\n                    Value = 10,\r\n                    MinValue = 0,\r\n                    MaxValue = 20,\r\n                },\r\n            },\r\n        };\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(SkinManager manager)\r\n    {\r\n    }\r\n}\r\n\r\npublic enum Font\r\n{\r\n    F001,\r\n\r\n    F002,\r\n\r\n    F003,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Skin/Style/LyricShadowSection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Graphics.UserInterfaceV2;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.UserInterfaceV2;\r\nusing osu.Game.Skinning;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Skin.Style;\r\n\r\ninternal partial class LyricShadowSection : StyleSection\r\n{\r\n    private readonly LabelledSwitchButton displayShaderSwitchButton;\r\n    private readonly LabelledRealTimeSliderBar<float> shadowXSliderBar;\r\n    private readonly LabelledRealTimeSliderBar<float> shadowYSliderBar;\r\n\r\n    protected override LocalisableString Title => \"Shadow\";\r\n\r\n    public LyricShadowSection()\r\n    {\r\n        Children = new Drawable[]\r\n        {\r\n            displayShaderSwitchButton = new LabelledSwitchButton\r\n            {\r\n                Label = \"Shadow\",\r\n                Description = \"Display shadow or not.\",\r\n            },\r\n            shadowXSliderBar = new LabelledRealTimeSliderBar<float>\r\n            {\r\n                Label = \"Shadow X\",\r\n                Description = \"Adjust shadow x position.\",\r\n                Current = new BindableFloat\r\n                {\r\n                    Value = 10,\r\n                    MinValue = 0,\r\n                    MaxValue = 20,\r\n                },\r\n            },\r\n            shadowYSliderBar = new LabelledRealTimeSliderBar<float>\r\n            {\r\n                Label = \"Shadow Y\",\r\n                Description = \"Adjust shadow y position.\",\r\n                Current = new BindableFloat\r\n                {\r\n                    Value = 10,\r\n                    MinValue = 0,\r\n                    MaxValue = 20,\r\n                },\r\n            },\r\n        };\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(SkinManager manager)\r\n    {\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Skin/Style/LyricStylePreview.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Drawables;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Skin.Style;\r\n\r\ninternal partial class LyricStylePreview : CompositeDrawable\r\n{\r\n    [BackgroundDependencyLoader]\r\n    private void load(OverlayColourProvider colourProvider)\r\n    {\r\n        Masking = true;\r\n        CornerRadius = 15;\r\n        FillMode = FillMode.Fit;\r\n        FillAspectRatio = 1.25f;\r\n        InternalChildren = new Drawable[]\r\n        {\r\n            new Box\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n                Colour = colourProvider.Background1,\r\n            },\r\n            new PreviewDrawableLyricLine(createDefaultLyricLine()),\r\n        };\r\n    }\r\n\r\n    private Lyric createDefaultLyricLine()\r\n    {\r\n        double startTime = Time.Current;\r\n\r\n        return new Lyric\r\n        {\r\n            Text = \"カラオケ！\",\r\n            TimeTags = new List<TimeTag>\r\n            {\r\n                new(new TextIndex(0), startTime + 500),\r\n                new(new TextIndex(1), startTime + 600)\r\n                {\r\n                    FirstSyllable = true,\r\n                    RomanisedSyllable = \"ra\",\r\n                },\r\n                new(new TextIndex(2), startTime + 1000),\r\n                new(new TextIndex(3), startTime + 1500)\r\n                {\r\n                    FirstSyllable = true,\r\n                    RomanisedSyllable = \"ke\",\r\n                },\r\n                new(new TextIndex(4), startTime + 2000),\r\n            },\r\n            RubyTags = new[]\r\n            {\r\n                new RubyTag\r\n                {\r\n                    StartIndex = 0,\r\n                    EndIndex = 0,\r\n                    Text = \"か\",\r\n                },\r\n                new RubyTag\r\n                {\r\n                    StartIndex = 2,\r\n                    EndIndex = 2,\r\n                    Text = \"お\",\r\n                },\r\n            },\r\n        };\r\n    }\r\n\r\n    private partial class PreviewDrawableLyricLine : DrawableLyric\r\n    {\r\n        public PreviewDrawableLyricLine(Lyric hitObject)\r\n            : base(hitObject)\r\n        {\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Skin/Style/NoteColorSection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.UserInterfaceV2;\r\nusing osu.Game.Skinning;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Skin.Style;\r\n\r\ninternal partial class NoteColorSection : StyleSection\r\n{\r\n    private readonly LabelledColourSelector noteColorPicker;\r\n    private readonly LabelledColourSelector blinkColorPicker;\r\n\r\n    protected override LocalisableString Title => \"Color\";\r\n\r\n    public NoteColorSection()\r\n    {\r\n        Children = new Drawable[]\r\n        {\r\n            noteColorPicker = new LabelledColourSelector\r\n            {\r\n                Label = \"Note color\",\r\n                Description = \"Select color.\",\r\n            },\r\n            blinkColorPicker = new LabelledColourSelector\r\n            {\r\n                Label = \"Blink color\",\r\n                Description = \"Select color.\",\r\n            },\r\n        };\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(SkinManager manager)\r\n    {\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Skin/Style/NoteFontSection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Graphics.UserInterfaceV2;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.UserInterfaceV2;\r\nusing osu.Game.Skinning;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Skin.Style;\r\n\r\ninternal partial class NoteFontSection : StyleSection\r\n{\r\n    private readonly LabelledColourSelector textColorPicker;\r\n    private readonly LabelledSwitchButton boldSwitchButton;\r\n\r\n    protected override LocalisableString Title => \"Font\";\r\n\r\n    public NoteFontSection()\r\n    {\r\n        Children = new Drawable[]\r\n        {\r\n            textColorPicker = new LabelledColourSelector\r\n            {\r\n                Label = \"Color\",\r\n                Description = \"Select color.\",\r\n            },\r\n            boldSwitchButton = new LabelledSwitchButton\r\n            {\r\n                Label = \"Bold\",\r\n                Description = \"Select bold or not.\",\r\n            },\r\n        };\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(SkinManager manager)\r\n    {\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Skin/Style/NoteStylePreview.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.UI;\r\nusing osu.Game.Rulesets.Karaoke.UI.Components;\r\nusing osu.Game.Rulesets.Karaoke.UI.Position;\r\nusing osu.Game.Rulesets.Karaoke.UI.Scrolling;\r\nusing osu.Game.Rulesets.UI.Scrolling;\r\nusing osu.Game.Rulesets.UI.Scrolling.Algorithms;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Skin.Style;\r\n\r\npublic partial class NoteStylePreview : CompositeDrawable\r\n{\r\n    private const int columns = 9;\r\n\r\n    [Cached(typeof(IScrollingInfo))]\r\n    private readonly PreviewScrollingInfo scrollingInfo = new();\r\n\r\n    [Cached(typeof(INotePositionInfo))]\r\n    private readonly PreviewNotePositionInfo positionCalculator = new();\r\n\r\n    protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)\r\n    {\r\n        var config = Dependencies.Get<KaraokeRulesetConfigManager>();\r\n        var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));\r\n        dependencies.Cache(new KaraokeSessionStatics(config, null));\r\n\r\n        return dependencies;\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OverlayColourProvider colourProvider)\r\n    {\r\n        Masking = true;\r\n        CornerRadius = 15;\r\n        FillMode = FillMode.Fit;\r\n        FillAspectRatio = 2f;\r\n        InternalChildren = new Drawable[]\r\n        {\r\n            new Box\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n                Colour = colourProvider.Background1,\r\n            },\r\n            new PreviewDrawableNoteArea\r\n            {\r\n                Anchor = Anchor.Centre,\r\n                Origin = Anchor.Centre,\r\n            },\r\n        };\r\n    }\r\n\r\n    public partial class PreviewDrawableNoteArea : NotePlayfield\r\n    {\r\n        public PreviewDrawableNoteArea()\r\n            : base(columns)\r\n        {\r\n        }\r\n    }\r\n\r\n    public class PreviewScrollingInfo : IScrollingInfo\r\n    {\r\n        public IBindable<ScrollingDirection> Direction { get; } = new Bindable<ScrollingDirection>();\r\n\r\n        public IBindable<double> TimeRange { get; } = new BindableDouble();\r\n\r\n        public IBindable<IScrollAlgorithm> Algorithm { get; } = new Bindable<IScrollAlgorithm>(new ZeroScrollAlgorithm());\r\n\r\n        private class ZeroScrollAlgorithm : IScrollAlgorithm\r\n        {\r\n            protected const double START_TIME = 1000000000;\r\n\r\n            public double GetDisplayStartTime(double originTime, float offset, double timeRange, float scrollLength)\r\n                => double.MinValue;\r\n\r\n            public float GetLength(double startTime, double endTime, double timeRange, float scrollLength)\r\n                => scrollLength;\r\n\r\n            public float PositionAt(double time, double currentTime, double timeRange, float scrollLength, double? originTime = null)\r\n                => (float)((time - START_TIME) / timeRange) * scrollLength;\r\n\r\n            public double TimeAt(float position, double currentTime, double timeRange, float scrollLength)\r\n                => 0;\r\n\r\n            public void Reset()\r\n            {\r\n            }\r\n        }\r\n    }\r\n\r\n    private class PreviewNotePositionInfo : INotePositionInfo\r\n    {\r\n        public IBindable<NotePositionCalculator> Position { get; } =\r\n            new Bindable<NotePositionCalculator>(new NotePositionCalculator(columns, DefaultColumnBackground.COLUMN_HEIGHT, ScrollingNotePlayfield.COLUMN_SPACING));\r\n\r\n        public NotePositionCalculator Calculator => Position.Value;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Skin/Style/StyleScreen.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Skinning;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Skin.Style;\r\n\r\npublic partial class StyleScreen : KaraokeSkinEditorScreen\r\n{\r\n    public StyleScreen(ISkin skin)\r\n        : base(skin, KaraokeSkinEditorScreenMode.Style)\r\n    {\r\n    }\r\n\r\n    protected override Section[] CreateSelectionContainer()\r\n        => Array.Empty<Section>();\r\n\r\n    protected override Section[] CreatePropertiesContainer()\r\n        => new Section[]\r\n        {\r\n            // style\r\n            new LyricColorSection(),\r\n            new LyricFontSection(),\r\n            new LyricShadowSection(),\r\n            // note\r\n            new NoteColorSection(),\r\n            new NoteFontSection(),\r\n        };\r\n\r\n    protected override Drawable CreatePreviewArea()\r\n        => new LyricStylePreview\r\n        {\r\n            Name = \"Lyric style preview area\",\r\n            Anchor = Anchor.Centre,\r\n            Origin = Anchor.Centre,\r\n            Size = new Vector2(0.95f),\r\n            RelativeSizeAxes = Axes.Both,\r\n        };\r\n\r\n    /*\r\n    protected override Container CreatePreviewArea()\r\n        => new NoteStylePreview\r\n        {\r\n            Name = \"Note style preview area\",\r\n            Anchor = Anchor.Centre,\r\n            Origin = Anchor.Centre,\r\n            Size = new Vector2(0.95f),\r\n            RelativeSizeAxes = Axes.Both\r\n        };\r\n    */\r\n}\r\n\r\npublic enum Style\r\n{\r\n    [System.ComponentModel.Description(\"Lyric\")]\r\n    Lyric,\r\n\r\n    [System.ComponentModel.Description(\"Note\")]\r\n    Note,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Screens/Skin/Style/StyleSection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Screens.Skin.Style;\r\n\r\ninternal abstract partial class StyleSection : Section;\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Skinning/Argon/KaraokeArgonSkinTransformer.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Skinning.Default;\r\nusing osu.Game.Skinning;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Skinning.Argon;\r\n\r\npublic class KaraokeArgonSkinTransformer : KaraokeDefaultSkinTransformer\r\n{\r\n    public KaraokeArgonSkinTransformer(ISkin skin, IBeatmap beatmap)\r\n        : base(skin, beatmap)\r\n    {\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Skinning/Default/DefaultBodyPiece.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Extensions.Color4Extensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Layout;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Drawables;\r\nusing osu.Game.Rulesets.Objects.Drawables;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Skinning.Default;\r\n\r\npublic partial class DefaultBodyPiece : Container\r\n{\r\n    public const float CORNER_RADIUS = 5;\r\n\r\n    public readonly Bindable<Color4> AccentColour = new();\r\n    public readonly Bindable<Color4> HitColour = new();\r\n\r\n    private readonly LayoutValue subtractionCache = new(Invalidation.DrawSize);\r\n    private readonly IBindable<bool> isHitting = new Bindable<bool>();\r\n    private readonly IBindable<bool> display = new Bindable<bool>();\r\n    private readonly IBindableDictionary<Singer, SingerState[]> singer = new BindableDictionary<Singer, SingerState[]>();\r\n\r\n    protected Drawable Background { get; private set; } = null!;\r\n    protected Drawable Foreground { get; private set; } = null!;\r\n\r\n    public DefaultBodyPiece()\r\n    {\r\n        CornerRadius = CORNER_RADIUS;\r\n        Masking = true;\r\n\r\n        AddLayout(subtractionCache);\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(DrawableHitObject drawableObject)\r\n    {\r\n        InternalChildren = new[]\r\n        {\r\n            Background = new Box\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n            },\r\n            Foreground = new Box\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n            },\r\n        };\r\n\r\n        var note = (DrawableNote)drawableObject;\r\n\r\n        isHitting.BindTo(note.IsHitting);\r\n        display.BindTo(note.DisplayBindable);\r\n        singer.BindTo(note.SingersBindable);\r\n\r\n        AccentColour.BindValueChanged(onAccentChanged);\r\n        HitColour.BindValueChanged(onAccentChanged);\r\n        isHitting.BindValueChanged(_ => onAccentChanged(), true);\r\n        display.BindValueChanged(_ => onAccentChanged(), true);\r\n    }\r\n\r\n    private void onAccentChanged() => onAccentChanged(new ValueChangedEvent<Color4>(AccentColour.Value, AccentColour.Value));\r\n\r\n    private void onAccentChanged(ValueChangedEvent<Color4> accent)\r\n    {\r\n        Foreground.Colour = HitColour.Value;\r\n        Background.Colour = display.Value ? AccentColour.Value : new Color4(23, 41, 46, 255);\r\n\r\n        Foreground.ClearTransforms(false, nameof(Foreground.Colour));\r\n        Foreground.Alpha = 0;\r\n\r\n        if (isHitting.Value)\r\n        {\r\n            Foreground.Alpha = 1;\r\n\r\n            const float animation_length = 50;\r\n\r\n            // wait for the next sync point\r\n            double synchronisedOffset = animation_length * 2 - Time.Current % (animation_length * 2);\r\n            using (Foreground.BeginDelayedSequence(synchronisedOffset))\r\n                Foreground.FadeColour(accent.NewValue.Lighten(0.7f), animation_length).Then().FadeColour(Foreground.Colour, animation_length).Loop();\r\n        }\r\n\r\n        subtractionCache.Invalidate();\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Skinning/Default/KaraokeDefaultSkinTransformer.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.UI.HUD;\r\nusing osu.Game.Skinning;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Skinning.Default;\r\n\r\npublic class KaraokeDefaultSkinTransformer : SkinTransformer\r\n{\r\n    private readonly KaraokeSkin karaokeSkin;\r\n\r\n    public KaraokeDefaultSkinTransformer(ISkin skin, IBeatmap beatmap)\r\n        : base(skin)\r\n    {\r\n        karaokeSkin = new KaraokeSkin(new SkinInfo(), new InternalSkinStorageResourceProvider(\"Default\"));\r\n    }\r\n\r\n    public override IBindable<TValue>? GetConfig<TLookup, TValue>(TLookup lookup)\r\n        => karaokeSkin.GetConfig<TLookup, TValue>(lookup);\r\n\r\n    public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)\r\n    {\r\n        switch (lookup)\r\n        {\r\n            case GlobalSkinnableContainerLookup containerLookup:\r\n                // Only handle ruleset level defaults for now.\r\n                if (containerLookup.Ruleset == null)\r\n                    return base.GetDrawableComponent(lookup);\r\n\r\n                switch (containerLookup.Lookup)\r\n                {\r\n                    case GlobalSkinnableContainers.MainHUDComponents:\r\n                        // see the fall-back strategy in the SkinManager.AllSources.\r\n                        // will receive the:\r\n                        // 1. Legacy beatmap skin.\r\n                        // 2. default skin(e.g. argon skin) -> container will not be null only if skin is edited.\r\n                        // 3. triangle skin\r\n\r\n                        // component will not be null only if skin is edited.\r\n                        var component = base.GetDrawableComponent(lookup) as Container;\r\n\r\n                        // todo: technically can return non-null container if current skin is triangle skin.\r\n                        // but have no idea why still not showing the setting button.\r\n                        if (component != null && !component.Children.OfType<SettingButtonsDisplay>().Any())\r\n                        {\r\n                            // should add the setting button if not in the ruleset hud.\r\n                            component.Add(new SettingButtonsDisplay\r\n                            {\r\n                                Anchor = Anchor.CentreRight,\r\n                                Origin = Anchor.CentreRight,\r\n                            });\r\n                        }\r\n\r\n                        return component;\r\n\r\n                    case GlobalSkinnableContainers.SongSelect:\r\n                    default:\r\n                        return base.GetDrawableComponent(lookup);\r\n                }\r\n\r\n            default:\r\n                return base.GetDrawableComponent(lookup);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Skinning/Elements/ElementType.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Skinning.Elements;\r\n\r\npublic enum ElementType\r\n{\r\n    LyricFontInfo,\r\n\r\n    NoteStyle,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Skinning/Elements/IKaraokeSkinElement.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Skinning.Elements;\r\n\r\npublic interface IKaraokeSkinElement\r\n{\r\n    int ID { get; set; }\r\n\r\n    string Name { get; set; }\r\n\r\n    void ApplyTo(Drawable d);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Skinning/Elements/InvalidDrawableTypeException.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Skinning.Elements;\r\n\r\npublic class InvalidDrawableTypeException : Exception\r\n{\r\n    public InvalidDrawableTypeException(string message)\r\n        : base($\"Drawable type does not supported ({message})\")\r\n    {\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Skinning/Elements/LayoutGroup.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Skinning.Elements;\r\n\r\npublic class LayoutGroup\r\n{\r\n    public int Id { get; set; }\r\n\r\n    public string Name { get; set; } = string.Empty;\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Skinning/Elements/LyricFontInfo.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Drawables;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Skinning.Elements;\r\n\r\npublic class LyricFontInfo : IKaraokeSkinElement\r\n{\r\n    public static LyricFontInfo CreateDefault() => new()\r\n    {\r\n        Name = \"Default\",\r\n        SmartHorizon = KaraokeTextSmartHorizon.Multi,\r\n        LyricsInterval = 4,\r\n        RubyInterval = 2,\r\n        RomanisationInterval = 2,\r\n        RubyAlignment = LyricTextAlignment.EqualSpace,\r\n        RomanisationAlignment = LyricTextAlignment.EqualSpace,\r\n        RubyMargin = 4,\r\n        RomanisationMargin = 4,\r\n        MainTextFont = new FontUsage(\"Torus\", 48, \"Bold\"),\r\n        RubyTextFont = new FontUsage(\"Torus\", 20, \"Bold\"),\r\n        RomanisationTextFont = new FontUsage(\"Torus\", 20, \"Bold\"),\r\n    };\r\n\r\n    public int ID { get; set; }\r\n\r\n    public string Name { get; set; } = string.Empty;\r\n\r\n    /// <summary>\r\n    /// ???\r\n    /// </summary>\r\n    public KaraokeTextSmartHorizon SmartHorizon { get; set; } = KaraokeTextSmartHorizon.None;\r\n\r\n    /// <summary>\r\n    /// Interval between lyric texts\r\n    /// </summary>\r\n    public int LyricsInterval { get; set; }\r\n\r\n    /// <summary>\r\n    /// Interval between lyric rubies\r\n    /// </summary>\r\n    public int RubyInterval { get; set; }\r\n\r\n    /// <summary>\r\n    /// Interval between lyric romanisation\r\n    /// </summary>\r\n    public int RomanisationInterval { get; set; }\r\n\r\n    /// <summary>\r\n    /// Ruby position alignment\r\n    /// </summary>\r\n    public LyricTextAlignment RubyAlignment { get; set; } = LyricTextAlignment.Auto;\r\n\r\n    /// <summary>\r\n    /// Ruby position alignment\r\n    /// </summary>\r\n    public LyricTextAlignment RomanisationAlignment { get; set; } = LyricTextAlignment.Auto;\r\n\r\n    /// <summary>\r\n    /// Interval between lyric text and ruby\r\n    /// </summary>\r\n    public int RubyMargin { get; set; }\r\n\r\n    /// <summary>\r\n    /// (Additional) Interval between lyric text and romanisation.\r\n    /// </summary>\r\n    public int RomanisationMargin { get; set; }\r\n\r\n    /// <summary>\r\n    /// Main text font\r\n    /// </summary>\r\n    public FontUsage MainTextFont { get; set; } = new(\"Torus\", 48, \"Bold\");\r\n\r\n    /// <summary>\r\n    /// Ruby text font\r\n    /// </summary>\r\n    public FontUsage RubyTextFont { get; set; } = new(\"Torus\", 20, \"Bold\");\r\n\r\n    /// <summary>\r\n    /// Romanisation text font\r\n    /// </summary>\r\n    public FontUsage RomanisationTextFont { get; set; } = new(\"Torus\", 20, \"Bold\");\r\n\r\n    public void ApplyTo(Drawable d)\r\n    {\r\n        if (d is not DrawableLyric drawableLyric)\r\n            throw new InvalidDrawableTypeException(nameof(d));\r\n\r\n        drawableLyric.ApplyToLyricPieces(l =>\r\n        {\r\n            // Apply text font info\r\n            l.Font = getFont(KaraokeRulesetSetting.MainFont, MainTextFont);\r\n            l.TopTextFont = getFont(KaraokeRulesetSetting.RubyFont, RubyTextFont);\r\n            l.BottomTextFont = getFont(KaraokeRulesetSetting.RomanisationFont, RomanisationTextFont);\r\n\r\n            // Layout to text\r\n            l.KaraokeTextSmartHorizon = SmartHorizon;\r\n            l.Spacing = new Vector2(LyricsInterval, l.Spacing.Y);\r\n\r\n            // Top text\r\n            l.TopTextSpacing = new Vector2(RubyInterval, l.TopTextSpacing.Y);\r\n            l.TopTextAlignment = RubyAlignment;\r\n            l.TopTextMargin = RubyMargin;\r\n\r\n            // Bottom text\r\n            l.BottomTextSpacing = new Vector2(RomanisationInterval, l.BottomTextSpacing.Y);\r\n            l.BottomTextAlignment = RomanisationAlignment;\r\n            l.BottomTextMargin = RomanisationMargin;\r\n        });\r\n\r\n        // Apply translation font.\r\n        drawableLyric.ApplyToTranslationText(text =>\r\n        {\r\n            text.Font = getFont(KaraokeRulesetSetting.TranslationFont);\r\n        });\r\n\r\n        FontUsage getFont(KaraokeRulesetSetting setting, FontUsage? skinFont = null)\r\n        {\r\n            var config = drawableLyric.Dependencies.Get<KaraokeRulesetConfigManager>();\r\n            var font = config?.Get<FontUsage>(setting) ?? FontUsage.Default;\r\n\r\n            bool forceUseDefault = forceUseDefaultFont();\r\n            if (forceUseDefault || skinFont == null)\r\n                return font;\r\n\r\n            return font.With(size: skinFont.Value.Size);\r\n\r\n            bool forceUseDefaultFont()\r\n            {\r\n                switch (setting)\r\n                {\r\n                    case KaraokeRulesetSetting.MainFont:\r\n                    case KaraokeRulesetSetting.RubyFont:\r\n                    case KaraokeRulesetSetting.RomanisationFont:\r\n                        return config?.Get<bool>(KaraokeRulesetSetting.ForceUseDefaultFont) ?? false;\r\n\r\n                    case KaraokeRulesetSetting.TranslationFont:\r\n                        return config?.Get<bool>(KaraokeRulesetSetting.ForceUseDefaultTranslationFont) ?? false;\r\n\r\n                    default:\r\n                        throw new InvalidOperationException(nameof(setting));\r\n                }\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Skinning/Elements/NoteStyle.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Extensions.Color4Extensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Drawables;\r\nusing osu.Game.Rulesets.Karaoke.Skinning.Default;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Skinning.Elements;\r\n\r\npublic class NoteStyle : IKaraokeSkinElement\r\n{\r\n    public static NoteStyle CreateDefault() => new()\r\n    {\r\n        Name = \"Default\",\r\n        NoteColor = Color4Extensions.FromHex(\"#44AADD\"),\r\n        BlinkColor = Color4Extensions.FromHex(\"#FF66AA\"),\r\n        TextColor = Color4Extensions.FromHex(\"#FFFFFF\"),\r\n        BoldText = true,\r\n    };\r\n\r\n    public int ID { get; set; }\r\n\r\n    public string Name { get; set; } = string.Empty;\r\n\r\n    public Color4 NoteColor { get; set; }\r\n\r\n    public Color4 BlinkColor { get; set; }\r\n\r\n    public Color4 TextColor { get; set; }\r\n\r\n    public bool BoldText { get; set; }\r\n\r\n    public void ApplyTo(Drawable d)\r\n    {\r\n        if (d is not DrawableNote drawableNote)\r\n            throw new InvalidDrawableTypeException(nameof(d));\r\n\r\n        drawableNote.ApplyToLyricText(text =>\r\n        {\r\n            text.Colour = TextColor;\r\n        });\r\n\r\n        drawableNote.ApplyToBackground(background =>\r\n        {\r\n            if (background.Drawable is not DefaultBodyPiece defaultBodyPiece)\r\n                return;\r\n\r\n            defaultBodyPiece.AccentColour.Value = NoteColor;\r\n            defaultBodyPiece.HitColour.Value = BlinkColor;\r\n        });\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Skinning/Fonts/BitmapFontCompressor.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing Newtonsoft.Json;\r\nusing SharpFNT;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Skinning.Fonts;\r\n\r\npublic static class BitmapFontCompressor\r\n{\r\n    public static BitmapFont Compress(BitmapFont bitmapFont, char[] chars)\r\n    {\r\n        ArgumentNullException.ThrowIfNull(bitmapFont);\r\n\r\n        var characters = GenerateCharacters(bitmapFont.Info, bitmapFont.Common, bitmapFont.Characters, chars);\r\n\r\n        return new BitmapFont\r\n        {\r\n            Info = copyObject(bitmapFont.Info),\r\n            Common = copyObject(bitmapFont.Common),\r\n            Pages = GeneratePages(bitmapFont.Pages, characters.Values.ToArray()),\r\n            Characters = characters,\r\n            KerningPairs = GenerateKerningPairs(bitmapFont.KerningPairs, chars),\r\n        };\r\n    }\r\n\r\n    internal static IDictionary<int, string> GeneratePages(IDictionary<int, string> originPages, Character[] characters)\r\n    {\r\n        if (characters.Length == 0)\r\n            return new Dictionary<int, string>();\r\n\r\n        int maxStorePage = originPages.Max(x => x.Key);\r\n        int maxPage = characters.Max(x => x.Page);\r\n        if (maxPage > maxStorePage)\r\n            throw new ArgumentOutOfRangeException(nameof(maxPage));\r\n\r\n        return originPages\r\n               .Where(x => x.Key <= maxPage)\r\n               .ToDictionary(x => x.Key, x => x.Value);\r\n    }\r\n\r\n    internal static IDictionary<int, Character> GenerateCharacters(BitmapFontInfo originInfo, BitmapFontCommon originCommon,\r\n                                                                   IDictionary<int, Character> originCharacters, char[] chars)\r\n    {\r\n        chars = chars.Distinct().ToArray();\r\n        if (chars.Length < 1)\r\n            return new Dictionary<int, Character>();\r\n\r\n        // got the characters need to be precessed.\r\n        var charCodeList = chars.Select(x => (int)x).ToList();\r\n        var filteredCharacters = originCharacters\r\n                                 .Where(x => charCodeList.Contains(x.Key))\r\n                                 .ToDictionary(x => x.Key, x => copyObject(x.Value));\r\n\r\n        // first, sort by character height.\r\n        var processingCharacters = filteredCharacters.OrderByDescending(x => x.Value.Height).ToArray();\r\n\r\n        // second, give then a suitable width and height.\r\n        var pageSize = new\r\n        {\r\n            Width = originCommon.ScaleWidth,\r\n            Height = originCommon.ScaleHeight,\r\n        };\r\n        var padding = new\r\n        {\r\n            Top = originInfo.PaddingUp,\r\n            Bottom = originInfo.PaddingDown,\r\n            Left = originInfo.PaddingLeft,\r\n            Right = originInfo.PaddingRight,\r\n        };\r\n        var spacing = new\r\n        {\r\n            X = originInfo.SpacingHorizontal,\r\n            Y = originInfo.SpacingVertical,\r\n        };\r\n\r\n        int page = 0;\r\n        var currentTopLeftPosition = new\r\n        {\r\n            X = padding.Left,\r\n            Y = padding.Top,\r\n        };\r\n\r\n        foreach (var (_, character) in processingCharacters)\r\n        {\r\n            if (currentTopLeftPosition.Y + character.Height > pageSize.Width - padding.Bottom)\r\n            {\r\n                // it's time to change to next page.\r\n                page++;\r\n                currentTopLeftPosition = new\r\n                {\r\n                    X = padding.Left,\r\n                    Y = padding.Top,\r\n                };\r\n            }\r\n            else if (currentTopLeftPosition.X + character.Width > pageSize.Height - padding.Right)\r\n            {\r\n                // it's time to change to next line.\r\n                currentTopLeftPosition = new\r\n                {\r\n                    X = padding.Left,\r\n                    Y = currentTopLeftPosition.Y + spacing.Y,\r\n                };\r\n            }\r\n\r\n            // memo:\r\n            // x-offset = how many pixels to shift the character right (= extra blank pixels to left of character)\r\n            // y-offset = how many pixels to shift the character down (= extra blank pixels above character)\r\n            // x-advance = total width of character in pixels (so adds x-advance x-offset-width extra blank pixels to the right of character)\r\n            // so we need to do in here is just change the position.\r\n            character.Page = page;\r\n            character.X = currentTopLeftPosition.X;\r\n            character.Y = currentTopLeftPosition.Y;\r\n\r\n            // assign next position for drawing.\r\n            currentTopLeftPosition = currentTopLeftPosition with\r\n            {\r\n                X = currentTopLeftPosition.X + character.Width + spacing.X,\r\n            };\r\n        }\r\n\r\n        return processingCharacters.OrderBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value);\r\n    }\r\n\r\n    internal static IDictionary<KerningPair, int> GenerateKerningPairs(IDictionary<KerningPair, int> originKerningPairs, char[] chars)\r\n    {\r\n        chars = chars.Distinct().ToArray();\r\n        if (chars.Length < 2)\r\n            return new Dictionary<KerningPair, int>();\r\n\r\n        // kerning pairs is the spacing between two chars.\r\n        // should query all the kerning that contain chars.\r\n        var charCodeList = chars.Select(x => (int)x).ToList();\r\n        return originKerningPairs\r\n               .Where(x => charCodeList.Contains(x.Key.First) && charCodeList.Contains(x.Key.Second))\r\n               .ToDictionary(x => x.Key, x => x.Value);\r\n    }\r\n\r\n    private static T copyObject<T>(T obj)\r\n    {\r\n        string str = JsonConvert.SerializeObject(obj);\r\n        return JsonConvert.DeserializeObject<T>(str)!;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Skinning/Fonts/BitmapFontImageGenerator.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Graphics.Textures;\r\nusing osu.Game.Rulesets.Karaoke.IO.Stores;\r\nusing SharpFNT;\r\nusing SixLabors.ImageSharp;\r\nusing SixLabors.ImageSharp.Advanced;\r\nusing SixLabors.ImageSharp.PixelFormats;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Skinning.Fonts;\r\n\r\npublic class BitmapFontImageGenerator\r\n{\r\n    private readonly FntGlyphStore store;\r\n\r\n    public BitmapFontImageGenerator(FntGlyphStore store)\r\n    {\r\n        this.store = store;\r\n    }\r\n\r\n    public TextureUpload[] Generate(BitmapFont bitmapFont)\r\n    {\r\n        var pages = bitmapFont.Characters?.GroupBy(x => x.Value.Page)\r\n                              .ToDictionary(x => x.Key, x\r\n                                  => x.ToDictionary(v => v.Key, v => v.Value));\r\n\r\n        if (pages == null || !pages.Any())\r\n            return Array.Empty<TextureUpload>();\r\n\r\n        return pages.Select(x => GeneratePage(bitmapFont.Info, bitmapFont.Common, x.Value)).ToArray();\r\n    }\r\n\r\n    internal TextureUpload GeneratePage(BitmapFontInfo originInfo, BitmapFontCommon common, IDictionary<int, Character> characters)\r\n    {\r\n        int width = common.ScaleWidth;\r\n        int height = common.ScaleHeight;\r\n\r\n        int paddingTop = originInfo.PaddingUp;\r\n        int paddingLeft = originInfo.PaddingLeft;\r\n\r\n        // should check that all image is in the range.\r\n        if (characters.Any(x => x.Value.X + x.Value.Width > width))\r\n            throw new ArgumentOutOfRangeException(nameof(characters));\r\n\r\n        if (characters.Any(x => x.Value.Y + x.Value.Height > height))\r\n            throw new ArgumentOutOfRangeException(nameof(characters));\r\n\r\n        // start drawing all the characters into single image.\r\n        var page = new Image<Rgba32>(SixLabors.ImageSharp.Configuration.Default, width, height, new Rgba32(255, 255, 255, 0));\r\n\r\n        foreach ((int c, var character) in characters)\r\n        {\r\n            // get the character image from source, and should make sure that image is exist.\r\n            var characterImage = store.Get(new string(new[] { (char)c }));\r\n            if (characterImage == null)\r\n                throw new ArgumentNullException(nameof(characterImage));\r\n\r\n            // paste this shit into here.\r\n            pasteImage(page, characterImage, paddingLeft + character.X, paddingTop + character.Y);\r\n        }\r\n\r\n        return new TextureUpload(page);\r\n    }\r\n\r\n    private static void pasteImage(Image<Rgba32> page, TextureUpload character, int startFromX, int startFromY)\r\n    {\r\n        int characterWidth = character.Width;\r\n        int characterHeight = character.Height;\r\n        var rowData = character.Data;\r\n\r\n        for (int y = 0; y < characterHeight; y++)\r\n        {\r\n            var pixelRowMemory = page.DangerousGetPixelRowMemory(startFromY + y);\r\n            int readOffset = y * character.Width;\r\n\r\n            for (int x = 0; x < characterWidth; x++)\r\n            {\r\n                pixelRowMemory.Span[startFromX + x] = rowData[readOffset + x];\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Skinning/Fonts/FontInfo.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Skinning.Fonts;\r\n\r\npublic readonly struct FontInfo\r\n{\r\n    public string FontName { get; }\r\n\r\n    public string Family { get; }\r\n\r\n    public string? Weight { get; }\r\n\r\n    public FontFormat FontFormat { get; }\r\n\r\n    public FontInfo(string fontName, FontFormat fontFormat)\r\n    {\r\n        FontName = fontName;\r\n        FontFormat = fontFormat;\r\n\r\n        string[] parts = fontName.Split('-');\r\n\r\n        switch (parts.Length)\r\n        {\r\n            case 1:\r\n                Family = parts[0];\r\n                Weight = null;\r\n                break;\r\n\r\n            default:\r\n                Family = string.Join('-', parts.Take(parts.Length - 1));\r\n                Weight = fontName.Split('-').LastOrDefault();\r\n                break;\r\n        }\r\n    }\r\n}\r\n\r\npublic enum FontFormat\r\n{\r\n    Internal,\r\n\r\n    Fnt,\r\n\r\n    Ttf,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Skinning/Fonts/FontManager.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.IO;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Graphics.Textures;\r\nusing osu.Framework.IO.Stores;\r\nusing osu.Framework.Platform;\r\nusing osu.Game.Rulesets.Karaoke.IO.Archives;\r\nusing osu.Game.Rulesets.Karaoke.IO.Stores;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Skinning.Fonts;\r\n\r\npublic partial class FontManager : Component\r\n{\r\n    public const string FONT_BASE_PATH = \"fonts\";\r\n\r\n    [Resolved]\r\n    private GameHost host { get; set; } = null!;\r\n\r\n    private Storage storage => host.Storage.GetStorageForDirectory(FONT_BASE_PATH);\r\n\r\n    private readonly FontFormat[] supportedFormat = { FontFormat.Fnt, FontFormat.Ttf };\r\n\r\n    public readonly BindableList<FontInfo> Fonts = new();\r\n\r\n    public FontManager()\r\n    {\r\n        Fonts.AddRange(new[]\r\n        {\r\n            // From osu-framework\r\n            new FontInfo(\"OpenSans-Regular\", FontFormat.Internal),\r\n            new FontInfo(\"OpenSans-Bold\", FontFormat.Internal),\r\n            new FontInfo(\"OpenSans-RegularItalic\", FontFormat.Internal),\r\n            new FontInfo(\"OpenSans-BoldItalic\", FontFormat.Internal),\r\n\r\n            new FontInfo(\"Roboto-Regular\", FontFormat.Internal),\r\n            new FontInfo(\"Roboto-Bold\", FontFormat.Internal),\r\n            new FontInfo(\"RobotoCondensed-Regular\", FontFormat.Internal),\r\n            new FontInfo(\"RobotoCondensed-Bold\", FontFormat.Internal),\r\n            // From osu.game\r\n            new FontInfo(\"osuFont\", FontFormat.Internal),\r\n\r\n            new FontInfo(\"Torus-Regular\", FontFormat.Internal),\r\n            new FontInfo(\"Torus-Light\", FontFormat.Internal),\r\n            new FontInfo(\"Torus-SemiBold\", FontFormat.Internal),\r\n            new FontInfo(\"Torus-Bold\", FontFormat.Internal),\r\n\r\n            new FontInfo(\"Inter-Regular\", FontFormat.Internal),\r\n            new FontInfo(\"Inter-RegularItalic\", FontFormat.Internal),\r\n            new FontInfo(\"Inter-Light\", FontFormat.Internal),\r\n            new FontInfo(\"Inter-LightItalic\", FontFormat.Internal),\r\n            new FontInfo(\"Inter-SemiBold\", FontFormat.Internal),\r\n            new FontInfo(\"Inter-SemiBoldItalic\", FontFormat.Internal),\r\n            new FontInfo(\"Inter-Bold\", FontFormat.Internal),\r\n            new FontInfo(\"Inter-BoldItalic\", FontFormat.Internal),\r\n\r\n            new FontInfo(\"Noto-Basic\", FontFormat.Internal),\r\n            new FontInfo(\"Noto-Hangul\", FontFormat.Internal),\r\n            new FontInfo(\"Noto-CJK-Basic\", FontFormat.Internal),\r\n            new FontInfo(\"Noto-CJK-Compatibility\", FontFormat.Internal),\r\n            new FontInfo(\"Noto-Thai\", FontFormat.Internal),\r\n\r\n            new FontInfo(\"Venera-Light\", FontFormat.Internal),\r\n            new FontInfo(\"Venera-Bold\", FontFormat.Internal),\r\n            new FontInfo(\"Venera-Black\", FontFormat.Internal),\r\n\r\n            new FontInfo(\"Compatibility\", FontFormat.Internal),\r\n        });\r\n    }\r\n\r\n    private FileSystemWatcher watcher = null!;\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        foreach (var fontFormat in supportedFormat)\r\n        {\r\n            // will create folder if not exist.\r\n            string path = getPathByFontType(fontFormat);\r\n            string extension = getExtensionByFontType(fontFormat);\r\n\r\n            var fontFiles = storage.GetStorageForDirectory(path)\r\n                                   .GetFiles(string.Empty, $\"*.{extension}\").ToList();\r\n\r\n            foreach (string fontFile in fontFiles)\r\n            {\r\n                addFontToList(fontFile, fontFormat);\r\n            }\r\n        }\r\n\r\n        watcher = new FileSystemWatcher(storage.GetFullPath(string.Empty))\r\n        {\r\n            EnableRaisingEvents = true,\r\n            IncludeSubdirectories = true,\r\n            NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.CreationTime | NotifyFilters.FileName,\r\n        };\r\n\r\n        watcher.Renamed += onChange;\r\n        watcher.Deleted += onChange;\r\n        watcher.Created += onChange;\r\n\r\n        void onChange(object sender, FileSystemEventArgs args)\r\n        {\r\n            // check is valid format.\r\n            string extension = Path.GetExtension(args.FullPath);\r\n            bool validFormat = supportedFormat.Any(x => $\".{getPathByFontType(x)}\" == extension);\r\n            if (!validFormat)\r\n                return;\r\n\r\n            // then doing action by type.\r\n            switch (args.ChangeType)\r\n            {\r\n                case WatcherChangeTypes.Created:\r\n                    addFontToList(args.FullPath);\r\n                    return;\r\n\r\n                case WatcherChangeTypes.Deleted:\r\n                    removeFontFromList(args.FullPath);\r\n                    return;\r\n\r\n                case WatcherChangeTypes.Renamed:\r\n                    if (args is not RenamedEventArgs renamedEventArgs)\r\n                        throw new InvalidCastException(nameof(args));\r\n\r\n                    removeFontFromList(renamedEventArgs.OldFullPath);\r\n                    addFontToList(renamedEventArgs.FullPath);\r\n\r\n                    return;\r\n            }\r\n        }\r\n    }\r\n\r\n    private void addFontToList(string path)\r\n    {\r\n        var fontFormat = getFontTypeByExtension(Path.GetExtension(path));\r\n        addFontToList(path, fontFormat);\r\n    }\r\n\r\n    private void removeFontFromList(string path)\r\n    {\r\n        var fontFormat = getFontTypeByExtension(Path.GetExtension(path));\r\n        removeFontFromList(path, fontFormat);\r\n    }\r\n\r\n    private void addFontToList(string path, FontFormat fontFormat)\r\n    {\r\n        string fontName = Path.GetFileNameWithoutExtension(path);\r\n        var fontInfo = new FontInfo(fontName, fontFormat);\r\n        Fonts.Add(fontInfo);\r\n    }\r\n\r\n    private void removeFontFromList(string path, FontFormat fontFormat)\r\n    {\r\n        string fontName = Path.GetFileNameWithoutExtension(path);\r\n        var matchedFont = Fonts.FirstOrDefault(x => x.FontName == fontName && x.FontFormat == fontFormat);\r\n\r\n        if (!Fonts.Contains(matchedFont))\r\n            return;\r\n\r\n        Fonts.Remove(matchedFont);\r\n    }\r\n\r\n    public FontFormat? CheckFontFormat(FontUsage fontUsage)\r\n    {\r\n        string fontName = fontUsage.FontName;\r\n        if (Fonts.All(x => x.FontName != fontName))\r\n            return null;\r\n\r\n        return Fonts.FirstOrDefault(x => x.FontName == fontName).FontFormat;\r\n    }\r\n\r\n    public IResourceStore<TextureUpload>? GetGlyphStore(FontInfo fontInfo)\r\n    {\r\n        // do not import if this font is system font.\r\n        var fontFormat = fontInfo.FontFormat;\r\n        if (fontFormat == FontFormat.Internal)\r\n            return null;\r\n\r\n        string fontName = fontInfo.FontName;\r\n        return fontFormat switch\r\n        {\r\n            FontFormat.Fnt => getFntGlyphStore(fontName),\r\n            FontFormat.Ttf => getTtfGlyphStore(fontName),\r\n            FontFormat.Internal or _ => throw new ArgumentOutOfRangeException(nameof(fontFormat)),\r\n        };\r\n    }\r\n\r\n    private FntGlyphStore? getFntGlyphStore(string fontName)\r\n    {\r\n        string path = Path.Combine(getPathByFontType(FontFormat.Fnt), fontName);\r\n        string pathWithExtension = Path.ChangeExtension(path, getExtensionByFontType(FontFormat.Fnt));\r\n\r\n        if (!storage.Exists(pathWithExtension))\r\n            return null;\r\n\r\n        var resources = new CachedFontArchiveReader(storage.GetStream(pathWithExtension), fontName);\r\n        return new FntGlyphStore(new ResourceStore<byte[]>(resources), $\"{fontName}\", host.CreateTextureLoaderStore(resources));\r\n    }\r\n\r\n    private TtfGlyphStore? getTtfGlyphStore(string fontName)\r\n    {\r\n        string path = Path.Combine(getPathByFontType(FontFormat.Ttf), fontName);\r\n        string pathWithExtension = Path.ChangeExtension(path, getExtensionByFontType(FontFormat.Ttf));\r\n\r\n        if (!storage.Exists(pathWithExtension))\r\n            return null;\r\n\r\n        var resources = new StorageBackedResourceStore(storage.GetStorageForDirectory(getPathByFontType(FontFormat.Ttf)));\r\n        return new TtfGlyphStore(new ResourceStore<byte[]>(resources), $\"{fontName}\");\r\n    }\r\n\r\n    private static string getPathByFontType(FontFormat type) =>\r\n        type switch\r\n        {\r\n            FontFormat.Fnt => \"fnt\",\r\n            FontFormat.Ttf => \"ttf\",\r\n            FontFormat.Internal or _ => throw new ArgumentOutOfRangeException(nameof(type)),\r\n        };\r\n\r\n    private static string getExtensionByFontType(FontFormat type) =>\r\n        type switch\r\n        {\r\n            FontFormat.Fnt => \"zipfnt\",\r\n            FontFormat.Ttf => \"ttf\",\r\n            FontFormat.Internal or _ => throw new ArgumentOutOfRangeException(nameof(type)),\r\n        };\r\n\r\n    private static FontFormat getFontTypeByExtension(string extension) =>\r\n        extension switch\r\n        {\r\n            \".zipfnt\" => FontFormat.Fnt,\r\n            \".ttf\" => FontFormat.Ttf,\r\n            _ => throw new FormatException(nameof(extension)),\r\n        };\r\n\r\n    protected override void Dispose(bool isDisposing)\r\n    {\r\n        base.Dispose(isDisposing);\r\n\r\n        watcher.Dispose();\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Skinning/InternalSkinStorageResourceProvider.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Audio;\r\nusing osu.Framework.Graphics.Rendering;\r\nusing osu.Framework.Graphics.Rendering.Dummy;\r\nusing osu.Framework.Graphics.Textures;\r\nusing osu.Framework.IO.Stores;\r\nusing osu.Game.Database;\r\nusing osu.Game.IO;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Skinning;\r\n\r\npublic class InternalSkinStorageResourceProvider : IStorageResourceProvider\r\n{\r\n    public InternalSkinStorageResourceProvider(string skinName)\r\n    {\r\n        var store = new KaraokeRuleset().CreateResourceStore();\r\n        Files = Resources = new NamespacedResourceStore<byte[]>(store, $\"Skin/{skinName}\");\r\n    }\r\n\r\n    public IRenderer Renderer => new DummyRenderer();\r\n\r\n    public AudioManager AudioManager => null!;\r\n    public IResourceStore<byte[]> Files { get; }\r\n    public IResourceStore<byte[]> Resources { get; }\r\n    public RealmAccess RealmAccess => null!;\r\n    public IResourceStore<TextureUpload> CreateTextureLoaderStore(IResourceStore<byte[]> underlyingStore) => null!;\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Skinning/KaraokeBeatmapSkin.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.ComponentModel;\r\nusing System.Linq;\r\nusing Newtonsoft.Json;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.IO.Stores;\r\nusing osu.Framework.Logging;\r\nusing osu.Game.IO;\r\nusing osu.Game.Rulesets.Karaoke.IO.Serialization;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Skinning.Elements;\r\nusing osu.Game.Skinning;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Skinning;\r\n\r\n/// <summary>\r\n/// It's the skin that designed for reading the resource file and parse into the understandable format form beatmap skin.\r\n/// </summary>\r\npublic class KaraokeBeatmapSkin : KaraokeSkin\r\n{\r\n    public readonly IDictionary<ElementType, IList<IKaraokeSkinElement>> Elements = new Dictionary<ElementType, IList<IKaraokeSkinElement>>();\r\n\r\n    public KaraokeBeatmapSkin(SkinInfo skin, IStorageResourceProvider? resources, IResourceStore<byte[]>? storage = null)\r\n        : base(skin, resources, storage)\r\n    {\r\n        SkinInfo.PerformRead(s =>\r\n        {\r\n            var globalSetting = SkinJsonSerializableExtensions.CreateSkinElementGlobalSettings();\r\n\r\n            // we may want to move this to some kind of async operation in the future.\r\n            foreach (ElementType skinnableTarget in Enum.GetValues<ElementType>())\r\n            {\r\n                string filename = $\"{getFileNameByType(skinnableTarget)}.json\";\r\n\r\n                try\r\n                {\r\n                    Elements.Add(skinnableTarget, new List<IKaraokeSkinElement>());\r\n\r\n                    string? jsonContent = GetElementStringContentFromSkinInfo(s, filename);\r\n                    if (string.IsNullOrEmpty(jsonContent))\r\n                        return;\r\n\r\n                    var deserializedContent = JsonConvert.DeserializeObject<IKaraokeSkinElement[]>(jsonContent, globalSetting);\r\n\r\n                    if (deserializedContent == null)\r\n                        continue;\r\n\r\n                    Elements[skinnableTarget] = deserializedContent;\r\n                }\r\n                catch (Exception ex)\r\n                {\r\n                    Logger.Error(ex, \"Failed to load skin element.\");\r\n                }\r\n            }\r\n\r\n            static string getFileNameByType(ElementType elementType)\r\n                => elementType switch\r\n                {\r\n                    ElementType.LyricFontInfo => \"lyric-font-infos\",\r\n                    ElementType.NoteStyle => \"note-styles\",\r\n                    _ => throw new InvalidEnumArgumentException(nameof(elementType)),\r\n                };\r\n        });\r\n    }\r\n\r\n    public override IBindable<TValue>? GetConfig<TLookup, TValue>(TLookup lookup)\r\n    {\r\n        switch (lookup)\r\n        {\r\n            // get the target element by hit object.\r\n            case KaraokeHitObject hitObject:\r\n            {\r\n                var type = typeof(TValue);\r\n                var element = GetElementByHitObjectAndElementType(hitObject, type);\r\n                return SkinUtils.As<TValue>(new Bindable<TValue>((TValue)element!));\r\n            }\r\n\r\n            // in some cases, we still need to get target of element by type and id.\r\n            // e.d: get list of layout in the skin manager.\r\n            case KaraokeSkinLookup skinLookup:\r\n            {\r\n                var type = skinLookup.Type;\r\n                int lookupNumber = skinLookup.Lookup;\r\n                if (lookupNumber < 0)\r\n                    return base.GetConfig<KaraokeSkinLookup, TValue>(skinLookup);\r\n\r\n                var element = Elements[type].FirstOrDefault(x => x.ID == lookupNumber);\r\n                return SkinUtils.As<TValue>(new Bindable<TValue>((TValue)element!));\r\n            }\r\n\r\n            // Lookup list of name by type\r\n            case KaraokeIndexLookup indexLookup:\r\n                return indexLookup switch\r\n                {\r\n                    KaraokeIndexLookup.Note => SkinUtils.As<TValue>(getSelectionFromElementType(ElementType.NoteStyle)),\r\n                    _ => throw new InvalidEnumArgumentException(nameof(indexLookup)),\r\n                };\r\n\r\n            case KaraokeSkinConfigurationLookup skinConfigurationLookup:\r\n                return base.GetConfig<KaraokeSkinConfigurationLookup, TValue>(skinConfigurationLookup);\r\n        }\r\n\r\n        return null;\r\n\r\n        Bindable<IDictionary<int, string>> getSelectionFromElementType(ElementType elementType)\r\n            => new(Elements[elementType].ToDictionary(k => k.ID, k => k.Name));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Skinning/KaraokeIndexLookup.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Skinning;\r\n\r\n/// <summary>\r\n/// This enum is used to lookup list of name and it's lookup index\r\n/// </summary>\r\npublic enum KaraokeIndexLookup\r\n{\r\n    Layout,\r\n\r\n    Note,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Skinning/KaraokeSkin.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.ComponentModel;\r\nusing System.Linq;\r\nusing System.Text;\r\nusing Newtonsoft.Json;\r\nusing osu.Framework.Audio.Sample;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics.Textures;\r\nusing osu.Framework.IO.Stores;\r\nusing osu.Framework.Logging;\r\nusing osu.Game.Audio;\r\nusing osu.Game.Extensions;\r\nusing osu.Game.IO;\r\nusing osu.Game.Rulesets.Karaoke.IO.Serialization;\r\nusing osu.Game.Rulesets.Karaoke.IO.Serialization.Converters;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Skinning.Elements;\r\nusing osu.Game.Rulesets.Karaoke.UI.Components;\r\nusing osu.Game.Rulesets.Karaoke.UI.Scrolling;\r\nusing osu.Game.Skinning;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Skinning;\r\n\r\n/// <summary>\r\n///  It's the skin that designed for reading the resource file and parse into the understandable format.\r\n/// </summary>\r\npublic class KaraokeSkin : Skin\r\n{\r\n    public readonly IDictionary<ElementType, IKaraokeSkinElement> DefaultElement = new Dictionary<ElementType, IKaraokeSkinElement>\r\n    {\r\n        { ElementType.LyricFontInfo, LyricFontInfo.CreateDefault() },\r\n        { ElementType.NoteStyle, NoteStyle.CreateDefault() },\r\n    };\r\n\r\n    private readonly Bindable<float> bindableColumnHeight = new(DefaultColumnBackground.COLUMN_HEIGHT);\r\n    private readonly Bindable<float> bindableColumnSpacing = new(ScrollingNotePlayfield.COLUMN_SPACING);\r\n\r\n    private readonly IStorageResourceProvider? resources;\r\n\r\n    public KaraokeSkin(SkinInfo skin, IStorageResourceProvider? resources, IResourceStore<byte[]>? storage = null)\r\n        : base(skin, resources, storage)\r\n    {\r\n        this.resources = resources;\r\n\r\n        SkinInfo.PerformRead(s =>\r\n        {\r\n            const string filename = \"default.json\";\r\n\r\n            try\r\n            {\r\n                string? jsonContent = GetElementStringContentFromSkinInfo(s, filename);\r\n                if (string.IsNullOrEmpty(jsonContent))\r\n                    return;\r\n\r\n                var globalSetting = SkinJsonSerializableExtensions.CreateSkinElementGlobalSettings();\r\n                var deserializedContent = JsonConvert.DeserializeObject<DefaultSkinFormat>(jsonContent, globalSetting);\r\n\r\n                if (deserializedContent == null)\r\n                    return;\r\n\r\n                DefaultElement[ElementType.LyricFontInfo] = deserializedContent.LyricFontInfo;\r\n                DefaultElement[ElementType.NoteStyle] = deserializedContent.NoteStyle;\r\n            }\r\n            catch (Exception ex)\r\n            {\r\n                Logger.Error(ex, \"Failed to load skin element.\");\r\n            }\r\n        });\r\n    }\r\n\r\n    protected string? GetElementStringContentFromSkinInfo(SkinInfo skinInfo, string filename)\r\n    {\r\n        // should get by file name if files is namespace resource store.\r\n        var files = resources?.Files;\r\n        if (files == null)\r\n            return null;\r\n\r\n        byte[]? bytes = files is NamespacedResourceStore<byte[]> ? getFileFromNamespaceStore(files, filename) : getFileFromSkinInfo(files, skinInfo, filename);\r\n\r\n        if (bytes == null)\r\n            return null;\r\n\r\n        return Encoding.UTF8.GetString(bytes);\r\n\r\n        static byte[]? getFileFromNamespaceStore(IResourceStore<byte[]> files, string filename)\r\n            => files.Get(filename);\r\n\r\n        static byte[]? getFileFromSkinInfo(IResourceStore<byte[]> files, SkinInfo skinInfo, string filename)\r\n        {\r\n            // skin element files may be null for default skin.\r\n            var fileInfo = skinInfo.Files.FirstOrDefault(f => f.Filename == filename);\r\n\r\n            if (fileInfo == null)\r\n                return null;\r\n\r\n            return files.Get(fileInfo.File.GetStoragePath());\r\n        }\r\n    }\r\n\r\n    public override ISample? GetSample(ISampleInfo sampleInfo)\r\n        => sampleInfo.LookupNames.Select(lookup => resources?.AudioManager?.Samples.Get(lookup)).FirstOrDefault(sample => sample != null);\r\n\r\n    public override Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)\r\n        => null;\r\n\r\n    public override IBindable<TValue>? GetConfig<TLookup, TValue>(TLookup lookup)\r\n    {\r\n        switch (lookup)\r\n        {\r\n            // get the target element by hit object.\r\n            case KaraokeHitObject hitObject:\r\n            {\r\n                var type = typeof(TValue);\r\n                var element = GetElementByHitObjectAndElementType(hitObject, type);\r\n                return SkinUtils.As<TValue>(new Bindable<TValue>((TValue)element!));\r\n            }\r\n\r\n            // in some cases, we still need to get target of element by type and id.\r\n            // e.d: get list of layout in the skin manager.\r\n            case KaraokeSkinLookup skinLookup:\r\n            {\r\n                var type = skinLookup.Type;\r\n\r\n                return type switch\r\n                {\r\n                    ElementType.LyricFontInfo or ElementType.NoteStyle => SkinUtils.As<TValue>(new Bindable<TValue>((TValue)DefaultElement[type])),\r\n                    _ => throw new InvalidEnumArgumentException(nameof(type)),\r\n                };\r\n            }\r\n\r\n            case KaraokeSkinConfigurationLookup skinConfigurationLookup:\r\n            {\r\n                return skinConfigurationLookup.Lookup switch\r\n                {\r\n                    // should use customize height for note playfield in lyric editor\r\n                    LegacyKaraokeSkinConfigurationLookups.ColumnHeight => SkinUtils.As<TValue>(bindableColumnHeight),\r\n\r\n                    // not have note playfield judgement spacing in lyric editor.\r\n                    LegacyKaraokeSkinConfigurationLookups.ColumnSpacing => SkinUtils.As<TValue>(bindableColumnSpacing),\r\n\r\n                    _ => null,\r\n                };\r\n            }\r\n\r\n            default:\r\n                return null;\r\n        }\r\n    }\r\n\r\n    protected virtual IKaraokeSkinElement? GetElementByHitObjectAndElementType(KaraokeHitObject hitObject, Type elementType)\r\n    {\r\n        var type = KaraokeSkinElementConverter.GetElementType(elementType);\r\n        return toElement(type);\r\n    }\r\n\r\n    private IKaraokeSkinElement? toElement(ElementType type)\r\n        => type switch\r\n        {\r\n            ElementType.LyricFontInfo or ElementType.NoteStyle => DefaultElement[type],\r\n            _ => throw new InvalidEnumArgumentException(nameof(type)),\r\n        };\r\n\r\n    private class DefaultSkinFormat\r\n    {\r\n        public LyricFontInfo LyricFontInfo { get; set; } = null!;\r\n\r\n        public NoteStyle NoteStyle { get; set; } = null!;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Skinning/KaraokeSkinConfigurationLookup.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Skinning;\r\n\r\npublic class KaraokeSkinConfigurationLookup\r\n{\r\n    public readonly int Columns;\r\n    public readonly LegacyKaraokeSkinConfigurationLookups Lookup;\r\n    public readonly int? TargetColumn;\r\n\r\n    public KaraokeSkinConfigurationLookup(int columns, LegacyKaraokeSkinConfigurationLookups lookup, int? targetColumn = null)\r\n    {\r\n        Columns = columns;\r\n        Lookup = lookup;\r\n        TargetColumn = targetColumn;\r\n    }\r\n}\r\n\r\npublic enum LegacyKaraokeSkinConfigurationLookups\r\n{\r\n    ColumnHeight,\r\n    ColumnSpacing,\r\n    LightImage,\r\n    UpLineWidth,\r\n    DownLineWidth,\r\n    LightPosition,\r\n    HitPosition,\r\n    JudgementLineHeadImage,\r\n    JudgementLineTailImage,\r\n    JudgementLineBodyImage,\r\n    JudgementAresPercentage,\r\n    ShowJudgementLine,\r\n    NoteHeadImage,\r\n    NoteTailImage,\r\n    NoteBodyImage,\r\n    ExplosionImage,\r\n    ExplosionScale,\r\n}\r\n\r\npublic enum LegacyKaraokeSkinNoteLayer\r\n{\r\n    Border,\r\n    Foreground,\r\n    Background,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Skinning/KaraokeSkinLookup.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.ComponentModel;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Skinning.Elements;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Skinning;\r\n\r\n/// <summary>\r\n/// todo: it might be better just throw the whole <see cref=\"KaraokeHitObject\"/> to get the config.\r\n/// because cannot get the result just by id.\r\n/// </summary>\r\npublic readonly struct KaraokeSkinLookup\r\n{\r\n    /// <summary>\r\n    /// Parts wants to be searched.\r\n    /// </summary>\r\n    public ElementType Type { get; }\r\n\r\n    /// <summary>\r\n    /// Lookup index\r\n    /// </summary>\r\n    public int Lookup { get; }\r\n\r\n    /// <summary>\r\n    /// Ctor for <see cref=\"ElementType.NoteStyle\"/>\r\n    /// </summary>\r\n    /// <param name=\"type\"></param>\r\n    /// <param name=\"singers\"></param>\r\n    public KaraokeSkinLookup(ElementType type, IEnumerable<int> singers)\r\n        : this(type, SingerUtils.GetShiftingStyleIndex(singers))\r\n    {\r\n        switch (type)\r\n        {\r\n            case ElementType.LyricFontInfo:\r\n            case ElementType.NoteStyle:\r\n                return;\r\n\r\n            default:\r\n                throw new InvalidEnumArgumentException($\"Cannot call lookup with {type}\");\r\n        }\r\n    }\r\n\r\n    public KaraokeSkinLookup(ElementType type, int lookup = -1)\r\n    {\r\n        Type = type;\r\n        Lookup = lookup;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Skinning/Legacy/KaraokeClassicSkinTransformer.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Skinning;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Skinning.Legacy;\r\n\r\npublic class KaraokeClassicSkinTransformer : KaraokeLegacySkinTransformer\r\n{\r\n    public KaraokeClassicSkinTransformer(ISkin skin, IBeatmap beatmap)\r\n        : base(skin, beatmap)\r\n    {\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Skinning/Legacy/KaraokeLegacySkinTransformer.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.ComponentModel;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Skinning.Default;\r\nusing osu.Game.Rulesets.Scoring;\r\nusing osu.Game.Skinning;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Skinning.Legacy;\r\n\r\n/// <summary>\r\n/// Not inherit the <see cref=\"LegacySkinTransformer\"/> because:\r\n/// 1. Karaoke ruleset does not have the legacy skin.\r\n/// 2. There's not much logic in the <see cref=\"LegacySkinTransformer\"/>\r\n/// </summary>\r\npublic class KaraokeLegacySkinTransformer : KaraokeDefaultSkinTransformer\r\n{\r\n    private readonly Lazy<bool> isLegacySkin;\r\n    private readonly KaraokeBeatmapSkin karaokeSkin;\r\n\r\n    public KaraokeLegacySkinTransformer(ISkin skin, IBeatmap beatmap)\r\n        : base(skin, beatmap)\r\n    {\r\n        // we should get config by default karaoke skin.\r\n        // if has resource or texture, then try to get from legacy skin.\r\n        karaokeSkin = new KaraokeBeatmapSkin(new SkinInfo(), new InternalSkinStorageResourceProvider(\"Default\"));\r\n        isLegacySkin = new Lazy<bool>(() => GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version) != null);\r\n    }\r\n\r\n    public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)\r\n    {\r\n        switch (lookup)\r\n        {\r\n            case SkinComponentLookup<HitResult> resultComponent:\r\n                return getResult(resultComponent.Component);\r\n\r\n            case KaraokeSkinComponentLookup karaokeComponent:\r\n                if (!isLegacySkin.Value)\r\n                    return null;\r\n\r\n                return karaokeComponent.Component switch\r\n                {\r\n                    KaraokeSkinComponents.ColumnBackground => new LegacyColumnBackground(),\r\n                    KaraokeSkinComponents.StageBackground => new LegacyStageBackground(),\r\n                    KaraokeSkinComponents.JudgementLine => new LegacyJudgementLine(),\r\n                    KaraokeSkinComponents.Note => new LegacyNotePiece(),\r\n                    KaraokeSkinComponents.HitExplosion => new LegacyHitExplosion(),\r\n                    _ => throw new InvalidEnumArgumentException(nameof(karaokeComponent.Component)),\r\n                };\r\n\r\n            default:\r\n                return base.GetDrawableComponent(lookup);\r\n        }\r\n    }\r\n\r\n    private Drawable? getResult(HitResult result)\r\n    {\r\n        // todo : get real component\r\n        return null;\r\n    }\r\n\r\n    public override IBindable<TValue>? GetConfig<TLookup, TValue>(TLookup lookup)\r\n        => karaokeSkin.GetConfig<TLookup, TValue>(lookup);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Skinning/Legacy/LegacyColumnBackground.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.UI.Scrolling;\r\nusing osu.Game.Skinning;\r\nusing osuTK;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Skinning.Legacy;\r\n\r\npublic partial class LegacyColumnBackground : LegacyKaraokeColumnElement\r\n{\r\n    private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();\r\n\r\n    private Container lightContainer = null!;\r\n    private Sprite light = null!;\r\n\r\n    public LegacyColumnBackground()\r\n    {\r\n        RelativeSizeAxes = Axes.Both;\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(ISkinSource skin, IScrollingInfo scrollingInfo)\r\n    {\r\n        string lightImage = GetKaraokeSkinConfig<string>(skin, LegacyKaraokeSkinConfigurationLookups.LightImage)?.Value\r\n                            ?? GetTextureName();\r\n\r\n        float leftLineWidth = GetKaraokeSkinConfig<float>(skin, LegacyKaraokeSkinConfigurationLookups.UpLineWidth)\r\n            ?.Value ?? 1;\r\n        float rightLineWidth = GetKaraokeSkinConfig<float>(skin, LegacyKaraokeSkinConfigurationLookups.DownLineWidth)\r\n            ?.Value ?? 1;\r\n\r\n        bool hasLeftLine = false;\r\n        bool hasRightLine = false;\r\n\r\n        float lightPosition = GetKaraokeSkinConfig<float>(skin, LegacyKaraokeSkinConfigurationLookups.LightPosition)?.Value\r\n                              ?? 0;\r\n\r\n        InternalChildren = new Drawable[]\r\n        {\r\n            new Box\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n                Colour = Color4.Black,\r\n            },\r\n            new Box\r\n            {\r\n                RelativeSizeAxes = Axes.X,\r\n                Height = leftLineWidth,\r\n                Alpha = hasLeftLine ? 1 : 0,\r\n            },\r\n            new Box\r\n            {\r\n                Anchor = Anchor.BottomRight,\r\n                Origin = Anchor.BottomRight,\r\n                RelativeSizeAxes = Axes.X,\r\n                Height = rightLineWidth,\r\n                Alpha = hasRightLine ? 1 : 0,\r\n            },\r\n            lightContainer = new Container\r\n            {\r\n                Origin = Anchor.CentreLeft,\r\n                RelativeSizeAxes = Axes.Both,\r\n                Padding = new MarginPadding { Left = lightPosition },\r\n                Child = light = new Sprite\r\n                {\r\n                    Anchor = Anchor.CentreLeft,\r\n                    Origin = Anchor.CentreLeft,\r\n                    Texture = skin.GetTexture(lightImage),\r\n                    RelativeSizeAxes = Axes.Y,\r\n                    Height = 1,\r\n                    Alpha = 0,\r\n                },\r\n            },\r\n        };\r\n\r\n        direction.BindTo(scrollingInfo.Direction);\r\n        direction.BindValueChanged(onDirectionChanged, true);\r\n    }\r\n\r\n    private void onDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)\r\n    {\r\n        if (direction.NewValue == ScrollingDirection.Left)\r\n        {\r\n            lightContainer.Anchor = Anchor.CentreLeft;\r\n            lightContainer.Scale = new Vector2(1, -1);\r\n        }\r\n        else\r\n        {\r\n            lightContainer.Anchor = Anchor.CentreRight;\r\n            lightContainer.Scale = Vector2.One;\r\n        }\r\n    }\r\n\r\n    public static string GetTextureName() => \"karaoke-stage-light\";\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Skinning/Legacy/LegacyHitExplosion.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Animations;\r\nusing osu.Game.Rulesets.UI.Scrolling;\r\nusing osu.Game.Skinning;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Skinning.Legacy;\r\n\r\npublic partial class LegacyHitExplosion : LegacyKaraokeColumnElement\r\n{\r\n    private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();\r\n\r\n    private Drawable? explosion;\r\n\r\n    public LegacyHitExplosion()\r\n    {\r\n        RelativeSizeAxes = Axes.Both;\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(ISkinSource skin, IScrollingInfo scrollingInfo)\r\n    {\r\n        string imageName = GetKaraokeSkinConfig<string>(skin, LegacyKaraokeSkinConfigurationLookups.ExplosionImage)?.Value\r\n                           ?? GetTextureName();\r\n\r\n        float explosionScale = GetKaraokeSkinConfig<float>(skin, LegacyKaraokeSkinConfigurationLookups.ExplosionScale)?.Value\r\n                               ?? 1;\r\n\r\n        // Create a temporary animation to retrieve the number of frames, in an effort to calculate the intended frame length.\r\n        // This animation is discarded and re-queried with the appropriate frame length afterwards.\r\n        var tmp = skin.GetAnimation(imageName, true, false);\r\n        double frameLength = 0;\r\n        if (tmp is IFramedAnimation { FrameCount: > 0 } tmpAnimation)\r\n            frameLength = Math.Max(1000 / 60.0, 170.0 / tmpAnimation.FrameCount);\r\n\r\n        explosion = skin.GetAnimation(imageName, true, false, frameLength: frameLength)?.With(d =>\r\n        {\r\n            d.Origin = Anchor.Centre;\r\n            d.Blending = BlendingParameters.Additive;\r\n            d.Scale = new Vector2(explosionScale);\r\n        });\r\n\r\n        if (explosion != null)\r\n            InternalChild = explosion;\r\n\r\n        direction.BindTo(scrollingInfo.Direction);\r\n        direction.BindValueChanged(onDirectionChanged, true);\r\n    }\r\n\r\n    private void onDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)\r\n    {\r\n        if (explosion != null)\r\n            explosion.Anchor = direction.NewValue == ScrollingDirection.Left ? Anchor.CentreLeft : Anchor.CentreRight;\r\n    }\r\n\r\n    protected override void LoadComplete()\r\n    {\r\n        base.LoadComplete();\r\n\r\n        explosion?.FadeInFromZero(80)\r\n                 .Then().FadeOut(120);\r\n    }\r\n\r\n    public static string GetTextureName() => \"karaoke-lighting\";\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Skinning/Legacy/LegacyJudgementLine.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Graphics.Textures;\r\nusing osu.Framework.Layout;\r\nusing osu.Game.Rulesets.UI.Scrolling;\r\nusing osu.Game.Skinning;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Skinning.Legacy;\r\n\r\npublic partial class LegacyJudgementLine : LegacyKaraokeElement\r\n{\r\n    private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();\r\n    private readonly LayoutValue subtractionCache = new(Invalidation.DrawSize);\r\n\r\n    public LegacyJudgementLine()\r\n    {\r\n        RelativeSizeAxes = Axes.Y;\r\n        Anchor = Anchor.Centre;\r\n        Origin = Anchor.Centre;\r\n\r\n        AddLayout(subtractionCache);\r\n    }\r\n\r\n    private Sprite judgementLineBodySprite = null!;\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(ISkinSource skin, IScrollingInfo scrollingInfo)\r\n    {\r\n        InternalChildren = new Drawable[]\r\n        {\r\n            new Sprite\r\n            {\r\n                Anchor = Anchor.TopCentre,\r\n                Origin = Anchor.Centre,\r\n                Name = \"Judgement line head\",\r\n                Texture = getTextureFromLookup(skin, LegacyKaraokeSkinConfigurationLookups.JudgementLineHeadImage),\r\n            },\r\n            judgementLineBodySprite = new Sprite\r\n            {\r\n                Anchor = Anchor.Centre,\r\n                Origin = Anchor.Centre,\r\n                RelativeSizeAxes = Axes.Y,\r\n                Name = \"Judgement line body\",\r\n                Size = Vector2.One,\r\n                FillMode = FillMode.Stretch,\r\n                Depth = 1,\r\n                Texture = getTextureFromLookup(skin, LegacyKaraokeSkinConfigurationLookups.JudgementLineBodyImage),\r\n            },\r\n            new Sprite\r\n            {\r\n                Anchor = Anchor.BottomCentre,\r\n                Origin = Anchor.Centre,\r\n                Name = \"Judgement line tail\",\r\n                Texture = getTextureFromLookup(skin, LegacyKaraokeSkinConfigurationLookups.JudgementLineTailImage),\r\n            },\r\n        };\r\n\r\n        direction.BindTo(scrollingInfo.Direction);\r\n        direction.BindValueChanged(OnDirectionChanged, true);\r\n    }\r\n\r\n    protected virtual void OnDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)\r\n    {\r\n        Scale = direction.NewValue == ScrollingDirection.Left ? Vector2.One : new Vector2(-1, 1);\r\n    }\r\n\r\n    protected override void Update()\r\n    {\r\n        base.Update();\r\n\r\n        if (!subtractionCache.IsValid && DrawHeight > 0)\r\n        {\r\n            if (judgementLineBodySprite.Texture != null)\r\n            {\r\n                judgementLineBodySprite.Width = getWidth(judgementLineBodySprite);\r\n            }\r\n\r\n            subtractionCache.Validate();\r\n        }\r\n\r\n        static float getWidth(Sprite s) => s.Texture?.DisplayWidth ?? 0;\r\n    }\r\n\r\n    private static Texture? getTextureFromLookup(ISkin skin, LegacyKaraokeSkinConfigurationLookups lookup)\r\n        => skin.GetTexture(getTextureNameFromLookup(lookup));\r\n\r\n    private static string getTextureNameFromLookup(LegacyKaraokeSkinConfigurationLookups lookup)\r\n    {\r\n        string suffix = lookup switch\r\n        {\r\n            LegacyKaraokeSkinConfigurationLookups.JudgementLineBodyImage => \"body\",\r\n            LegacyKaraokeSkinConfigurationLookups.JudgementLineHeadImage => \"head\",\r\n            LegacyKaraokeSkinConfigurationLookups.JudgementLineTailImage => \"tail\",\r\n            _ => throw new ArgumentOutOfRangeException(nameof(lookup)),\r\n        };\r\n\r\n        return $\"karaoke-judgement-line-{suffix}\";\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Skinning/Legacy/LegacyKaraokeColumnElement.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.UI.Scrolling;\r\nusing osu.Game.Skinning;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Skinning.Legacy;\r\n\r\npublic partial class LegacyKaraokeColumnElement : LegacyKaraokeElement\r\n{\r\n    protected ScrollingNotePlayfield? NotePlayfield => Playfield?.NotePlayfield;\r\n\r\n    // TODO : get current index\r\n    protected override IBindable<T>? GetKaraokeSkinConfig<T>(ISkin skin, LegacyKaraokeSkinConfigurationLookups lookup, int? index = null)\r\n        => base.GetKaraokeSkinConfig<T>(skin, lookup, index);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Skinning/Legacy/LegacyKaraokeElement.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Rulesets.Karaoke.UI;\r\nusing osu.Game.Skinning;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Skinning.Legacy;\r\n\r\n/// <summary>\r\n/// A karaoke legacy skin element.\r\n/// </summary>\r\npublic partial class LegacyKaraokeElement : CompositeDrawable\r\n{\r\n    [Resolved]\r\n    protected KaraokePlayfield? Playfield { get; private set; }\r\n\r\n    /// <summary>\r\n    /// Retrieve a per-column-count skin configuration.\r\n    /// </summary>\r\n    /// <param name=\"skin\">The skin from which configuration is retrieved.</param>\r\n    /// <param name=\"lookup\">The value to retrieve.</param>\r\n    /// <param name=\"index\">If not null, denotes the index of the column to which the entry applies.</param>\r\n    protected virtual IBindable<T>? GetKaraokeSkinConfig<T>(ISkin skin, LegacyKaraokeSkinConfigurationLookups lookup, int? index = null) where T : notnull\r\n        => skin.GetConfig<KaraokeSkinConfigurationLookup, T>(\r\n            new KaraokeSkinConfigurationLookup(Playfield?.NotePlayfield.Columns ?? 4, lookup, index));\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Skinning/Legacy/LegacyNotePiece.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Extensions.Color4Extensions;\r\nusing osu.Framework.Extensions.IEnumerableExtensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Animations;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Layout;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Drawables;\r\nusing osu.Game.Rulesets.Karaoke.Skinning.Elements;\r\nusing osu.Game.Rulesets.Objects.Drawables;\r\nusing osu.Game.Rulesets.UI.Scrolling;\r\nusing osu.Game.Skinning;\r\nusing osuTK;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Skinning.Legacy;\r\n\r\npublic partial class LegacyNotePiece : LegacyKaraokeColumnElement\r\n{\r\n    protected readonly Bindable<Color4> AccentColour = new();\r\n    protected readonly Bindable<Color4> HitColour = new();\r\n\r\n    private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();\r\n    private readonly LayoutValue subtractionCache = new(Invalidation.DrawSize);\r\n    private readonly IBindable<bool> isHitting = new Bindable<bool>();\r\n    private readonly IBindable<bool> display = new Bindable<bool>();\r\n    private readonly IBindableDictionary<Singer, SingerState[]> singer = new BindableDictionary<Singer, SingerState[]>();\r\n\r\n    public LegacyNotePiece()\r\n    {\r\n        Anchor = Anchor.Centre;\r\n        Origin = Anchor.Centre;\r\n        RelativeSizeAxes = Axes.Both;\r\n\r\n        AddLayout(subtractionCache);\r\n    }\r\n\r\n    private LayerContainer background = null!;\r\n    private LayerContainer foreground = null!;\r\n    private LayerContainer border = null!;\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(DrawableHitObject drawableObject, ISkinSource skin, IScrollingInfo scrollingInfo)\r\n    {\r\n        InternalChildren = new[]\r\n        {\r\n            background = createLayer(\"Background layer\", skin, LegacyKaraokeSkinNoteLayer.Background),\r\n            foreground = createLayer(\"Foreground layer\", skin, LegacyKaraokeSkinNoteLayer.Foreground),\r\n            border = createLayer(\"Border layer\", skin, LegacyKaraokeSkinNoteLayer.Border),\r\n        };\r\n\r\n        var note = (DrawableNote)drawableObject;\r\n\r\n        direction.BindTo(scrollingInfo.Direction);\r\n        direction.BindValueChanged(OnDirectionChanged, true);\r\n        isHitting.BindTo(note.IsHitting);\r\n        display.BindTo(note.DisplayBindable);\r\n        singer.BindTo(note.SingersBindable);\r\n\r\n        AccentColour.BindValueChanged(onAccentChanged);\r\n        HitColour.BindValueChanged(onAccentChanged);\r\n        isHitting.BindValueChanged(onIsHittingChanged, true);\r\n        display.BindValueChanged(_ => onAccentChanged(), true);\r\n        singer.BindCollectionChanged((_, _) => applySingerStyle(skin, note.HitObject), true);\r\n    }\r\n\r\n    private void onIsHittingChanged(ValueChangedEvent<bool> isHitting)\r\n    {\r\n        // Update animate\r\n        InternalChildren.OfType<LayerContainer>().ForEach(x =>\r\n        {\r\n            x.Reset();\r\n            x.IsPlaying = isHitting.NewValue;\r\n        });\r\n\r\n        // Foreground sparkle\r\n        foreground.ClearTransforms(false, nameof(foreground.Colour));\r\n        foreground.Alpha = 0;\r\n\r\n        if (!isHitting.NewValue)\r\n            return;\r\n\r\n        foreground.Alpha = 1;\r\n\r\n        const float animation_length = 50;\r\n\r\n        // wait for the next sync point\r\n        double synchronisedOffset = animation_length * 2 - Time.Current % (animation_length * 2);\r\n        using (foreground.BeginDelayedSequence(synchronisedOffset))\r\n            foreground.FadeColour(AccentColour.Value.Lighten(0.7f), animation_length).Then().FadeColour(foreground.Colour, animation_length).Loop();\r\n    }\r\n\r\n    private void applySingerStyle(ISkinSource skin, Note note)\r\n    {\r\n        var noteSkin = skin.GetConfig<Note, NoteStyle>(note)?.Value;\r\n        if (noteSkin == null)\r\n            return;\r\n\r\n        AccentColour.Value = noteSkin.NoteColor;\r\n        HitColour.Value = noteSkin.BlinkColor;\r\n    }\r\n\r\n    protected override void Update()\r\n    {\r\n        base.Update();\r\n\r\n        if (!subtractionCache.IsValid && DrawWidth > 0)\r\n        {\r\n            // TODO : maybe do something\r\n            subtractionCache.Validate();\r\n        }\r\n    }\r\n\r\n    protected virtual void OnDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)\r\n    {\r\n        if (direction.NewValue == ScrollingDirection.Left)\r\n        {\r\n            InternalChildren.ForEach(x => x.Scale = Vector2.One);\r\n        }\r\n        else\r\n        {\r\n            InternalChildren.ForEach(x => x.Scale = new Vector2(-1, 1));\r\n        }\r\n    }\r\n\r\n    private LayerContainer createLayer(string name, ISkin skin, LegacyKaraokeSkinNoteLayer layer) =>\r\n        new()\r\n        {\r\n            RelativeSizeAxes = Axes.Both,\r\n            Name = name,\r\n            Children = new[]\r\n            {\r\n                getSpriteFromLookup(skin, LegacyKaraokeSkinConfigurationLookups.NoteHeadImage, layer)?.With(d =>\r\n                {\r\n                    d.Name = \"Head\";\r\n                    d.Anchor = Anchor.CentreLeft;\r\n                    d.Origin = Anchor.Centre;\r\n                }),\r\n                getSpriteFromLookup(skin, LegacyKaraokeSkinConfigurationLookups.NoteBodyImage, layer)?.With(d =>\r\n                {\r\n                    d.Name = \"Body\";\r\n                    d.Anchor = Anchor.Centre;\r\n                    d.Origin = Anchor.Centre;\r\n                    d.Size = Vector2.One;\r\n                    d.FillMode = FillMode.Stretch;\r\n                    d.RelativeSizeAxes = Axes.X;\r\n                    d.Depth = 1;\r\n\r\n                    d.Height = d.Texture?.DisplayHeight ?? 0;\r\n                }),\r\n                getSpriteFromLookup(skin, LegacyKaraokeSkinConfigurationLookups.NoteTailImage, layer)?.With(d =>\r\n                {\r\n                    d.Name = \"Tail\";\r\n                    d.Anchor = Anchor.CentreRight;\r\n                    d.Origin = Anchor.Centre;\r\n                }),\r\n            },\r\n        };\r\n\r\n    private static Sprite? getSpriteFromLookup(ISkin skin, LegacyKaraokeSkinConfigurationLookups lookup, LegacyKaraokeSkinNoteLayer layer)\r\n    {\r\n        string name = getTextureNameFromLookup(lookup, layer);\r\n\r\n        switch (layer)\r\n        {\r\n            case LegacyKaraokeSkinNoteLayer.Background:\r\n            case LegacyKaraokeSkinNoteLayer.Border:\r\n                return getSpriteByName(name) ?? new Sprite();\r\n\r\n            case LegacyKaraokeSkinNoteLayer.Foreground:\r\n                return getSpriteByName(name)\r\n                       ?? getSpriteByName(getTextureNameFromLookup(lookup, LegacyKaraokeSkinNoteLayer.Background))\r\n                       ?? new Sprite();\r\n\r\n            default:\r\n                return null;\r\n        }\r\n\r\n        Sprite? getSpriteByName(string spriteName) => (Sprite?)skin.GetAnimation(spriteName, true, true)?.With(d =>\r\n        {\r\n            switch (d)\r\n            {\r\n                case TextureAnimation animation:\r\n                    animation.IsPlaying = false;\r\n                    break;\r\n            }\r\n        });\r\n    }\r\n\r\n    private static string getTextureNameFromLookup(LegacyKaraokeSkinConfigurationLookups lookup, LegacyKaraokeSkinNoteLayer layer)\r\n    {\r\n        string suffix = lookup switch\r\n        {\r\n            LegacyKaraokeSkinConfigurationLookups.NoteBodyImage => \"body\",\r\n            LegacyKaraokeSkinConfigurationLookups.NoteHeadImage => \"head\",\r\n            LegacyKaraokeSkinConfigurationLookups.NoteTailImage => \"tail\",\r\n            _ => throw new ArgumentOutOfRangeException(nameof(lookup)),\r\n        };\r\n\r\n        string layerSuffix = layer switch\r\n        {\r\n            LegacyKaraokeSkinNoteLayer.Border => \"border\",\r\n            LegacyKaraokeSkinNoteLayer.Background => \"background\",\r\n            _ => string.Empty,\r\n        };\r\n\r\n        return $\"karaoke-note-{layerSuffix}-{suffix}\";\r\n    }\r\n\r\n    private void onAccentChanged() => onAccentChanged(new ValueChangedEvent<Color4>(AccentColour.Value, AccentColour.Value));\r\n\r\n    private void onAccentChanged(ValueChangedEvent<Color4> accent)\r\n    {\r\n        foreground.Colour = HitColour.Value;\r\n        background.Colour = display.Value ? accent.NewValue : new Color4(23, 41, 46, 255);\r\n\r\n        subtractionCache.Invalidate();\r\n    }\r\n\r\n    private partial class LayerContainer : Container\r\n    {\r\n        public IEnumerable<TextureAnimation> AnimateChildren => Children.OfType<TextureAnimation>();\r\n\r\n        public bool IsPlaying\r\n        {\r\n            set => AnimateChildren.ForEach(d => d.IsPlaying = value);\r\n        }\r\n\r\n        public void Reset() => AnimateChildren.ForEach(d => d.GotoFrame(0));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Skinning/Legacy/LegacyStageBackground.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Graphics.Textures;\r\nusing osu.Game.Rulesets.UI.Scrolling;\r\nusing osu.Game.Skinning;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Skinning.Legacy;\r\n\r\npublic partial class LegacyStageBackground : LegacyKaraokeElement\r\n{\r\n    private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();\r\n\r\n    public LegacyStageBackground()\r\n    {\r\n        RelativeSizeAxes = Axes.Both;\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(ISkinSource skin, IScrollingInfo scrollingInfo)\r\n    {\r\n        InternalChild = new Sprite\r\n        {\r\n            Anchor = Anchor.BottomRight,\r\n            Origin = Anchor.BottomRight,\r\n            Texture = getTexture(skin),\r\n        };\r\n\r\n        direction.BindTo(scrollingInfo.Direction);\r\n        direction.BindValueChanged(OnDirectionChanged, true);\r\n    }\r\n\r\n    protected virtual void OnDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)\r\n    {\r\n        Scale = direction.NewValue == ScrollingDirection.Left ? Vector2.One : new Vector2(-1, 1);\r\n    }\r\n\r\n    private Texture? getTexture(ISkinSource skin) => skin.GetTexture(GetTextureName());\r\n\r\n    public static string GetTextureName() => \"karaoke-stage-background\";\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Skinning/Tools/SkinConverterTool.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Extensions;\r\nusing osu.Framework.Graphics.Shaders;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.Sprites;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Skinning.Tools;\r\n\r\n// it's the temp logic to collect logic.\r\npublic static class SkinConverterTool\r\n{\r\n    public static ICustomizedShader[] ConvertLeftSideShader(ShaderManager? shaderManager, LyricStyle lyricStyle)\r\n    {\r\n        if (shaderManager == null)\r\n            return Array.Empty<ICustomizedShader>();\r\n\r\n        var shaders = lyricStyle.LeftLyricTextShaders.ToArray();\r\n        attachShaders(shaderManager, shaders);\r\n\r\n        return shaders;\r\n    }\r\n\r\n    public static ICustomizedShader[] ConvertRightSideShader(ShaderManager? shaderManager, LyricStyle lyricStyle)\r\n    {\r\n        if (shaderManager == null)\r\n            return Array.Empty<ICustomizedShader>();\r\n\r\n        var shaders = lyricStyle.RightLyricTextShaders.ToArray();\r\n        attachShaders(shaderManager, shaders);\r\n\r\n        return shaders;\r\n    }\r\n\r\n    private static void attachShaders(ShaderManager shaderManager, IEnumerable<ICustomizedShader> shaders)\r\n    {\r\n        foreach (var shader in shaders)\r\n        {\r\n            switch (shader)\r\n            {\r\n                case InternalShader internalShader:\r\n                    shaderManager.AttachShader(internalShader);\r\n                    break;\r\n\r\n                case StepShader stepShader:\r\n                    attachShaders(shaderManager, stepShader.StepShaders.ToArray());\r\n                    break;\r\n\r\n                case null:\r\n                    throw new InvalidCastException(\"shader cannot be null.\");\r\n\r\n                default:\r\n                    throw new InvalidCastException($\"{shader.GetType()} cannot attach shader.\");\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Skinning/Triangles/KaraokeTrianglesSkinTransformer.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Skinning.Default;\r\nusing osu.Game.Skinning;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Skinning.Triangles;\r\n\r\npublic class KaraokeTrianglesSkinTransformer : KaraokeDefaultSkinTransformer\r\n{\r\n    public KaraokeTrianglesSkinTransformer(ISkin skin, IBeatmap beatmap)\r\n        : base(skin, beatmap)\r\n    {\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/Commands/IStageCommand.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Transforms;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages.Commands;\r\n\r\npublic interface IStageCommand\r\n{\r\n    /// <summary>\r\n    /// The start time of the stage command.\r\n    /// </summary>\r\n    double StartTime { get; }\r\n\r\n    /// <summary>\r\n    /// The end time of the stage command.\r\n    /// </summary>\r\n    double EndTime { get; }\r\n\r\n    /// <summary>\r\n    /// The name of the <see cref=\"Drawable\"/> property affected by this stage command.\r\n    /// Used to apply initial property values based on the list of commands given in <see cref=\"Drawable\"/>.\r\n    /// </summary>\r\n    string PropertyName { get; }\r\n\r\n    /// <summary>\r\n    /// Sets the value of the corresponding property in <see cref=\"Drawable\"/> to the start value of this command.\r\n    /// </summary>\r\n    /// <param name=\"d\">The target drawable.</param>\r\n    void ApplyInitialValue<TDrawable>(TDrawable d)\r\n        where TDrawable : Drawable;\r\n\r\n    /// <summary>\r\n    /// Applies the transforms described by this stage command to the target drawable.\r\n    /// </summary>\r\n    /// <param name=\"d\">The target drawable.</param>\r\n    /// <returns>The sequence of transforms applied to the target drawable.</returns>\r\n    TransformSequence<TDrawable> ApplyTransforms<TDrawable>(TDrawable d)\r\n        where TDrawable : Drawable;\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/Commands/Lyrics/LyricStyleCommand.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Transforms;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Drawables;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages.Commands.Lyrics;\r\n\r\npublic class LyricStyleCommand : StageCommand<LyricStyle>\r\n{\r\n    public LyricStyleCommand(Easing easing, double startTime, double endTime, LyricStyle startValue, LyricStyle endValue)\r\n        : base(easing, startTime, endTime, startValue, endValue)\r\n    {\r\n    }\r\n\r\n    public override string PropertyName => nameof(LyricStyle);\r\n\r\n    public override void ApplyInitialValue<TDrawable>(TDrawable d)\r\n    {\r\n        if (d is not DrawableLyric drawableLyric)\r\n            throw new InvalidOperationException();\r\n\r\n        drawableLyric.ApplyToLyricPieces(l =>\r\n        {\r\n            l.UpdateStyle(StartValue);\r\n        });\r\n    }\r\n\r\n    public override TransformSequence<TDrawable> ApplyTransforms<TDrawable>(TDrawable d)\r\n    {\r\n        // note: because update shader cost lots of effect, if the duration is 0, we just use the initial value.\r\n        if (Duration == 0)\r\n            return d.Delay(0);\r\n\r\n        return d.TransformTo(d.PopulateTransform(new ApplyLyricFontTransform(), StartValue))\r\n                .Delay(Duration)\r\n                .Append(o => o.TransformTo(d.PopulateTransform(new ApplyLyricFontTransform(), EndValue)));\r\n    }\r\n\r\n    private class ApplyLyricFontTransform : Transform<LyricStyle, Drawable>\r\n    {\r\n        public override string TargetMember => nameof(LyricStyle);\r\n\r\n        protected override void Apply(Drawable d, double time)\r\n        {\r\n            if (d is not DrawableLyric drawableLyric)\r\n                throw new InvalidOperationException();\r\n\r\n            drawableLyric.ApplyToLyricPieces(l =>\r\n            {\r\n                l.UpdateStyle(EndValue);\r\n            });\r\n        }\r\n\r\n        protected override void ReadIntoStartValue(Drawable d)\r\n        {\r\n            // there's no start value for it.\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/Commands/StageAlphaCommand.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Transforms;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages.Commands;\r\n\r\npublic class StageAlphaCommand : StageCommand<float>\r\n{\r\n    public StageAlphaCommand(Easing easing, double startTime, double endTime, float startValue, float endValue)\r\n        : base(easing, startTime, endTime, startValue, endValue)\r\n    {\r\n    }\r\n\r\n    public override string PropertyName => nameof(Drawable.Alpha);\r\n\r\n    public override void ApplyInitialValue<TDrawable>(TDrawable d) => d.Alpha = StartValue;\r\n\r\n    public override TransformSequence<TDrawable> ApplyTransforms<TDrawable>(TDrawable d)\r\n        => d.FadeTo(StartValue).Then().FadeTo(EndValue, Duration, Easing);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/Commands/StageAnchorCommand.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Transforms;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages.Commands;\r\n\r\npublic class StageAnchorCommand : StageCommand<Anchor>\r\n{\r\n    public StageAnchorCommand(Easing easing, double startTime, double endTime, Anchor startValue, Anchor endValue)\r\n        : base(easing, startTime, endTime, startValue, endValue)\r\n    {\r\n    }\r\n\r\n    public override string PropertyName => nameof(Drawable.Anchor);\r\n\r\n    public override void ApplyInitialValue<TDrawable>(TDrawable d)\r\n    {\r\n        if (StartTime == EndTime)\r\n            d.Anchor = StartValue;\r\n    }\r\n    public override TransformSequence<TDrawable> ApplyTransforms<TDrawable>(TDrawable d)\r\n        => d.TransformTo(nameof(Drawable.Anchor), StartValue).Delay(Duration)\r\n            .TransformTo(nameof(Drawable.Anchor), EndValue);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/Commands/StageCommand.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Transforms;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages.Commands;\r\n\r\npublic abstract class StageCommand<T> : IStageCommand, IComparable<StageCommand<T>>\r\n{\r\n    public double StartTime { get; }\r\n    public double EndTime { get; }\r\n\r\n    public T StartValue { get; }\r\n    public T EndValue { get; }\r\n    public Easing Easing { get; }\r\n\r\n    public double Duration => EndTime - StartTime;\r\n\r\n    protected StageCommand(Easing easing, double startTime, double endTime, T startValue, T endValue)\r\n    {\r\n        if (endTime < startTime)\r\n            endTime = startTime;\r\n\r\n        StartTime = startTime;\r\n        StartValue = startValue;\r\n        EndTime = endTime;\r\n        EndValue = endValue;\r\n        Easing = easing;\r\n    }\r\n\r\n    public abstract string PropertyName { get; }\r\n\r\n    public abstract void ApplyInitialValue<TDrawable>(TDrawable d)\r\n        where TDrawable : Drawable;\r\n\r\n    public abstract TransformSequence<TDrawable> ApplyTransforms<TDrawable>(TDrawable d)\r\n        where TDrawable : Drawable;\r\n\r\n    public int CompareTo(StageCommand<T>? other)\r\n    {\r\n        if (other == null)\r\n            return 1;\r\n\r\n        int result = StartTime.CompareTo(other.StartTime);\r\n        if (result != 0)\r\n            return result;\r\n\r\n        return EndTime.CompareTo(other.EndTime);\r\n    }\r\n\r\n    public override string ToString() => $\"{StartTime} -> {EndTime}, {StartValue} -> {EndValue} {Easing}\";\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/Commands/StageHeightCommand.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Transforms;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages.Commands;\r\n\r\npublic class StageHeightCommand : StageCommand<float>\r\n{\r\n    public StageHeightCommand(Easing easing, double startTime, double endTime, float startValue, float endValue)\r\n        : base(easing, startTime, endTime, startValue, endValue)\r\n    {\r\n    }\r\n\r\n    public override string PropertyName => nameof(Drawable.Height);\r\n\r\n    public override void ApplyInitialValue<TDrawable>(TDrawable d) => d.Height = StartValue;\r\n\r\n    public override TransformSequence<TDrawable> ApplyTransforms<TDrawable>(TDrawable d)\r\n        => d.ResizeHeightTo(StartValue).Then().ResizeHeightTo(EndValue, Duration, Easing);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/Commands/StageOriginCommand.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Transforms;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages.Commands;\r\n\r\npublic class StageOriginCommand : StageCommand<Anchor>\r\n{\r\n    public StageOriginCommand(Easing easing, double startTime, double endTime, Anchor startValue, Anchor endValue)\r\n        : base(easing, startTime, endTime, startValue, endValue)\r\n    {\r\n    }\r\n\r\n    public override string PropertyName => nameof(Drawable.Origin);\r\n\r\n    public override void ApplyInitialValue<TDrawable>(TDrawable d)\r\n    {\r\n        if (StartTime == EndTime)\r\n            d.Origin = StartValue;\r\n    }\r\n    public override TransformSequence<TDrawable> ApplyTransforms<TDrawable>(TDrawable d)\r\n        => d.TransformTo(nameof(Drawable.Origin), StartValue).Delay(Duration)\r\n            .TransformTo(nameof(Drawable.Origin), EndValue);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/Commands/StagePaddingCommand.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Transforms;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages.Commands;\r\n\r\npublic class StagePaddingCommand : StageCommand<MarginPadding>\r\n{\r\n    public StagePaddingCommand(Easing easing, double startTime, double endTime, MarginPadding startValue, MarginPadding endValue)\r\n        : base(easing, startTime, endTime, startValue, endValue)\r\n    {\r\n    }\r\n\r\n    public override string PropertyName => nameof(CompositeDrawable.Padding);\r\n\r\n    public override void ApplyInitialValue<TDrawable>(TDrawable d)\r\n    {\r\n        // todo: composite drawable can only change the padding by transform.\r\n    }\r\n    public override TransformSequence<TDrawable> ApplyTransforms<TDrawable>(TDrawable d)\r\n        => d.TransformTo(nameof(CompositeDrawable.Padding), StartValue).Delay(Duration)\r\n            .TransformTo(nameof(CompositeDrawable.Padding), EndValue);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/Commands/StageScaleCommand.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Transforms;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages.Commands;\r\n\r\npublic class StageScaleCommand : StageCommand<float>\r\n{\r\n    public StageScaleCommand(Easing easing, double startTime, double endTime, float startValue, float endValue)\r\n        : base(easing, startTime, endTime, startValue, endValue)\r\n    {\r\n    }\r\n\r\n    public override string PropertyName => nameof(Drawable.Scale);\r\n\r\n    public override void ApplyInitialValue<TDrawable>(TDrawable d) => d.Scale = new Vector2(StartValue);\r\n\r\n    public override TransformSequence<TDrawable> ApplyTransforms<TDrawable>(TDrawable d)\r\n        => d.ScaleTo(StartValue).Then().ScaleTo(EndValue, Duration, Easing);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/Commands/StageWidthCommand.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Transforms;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages.Commands;\r\n\r\npublic class StageWidthCommand : StageCommand<float>\r\n{\r\n    public StageWidthCommand(Easing easing, double startTime, double endTime, float startValue, float endValue)\r\n        : base(easing, startTime, endTime, startValue, endValue)\r\n    {\r\n    }\r\n\r\n    public override string PropertyName => nameof(Drawable.Width);\r\n\r\n    public override void ApplyInitialValue<TDrawable>(TDrawable d) => d.Width = StartValue;\r\n\r\n    public override TransformSequence<TDrawable> ApplyTransforms<TDrawable>(TDrawable d)\r\n        => d.ResizeWidthTo(StartValue).Then().ResizeWidthTo(EndValue, Duration, Easing);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/Commands/StageXCommand.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Transforms;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages.Commands;\r\n\r\npublic class StageXCommand : StageCommand<float>\r\n{\r\n    public StageXCommand(Easing easing, double startTime, double endTime, float startValue, float endValue)\r\n        : base(easing, startTime, endTime, startValue, endValue)\r\n    {\r\n    }\r\n\r\n    public override string PropertyName => nameof(Drawable.X);\r\n\r\n    public override void ApplyInitialValue<TDrawable>(TDrawable d) => d.X = StartValue;\r\n\r\n    public override TransformSequence<TDrawable> ApplyTransforms<TDrawable>(TDrawable d)\r\n        => d.MoveToX(StartValue).Then().MoveToX(EndValue, Duration, Easing);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/Commands/StageYCommand.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Transforms;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages.Commands;\r\n\r\npublic class StageYCommand : StageCommand<float>\r\n{\r\n    public StageYCommand(Easing easing, double startTime, double endTime, float startValue, float endValue)\r\n        : base(easing, startTime, endTime, startValue, endValue)\r\n    {\r\n    }\r\n\r\n    public override string PropertyName => nameof(Drawable.Y);\r\n\r\n    public override void ApplyInitialValue<TDrawable>(TDrawable d) => d.Y = StartValue;\r\n\r\n    public override TransformSequence<TDrawable> ApplyTransforms<TDrawable>(TDrawable d)\r\n        => d.MoveToY(StartValue).Then().MoveToY(EndValue, Duration, Easing);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/Drawables/DrawableStage.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Stages.Preview;\r\nusing osu.Game.Rulesets.Karaoke.Mods;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos.Preview;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos.Types;\r\nusing osu.Game.Rulesets.Karaoke.UI;\r\nusing osu.Game.Rulesets.Mods;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages.Drawables;\r\n\r\n/// <summary>\r\n/// Environment for execute the stage.\r\n/// </summary>\r\npublic partial class DrawableStage : Container\r\n{\r\n    [Cached(typeof(IStageHitObjectRunner))]\r\n    private readonly StageHitObjectRunner stageRunner = new();\r\n\r\n    [Cached(typeof(IStagePlayfieldRunner))]\r\n    private readonly StagePlayfieldRunner stagePlayfieldRunner = new();\r\n\r\n    [Cached(typeof(IStageElementRunner))]\r\n    private readonly StageElementRunner stageElementRunner = new();\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(IReadOnlyList<Mod> mods, IBeatmap beatmap)\r\n    {\r\n        Container stageLayer = new Container\r\n        {\r\n            RelativeSizeAxes = Axes.Both,\r\n        };\r\n\r\n        AddInternal(stageLayer);\r\n        stageElementRunner.UpdateStageElements(stageLayer);\r\n\r\n        if (beatmap is not KaraokeBeatmap karaokeBeatmap)\r\n            throw new InvalidOperationException();\r\n\r\n        TriggerRecalculate(karaokeBeatmap, mods);\r\n    }\r\n\r\n    public void TriggerRecalculate(KaraokeBeatmap karaokeBeatmap, IReadOnlyList<Mod> mods)\r\n    {\r\n        var stageInfo = getStageInfo(mods, karaokeBeatmap);\r\n\r\n        // fill the working property.\r\n        if (stageInfo is IHasCalculatedProperty calculatedProperty)\r\n            calculatedProperty.ValidateCalculatedProperty(karaokeBeatmap);\r\n\r\n        bool scorable = karaokeBeatmap.IsScorable();\r\n\r\n        stageRunner.OnStageInfoChanged(stageInfo, scorable, mods);\r\n        stagePlayfieldRunner.OnStageInfoChanged(stageInfo, scorable, mods);\r\n        stageElementRunner.OnStageInfoChanged(stageInfo, scorable, mods);\r\n    }\r\n\r\n    public override void Add(Drawable drawable)\r\n    {\r\n        base.Add(drawable);\r\n\r\n        if (drawable is KaraokePlayfield karaokePlayfield)\r\n            stagePlayfieldRunner.UpdatePlayfieldTransforms(karaokePlayfield);\r\n    }\r\n\r\n    private static StageInfo getStageInfo(IReadOnlyList<Mod> mods, KaraokeBeatmap beatmap)\r\n    {\r\n        // todo: get all available stages from resource provider.\r\n        var availableStageInfos = Array.Empty<StageInfo>();\r\n\r\n        // Get list of matched mods.\r\n        // Return the first stage info if no stage mod is found.\r\n        var stageMod = mods.OfType<IApplicableToStageInfo>().SingleOrDefault();\r\n        if (stageMod == null)\r\n            return availableStageInfos.FirstOrDefault() ?? createDefaultStageInfo(beatmap);\r\n\r\n        // If user select a stage mod, means user want to use the specific type of stage.\r\n        // We should find the matched stage info from the available stage infos.\r\n        var matchedStageInfo = availableStageInfos.FirstOrDefault(x => stageMod.CanApply(x));\r\n\r\n        // If the matched stage info is not found, then trying to create a default one.\r\n        if (matchedStageInfo == null)\r\n        {\r\n            // Note that not every stage mod can create the default stage info.\r\n            // If not possible to create, then use the default one and not override the value in the stage info.\r\n            var newStageInfo = stageMod.CreateDefaultStageInfo(beatmap);\r\n            if (newStageInfo == null)\r\n            {\r\n                return createDefaultStageInfo(beatmap);\r\n            }\r\n\r\n            matchedStageInfo = newStageInfo;\r\n        }\r\n\r\n        stageMod.ApplyToStageInfo(matchedStageInfo);\r\n        return matchedStageInfo;\r\n    }\r\n\r\n    private static StageInfo createDefaultStageInfo(KaraokeBeatmap beatmap)\r\n    {\r\n        var config = new PreviewStageInfoGeneratorConfig();\r\n        var generator = new PreviewStageInfoGenerator(config);\r\n\r\n        return (PreviewStageInfo)generator.Generate(beatmap);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/Drawables/DrawableStageBeatmapCoverInfo.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Graphics.Textures;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Containers;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Resources.Localisation.Web;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages.Drawables;\r\n\r\npublic partial class DrawableStageBeatmapCoverInfo : CompositeDrawable\r\n{\r\n    private readonly StageBeatmapCoverInfo stageBeatmapCoverInfo;\r\n\r\n    public DrawableStageBeatmapCoverInfo(StageBeatmapCoverInfo info)\r\n    {\r\n        stageBeatmapCoverInfo = info;\r\n\r\n        Masking = true;\r\n        CornerRadius = 10;\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(IBindable<WorkingBeatmap> beatmap, OsuColour colours)\r\n    {\r\n        stageBeatmapCoverInfo.ApplyTransforms(this);\r\n\r\n        var metadata = beatmap.Value.Metadata;\r\n\r\n        InternalChildren = new Drawable[]\r\n        {\r\n            new BeatmapCover\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n            },\r\n            new Box\r\n            {\r\n                Colour = colours.Gray1,\r\n                Alpha = 0.6f,\r\n                Anchor = Anchor.BottomCentre,\r\n                Origin = Anchor.BottomCentre,\r\n                RelativeSizeAxes = Axes.X,\r\n                Height = 64,\r\n            },\r\n            new FillFlowContainer\r\n            {\r\n                Anchor = Anchor.BottomCentre,\r\n                Origin = Anchor.BottomCentre,\r\n                RelativeSizeAxes = Axes.X,\r\n                Height = 64,\r\n                Padding = new MarginPadding\r\n                {\r\n                    Horizontal = 10,\r\n                },\r\n                Direction = FillDirection.Vertical,\r\n                Children = new Drawable[]\r\n                {\r\n                    new TruncatingSpriteText\r\n                    {\r\n                        Text = new RomanisableString(metadata.TitleUnicode, metadata.Title),\r\n                        Font = OsuFont.Default.With(size: 22.5f, weight: FontWeight.SemiBold),\r\n                        RelativeSizeAxes = Axes.X,\r\n                    },\r\n                    new TruncatingSpriteText\r\n                    {\r\n                        Text = createArtistText(metadata),\r\n                        Font = OsuFont.Default.With(size: 17.5f, weight: FontWeight.SemiBold),\r\n                        RelativeSizeAxes = Axes.X,\r\n                    },\r\n                    new LinkFlowContainer(s =>\r\n                    {\r\n                        s.Shadow = false;\r\n                        s.Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold);\r\n                    }).With(d =>\r\n                    {\r\n                        d.AutoSizeAxes = Axes.Both;\r\n                        d.Margin = new MarginPadding { Top = 2 };\r\n                        d.AddText(\"mapped by \", t => t.Colour = colours.GrayB);\r\n                        d.AddUserLink(metadata.Author);\r\n                    }),\r\n                },\r\n            },\r\n        };\r\n    }\r\n\r\n    private static LocalisableString createArtistText(IBeatmapMetadataInfo beatmapMetadata)\r\n    {\r\n        var romanisableArtist = new RomanisableString(beatmapMetadata.ArtistUnicode, beatmapMetadata.Artist);\r\n        return BeatmapsetsStrings.ShowDetailsByArtist(romanisableArtist);\r\n    }\r\n\r\n    private partial class BeatmapCover : CompositeDrawable\r\n    {\r\n        private const string fallback_texture_name = \"Backgrounds/bg1\";\r\n\r\n        private readonly Sprite sprite;\r\n\r\n        public BeatmapCover()\r\n        {\r\n            AddInternal(sprite = new Sprite\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n                FillMode = FillMode.Fill,\r\n                Anchor = Anchor.Centre,\r\n                Origin = Anchor.Centre,\r\n            });\r\n        }\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(IBindable<WorkingBeatmap> beatmap, LargeTextureStore textures)\r\n        {\r\n            sprite.Texture = beatmap.Value?.GetBackground() ?? textures.Get(fallback_texture_name);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/Drawables/IStageElementRunner.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics.Containers;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages.Drawables;\r\n\r\npublic interface IStageElementRunner\r\n{\r\n    /// <summary>\r\n    /// Apply <see cref=\"IStageElement\"/> to the stage.\r\n    /// </summary>\r\n    /// <param name=\"container\"></param>\r\n    void UpdateStageElements(Container container);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/Drawables/IStageHitObjectRunner.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Drawables;\r\nusing osu.Game.Rulesets.Objects;\r\nusing osu.Game.Rulesets.Objects.Drawables;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages.Drawables;\r\n\r\npublic interface IStageHitObjectRunner\r\n{\r\n    event Action? OnCommandUpdated;\r\n\r\n    /// <summary>\r\n    /// Get the preempt time for the <see cref=\"DrawableKaraokeHitObject\"/>.\r\n    /// </summary>\r\n    /// <param name=\"hitObject\"></param>\r\n    double GetPreemptTime(HitObject hitObject);\r\n\r\n    /// <summary>\r\n    /// Get the offset time between <see cref=\"HitObject.StartTime\"/> and stage start time.\r\n    /// </summary>\r\n    /// <param name=\"hitObject\"></param>\r\n    /// <returns></returns>\r\n    double GetStartTimeOffset(HitObject hitObject);\r\n\r\n    /// <summary>\r\n    /// Get the offset time between <see cref=\"HitObjectExtensions.GetEndTime\"/> and stage start time.\r\n    /// </summary>\r\n    /// <param name=\"hitObject\"></param>\r\n    /// <returns></returns>\r\n    double GetEndTimeOffset(HitObject hitObject);\r\n\r\n    /// <summary>\r\n    /// Apply (generally fade-in) transforms leading into the <see cref=\"KaraokeHitObject\"/> start time.\r\n    /// </summary>\r\n    /// <param name=\"drawableHitObject\"></param>\r\n    void UpdateInitialTransforms(DrawableHitObject drawableHitObject);\r\n\r\n    /// <summary>\r\n    /// Apply passive transforms at the <see cref=\"KaraokeHitObject\"/>'s StartTime.\r\n    /// </summary>\r\n    /// <param name=\"drawableHitObject\"></param>\r\n    void UpdateStartTimeStateTransforms(DrawableHitObject drawableHitObject);\r\n\r\n    /// <summary>\r\n    /// Apply transforms based on the current <see cref=\"ArmedState\"/>.\r\n    /// This call is offset by (HitObject.EndTime + Result.Offset), equivalent to when the user hit the object.\r\n    /// </summary>\r\n    /// <param name=\"drawableHitObject\"></param>\r\n    /// <param name=\"state\"></param>\r\n    void UpdateHitStateTransforms(DrawableHitObject drawableHitObject, ArmedState state);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/Drawables/IStagePlayfieldRunner.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.UI;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages.Drawables;\r\n\r\npublic interface IStagePlayfieldRunner\r\n{\r\n    /// <summary>\r\n    /// Apply transforms to the main playfield and child playfield.\r\n    /// </summary>\r\n    /// <param name=\"playfield\"></param>\r\n    void UpdatePlayfieldTransforms(KaraokePlayfield playfield);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/Drawables/StageElementRunner.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Rulesets.Karaoke.Mods;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos;\r\nusing osu.Game.Rulesets.Mods;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages.Drawables;\r\n\r\npublic class StageElementRunner : StageRunner, IStageElementRunner\r\n{\r\n    private IStageElementProvider? elementProvider;\r\n    private IList<IApplicableToStageElement>? stageMods;\r\n    private Container? elementContainer;\r\n\r\n    public override void OnStageInfoChanged(StageInfo stageInfo, bool scorable, IReadOnlyList<Mod> mods)\r\n    {\r\n        elementProvider = stageInfo.CreateStageElementProvider(scorable);\r\n        stageMods = mods.OfType<IApplicableToStageElement>().Where(x => x.CanApply(stageInfo)).ToList();\r\n        applyTransforms();\r\n    }\r\n\r\n    public override void TriggerUpdateCommand()\r\n    {\r\n        applyTransforms();\r\n    }\r\n\r\n    public void UpdateStageElements(Container container)\r\n    {\r\n        elementContainer = container;\r\n        applyTransforms();\r\n    }\r\n\r\n    private void applyTransforms()\r\n    {\r\n        if (elementContainer == null)\r\n            return;\r\n\r\n        elementContainer.Clear();\r\n\r\n        foreach (var element in getCommand())\r\n            elementContainer.Add(element.CreateDrawable());\r\n    }\r\n\r\n    private IEnumerable<IStageElement> getCommand()\r\n    {\r\n        if (elementProvider == null)\r\n            return Array.Empty<IStageElement>();\r\n\r\n        var commands = elementProvider.GetElements();\r\n\r\n        if (stageMods == null)\r\n            return commands;\r\n\r\n        return stageMods.Aggregate(commands, (current, mod) => mod.PostProcess(current));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/Drawables/StageHitObjectRunner.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Game.Rulesets.Karaoke.Mods;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Commands;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos;\r\nusing osu.Game.Rulesets.Mods;\r\nusing osu.Game.Rulesets.Objects;\r\nusing osu.Game.Rulesets.Objects.Drawables;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages.Drawables;\r\n\r\npublic class StageHitObjectRunner : StageRunner, IStageHitObjectRunner\r\n{\r\n    public event Action? OnCommandUpdated;\r\n\r\n    private IHitObjectCommandProvider commandProvider = null!;\r\n    private IList<IApplicableToStageHitObjectCommand> stageMods = null!;\r\n\r\n    public override void OnStageInfoChanged(StageInfo stageInfo, bool scorable, IReadOnlyList<Mod> mods)\r\n    {\r\n        commandProvider = stageInfo.CreateHitObjectCommandProvider<Lyric>()!;\r\n        stageMods = mods.OfType<IApplicableToStageHitObjectCommand>().Where(x => x.CanApply(stageInfo)).ToList();\r\n\r\n        OnCommandUpdated?.Invoke();\r\n    }\r\n\r\n    public override void TriggerUpdateCommand()\r\n    {\r\n        OnCommandUpdated?.Invoke();\r\n    }\r\n\r\n    public double GetPreemptTime(HitObject hitObject)\r\n    {\r\n        return commandProvider.GetPreemptTime(hitObject);\r\n    }\r\n\r\n    public double GetStartTimeOffset(HitObject hitObject)\r\n    {\r\n        return commandProvider.GetStartTimeOffset(hitObject);\r\n    }\r\n\r\n    public double GetEndTimeOffset(HitObject hitObject)\r\n    {\r\n        return commandProvider.GetEndTimeOffset(hitObject);\r\n    }\r\n\r\n    public void UpdateInitialTransforms(DrawableHitObject drawableHitObject)\r\n    {\r\n        var commands = commandProvider.GetInitialCommands(drawableHitObject.HitObject);\r\n\r\n        commands = postProcessCommand(drawableHitObject.HitObject, commands, x => x.PostProcessInitialCommands);\r\n        applyTransforms(drawableHitObject, commands);\r\n    }\r\n\r\n    public void UpdateStartTimeStateTransforms(DrawableHitObject drawableHitObject)\r\n    {\r\n        var commands = commandProvider.GetStartTimeStateCommands(drawableHitObject.HitObject);\r\n        double startTimeOffset = -commandProvider.GetStartTimeOffset(drawableHitObject.HitObject);\r\n\r\n        commands = postProcessCommand(drawableHitObject.HitObject, commands, x => x.PostProcessStartTimeStateCommands);\r\n        applyTransforms(drawableHitObject, commands, startTimeOffset);\r\n    }\r\n\r\n    public void UpdateHitStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)\r\n    {\r\n        if (state == ArmedState.Idle)\r\n            return;\r\n\r\n        var commands = commandProvider.GetHitStateCommands(drawableHitObject.HitObject, state);\r\n\r\n        commands = postProcessCommand(drawableHitObject.HitObject, commands, x => x.PostProcessHitStateCommands);\r\n        applyTransforms(drawableHitObject, commands);\r\n    }\r\n\r\n    private IEnumerable<IStageCommand> postProcessCommand(\r\n        HitObject hitObject,\r\n        IEnumerable<IStageCommand> commands,\r\n        Func<IApplicableToStageHitObjectCommand, Func<HitObject, IEnumerable<IStageCommand>, IEnumerable<IStageCommand>>> postProcess)\r\n    {\r\n        return stageMods.Aggregate(commands, (current, mod) => postProcess(mod).Invoke(hitObject, current));\r\n    }\r\n\r\n    private static void applyTransforms<TDrawable>(TDrawable drawable, IEnumerable<IStageCommand> commands, double offset = 0)\r\n        where TDrawable : DrawableHitObject\r\n    {\r\n        var appliedProperties = new HashSet<string>();\r\n\r\n        foreach (var command in commands.OrderBy(c => c.StartTime))\r\n        {\r\n            if (appliedProperties.Add(command.PropertyName))\r\n                command.ApplyInitialValue(drawable);\r\n\r\n            using (drawable.BeginDelayedSequence(command.StartTime + offset))\r\n                command.ApplyTransforms(drawable);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/Drawables/StagePlayfieldRunner.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Game.Rulesets.Karaoke.Mods;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Commands;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos;\r\nusing osu.Game.Rulesets.Karaoke.UI;\r\nusing osu.Game.Rulesets.Mods;\r\nusing osu.Game.Rulesets.UI;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages.Drawables;\r\n\r\npublic class StagePlayfieldRunner : StageRunner, IStagePlayfieldRunner\r\n{\r\n    private IPlayfieldCommandProvider? commandProvider;\r\n    private IList<IApplicableToStagePlayfieldCommand>? stageMods;\r\n    private KaraokePlayfield? karaokePlayfield;\r\n\r\n    public override void OnStageInfoChanged(StageInfo stageInfo, bool scorable, IReadOnlyList<Mod> mods)\r\n    {\r\n        commandProvider = stageInfo.CreatePlayfieldCommandProvider(scorable);\r\n        stageMods = mods.OfType<IApplicableToStagePlayfieldCommand>().Where(x => x.CanApply(stageInfo)).ToList();\r\n        applyTransforms();\r\n    }\r\n\r\n    public override void TriggerUpdateCommand()\r\n    {\r\n        applyTransforms();\r\n    }\r\n\r\n    public void UpdatePlayfieldTransforms(KaraokePlayfield playfield)\r\n    {\r\n        karaokePlayfield = playfield;\r\n\r\n        applyTransforms();\r\n    }\r\n\r\n    private void applyTransforms()\r\n    {\r\n        if (karaokePlayfield == null)\r\n            return;\r\n\r\n        var lyricPlayfield = karaokePlayfield.LyricPlayfield;\r\n        var notePlayfield = karaokePlayfield.NotePlayfield;\r\n\r\n        applyTransforms(karaokePlayfield, getCommand(karaokePlayfield));\r\n        applyTransforms(lyricPlayfield, getCommand(lyricPlayfield));\r\n        applyTransforms(notePlayfield, getCommand(notePlayfield));\r\n    }\r\n\r\n    private IEnumerable<IStageCommand> getCommand(\r\n        Playfield playfield)\r\n    {\r\n        if (commandProvider == null)\r\n            return Array.Empty<IStageCommand>();\r\n\r\n        var commands = commandProvider.GetCommands(playfield);\r\n\r\n        if (stageMods == null)\r\n            return commands;\r\n\r\n        return stageMods.Aggregate(commands, (current, mod) => mod.PostProcessCommands(playfield, current));\r\n    }\r\n\r\n    private static void applyTransforms<TDrawable>(TDrawable drawable, IEnumerable<IStageCommand> commands)\r\n        where TDrawable : Playfield\r\n    {\r\n        drawable.ClearTransforms();\r\n\r\n        var appliedProperties = new HashSet<string>();\r\n\r\n        foreach (var command in commands.OrderBy(c => c.StartTime))\r\n        {\r\n            if (appliedProperties.Add(command.PropertyName))\r\n                command.ApplyInitialValue(drawable);\r\n\r\n            using (drawable.BeginAbsoluteSequence(command.StartTime))\r\n                command.ApplyTransforms(drawable);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/Drawables/StageRunner.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos;\r\nusing osu.Game.Rulesets.Mods;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages.Drawables;\r\n\r\npublic abstract partial class StageRunner\r\n{\r\n    public abstract void OnStageInfoChanged(StageInfo stageInfo, bool scorable, IReadOnlyList<Mod> mods);\r\n\r\n    public abstract void TriggerUpdateCommand();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/HitObjectCommandProvider.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Commands;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos;\r\nusing osu.Game.Rulesets.Objects;\r\nusing osu.Game.Rulesets.Objects.Drawables;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages;\r\n\r\npublic abstract class HitObjectCommandProvider<TStageInfo, THitObject> : IHitObjectCommandProvider\r\n    where THitObject : KaraokeHitObject\r\n    where TStageInfo : StageInfo\r\n{\r\n    protected readonly TStageInfo StageInfo;\r\n\r\n    protected HitObjectCommandProvider(TStageInfo stageInfo)\r\n    {\r\n        StageInfo = stageInfo;\r\n    }\r\n\r\n    public double GetPreemptTime(HitObject hitObject)\r\n    {\r\n        if (hitObject is not THitObject tHitObject)\r\n            throw new InvalidCastException();\r\n\r\n        return GeneratePreemptTime(tHitObject);\r\n    }\r\n\r\n    public double GetStartTimeOffset(HitObject hitObject)\r\n    {\r\n        if (hitObject is not THitObject tHitObject)\r\n            throw new InvalidCastException();\r\n\r\n        (double? startTime, _) = GetStartAndEndTime(tHitObject);\r\n\r\n        if (startTime == null)\r\n            return 0;\r\n\r\n\r\n        return hitObject.StartTime - startTime.Value;\r\n    }\r\n\r\n    public double GetEndTimeOffset(HitObject hitObject)\r\n    {\r\n        if (hitObject is not THitObject tHitObject)\r\n            throw new InvalidCastException();\r\n\r\n        (_, double? endTime) = GetStartAndEndTime(tHitObject);\r\n\r\n        if (endTime == null)\r\n            return 0;\r\n\r\n        return endTime.Value - hitObject.GetEndTime();\r\n    }\r\n\r\n    public IEnumerable<IStageCommand> GetInitialCommands(HitObject hitObject)\r\n    {\r\n        if (hitObject is not THitObject tHitObject)\r\n            throw new InvalidCastException();\r\n\r\n        return GetInitialCommands(tHitObject);\r\n    }\r\n\r\n    public IEnumerable<IStageCommand> GetStartTimeStateCommands(HitObject hitObject)\r\n    {\r\n        if (hitObject is not THitObject tHitObject)\r\n            throw new InvalidCastException();\r\n\r\n        return GetStartTimeStateCommands(tHitObject);\r\n    }\r\n\r\n    public IEnumerable<IStageCommand> GetHitStateCommands(HitObject hitObject, ArmedState state)\r\n    {\r\n        if (hitObject is not THitObject tHitObject)\r\n            throw new InvalidCastException();\r\n\r\n        return GetHitStateCommands(tHitObject, state);\r\n    }\r\n\r\n    protected abstract double GeneratePreemptTime(THitObject hitObject);\r\n\r\n    protected abstract IEnumerable<IStageCommand> GetInitialCommands(THitObject hitObject);\r\n\r\n    protected abstract Tuple<double?, double?> GetStartAndEndTime(THitObject lyric);\r\n\r\n    protected abstract IEnumerable<IStageCommand> GetStartTimeStateCommands(THitObject hitObject);\r\n\r\n    protected abstract IEnumerable<IStageCommand> GetHitStateCommands(THitObject hitObject, ArmedState state);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/IHitObjectCommandProvider.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Drawables;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Commands;\r\nusing osu.Game.Rulesets.Objects;\r\nusing osu.Game.Rulesets.Objects.Drawables;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages;\r\n\r\n/// <summary>\r\n/// Provide commands for <see cref=\"DrawableKaraokeHitObject\"/> by <see cref=\"HitObject\"/>,\r\n/// </summary>\r\npublic interface IHitObjectCommandProvider\r\n{\r\n    /// <summary>\r\n    /// Get the preempt time for the <see cref=\"DrawableKaraokeHitObject\"/>.\r\n    /// </summary>\r\n    /// <param name=\"hitObject\"></param>\r\n    /// <returns></returns>\r\n    double GetPreemptTime(HitObject hitObject);\r\n\r\n    /// <summary>\r\n    /// Get the offset time between <see cref=\"HitObject.StartTime\"/> and stage start time.\r\n    /// </summary>\r\n    /// <param name=\"hitObject\"></param>\r\n    /// <returns></returns>\r\n    double GetStartTimeOffset(HitObject hitObject);\r\n\r\n    /// <summary>\r\n    /// Get the offset time between <see cref=\"HitObjectExtensions.GetEndTime\"/> and stage start time.\r\n    /// </summary>\r\n    /// <param name=\"hitObject\"></param>\r\n    /// <returns></returns>\r\n    double GetEndTimeOffset(HitObject hitObject);\r\n\r\n    /// <summary>\r\n    /// Get the list of <see cref=\"IStageCommand\"/> that apply (generally fade-in) transforms leading into the <see cref=\"DrawableKaraokeHitObject\"/> start time.\r\n    /// </summary>\r\n    /// <param name=\"hitObject\"></param>\r\n    IEnumerable<IStageCommand> GetInitialCommands(HitObject hitObject);\r\n\r\n    /// <summary>\r\n    /// Get the list of <see cref=\"IStageCommand\"/> that apply passive transforms at the <see cref=\"DrawableKaraokeHitObject\"/>'s StartTime.\r\n    /// </summary>\r\n    /// <param name=\"hitObject\"></param>\r\n    IEnumerable<IStageCommand> GetStartTimeStateCommands(HitObject hitObject);\r\n\r\n    /// <summary>\r\n    /// Get the list of <see cref=\"IStageCommand\"/> that transforms based on the current <see cref=\"ArmedState\"/> for <see cref=\"DrawableKaraokeHitObject\"/>\r\n    /// This call is offset by (HitObject.EndTime + Result.Offset), equivalent to when the user hit the object.\r\n    /// </summary>\r\n    /// <param name=\"hitObject\"></param>\r\n    /// <param name=\"state\"></param>\r\n    IEnumerable<IStageCommand> GetHitStateCommands(HitObject hitObject, ArmedState state);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/IPlayfieldCommandProvider.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Commands;\r\nusing osu.Game.Rulesets.UI;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages;\r\n\r\npublic interface IPlayfieldCommandProvider\r\n{\r\n    /// <summary>\r\n    /// Get the list of <see cref=\"IStageCommand\"/> that apply to the  <see cref=\"Playfield\"/>.\r\n    /// </summary>\r\n    /// <param name=\"playfield\"></param>\r\n    IEnumerable<IStageCommand> GetCommands(Playfield playfield);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/IStageElement.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages;\r\n\r\npublic interface IStageElement\r\n{\r\n    double StartTime { get; }\r\n\r\n    Drawable CreateDrawable();\r\n}\r\n\r\npublic static class StageElementExtensions\r\n{\r\n    /// <summary>\r\n    /// Returns the end time of this stage element.\r\n    /// </summary>\r\n    /// <remarks>\r\n    /// This returns the <see cref=\"IStageElementWithDuration.EndTime\"/> where available, falling back to <see cref=\"IStageElement.StartTime\"/> otherwise.\r\n    /// </remarks>\r\n    /// <param name=\"element\">The stage element.</param>\r\n    /// <returns>The end time of this element.</returns>\r\n    public static double GetEndTime(this IStageElement element) => (element as IStageElementWithDuration)?.EndTime ?? element.StartTime;\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/IStageElementProvider.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages;\r\n\r\npublic interface IStageElementProvider\r\n{\r\n    IEnumerable<IStageElement> GetElements();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/IStageElementWithDuration.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages;\r\n\r\n/// <summary>\r\n/// A <see cref=\"IStageElement\"/> that ends at a different time than its start time.\r\n/// </summary>\r\npublic interface IStageElementWithDuration : IStageElement\r\n{\r\n    /// <summary>\r\n    /// The time at which the <see cref=\"IStageElement\"/> ends.\r\n    /// This is consumed to extend the length of a stage to ensure all visuals are played to completion.\r\n    /// </summary>\r\n    double EndTime { get; }\r\n\r\n    /// <summary>\r\n    /// The time this element displays until.\r\n    /// This is used for lifetime purposes, and includes long playing animations which don't necessarily extend\r\n    /// a stage's play time.\r\n    /// </summary>\r\n    double EndTimeForDisplay { get; }\r\n\r\n    /// <summary>\r\n    /// The duration of the <see cref=\"IStageElement\"/>.\r\n    /// </summary>\r\n    double Duration => EndTime - StartTime;\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/Infos/Classic/ClassicLyricCommandProvider.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Commands;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Commands.Lyrics;\r\nusing osu.Game.Rulesets.Objects.Drawables;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages.Infos.Classic;\r\n\r\npublic class ClassicLyricCommandProvider : HitObjectCommandProvider<ClassicStageInfo, Lyric>\r\n{\r\n    public ClassicLyricCommandProvider(ClassicStageInfo stageInfo)\r\n        : base(stageInfo)\r\n    {\r\n    }\r\n\r\n    protected override double GeneratePreemptTime(Lyric hitObject)\r\n    {\r\n        return StageInfo.StageDefinition.FadeInTime;\r\n    }\r\n\r\n    protected override Tuple<double?, double?> GetStartAndEndTime(Lyric lyric)\r\n    {\r\n        (double? startTime, double? endTime) = StageInfo.LyricTimingInfo.GetStartAndEndTime(lyric);\r\n        return new Tuple<double?, double?>(startTime + getStartTimeOffset(StageInfo, startTime), endTime + getEndTimeOffset(StageInfo, endTime));\r\n\r\n        static double? getStartTimeOffset(ClassicStageInfo stageInfo, double? lyricStartTime)\r\n        {\r\n            if (lyricStartTime == null)\r\n                return null;\r\n\r\n            bool isFirstAppearLyric = lyricStartTime.Value == stageInfo.LyricTimingInfo.GetStartTime();\r\n            var stageDefinition = stageInfo.StageDefinition;\r\n\r\n            if (isFirstAppearLyric)\r\n            {\r\n                return stageDefinition.FirstLyricStartTimeOffset + stageDefinition.FadeOutTime;\r\n            }\r\n\r\n            // should add the previous lyric's end time offset.\r\n            return stageDefinition.LyricEndTimeOffset + stageDefinition.FadeOutTime + stageDefinition.FadeInTime;\r\n        }\r\n\r\n        static double? getEndTimeOffset(ClassicStageInfo stageInfo, double? lyricEndTime)\r\n        {\r\n            if (lyricEndTime == null)\r\n                return null;\r\n\r\n            bool isLastDisappearLyric = lyricEndTime.Value == stageInfo.LyricTimingInfo.GetEndTime();\r\n            var stageDefinition = stageInfo.StageDefinition;\r\n\r\n            return isLastDisappearLyric ? stageDefinition.LastLyricEndTimeOffset : stageDefinition.LyricEndTimeOffset;\r\n        }\r\n    }\r\n\r\n    protected override IEnumerable<IStageCommand> GetInitialCommands(Lyric hitObject)\r\n    {\r\n        var elements = StageInfo.GetStageElements(hitObject);\r\n        return elements.Select(e => e switch\r\n        {\r\n            ClassicLyricLayout layout => updateInitialTransforms(layout),\r\n            ClassicStyle style => updateInitialTransforms(style),\r\n            _ => throw new NotSupportedException(),\r\n        }).SelectMany(x => x);\r\n    }\r\n\r\n    private IEnumerable<IStageCommand> updateInitialTransforms(ClassicLyricLayout layout)\r\n    {\r\n        var definition = StageInfo.StageDefinition;\r\n        yield return new StageAnchorCommand(Easing.None, 0, 0, getLyricAnchor(layout.Alignment), getLyricAnchor(layout.Alignment));\r\n        yield return new StageOriginCommand(Easing.None, 0, 0, getLyricAnchor(layout.Alignment), getLyricAnchor(layout.Alignment));\r\n        yield return new StagePaddingCommand(Easing.None, 0, 0, getPosition(definition, layout), getPosition(definition, layout));\r\n        yield return new StageScaleCommand(Easing.None, 0, 0, definition.LyricScale, definition.LyricScale);\r\n        yield return new StageAlphaCommand(definition.FadeInEasing, 0, definition.FadeInTime, 1, 1);\r\n        yield break;\r\n\r\n        static Anchor getLyricAnchor(ClassicLyricLayoutAlignment alignment) =>\r\n            alignment switch\r\n            {\r\n                ClassicLyricLayoutAlignment.Left => Anchor.BottomLeft,\r\n                ClassicLyricLayoutAlignment.Center => Anchor.BottomCentre,\r\n                ClassicLyricLayoutAlignment.Right => Anchor.BottomRight,\r\n                _ => throw new ArgumentOutOfRangeException(nameof(alignment), alignment, null),\r\n            };\r\n\r\n        static MarginPadding getPosition(ClassicStageDefinition definition, ClassicLyricLayout layout)\r\n        {\r\n            float paddingX = definition.BorderWidth + layout.HorizontalMargin;\r\n            float paddingY = definition.BorderHeight + definition.LineHeight * layout.Line;\r\n\r\n            var padding = new MarginPadding\r\n            {\r\n                Bottom = paddingY,\r\n            };\r\n\r\n            switch (layout.Alignment)\r\n            {\r\n                case ClassicLyricLayoutAlignment.Left:\r\n                    padding.Left = paddingX;\r\n                    return padding;\r\n\r\n                case ClassicLyricLayoutAlignment.Center:\r\n                    return padding;\r\n\r\n                case ClassicLyricLayoutAlignment.Right:\r\n                    padding.Right = paddingX;\r\n                    return padding;\r\n\r\n                default:\r\n                    throw new ArgumentOutOfRangeException();\r\n            }\r\n        }\r\n    }\r\n\r\n    private IEnumerable<IStageCommand> updateInitialTransforms(ClassicStyle style)\r\n    {\r\n        if (style.LyricStyle != null)\r\n        {\r\n            yield return new LyricStyleCommand(Easing.None, 0, 0, style.LyricStyle, style.LyricStyle);\r\n        }\r\n    }\r\n\r\n    protected override IEnumerable<IStageCommand> GetStartTimeStateCommands(Lyric hitObject)\r\n    {\r\n        // there's no transformer in here.\r\n        yield break;\r\n    }\r\n\r\n    protected override IEnumerable<IStageCommand> GetHitStateCommands(Lyric hitObject, ArmedState state)\r\n    {\r\n        var definition = StageInfo.StageDefinition;\r\n        yield return new StageAlphaCommand(definition.FadeOutEasing, 0, definition.FadeOutTime, 1, 0);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/Infos/Classic/ClassicLyricLayout.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing Newtonsoft.Json;\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages.Infos.Classic;\r\n\r\npublic class ClassicLyricLayout : StageElement\r\n{\r\n    [JsonIgnore]\r\n    public readonly Bindable<ClassicLyricLayoutAlignment> AlignmentBindable = new(ClassicLyricLayoutAlignment.Center);\r\n\r\n    /// <summary>\r\n    /// <see cref=\"Lyric\"/>'s alignment.\r\n    /// </summary>\r\n    public ClassicLyricLayoutAlignment Alignment\r\n    {\r\n        get => AlignmentBindable.Value;\r\n        set => AlignmentBindable.Value = value;\r\n    }\r\n\r\n    [JsonIgnore]\r\n    public readonly Bindable<float> HorizontalMarginBindable = new();\r\n\r\n    /// <summary>\r\n    /// <see cref=\"Lyric\"/>'s horizontal margin.\r\n    /// </summary>\r\n    public float HorizontalMargin\r\n    {\r\n        get => HorizontalMarginBindable.Value;\r\n        set => HorizontalMarginBindable.Value = value;\r\n    }\r\n\r\n    [JsonIgnore]\r\n    public readonly Bindable<int> LineBindable = new();\r\n\r\n    /// <summary>\r\n    /// <see cref=\"Lyric\"/>'s line number.<br/>\r\n    /// <see cref=\"Lyric\"/> will at bottom if <see cref=\"Line\"/> is zero.\r\n    /// </summary>\r\n    public int Line\r\n    {\r\n        get => LineBindable.Value;\r\n        set => LineBindable.Value = value;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/Infos/Classic/ClassicLyricLayoutAlignment.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages.Infos.Classic;\r\n\r\npublic enum ClassicLyricLayoutAlignment\r\n{\r\n    Left,\r\n\r\n    Center,\r\n\r\n    Right,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/Infos/Classic/ClassicLyricLayoutCategory.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages.Infos.Classic;\r\n\r\npublic class ClassicLyricLayoutCategory : StageElementCategory<ClassicLyricLayout, Lyric>\r\n{\r\n    protected override ClassicLyricLayout CreateDefaultElement()\r\n        => new();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/Infos/Classic/ClassicLyricTimingInfo.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Collections.Specialized;\r\nusing System.Diagnostics;\r\nusing System.Linq;\r\nusing Newtonsoft.Json;\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages.Infos.Classic;\r\n\r\npublic class ClassicLyricTimingInfo\r\n{\r\n    [JsonIgnore]\r\n    public IBindable<int> TimingVersion => timingVersion;\r\n\r\n    private readonly Bindable<int> timingVersion = new();\r\n\r\n    // todo: should be readonly.\r\n    public BindableList<ClassicLyricTimingPoint> Timings = new();\r\n\r\n    [JsonIgnore]\r\n    public List<ClassicLyricTimingPoint> SortedTimings { get; private set; } = new();\r\n\r\n    [JsonIgnore]\r\n    public IBindable<int> MappingVersion => mappingVersion;\r\n\r\n    private readonly Bindable<int> mappingVersion = new();\r\n\r\n    /// <summary>\r\n    /// Mapping between <see cref=\"Lyric.ID\"/> and <see cref=\"ClassicLyricTimingPoint.ID\"/>\r\n    /// This is the 1st mapping roles.\r\n    /// todo: should be private.\r\n    /// </summary>\r\n    public BindableDictionary<ElementId, ElementId[]> Mappings = new();\r\n\r\n    public ClassicLyricTimingInfo()\r\n    {\r\n        Timings.CollectionChanged += (_, args) =>\r\n        {\r\n            switch (args.Action)\r\n            {\r\n                case NotifyCollectionChangedAction.Add:\r\n                    Debug.Assert(args.NewItems != null);\r\n\r\n                    foreach (var c in args.NewItems.Cast<ClassicLyricTimingPoint>())\r\n                        c.TimeBindable.ValueChanged += timeValueChanged;\r\n                    break;\r\n\r\n                case NotifyCollectionChangedAction.Reset:\r\n                case NotifyCollectionChangedAction.Remove:\r\n                    Debug.Assert(args.OldItems != null);\r\n\r\n                    foreach (var c in args.OldItems.Cast<ClassicLyricTimingPoint>())\r\n                        c.TimeBindable.ValueChanged -= timeValueChanged;\r\n                    break;\r\n            }\r\n\r\n            onTimingChanged();\r\n\r\n            void timeValueChanged(ValueChangedEvent<double> e) => onTimingChanged();\r\n        };\r\n\r\n        Mappings.CollectionChanged += (_, args) =>\r\n        {\r\n            switch (args.Action)\r\n            {\r\n                case NotifyDictionaryChangedAction.Add:\r\n                case NotifyDictionaryChangedAction.Replace:\r\n                case NotifyDictionaryChangedAction.Remove:\r\n                    onMappingChanged();\r\n                    break;\r\n\r\n                default:\r\n                    throw new ArgumentOutOfRangeException();\r\n            }\r\n        };\r\n\r\n        void onTimingChanged()\r\n        {\r\n            SortedTimings = Timings.OrderBy(x => x.Time).ToList();\r\n            timingVersion.Value++;\r\n        }\r\n\r\n        void onMappingChanged() => mappingVersion.Value++;\r\n    }\r\n\r\n    #region Edit\r\n\r\n    public ClassicLyricTimingPoint AddTimingPoint(Action<ClassicLyricTimingPoint>? action = null)\r\n    {\r\n        var timingPoint = new ClassicLyricTimingPoint();\r\n\r\n        action?.Invoke(timingPoint);\r\n        Timings.Add(timingPoint);\r\n\r\n        return timingPoint;\r\n    }\r\n\r\n    public void RemoveTimingPoint(ClassicLyricTimingPoint point)\r\n    {\r\n        ClearTimingPointFromMapping(point);\r\n\r\n        Timings.Remove(point);\r\n    }\r\n\r\n    public void AddToMapping(ClassicLyricTimingPoint point, Lyric lyric)\r\n    {\r\n        var key = lyric.ID;\r\n        var value = point.ID;\r\n\r\n        if (!Timings.Contains(point))\r\n            throw new InvalidOperationException($\"{nameof(point)} does ont in the {nameof(point)}.\");\r\n\r\n        if (Mappings.TryGetValue(key, out ElementId[]? timingIds))\r\n        {\r\n            Mappings[key] = timingIds.Concat(new[] { value }).ToArray();\r\n        }\r\n        else\r\n        {\r\n            Mappings.Add(key, new[] { value });\r\n        }\r\n    }\r\n\r\n    public void RemoveFromMapping(ClassicLyricTimingPoint point, Lyric lyric)\r\n    {\r\n        var key = lyric.ID;\r\n        var value = point.ID;\r\n\r\n        if (!Timings.Contains(point))\r\n            throw new InvalidOperationException($\"{nameof(point)} does ont in the {nameof(point)}.\");\r\n\r\n        if (!Mappings.TryGetValue(key, out ElementId[]? values))\r\n            return;\r\n\r\n        if (values.All(x => x == point.ID))\r\n        {\r\n            ClearLyricFromMapping(lyric);\r\n        }\r\n        else\r\n        {\r\n            Mappings[key] = values.Where(x => x != value).ToArray();\r\n        }\r\n    }\r\n\r\n    public void ClearTimingPointFromMapping(ClassicLyricTimingPoint point)\r\n    {\r\n        var value = point.ID;\r\n\r\n        foreach ((var key, ElementId[]? values) in Mappings)\r\n        {\r\n            if (values.All(x => x == point.ID))\r\n            {\r\n                Mappings.Remove(key);\r\n            }\r\n            else\r\n            {\r\n                Mappings[key] = values.Where(x => x != value).ToArray();\r\n            }\r\n        }\r\n    }\r\n\r\n    public void ClearLyricFromMapping(Lyric lyric)\r\n    {\r\n        Mappings.Remove(lyric.ID);\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Query\r\n\r\n    public int? GetTimingPointOrder(ClassicLyricTimingPoint point)\r\n    {\r\n        int index = SortedTimings.IndexOf(point);\r\n        return index == -1 ? null : index + 1;\r\n    }\r\n\r\n    public IEnumerable<ClassicLyricTimingPoint> GetLyricTimingPoints(Lyric lyric)\r\n    {\r\n        if (!Mappings.TryGetValue(lyric.ID, out ElementId[]? ids))\r\n            return Array.Empty<ClassicLyricTimingPoint>();\r\n\r\n        return SortedTimings.Where(x => ids.Contains(x.ID));\r\n    }\r\n\r\n    public Tuple<double?, double?> GetStartAndEndTime(Lyric lyric)\r\n    {\r\n        // already sorted.\r\n        double[] matchedTimings = GetLyricTimingPoints(lyric).Select(x => x.Time).ToArray();\r\n\r\n        double? matchedStartTime = matchedTimings.Any() ? matchedTimings.Min() : default(double?);\r\n        double? matchedEndTime = matchedTimings.Any() ? matchedTimings.Max() : default(double?);\r\n\r\n        return new Tuple<double?, double?>(matchedStartTime, matchedEndTime);\r\n    }\r\n\r\n    public double? GetStartTime()\r\n    {\r\n        return Timings.Any() ? Timings.Min(x => x.Time) : default(double?);\r\n    }\r\n\r\n    public double? GetEndTime()\r\n    {\r\n        return Timings.Any() ? Timings.Max(x => x.Time) : default(double?);\r\n    }\r\n\r\n    public IEnumerable<ElementId> GetMatchedLyricIds(ClassicLyricTimingPoint point)\r\n    {\r\n        return Mappings.Where(x => x.Value.Contains(point.ID)).Select(x => x.Key);\r\n    }\r\n\r\n    #endregion\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/Infos/Classic/ClassicLyricTimingPoint.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing Newtonsoft.Json;\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\nusing osu.Game.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages.Infos.Classic;\r\n\r\npublic class ClassicLyricTimingPoint : IDeepCloneable<ClassicLyricTimingPoint>, IComparable<ClassicLyricTimingPoint>, IHasPrimaryKey\r\n{\r\n    [JsonProperty]\r\n    public ElementId ID { get; private set; } = ElementId.NewElementId();\r\n\r\n    [JsonIgnore]\r\n    public readonly Bindable<double> TimeBindable = new();\r\n\r\n    public double Time\r\n    {\r\n        get => TimeBindable.Value;\r\n        set => TimeBindable.Value = value;\r\n    }\r\n\r\n    public ClassicLyricTimingPoint DeepClone()\r\n    {\r\n        return new ClassicLyricTimingPoint\r\n        {\r\n            Time = Time,\r\n        };\r\n    }\r\n\r\n    public int CompareTo(ClassicLyricTimingPoint? other)\r\n    {\r\n        return ComparableUtils.CompareByProperty(this, other,\r\n            t => t.Time,\r\n            t => t.ID);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/Infos/Classic/ClassicPlayfieldCommandProvider.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Commands;\r\nusing osu.Game.Rulesets.Karaoke.UI;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages.Infos.Classic;\r\n\r\npublic class ClassicPlayfieldCommandProvider : PlayfieldCommandProvider<ClassicStageInfo>\r\n{\r\n    public ClassicPlayfieldCommandProvider(ClassicStageInfo stageInfo, bool displayNotePlayfield)\r\n        : base(stageInfo, displayNotePlayfield)\r\n    {\r\n    }\r\n\r\n    protected override IEnumerable<IStageCommand> GetMainPlayfieldCommands(KaraokePlayfield playfield)\r\n    {\r\n        yield break;\r\n    }\r\n\r\n    protected override IEnumerable<IStageCommand> GetLyricPlayfieldCommands(LyricPlayfield playfield)\r\n    {\r\n        yield return new StageAlphaCommand(Easing.In, 0, 100, 0, 1);\r\n    }\r\n\r\n    protected override IEnumerable<IStageCommand> GetNotePlayfieldCommands(NotePlayfield playfield)\r\n    {\r\n        yield return new StageAnchorCommand(Easing.None, 0, 0, Anchor.Centre, Anchor.Centre);\r\n        yield return new StageOriginCommand(Easing.None, 0, 0, Anchor.Centre, Anchor.Centre);\r\n        yield return new StageYCommand(Easing.None, 0, 0, 0, -200);\r\n        yield return new StagePaddingCommand(Easing.None, 0, 0, new MarginPadding(50), new MarginPadding(50));\r\n        yield return new StageAlphaCommand(Easing.In, 0, 200, 0, 1);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/Infos/Classic/ClassicStageDefinition.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages.Infos.Classic;\r\n\r\npublic class ClassicStageDefinition : StageDefinition\r\n{\r\n    #region Playfield\r\n\r\n    /// <summary>\r\n    /// The border between <see cref=\"Lyric\"/> to the left/right side of the playfield.\r\n    /// </summary>\r\n    public float BorderWidth { get; set; } = 25f;\r\n\r\n    /// <summary>\r\n    /// The border between <see cref=\"Lyric\"/> to the bottom side of the playfield.\r\n    /// </summary>\r\n    public float BorderHeight { get; set; } = 25;\r\n\r\n    #endregion\r\n\r\n    #region Fade in/out effect\r\n\r\n    /// <summary>\r\n    /// Fade-in/out time for the lyric showing to the screen or hiding from the screen.\r\n    /// </summary>\r\n    public double FadeInTime { get; set; } = 150;\r\n\r\n    /// <summary>\r\n    /// Fade-in/out time for the lyric showing to the screen or hiding from the screen.\r\n    /// </summary>\r\n    public double FadeOutTime { get; set; } = 150;\r\n\r\n    /// <summary>\r\n    /// Fade-in easing for the lyric showing to the screen.\r\n    /// </summary>\r\n    public Easing FadeInEasing { get; set; } = Easing.OutCirc;\r\n\r\n    /// <summary>\r\n    /// Fade-out easing for the lyric hiding from the screen.\r\n    /// </summary>\r\n    public Easing FadeOutEasing { get; set; } = Easing.OutCirc;\r\n\r\n    #endregion\r\n\r\n    #region Lyrics arrangement\r\n\r\n    /// <summary>\r\n    /// Text scale for the lyric.\r\n    /// </summary>\r\n    public float LyricScale { get; set; } = 2;\r\n\r\n    /// <summary>\r\n    /// Line height for the lyric.\r\n    /// </summary>\r\n    public float LineHeight { get; set; } = 72;\r\n\r\n    /// <summary>\r\n    /// The delay time after first lyric appear.\r\n    /// </summary>\r\n    public double FirstLyricStartTimeOffset { get; set; } = 1000;\r\n\r\n    /// <summary>\r\n    /// The delay disappear time after touch to the <see cref=\"Lyric\"/>'s <see cref=\"Lyric.StartTime\"/>\r\n    /// </summary>\r\n    public double LyricEndTimeOffset { get; set; } = 300;\r\n\r\n    /// <summary>\r\n    /// The delay disappear time after touch to the last <see cref=\"Lyric\"/>'s <see cref=\"Lyric.EndTime\"/>\r\n    /// </summary>\r\n    public double LastLyricEndTimeOffset { get; set; } = 10000;\r\n\r\n    #endregion\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/Infos/Classic/ClassicStageInfo.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages.Infos.Classic;\r\n\r\npublic class ClassicStageInfo : StageInfo\r\n{\r\n    #region Category\r\n\r\n    /// <summary>\r\n    /// The definition for the <see cref=\"Lyric\"/>.<br/>\r\n    /// Like the line height or font size.\r\n    /// </summary>\r\n    public ClassicStageDefinition StageDefinition { get; set; } = new();\r\n\r\n    /// <summary>\r\n    /// Category to save the <see cref=\"Lyric\"/>'s and <see cref=\"Note\"/>'s style.\r\n    /// </summary>\r\n    public ClassicStyleCategory StyleCategory { get; set; } = new();\r\n\r\n    /// <summary>\r\n    /// Category to save the <see cref=\"Lyric\"/>'s layout.\r\n    /// </summary>\r\n    public ClassicLyricLayoutCategory LyricLayoutCategory { get; set; } = new();\r\n\r\n    /// <summary>\r\n    /// Timing info for saving the <see cref=\"Lyric\"/>'s appear and disappear time.\r\n    /// </summary>\r\n    public ClassicLyricTimingInfo LyricTimingInfo { get; set; } = new();\r\n\r\n    #endregion\r\n\r\n    #region Stage element\r\n\r\n    protected override IEnumerable<StageElement> GetLyricStageElements(Lyric lyric)\r\n    {\r\n        yield return StyleCategory.GetElementByItem(lyric);\r\n        yield return LyricLayoutCategory.GetElementByItem(lyric);\r\n    }\r\n\r\n    protected override IEnumerable<StageElement> GetNoteStageElements(Note note)\r\n    {\r\n        // todo: should check the real-time mapping result.\r\n        yield return StyleCategory.GetElementByItem(note.ReferenceLyric!);\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Provider\r\n\r\n    public override IPlayfieldCommandProvider CreatePlayfieldCommandProvider(bool displayNotePlayfield)\r\n        => new ClassicPlayfieldCommandProvider(this, displayNotePlayfield);\r\n\r\n    public override IStageElementProvider? CreateStageElementProvider(bool displayNotePlayfield)\r\n        => null;\r\n\r\n    public override IHitObjectCommandProvider? CreateHitObjectCommandProvider<TObject>() =>\r\n        typeof(TObject) switch\r\n        {\r\n            Type type when type == typeof(Lyric) => new ClassicLyricCommandProvider(this),\r\n            Type type when type == typeof(Note) => null,\r\n            _ => null\r\n        };\r\n\r\n    #endregion\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/Infos/Classic/ClassicStyle.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing Newtonsoft.Json;\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages.Infos.Classic;\r\n\r\npublic class ClassicStyle : StageElement\r\n{\r\n    /// <summary>\r\n    /// <see cref=\"Lyric\"/>'s text style.\r\n    /// </summary>\r\n    public LyricStyle? LyricStyle { get; set; }\r\n\r\n    [JsonIgnore]\r\n    public readonly Bindable<int?> NoteStyleIndexBindable = new();\r\n\r\n    /// <summary>\r\n    /// <see cref=\"Note\"/>'s skin lookup index.\r\n    /// </summary>\r\n    public int? NoteStyleIndex\r\n    {\r\n        get => NoteStyleIndexBindable.Value;\r\n        set => NoteStyleIndexBindable.Value = value;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/Infos/Classic/ClassicStyleCategory.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages.Infos.Classic;\r\n\r\npublic class ClassicStyleCategory : StageElementCategory<ClassicStyle, Lyric>\r\n{\r\n    protected override ClassicStyle CreateDefaultElement()\r\n        => new()\r\n        {\r\n            LyricStyle = LyricStyle.CreateDefault(),\r\n        };\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/Infos/Preview/PreviewElementProvider.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Commands;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages.Infos.Preview;\r\n\r\npublic class PreviewElementProvider : StageElementProvider<PreviewStageInfo>\r\n{\r\n    public PreviewElementProvider(PreviewStageInfo stageInfo, bool displayNotePlayfield)\r\n        : base(stageInfo, displayNotePlayfield)\r\n    {\r\n    }\r\n\r\n    public override IEnumerable<IStageElement> GetElements()\r\n    {\r\n        int size = DisplayNotePlayfield ? 200 : 380;\r\n        int x = DisplayNotePlayfield ? -360 : -270;\r\n        int y = DisplayNotePlayfield ? 100 : 0;\r\n\r\n        yield return new StageBeatmapCoverInfo\r\n        {\r\n            Commands = new IStageCommand[]\r\n            {\r\n                new StageWidthCommand(Easing.None, 0, 0, size, size),\r\n                new StageHeightCommand(Easing.None, 0, 0, size, size),\r\n                new StageAnchorCommand(Easing.None, 0, 0, Anchor.Centre, Anchor.Centre),\r\n                new StageOriginCommand(Easing.None, 0, 0, Anchor.Centre, Anchor.Centre),\r\n                new StageXCommand(Easing.None, 0, 0, x, x),\r\n                new StageYCommand(Easing.None, 0, 0, y, y),\r\n            }\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/Infos/Preview/PreviewLyricCommandProvider.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Commands;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Commands.Lyrics;\r\nusing osu.Game.Rulesets.Objects.Drawables;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages.Infos.Preview;\r\n\r\npublic class PreviewLyricCommandProvider : HitObjectCommandProvider<PreviewStageInfo, Lyric>\r\n{\r\n    public PreviewLyricCommandProvider(PreviewStageInfo stageInfo)\r\n        : base(stageInfo)\r\n    {\r\n    }\r\n\r\n    protected override double GeneratePreemptTime(Lyric hitObject)\r\n    {\r\n        return StageInfo.StageDefinition.FadingTime;\r\n    }\r\n\r\n    protected override Tuple<double?, double?> GetStartAndEndTime(Lyric lyric)\r\n    {\r\n        var element = StageInfo.GetStageElements(lyric).OfType<PreviewLyricLayout>().Single();\r\n        return new Tuple<double?, double?>(element.StartTime, element.EndTime);\r\n    }\r\n\r\n    protected override IEnumerable<IStageCommand> GetInitialCommands(Lyric hitObject)\r\n    {\r\n        var elements = StageInfo.GetStageElements(hitObject);\r\n        return elements.Select(e => e switch\r\n        {\r\n            PreviewLyricLayout previewLyricLayout => updateInitialTransforms(previewLyricLayout),\r\n            PreviewStyle style => updateInitialTransforms(style),\r\n            _ => throw new NotSupportedException(),\r\n        }).SelectMany(x => x);\r\n    }\r\n\r\n    private IEnumerable<IStageCommand> updateInitialTransforms(PreviewLyricLayout layout)\r\n    {\r\n        var definition = StageInfo.StageDefinition;\r\n\r\n        double duration = getFadeInDuration(definition, layout);\r\n        float preemptPosition = getPreemptPosition(definition, layout);\r\n        float targetPosition = getTargetPosition(definition, layout);\r\n        float preemptAlpha = getPreemptAlpha(definition, layout);\r\n        float targetAlpha = getTargetAlpha(definition, layout);\r\n\r\n        yield return new StageYCommand(definition.MovingInEasing, 0, duration, preemptPosition, targetPosition);\r\n        yield return new StageAlphaCommand(definition.FadeInEasing, 0, duration, preemptAlpha, targetAlpha);\r\n        yield break;\r\n\r\n        static double getFadeInDuration(PreviewStageDefinition definition, PreviewLyricLayout layout)\r\n        {\r\n            return isLastLyricInView(layout) ? definition.FadingTime : 0;\r\n        }\r\n\r\n        static float getPreemptPosition(PreviewStageDefinition definition, PreviewLyricLayout layout)\r\n        {\r\n            float position = getTargetPosition(definition, layout);\r\n            return isLastLyricInView(layout) ? position + definition.FadingOffsetPosition : position;\r\n        }\r\n\r\n        static float getTargetPosition(PreviewStageDefinition definition, PreviewLyricLayout layout)\r\n        {\r\n            int line = isLastLyricInView(layout) ? definition.NumberOfLyrics - 1 : layout.Timings.Count;\r\n            return definition.LyricHeight * line;\r\n        }\r\n\r\n        static float getPreemptAlpha(PreviewStageDefinition definition, PreviewLyricLayout layout)\r\n        {\r\n            if (isFirstLyricInView(layout))\r\n                return 1;\r\n\r\n            if (isLastLyricInView(layout))\r\n                return 0;\r\n\r\n            return definition.InactiveAlpha;\r\n        }\r\n\r\n        static float getTargetAlpha(PreviewStageDefinition definition, PreviewLyricLayout layout)\r\n        {\r\n            if (isFirstLyricInView(layout))\r\n                return 1;\r\n\r\n            return definition.InactiveAlpha;\r\n        }\r\n\r\n        static bool isFirstLyricInView(PreviewLyricLayout layout) => !layout.Timings.Any();\r\n\r\n        static bool isLastLyricInView(PreviewLyricLayout layout) => layout.StartTime != 0;\r\n    }\r\n\r\n    private IEnumerable<IStageCommand> updateInitialTransforms(PreviewStyle style)\r\n    {\r\n        if (style.LyricStyle != null)\r\n        {\r\n            yield return new LyricStyleCommand(Easing.None, 0, 0, style.LyricStyle, style.LyricStyle);\r\n        }\r\n    }\r\n\r\n    protected override IEnumerable<IStageCommand> GetStartTimeStateCommands(Lyric hitObject)\r\n    {\r\n        var elements = StageInfo.GetStageElements(hitObject);\r\n        return elements.Select(e => e switch\r\n        {\r\n            PreviewLyricLayout previewLyricLayout => updateStartTimeStateTransforms(previewLyricLayout),\r\n            PreviewStyle => Array.Empty<IStageCommand>(), // todo: implement.\r\n            _ => throw new NotSupportedException(),\r\n        }).SelectMany(x => x);\r\n    }\r\n\r\n    private IEnumerable<IStageCommand> updateStartTimeStateTransforms(PreviewLyricLayout layout)\r\n    {\r\n        var definition = StageInfo.StageDefinition;\r\n        double startTime = layout.StartTime;\r\n\r\n        foreach ((int line, double time) in layout.Timings)\r\n        {\r\n            double relativeTime = time - startTime;\r\n\r\n            // move the lyric to the target position.\r\n            float previousPosition = definition.LyricHeight * (line + 1);\r\n            float position = definition.LyricHeight * line;\r\n            yield return new StageYCommand(definition.LineMovingEasing, relativeTime, relativeTime + definition.LineMovingTime, previousPosition, position);\r\n\r\n            if (line != 0)\r\n                continue;\r\n\r\n            // change the alpha if lyric move to the first line.\r\n            double fadingTime = Math.Clamp(definition.ActiveTime, 0, definition.LineMovingTime);\r\n            yield return new StageAlphaCommand(definition.ActiveEasing, relativeTime, relativeTime + fadingTime, definition.InactiveAlpha, 1);\r\n        }\r\n    }\r\n\r\n    protected override IEnumerable<IStageCommand> GetHitStateCommands(Lyric hitObject, ArmedState state)\r\n    {\r\n        var elements = StageInfo.GetStageElements(hitObject);\r\n        return elements.Select(e => e switch\r\n        {\r\n            PreviewLyricLayout layout => updateHitStateTransforms(state, layout),\r\n            PreviewStyle => Array.Empty<IStageCommand>(), // todo: implement.\r\n            _ => throw new NotSupportedException(),\r\n        }).SelectMany(x => x);\r\n    }\r\n\r\n    private IEnumerable<IStageCommand> updateHitStateTransforms(ArmedState state, PreviewLyricLayout layout)\r\n    {\r\n        var definition = StageInfo.StageDefinition;\r\n        float targetPosition = -definition.FadingOffsetPosition;\r\n\r\n        yield return new StageAlphaCommand(definition.FadeOutEasing, 0, definition.FadingTime, 1, 0);\r\n        yield return new StageYCommand(definition.MoveOutEasing, 0, definition.FadingTime, 0, targetPosition);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/Infos/Preview/PreviewLyricLayout.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages.Infos.Preview;\r\n\r\npublic class PreviewLyricLayout : StageElement\r\n{\r\n    /// <summary>\r\n    /// <see cref=\"Lyric\"/>'s timing with row index.\r\n    /// </summary>\r\n    public IDictionary<int, double> Timings { get; set; } = new Dictionary<int, double>();\r\n\r\n    /// <summary>\r\n    /// <see cref=\"Lyric\"/>'s start time.\r\n    /// </summary>\r\n    public double StartTime { get; set; }\r\n\r\n    /// <summary>\r\n    /// <see cref=\"Lyric\"/>'s end time.\r\n    /// </summary>\r\n    public double EndTime { get; set; }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/Infos/Preview/PreviewLyricLayoutCategory.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages.Infos.Preview;\r\n\r\npublic class PreviewLyricLayoutCategory : StageElementCategory<PreviewLyricLayout, Lyric>\r\n{\r\n    protected override PreviewLyricLayout CreateDefaultElement()\r\n        => new();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/Infos/Preview/PreviewPlayfieldCommandProvider.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Commands;\r\nusing osu.Game.Rulesets.Karaoke.UI;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages.Infos.Preview;\r\n\r\npublic class PreviewPlayfieldCommandProvider : PlayfieldCommandProvider<PreviewStageInfo>\r\n{\r\n    public PreviewPlayfieldCommandProvider(PreviewStageInfo stageInfo, bool displayNotePlayfield)\r\n        : base(stageInfo, displayNotePlayfield)\r\n    {\r\n    }\r\n\r\n    protected override IEnumerable<IStageCommand> GetMainPlayfieldCommands(KaraokePlayfield playfield)\r\n    {\r\n        yield break;\r\n    }\r\n\r\n    protected override IEnumerable<IStageCommand> GetLyricPlayfieldCommands(LyricPlayfield playfield)\r\n    {\r\n        int xPosition = DisplayNotePlayfield ? -190 : 0;\r\n        int yPosition = DisplayNotePlayfield ? 0 : -32;\r\n\r\n        yield return new StageAnchorCommand(Easing.None, 0, 0, Anchor.Centre, Anchor.Centre);\r\n        yield return new StageOriginCommand(Easing.None, 0, 0, Anchor.TopLeft, Anchor.TopLeft);\r\n        yield return new StageXCommand(Easing.None, 0, 0, xPosition, xPosition);\r\n        yield return new StageYCommand(Easing.None, 0, 0, yPosition, yPosition);\r\n        yield return new StageAlphaCommand(Easing.In, 0, 100, 0, 1);\r\n    }\r\n\r\n    protected override IEnumerable<IStageCommand> GetNotePlayfieldCommands(NotePlayfield playfield)\r\n    {\r\n        yield return new StageAnchorCommand(Easing.None, 0, 0, Anchor.Centre, Anchor.Centre);\r\n        yield return new StageOriginCommand(Easing.None, 0, 0, Anchor.Centre, Anchor.Centre);\r\n        yield return new StageYCommand(Easing.None, 0, 0, -200, -200);\r\n        yield return new StagePaddingCommand(Easing.None, 0, 0, new MarginPadding(50), new MarginPadding(50));\r\n        yield return new StageAlphaCommand(Easing.In, 0, 100, 0, 1);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/Infos/Preview/PreviewStageDefinition.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages.Infos.Preview;\r\n\r\npublic class PreviewStageDefinition : StageDefinition\r\n{\r\n    #region Playfield\r\n\r\n    /// <summary>\r\n    /// The <see cref=\"PreviewStyle\"/> will use it's own blur level.\r\n    /// </summary>\r\n    public double BlueLevel { get; set; } = 0.5f;\r\n\r\n    /// <summary>\r\n    /// The <see cref=\"PreviewStyle\"/> will use it's own dim level.\r\n    /// </summary>\r\n    public double DimLevel { get; set; } = 0.5f;\r\n\r\n    #endregion\r\n\r\n    #region Fade in/out effect\r\n\r\n    /// <summary>\r\n    /// Fade-in/out time for the lyric showing to the screen or hiding from the screen.\r\n    /// </summary>\r\n    public double FadingTime { get; set; } = 300;\r\n\r\n    /// <summary>\r\n    /// The offset position for the lyric showing to the screen or hiding from the screen.\r\n    /// </summary>\r\n    public float FadingOffsetPosition { get; set; } = 64;\r\n\r\n    /// <summary>\r\n    /// Fade-in easing for the lyric showing to the screen.\r\n    /// </summary>\r\n    public Easing FadeInEasing { get; set; } = Easing.OutCirc;\r\n\r\n    /// <summary>\r\n    /// Fade-out easing for the lyric hiding from the screen.\r\n    /// </summary>\r\n    public Easing FadeOutEasing { get; set; } = Easing.OutCirc;\r\n\r\n    /// <summary>\r\n    /// Easing for the lyric move to the start position.\r\n    /// </summary>\r\n    public Easing MovingInEasing { get; set; } = Easing.OutCirc;\r\n\r\n    /// <summary>\r\n    /// Easing for the lyric move out to the end position.\r\n    /// </summary>\r\n    public Easing MoveOutEasing { get; set; } = Easing.OutCirc;\r\n\r\n    #endregion\r\n\r\n    #region Active/inactive effect\r\n\r\n    /// <summary>\r\n    /// The alpha for the lyric that is not active.\r\n    /// </summary>\r\n    public float InactiveAlpha { get; set; } = 0.3f;\r\n\r\n    /// <summary>\r\n    /// Time for the inactive state to the active stage.\r\n    /// </summary>\r\n    public double ActiveTime { get; set; } = 350;\r\n\r\n    /// <summary>\r\n    /// The alpha easing for the inactive to the active state.\r\n    /// </summary>\r\n    public Easing ActiveEasing { get; set; } = Easing.OutCirc;\r\n\r\n    #endregion\r\n\r\n    #region Lyrics arrangement\r\n\r\n    /// <summary>\r\n    /// Maximum of lyrics in the stage.\r\n    /// </summary>\r\n    public int NumberOfLyrics { get; set; } = 5;\r\n\r\n    /// <summary>\r\n    /// The height for the single lyric.\r\n    /// </summary>\r\n    public float LyricHeight { get; set; } = 64;\r\n\r\n    /// <summary>\r\n    /// The duration for the time moving up.\r\n    /// </summary>\r\n    public double LineMovingTime { get; set; } = 500;\r\n\r\n    /// <summary>\r\n    /// The easing for the time moving up.\r\n    /// </summary>\r\n    public Easing LineMovingEasing { get; set; } = Easing.OutCirc;\r\n\r\n    /// <summary>\r\n    /// If the first lyric is moved-up, the offset for the second lyric to be moved-up.\r\n    /// </summary>\r\n    public double LineMovingOffsetTime { get; set; } = 50;\r\n\r\n    #endregion\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/Infos/Preview/PreviewStageInfo.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing Newtonsoft.Json;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos.Types;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages.Infos.Preview;\r\n\r\npublic class PreviewStageInfo : StageInfo, IHasCalculatedProperty\r\n{\r\n    #region Category\r\n\r\n    /// <summary>\r\n    /// The definition for the <see cref=\"Lyric\"/>.\r\n    /// Like how many lyrics can in the playfield at the same time.\r\n    /// </summary>\r\n    public PreviewStageDefinition StageDefinition { get; set; } = new();\r\n\r\n    /// <summary>\r\n    /// Category to save the <see cref=\"Lyric\"/>'s and <see cref=\"Note\"/>'s style.\r\n    /// This property will not be saved because it's real-time calculated.\r\n    /// </summary>\r\n    [JsonIgnore]\r\n    private PreviewStyleCategory styleCategory { get; set; } = new();\r\n\r\n    /// <summary>\r\n    /// Category to save the <see cref=\"Lyric\"/>'s layout.\r\n    /// This property will not be saved because it's real-time calculated.\r\n    /// </summary>\r\n    [JsonIgnore]\r\n    private PreviewLyricLayoutCategory layoutCategory { get; set; } = new();\r\n\r\n    #endregion\r\n\r\n    #region Validation\r\n\r\n    /// <summary>\r\n    /// If the calculated property is not updated, then re-calculate the property inside the stage info in the <see cref=\"KaraokeBeatmapProcessor\"/>\r\n    /// </summary>\r\n    /// <param name=\"beatmap\"></param>\r\n    public void ValidateCalculatedProperty(IBeatmap beatmap)\r\n    {\r\n        var calculator = new PreviewStageTimingCalculator(beatmap, StageDefinition);\r\n\r\n        // also, clear all mapping in the layout and re-create one.\r\n        layoutCategory.ClearElements();\r\n\r\n        // Note: only deal with those lyrics has time.\r\n        var matchedLyrics = beatmap.HitObjects.OfType<Lyric>().Where(x => x.TimeValid).OrderBy(x => x.StartTime).ToArray();\r\n\r\n        foreach (var lyric in matchedLyrics)\r\n        {\r\n            var element = layoutCategory.AddElement(x =>\r\n            {\r\n                x.Name = $\"Auto-generated layout with lyric {lyric.ID}\";\r\n                x.StartTime = calculator.CalculateStartTime(lyric);\r\n                x.EndTime = calculator.CalculateEndTime(lyric);\r\n                x.Timings = calculator.CalculateTimings(lyric);\r\n            });\r\n            layoutCategory.AddToMapping(element, lyric);\r\n        }\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Stage element\r\n\r\n    protected override IEnumerable<StageElement> GetLyricStageElements(Lyric lyric)\r\n    {\r\n        yield return styleCategory.GetElementByItem(lyric);\r\n        yield return layoutCategory.GetElementByItem(lyric);\r\n    }\r\n\r\n    protected override IEnumerable<StageElement> GetNoteStageElements(Note note)\r\n    {\r\n        // todo: should check the real-time mapping result.\r\n        yield return styleCategory.GetElementByItem(note.ReferenceLyric!);\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Provider\r\n\r\n    public override IPlayfieldCommandProvider CreatePlayfieldCommandProvider(bool displayNotePlayfield)\r\n        => new PreviewPlayfieldCommandProvider(this, displayNotePlayfield);\r\n\r\n    public override IStageElementProvider? CreateStageElementProvider(bool displayNotePlayfield)\r\n        => new PreviewElementProvider(this, displayNotePlayfield);\r\n\r\n    public override IHitObjectCommandProvider? CreateHitObjectCommandProvider<TObject>() =>\r\n        typeof(TObject) switch\r\n        {\r\n            Type type when type == typeof(Lyric) => new PreviewLyricCommandProvider(this),\r\n            Type type when type == typeof(Note) => null,\r\n            _ => null\r\n        };\r\n\r\n    #endregion\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/Infos/Preview/PreviewStageTimingCalculator.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages.Infos.Preview;\r\n\r\npublic class PreviewStageTimingCalculator\r\n{\r\n    private readonly Lyric[] orderedLyrics;\r\n\r\n    // lyrics in the stage.\r\n    private readonly int numberOfLyrics;\r\n\r\n    // offset time in the fade in/out\r\n    private readonly double fadingTime;\r\n\r\n    // offset time in the Lyrics arrangement.\r\n    private readonly double lineMovingOffsetTime;\r\n\r\n    public PreviewStageTimingCalculator(IBeatmap beatmap, PreviewStageDefinition definition)\r\n    {\r\n        orderedLyrics = beatmap.HitObjects.OfType<Lyric>().Where(x => x.TimeValid).OrderBy(x => x.StartTime).ToArray();\r\n        numberOfLyrics = definition.NumberOfLyrics;\r\n        fadingTime = definition.FadingTime;\r\n        lineMovingOffsetTime = definition.LineMovingOffsetTime;\r\n    }\r\n\r\n    public double CalculateStartTime(Lyric lyric)\r\n    {\r\n        if (!lyric.TimeValid)\r\n            throw new InvalidOperationException();\r\n\r\n        var matchedLyrics = getRelatedLyrics(lyric, numberOfLyrics + 1).ToArray();\r\n\r\n        // if true, means those lyrics show at the screening at the beginning.\r\n        bool showAtBeginning = matchedLyrics.Length <= numberOfLyrics;\r\n\r\n        if (showAtBeginning)\r\n        {\r\n            return 0;\r\n        }\r\n\r\n        double startEffectTime = matchedLyrics.Min(x => x.EndTime) + numberOfLyrics * lineMovingOffsetTime;\r\n        return startEffectTime + fadingTime;\r\n    }\r\n\r\n    public double CalculateEndTime(Lyric lyric)\r\n    {\r\n        if (!lyric.TimeValid)\r\n            throw new InvalidOperationException();\r\n\r\n        return lyric.EndTime;\r\n    }\r\n\r\n    /// <summary>\r\n    /// Calculate the line and the timing the lyric should move-up.\r\n    /// </summary>\r\n    /// <param name=\"lyric\"></param>\r\n    /// <returns>The value should start from 0</returns>\r\n    public IDictionary<int, double> CalculateTimings(Lyric lyric)\r\n    {\r\n        var matchedLyrics = getRelatedLyrics(lyric, numberOfLyrics).ToArray();\r\n        var dictionary = new Dictionary<int, double>();\r\n\r\n        // Should not include the current lyric.\r\n        for (int i = 0; i < matchedLyrics.Length - 1; i++)\r\n        {\r\n            // line should start from zero.\r\n            int line = matchedLyrics.Length - i - 2;\r\n            double time = matchedLyrics[i].EndTime + line * lineMovingOffsetTime;\r\n\r\n            dictionary.Add(line, time);\r\n        }\r\n\r\n        return dictionary;\r\n    }\r\n\r\n    /// <summary>\r\n    /// will take the current lyric and the previous n lyrics.<br/>\r\n    /// note that the order should be p3, p2, p1, p0, current.\r\n    /// </summary>\r\n    /// <param name=\"lyric\"></param>\r\n    /// <param name=\"take\">if the number is 5, means will get p3, p2, p1, p0 and current</param>\r\n    /// <returns></returns>\r\n    private IEnumerable<Lyric> getRelatedLyrics(Lyric lyric, int take)\r\n        => orderedLyrics.Reverse().SkipWhile(x => x != lyric).Take(take).Reverse().ToArray();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/Infos/Preview/PreviewStyle.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages.Infos.Preview;\r\n\r\npublic class PreviewStyle : StageElement\r\n{\r\n    /// <summary>\r\n    /// <see cref=\"Lyric\"/>'s text style.\r\n    /// </summary>\r\n    public LyricStyle? LyricStyle { get; set; }\r\n\r\n    /// <summary>\r\n    /// <see cref=\"Note\"/>'s skin lookup index.\r\n    /// </summary>\r\n    public int? NoteStyleIndex { get; set; }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/Infos/Preview/PreviewStyleCategory.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages.Infos.Preview;\r\n\r\npublic class PreviewStyleCategory : StageElementCategory<PreviewStyle, Lyric>\r\n{\r\n    protected override PreviewStyle CreateDefaultElement()\r\n        => new()\r\n        {\r\n            LyricStyle = LyricStyle.CreateDefault(),\r\n        };\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/Infos/StageDefinition.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages.Infos;\r\n\r\n/// <summary>\r\n/// Class to store all the definition for the <see cref=\"StageInfo\"/>\r\n/// </summary>\r\npublic abstract class StageDefinition;\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/Infos/StageElement.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing Newtonsoft.Json;\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages.Infos;\r\n\r\npublic abstract class StageElement : IHasPrimaryKey\r\n{\r\n    /// <summary>\r\n    /// Index of the element.\r\n    /// </summary>\r\n    [JsonProperty]\r\n    public ElementId ID { get; private set; } = ElementId.NewElementId();\r\n\r\n    [JsonIgnore]\r\n    public readonly Bindable<string> NameBindable = new();\r\n\r\n    /// <summary>\r\n    /// Name of the element.\r\n    /// </summary>\r\n    public string Name\r\n    {\r\n        get => NameBindable.Value;\r\n        set => NameBindable.Value = value;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/Infos/StageElementCategory.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages.Infos;\r\n\r\n/// <summary>\r\n/// It's a category to record the list of <typeparamref name=\"TStageElement\"/> and handle the mapping by default role.<br/>\r\n/// Can add more customised role by inherit this class.<br/>\r\n/// </summary>\r\npublic abstract class StageElementCategory<TStageElement, THitObject>\r\n    where TStageElement : StageElement\r\n    where THitObject : KaraokeHitObject, IHasPrimaryKey\r\n{\r\n    /// <summary>\r\n    /// Default value.<br/>\r\n    /// Will use this value as default if there's no mapping result in the <typeparamref name=\"TStageElement\"/>\r\n    /// </summary>\r\n    public TStageElement DefaultElement { get; protected set; }\r\n\r\n    /// <summary>\r\n    /// All available elements.\r\n    /// </summary>\r\n    public BindableList<TStageElement> AvailableElements { get; protected set; } = new();\r\n\r\n    /// <summary>\r\n    /// Mapping between <typeparamref name=\"THitObject.ID\"/> and <typeparamref name=\"TStageElement.ID\"/><br/>\r\n    /// This is the 1st mapping roles.\r\n    /// </summary>\r\n    public IDictionary<ElementId, ElementId> Mappings { get; protected set; } = new Dictionary<ElementId, ElementId>();\r\n\r\n    protected StageElementCategory()\r\n    {\r\n        DefaultElement = CreateDefaultElement();\r\n        DefaultElement.Name = \"Default\";\r\n    }\r\n\r\n    #region Edit\r\n\r\n    public TStageElement AddElement(Action<TStageElement>? action = null)\r\n    {\r\n        var element = CreateDefaultElement();\r\n\r\n        action?.Invoke(element);\r\n        AvailableElements.Add(element);\r\n\r\n        return element;\r\n    }\r\n\r\n    public void EditElement(ElementId? id, Action<TStageElement> action)\r\n    {\r\n        var element = getElementById(id);\r\n        action(element);\r\n\r\n        TStageElement getElementById(ElementId? elementID) =>\r\n            elementID == null\r\n                ? DefaultElement\r\n                : AvailableElements.First(x => x.ID == elementID);\r\n    }\r\n\r\n    public void RemoveElement(TStageElement element)\r\n    {\r\n        RemoveElementFromMapping(element);\r\n\r\n        AvailableElements.Remove(element);\r\n    }\r\n\r\n    public virtual void ClearElements()\r\n    {\r\n        Mappings.Clear();\r\n        AvailableElements.Clear();\r\n    }\r\n\r\n    public virtual void AddToMapping(TStageElement element, THitObject hitObject)\r\n    {\r\n        var key = hitObject.ID;\r\n        var value = element.ID;\r\n\r\n        if (!AvailableElements.Contains(element))\r\n            throw new InvalidOperationException();\r\n\r\n        if (element == DefaultElement)\r\n            throw new InvalidOperationException();\r\n\r\n        if (!Mappings.TryAdd(key, value))\r\n        {\r\n            Mappings[key] = value;\r\n        }\r\n    }\r\n\r\n    public virtual void RemoveHitObjectFromMapping(THitObject hitObject)\r\n    {\r\n        Mappings.Remove(hitObject.ID);\r\n    }\r\n\r\n    public virtual void RemoveElementFromMapping(TStageElement element)\r\n    {\r\n        var objectIds = getMappingHitObjectIds(element);\r\n\r\n        foreach (var objectId in objectIds)\r\n        {\r\n            Mappings.Remove(objectId);\r\n        }\r\n\r\n        IEnumerable<ElementId> getMappingHitObjectIds(TStageElement stageElement)\r\n            => Mappings.Where(x => x.Value == stageElement.ID).Select(x => x.Key).ToArray();\r\n    }\r\n\r\n    public virtual void ClearUnusedMapping(Func<ElementId, bool> checkExist)\r\n    {\r\n        var unusedIds = Mappings.Select(x => x.Key).Where(x => !checkExist(x));\r\n\r\n        foreach (var hitObjectId in unusedIds)\r\n        {\r\n            Mappings.Remove(hitObjectId);\r\n        }\r\n    }\r\n\r\n    protected abstract TStageElement CreateDefaultElement();\r\n\r\n    #endregion\r\n\r\n    #region Query\r\n\r\n    public virtual TStageElement GetElementByItem(THitObject hitObject)\r\n    {\r\n        var id = hitObject.ID;\r\n\r\n        if (!Mappings.TryGetValue(id, out var elementId))\r\n            return DefaultElement;\r\n\r\n        var matchedElements = AvailableElements.FirstOrDefault(x => x.ID == elementId);\r\n        return matchedElements ?? DefaultElement;\r\n    }\r\n\r\n    public virtual IEnumerable<ElementId> GetHitObjectIdsByElement(TStageElement element)\r\n    {\r\n        return Mappings.Where(x => x.Value == element.ID).Select(x => x.Key);\r\n    }\r\n\r\n    #endregion\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/Infos/StageInfo.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages.Infos;\r\n\r\npublic abstract class StageInfo\r\n{\r\n    #region Stage element\r\n\r\n    public IEnumerable<StageElement> GetStageElements(KaraokeHitObject hitObject) =>\r\n        hitObject switch\r\n        {\r\n            Lyric lyric => GetLyricStageElements(lyric),\r\n            Note note => GetNoteStageElements(note),\r\n            _ => Array.Empty<StageElement>(),\r\n        };\r\n\r\n    protected abstract IEnumerable<StageElement> GetLyricStageElements(Lyric lyric);\r\n\r\n    protected abstract IEnumerable<StageElement> GetNoteStageElements(Note note);\r\n\r\n    #endregion\r\n\r\n    #region Provider\r\n\r\n    public abstract IPlayfieldCommandProvider CreatePlayfieldCommandProvider(bool displayNotePlayfield);\r\n\r\n    public abstract IStageElementProvider? CreateStageElementProvider(bool displayNotePlayfield);\r\n\r\n    public abstract IHitObjectCommandProvider? CreateHitObjectCommandProvider<TObject>() where TObject : KaraokeHitObject;\r\n\r\n    #endregion\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/Infos/Types/IHasCalculatedProperty.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages.Infos.Types;\r\n\r\npublic interface IHasCalculatedProperty\r\n{\r\n    /// <summary>\r\n    /// If the calculated property is not updated, then re-calculate the property inside the stage info in the <see cref=\"KaraokeBeatmapProcessor\"/>\r\n    /// </summary>\r\n    /// <param name=\"beatmap\"></param>\r\n    void ValidateCalculatedProperty(IBeatmap beatmap);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/PlayfieldCommandProvider.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing osu.Game.Rulesets.Karaoke.Edit;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Commands;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos;\r\nusing osu.Game.Rulesets.Karaoke.UI;\r\nusing osu.Game.Rulesets.UI;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages;\r\n\r\npublic abstract class PlayfieldCommandProvider<TStageInfo> : IPlayfieldCommandProvider\r\n    where TStageInfo : StageInfo\r\n{\r\n    protected readonly TStageInfo StageInfo;\r\n\r\n    protected readonly bool DisplayNotePlayfield;\r\n\r\n    protected PlayfieldCommandProvider(TStageInfo stageInfo, bool displayNotePlayfield)\r\n    {\r\n        StageInfo = stageInfo;\r\n        DisplayNotePlayfield = displayNotePlayfield;\r\n    }\r\n\r\n    public IEnumerable<IStageCommand> GetCommands(Playfield playfield) =>\r\n        playfield switch\r\n        {\r\n            KaraokePlayfield karaokePlayfield => GetMainPlayfieldCommands(karaokePlayfield),\r\n            LyricPlayfield lyricPlayfield => GetLyricPlayfieldCommands(lyricPlayfield),\r\n            NotePlayfield notePlayfield => GetNotePlayfieldCommands(notePlayfield),\r\n            EditorNotePlayfield => Array.Empty<IStageCommand>(),\r\n            _ => throw new InvalidCastException(),\r\n        };\r\n\r\n    protected abstract IEnumerable<IStageCommand> GetMainPlayfieldCommands(KaraokePlayfield playfield);\r\n\r\n    protected abstract IEnumerable<IStageCommand> GetLyricPlayfieldCommands(LyricPlayfield playfield);\r\n\r\n    protected abstract IEnumerable<IStageCommand> GetNotePlayfieldCommands(NotePlayfield playfield);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/StageBeatmapCoverInfo.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Drawables;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages;\r\n\r\npublic class StageBeatmapCoverInfo : StageSprite\r\n{\r\n    public override Drawable CreateDrawable() => new DrawableStageBeatmapCoverInfo(this);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/StageElementProvider.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages;\r\n\r\npublic abstract class StageElementProvider<TStageInfo> : IStageElementProvider\r\n{\r\n    protected readonly TStageInfo StageInfo;\r\n\r\n    protected readonly bool DisplayNotePlayfield;\r\n\r\n    protected StageElementProvider(TStageInfo stageInfo, bool displayNotePlayfield)\r\n    {\r\n        StageInfo = stageInfo;\r\n        DisplayNotePlayfield = displayNotePlayfield;\r\n    }\r\n\r\n    public abstract IEnumerable<IStageElement> GetElements();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Stages/StageSprite.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Commands;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Stages;\r\n\r\npublic class StageSprite : IStageElementWithDuration\r\n{\r\n    public double StartTime { get; }\r\n\r\n    public double EndTime { get; }\r\n\r\n    public double EndTimeForDisplay { get; }\r\n\r\n    public IReadOnlyList<IStageCommand> Commands { get; init; } = new List<IStageCommand>();\r\n\r\n    public virtual Drawable CreateDrawable()\r\n    {\r\n        throw new System.NotImplementedException();\r\n    }\r\n\r\n    public void ApplyTransforms<TDrawable>(TDrawable drawable)\r\n        where TDrawable : Drawable\r\n    {\r\n        HashSet<string> appliedProperties = new HashSet<string>();\r\n\r\n        foreach (var command in Commands.OrderBy(c => c.StartTime))\r\n        {\r\n            if (appliedProperties.Add(command.PropertyName))\r\n                command.ApplyInitialValue(drawable);\r\n\r\n            using (drawable.BeginAbsoluteSequence(command.StartTime))\r\n                command.ApplyTransforms(drawable);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Statistics/BeatmapMetadataGraph.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Extensions.Color4Extensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Cursor;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Containers;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.Cursor;\r\nusing osuTK;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Statistics;\r\n\r\npublic partial class BeatmapMetadataGraph : Container\r\n{\r\n    private const float spacing = 10;\r\n    private const float transition_duration = 250;\r\n\r\n    public BeatmapMetadataGraph(IBeatmap beatmap)\r\n    {\r\n        Masking = true;\r\n        CornerRadius = 5;\r\n\r\n        var beatmapInfo = beatmap.BeatmapInfo;\r\n        var karaokeBeatmap = beatmap as KaraokeBeatmap;\r\n        InternalChildren = new Drawable[]\r\n        {\r\n            new Box\r\n            {\r\n                Name = \"Background\",\r\n                RelativeSizeAxes = Axes.Both,\r\n                Colour = Color4.Black.Opacity(0.5f),\r\n            },\r\n            new OsuScrollContainer\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n                ScrollbarVisible = false,\r\n                Padding = new MarginPadding { Left = spacing / 2, Top = spacing / 2 },\r\n                Child = new FillFlowContainer\r\n                {\r\n                    RelativeSizeAxes = Axes.X,\r\n                    AutoSizeAxes = Axes.Y,\r\n                    LayoutDuration = transition_duration,\r\n                    LayoutEasing = Easing.OutQuad,\r\n                    Spacing = new Vector2(spacing),\r\n                    Children = new MetadataSection[]\r\n                    {\r\n                        new TextMetadataSection(\"Description\")\r\n                        {\r\n                            Text = beatmapInfo.DifficultyName,\r\n                        },\r\n                        new TextMetadataSection(\"Source\")\r\n                        {\r\n                            Text = beatmapInfo.Metadata.Source,\r\n                        },\r\n                        new TextMetadataSection(\"Tags\")\r\n                        {\r\n                            Text = beatmapInfo.Metadata.Tags,\r\n                        },\r\n                        new SingerMetadataSection(\"Singer\")\r\n                        {\r\n                            Singers = karaokeBeatmap?.SingerInfo.GetAllSingers().ToArray() ?? Array.Empty<Singer>(),\r\n                        },\r\n                    },\r\n                },\r\n            },\r\n        };\r\n    }\r\n\r\n    private abstract partial class MetadataSection : Container\r\n    {\r\n        protected FillFlowContainer TextContainer { get; }\r\n\r\n        protected MetadataSection(string title)\r\n        {\r\n            RelativeSizeAxes = Axes.X;\r\n            AutoSizeAxes = Axes.Y;\r\n\r\n            InternalChild = TextContainer = new FillFlowContainer\r\n            {\r\n                RelativeSizeAxes = Axes.X,\r\n                AutoSizeAxes = Axes.Y,\r\n                Spacing = new Vector2(spacing / 2),\r\n                Children = new Drawable[]\r\n                {\r\n                    new Container\r\n                    {\r\n                        RelativeSizeAxes = Axes.X,\r\n                        AutoSizeAxes = Axes.Y,\r\n                        Child = new OsuSpriteText\r\n                        {\r\n                            Text = title,\r\n                            Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 14),\r\n                        },\r\n                    },\r\n                },\r\n            };\r\n        }\r\n    }\r\n\r\n    private partial class TextMetadataSection : MetadataSection\r\n    {\r\n        private TextFlowContainer? textFlow;\r\n\r\n        public TextMetadataSection(string title)\r\n            : base(title)\r\n        {\r\n        }\r\n\r\n        public string Text\r\n        {\r\n            set\r\n            {\r\n                if (string.IsNullOrEmpty(value))\r\n                {\r\n                    this.FadeOut(transition_duration);\r\n                    return;\r\n                }\r\n\r\n                this.FadeIn(transition_duration);\r\n\r\n                setTextAsync(value);\r\n            }\r\n        }\r\n\r\n        private void setTextAsync(string text)\r\n        {\r\n            textFlow?.Expire();\r\n            TextContainer.Add(textFlow = new OsuTextFlowContainer(s => s.Font = s.Font.With(size: 14))\r\n            {\r\n                RelativeSizeAxes = Axes.X,\r\n                AutoSizeAxes = Axes.Y,\r\n                Colour = Color4.White.Opacity(0.75f),\r\n                Text = text,\r\n            });\r\n        }\r\n    }\r\n\r\n    private partial class SingerMetadataSection : MetadataSection\r\n    {\r\n        private FillFlowContainer<SingerSpriteText>? textFlow;\r\n\r\n        public SingerMetadataSection(string title)\r\n            : base(title)\r\n        {\r\n        }\r\n\r\n        public Singer[] Singers\r\n        {\r\n            set\r\n            {\r\n                if (!value.Any())\r\n                {\r\n                    this.FadeOut(transition_duration);\r\n                    return;\r\n                }\r\n\r\n                this.FadeIn(transition_duration);\r\n\r\n                setSingerAsync(value);\r\n            }\r\n        }\r\n\r\n        private void setSingerAsync(IEnumerable<Singer> singers)\r\n        {\r\n            textFlow?.Expire();\r\n            TextContainer.Add(textFlow = new FillFlowContainer<SingerSpriteText>\r\n            {\r\n                RelativeSizeAxes = Axes.X,\r\n                AutoSizeAxes = Axes.Y,\r\n                Spacing = new Vector2(10),\r\n                Colour = Color4.White.Opacity(0.75f),\r\n            });\r\n\r\n            foreach (var singer in singers)\r\n            {\r\n                textFlow.Add(new SingerSpriteText\r\n                {\r\n                    Singer = singer,\r\n                });\r\n            }\r\n        }\r\n\r\n        private partial class SingerSpriteText : CompositeDrawable, IHasCustomTooltip<Singer>\r\n        {\r\n            private readonly OsuSpriteText osuSpriteText;\r\n\r\n            public SingerSpriteText()\r\n            {\r\n                AutoSizeAxes = Axes.Both;\r\n                InternalChildren = new[]\r\n                {\r\n                    osuSpriteText = new OsuSpriteText\r\n                    {\r\n                        Font = OsuFont.GetFont(size: 14),\r\n                    },\r\n                };\r\n            }\r\n\r\n            private readonly Singer? singer;\r\n\r\n            public Singer? Singer\r\n            {\r\n                get => singer;\r\n                init\r\n                {\r\n                    singer = value;\r\n                    osuSpriteText.Text = singer?.Name ?? \"Known singer\";\r\n                }\r\n            }\r\n\r\n            public ITooltip<Singer> GetCustomTooltip() => new SingerToolTip();\r\n\r\n            public Singer? TooltipContent => Singer;\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Statistics/NotScorableGraph.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Extensions.Color4Extensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Statistics;\r\n\r\npublic partial class NotScorableGraph : Container\r\n{\r\n    public NotScorableGraph()\r\n    {\r\n        Masking = true;\r\n        CornerRadius = 5;\r\n        InternalChildren = new Drawable[]\r\n        {\r\n            new Box\r\n            {\r\n                Name = \"Background\",\r\n                RelativeSizeAxes = Axes.Both,\r\n                Colour = Color4.Black.Opacity(0.5f),\r\n            },\r\n            new OsuSpriteText\r\n            {\r\n                Anchor = Anchor.Centre,\r\n                Origin = Anchor.Centre,\r\n                Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 20),\r\n                Text = \"Sorry, this beatmap is not scorable.\",\r\n            },\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Statistics/ScoringResultGraph.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Extensions.IEnumerableExtensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Containers;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Scoring;\r\nusing osu.Game.Scoring;\r\nusing osuTK;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Statistics;\r\n\r\npublic partial class ScoringResultGraph : CompositeDrawable\r\n{\r\n    private readonly Box background;\r\n    private readonly ScoringResultLyricPreview lyricGraph;\r\n    private readonly NoteGraph noteGraph;\r\n\r\n    public ScoringResultGraph(ScoreInfo score, IBeatmap beatmap)\r\n    {\r\n        InternalChildren = new Drawable[]\r\n        {\r\n            new Container\r\n            {\r\n                Masking = true,\r\n                CornerRadius = 5,\r\n                RelativeSizeAxes = Axes.Both,\r\n                Children = new Drawable[]\r\n                {\r\n                    background = new Box\r\n                    {\r\n                        Name = \"Background\",\r\n                        Anchor = Anchor.Centre,\r\n                        Origin = Anchor.Centre,\r\n                        RelativeSizeAxes = Axes.Both,\r\n                    },\r\n                    lyricGraph = new ScoringResultLyricPreview(beatmap)\r\n                    {\r\n                        RelativeSizeAxes = Axes.Both,\r\n                        Spacing = new Vector2(5),\r\n                    },\r\n                    noteGraph = new NoteGraph(score),\r\n                },\r\n            },\r\n        };\r\n\r\n        lyricGraph.SelectedLyric.BindValueChanged(e =>\r\n        {\r\n            // todo : move noteGraph to target time.\r\n        });\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OsuColour colours)\r\n    {\r\n        background.Colour = colours.ContextMenuGray;\r\n    }\r\n\r\n    // todo: refactor needed.\r\n    public partial class LyricPreview : CompositeDrawable\r\n    {\r\n        public Bindable<Lyric> SelectedLyric { get; } = new();\r\n\r\n        private readonly FillFlowContainer<ClickableLyric> lyricTable;\r\n\r\n        public LyricPreview(IEnumerable<Lyric> lyrics)\r\n        {\r\n            InternalChild = new OsuScrollContainer\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n                Child = lyricTable = new FillFlowContainer<ClickableLyric>\r\n                {\r\n                    AutoSizeAxes = Axes.Y,\r\n                    RelativeSizeAxes = Axes.X,\r\n                    Direction = FillDirection.Vertical,\r\n                    Children = lyrics.Select(x => CreateLyricContainer(x).With(c =>\r\n                    {\r\n                        c.Selected = false;\r\n                        c.Action = () => triggerLyric(x);\r\n                    })).ToList(),\r\n                },\r\n            };\r\n\r\n            SelectedLyric.BindValueChanged(value =>\r\n            {\r\n                var oldValue = value.OldValue;\r\n                if (oldValue != null)\r\n                    lyricTable.Where(x => x.Lyric == oldValue).ForEach(x => { x.Selected = false; });\r\n\r\n                var newValue = value.NewValue;\r\n                if (newValue != null)\r\n                    lyricTable.Where(x => x.Lyric == newValue).ForEach(x => { x.Selected = true; });\r\n            });\r\n        }\r\n\r\n        private void triggerLyric(Lyric lyric)\r\n        {\r\n            if (SelectedLyric.Value == lyric)\r\n                SelectedLyric.TriggerChange();\r\n            else\r\n                SelectedLyric.Value = lyric;\r\n        }\r\n\r\n        public Vector2 Spacing\r\n        {\r\n            get => lyricTable.Spacing;\r\n            set => lyricTable.Spacing = value;\r\n        }\r\n\r\n        protected virtual ClickableLyric CreateLyricContainer(Lyric lyric) => new(lyric);\r\n\r\n        public partial class ClickableLyric : ClickableContainer\r\n        {\r\n            private const float fade_duration = 100;\r\n\r\n            private Color4 hoverTextColour;\r\n            private Color4 idolTextColour;\r\n\r\n            private readonly Box background;\r\n            private readonly Drawable icon;\r\n            private readonly DrawableLyricSpriteText drawableLyric;\r\n\r\n            public Lyric Lyric;\r\n\r\n            public ClickableLyric(Lyric lyric)\r\n            {\r\n                Lyric = lyric;\r\n\r\n                AutoSizeAxes = Axes.Y;\r\n                RelativeSizeAxes = Axes.X;\r\n                Masking = true;\r\n                CornerRadius = 5;\r\n                Children = new[]\r\n                {\r\n                    background = new Box\r\n                    {\r\n                        RelativeSizeAxes = Axes.Both,\r\n                    },\r\n                    icon = CreateIcon(),\r\n                    drawableLyric = CreateLyric(lyric),\r\n                };\r\n            }\r\n\r\n            protected virtual DrawableLyricSpriteText CreateLyric(Lyric lyric) => new(lyric)\r\n            {\r\n                Font = new FontUsage(size: 25),\r\n                TopTextFont = new FontUsage(size: 10),\r\n                BottomTextFont = new FontUsage(size: 10),\r\n                Margin = new MarginPadding { Left = 25 },\r\n            };\r\n\r\n            protected virtual Drawable CreateIcon() => new SpriteIcon\r\n            {\r\n                Anchor = Anchor.CentreLeft,\r\n                Origin = Anchor.CentreLeft,\r\n                Size = new Vector2(15),\r\n                Icon = FontAwesome.Solid.Play,\r\n                Margin = new MarginPadding { Left = 5 },\r\n            };\r\n\r\n            private bool selected;\r\n\r\n            public bool Selected\r\n            {\r\n                get => selected;\r\n                set\r\n                {\r\n                    if (value == selected) return;\r\n\r\n                    selected = value;\r\n\r\n                    background.FadeTo(Selected ? 1 : 0, fade_duration);\r\n                    icon.FadeTo(Selected ? 1 : 0, fade_duration);\r\n                    drawableLyric.FadeColour(Selected ? hoverTextColour : idolTextColour, fade_duration);\r\n                }\r\n            }\r\n\r\n            [BackgroundDependencyLoader]\r\n            private void load(OsuColour colours)\r\n            {\r\n                hoverTextColour = colours.Yellow;\r\n                idolTextColour = colours.Gray9;\r\n\r\n                drawableLyric.Colour = idolTextColour;\r\n                background.Colour = colours.Blue;\r\n                background.Alpha = 0;\r\n                icon.Colour = hoverTextColour;\r\n                icon.Alpha = 0;\r\n            }\r\n        }\r\n    }\r\n\r\n    private partial class ScoringResultLyricPreview : LyricPreview\r\n    {\r\n        public ScoringResultLyricPreview(IBeatmap beatmap)\r\n            : base(beatmap.HitObjects.OfType<Lyric>())\r\n        {\r\n        }\r\n\r\n        protected override ClickableLyric CreateLyricContainer(Lyric lyric)\r\n            => new ScoringResultClickableLyric(lyric);\r\n\r\n        private partial class ScoringResultClickableLyric : ClickableLyric\r\n        {\r\n            public ScoringResultClickableLyric(Lyric lyric)\r\n                : base(lyric)\r\n            {\r\n            }\r\n\r\n            protected override DrawableLyricSpriteText CreateLyric(Lyric lyric)\r\n                => new(lyric)\r\n                {\r\n                    Font = new FontUsage(size: 15),\r\n                    TopTextFont = new FontUsage(size: 7),\r\n                    BottomTextFont = new FontUsage(size: 7),\r\n                    Margin = new MarginPadding { Left = 5 },\r\n                };\r\n\r\n            protected override Drawable CreateIcon()\r\n                => Empty();\r\n        }\r\n    }\r\n\r\n    private partial class NoteGraph : CompositeDrawable\r\n    {\r\n        public NoteGraph(ScoreInfo score)\r\n        {\r\n            var noteEvents = score.HitEvents.Where(x => x.HitObject is Note { Display: true }).ToList();\r\n\r\n            foreach (var noteEvent in noteEvents)\r\n            {\r\n                // TODO : add note into here\r\n            }\r\n\r\n            // todo : add list of note colors to present state.\r\n        }\r\n\r\n        internal partial class DrawableNote : Box\r\n        {\r\n            internal DrawableNote(HitResult result)\r\n            {\r\n                // TODO : assign color with different hit result.\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Timing/StopClock.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Timing;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Timing;\r\n\r\npublic class StopClock : IFrameBasedClock\r\n{\r\n    public StopClock(double targetTime)\r\n    {\r\n        CurrentTime = targetTime;\r\n    }\r\n\r\n    public double ElapsedFrameTime => 0;\r\n\r\n    public double FramesPerSecond => 0;\r\n\r\n    public FrameTimeInfo TimeInfo => new() { Current = CurrentTime, Elapsed = ElapsedFrameTime };\r\n\r\n    public double CurrentTime { get; }\r\n\r\n    public double Rate => 0;\r\n\r\n    public bool IsRunning => false;\r\n\r\n    public void ProcessFrame()\r\n    {\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/UI/Components/CenterLine.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Game.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.UI.Components;\r\n\r\npublic partial class CenterLine : CompositeDrawable\r\n{\r\n    private readonly Box centerLineBox;\r\n\r\n    public CenterLine()\r\n    {\r\n        RelativeSizeAxes = Axes.X;\r\n\r\n        InternalChild = centerLineBox = new Box\r\n        {\r\n            RelativeSizeAxes = Axes.Both,\r\n        };\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OsuColour colours)\r\n    {\r\n        centerLineBox.Colour = colours.Red;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/UI/Components/DefaultColumnBackground.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Shapes;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.UI.Components;\r\n\r\npublic partial class DefaultColumnBackground : Box\r\n{\r\n    public const float COLUMN_HEIGHT = 20;\r\n\r\n    public DefaultColumnBackground(int index)\r\n    {\r\n        RelativeSizeAxes = Axes.X;\r\n        Height = COLUMN_HEIGHT;\r\n        Alpha = 0.15f;\r\n    }\r\n\r\n    private bool isSpecial;\r\n\r\n    public bool IsSpecial\r\n    {\r\n        get => isSpecial;\r\n        set\r\n        {\r\n            if (isSpecial == value)\r\n                return;\r\n\r\n            isSpecial = value;\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/UI/Components/DefaultJudgementLine.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Game.Graphics;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.UI.Components;\r\n\r\npublic partial class DefaultJudgementLine : CompositeDrawable\r\n{\r\n    private const float triangle_width = 20;\r\n    private const float triangle_height = 10;\r\n    private const float bar_width = 2;\r\n\r\n    public DefaultJudgementLine()\r\n    {\r\n        RelativeSizeAxes = Axes.Y;\r\n        Size = new Vector2(20, 1);\r\n\r\n        Anchor = Anchor.Centre;\r\n        Origin = Anchor.Centre;\r\n\r\n        InternalChildren = new Drawable[]\r\n        {\r\n            new Box\r\n            {\r\n                Anchor = Anchor.Centre,\r\n                Origin = Anchor.Centre,\r\n                RelativeSizeAxes = Axes.Y,\r\n                Width = bar_width,\r\n            },\r\n            new Triangle\r\n            {\r\n                Anchor = Anchor.TopCentre,\r\n                Origin = Anchor.BottomCentre,\r\n                Size = new Vector2(triangle_width, triangle_height),\r\n                Scale = new Vector2(1, -1),\r\n            },\r\n        };\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OsuColour colours)\r\n    {\r\n        Colour = colours.Red;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/UI/Components/RealTimeScoringVisualization.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Caching;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.UI.Position;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.UI.Components;\r\n\r\npublic partial class RealTimeScoringVisualization : VoiceVisualization<KeyValuePair<double, KaraokeScoringAction>>\r\n{\r\n    private readonly Cached addStateCache = new();\r\n\r\n    protected override float PathRadius => 2.5f;\r\n\r\n    protected override float Offset => DrawSize.X;\r\n\r\n    [Resolved]\r\n    private INotePositionInfo notePositionInfo { get; set; } = null!;\r\n\r\n    public RealTimeScoringVisualization()\r\n    {\r\n        Masking = true;\r\n    }\r\n\r\n    protected override double GetTime(KeyValuePair<double, KaraokeScoringAction> frame) => frame.Key;\r\n\r\n    protected override float GetPosition(KeyValuePair<double, KaraokeScoringAction> frame) => notePositionInfo.Calculator.YPositionAt(frame.Value);\r\n\r\n    private bool createNew = true;\r\n\r\n    private double minAvailableTime;\r\n\r\n    public void AddAction(KaraokeScoringAction action)\r\n    {\r\n        if (Time.Current <= minAvailableTime)\r\n            return;\r\n\r\n        minAvailableTime = Time.Current;\r\n\r\n        if (createNew)\r\n        {\r\n            createNew = false;\r\n\r\n            CreateNew(new KeyValuePair<double, KaraokeScoringAction>(Time.Current, action));\r\n        }\r\n        else\r\n        {\r\n            Append(new KeyValuePair<double, KaraokeScoringAction>(Time.Current, action));\r\n        }\r\n\r\n        // Trigger update last frame\r\n        addStateCache.Invalidate();\r\n    }\r\n\r\n    public void Release()\r\n    {\r\n        if (Time.Current < minAvailableTime)\r\n            return;\r\n\r\n        minAvailableTime = Time.Current;\r\n\r\n        createNew = true;\r\n    }\r\n\r\n    protected override void Update()\r\n    {\r\n        // If addStateCache is invalid, means last path should be re-calculate\r\n        if (!addStateCache.IsValid && Paths.Any())\r\n        {\r\n            var updatePath = Paths.Last();\r\n            MarkAsInvalid(updatePath);\r\n            addStateCache.Validate();\r\n        }\r\n\r\n        base.Update();\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OsuColour colours)\r\n    {\r\n        Colour = colours.Yellow;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/UI/Components/ReplayScoringVisualization.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Extensions.IEnumerableExtensions;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Replays;\r\nusing osu.Game.Rulesets.Karaoke.Replays;\r\nusing osu.Game.Rulesets.Karaoke.UI.Position;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.UI.Components;\r\n\r\npublic partial class ReplayScoringVisualization : VoiceVisualization<KaraokeReplayFrame>\r\n{\r\n    protected override float PathRadius => 1.5f;\r\n\r\n    [Resolved]\r\n    private INotePositionInfo notePositionInfo { get; set; } = null!;\r\n\r\n    public ReplayScoringVisualization(Replay? replay)\r\n    {\r\n        var frames = replay?.Frames.OfType<KaraokeReplayFrame>();\r\n        frames?.ForEach(Add);\r\n    }\r\n\r\n    protected override double GetTime(KaraokeReplayFrame frame) => frame.Time;\r\n\r\n    protected override float GetPosition(KaraokeReplayFrame frame) => notePositionInfo.Calculator.YPositionAt(frame);\r\n\r\n    private bool createNew = true;\r\n\r\n    private double minAvailableTime;\r\n\r\n    public void Add(KaraokeReplayFrame frame)\r\n    {\r\n        // Start time should be largest and cannot be removed.\r\n        double startTime = frame.Time;\r\n        if (startTime <= minAvailableTime)\r\n            throw new ArgumentOutOfRangeException(nameof(startTime));\r\n\r\n        minAvailableTime = startTime;\r\n\r\n        if (!frame.Sound)\r\n        {\r\n            // Next replay frame will create new path\r\n            createNew = true;\r\n            return;\r\n        }\r\n\r\n        if (createNew)\r\n        {\r\n            createNew = false;\r\n\r\n            CreateNew(frame);\r\n        }\r\n        else\r\n        {\r\n            Append(frame);\r\n        }\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OsuColour colours)\r\n    {\r\n        Colour = colours.GrayF;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/UI/Components/ScoringMarker.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Game.Graphics;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.UI.Components;\r\n\r\npublic partial class ScoringMarker : CompositeDrawable\r\n{\r\n    private const float triangle_width = 20;\r\n    private const float triangle_height = 20;\r\n\r\n    public ScoringMarker()\r\n    {\r\n        AutoSizeAxes = Axes.Both;\r\n        InternalChildren = new Drawable[]\r\n        {\r\n            new Triangle\r\n            {\r\n                Size = new Vector2(triangle_width, triangle_height),\r\n                Rotation = 90,\r\n            },\r\n        };\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OsuColour colours)\r\n    {\r\n        Colour = colours.Yellow;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/UI/Components/ScoringStatus.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.ComponentModel;\r\nusing System.Linq;\r\nusing Markdig;\r\nusing Markdig.Syntax;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Containers.Markdown;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Graphics.Containers.Markdown;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osuTK;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.UI.Components;\r\n\r\npublic partial class ScoringStatus : FillFlowContainer, IMarkdownTextComponent\r\n{\r\n    private const float size = 22;\r\n\r\n    private readonly SpriteIcon icon;\r\n    private readonly MarkdownTextFlowContainer messageText;\r\n\r\n    public SpriteText CreateSpriteText() => new OsuSpriteText();\r\n\r\n    public ScoringStatus(ScoringStatusMode statusMode)\r\n    {\r\n        Spacing = new Vector2(5);\r\n        Direction = FillDirection.Horizontal;\r\n        AutoSizeAxes = Axes.Both;\r\n        ScoringStatusMode = statusMode;\r\n\r\n        Children = new Drawable[]\r\n        {\r\n            icon = new SpriteIcon\r\n            {\r\n                Size = new Vector2(size),\r\n            },\r\n            messageText = new OsuMarkdownTextFlowContainer\r\n            {\r\n                RelativeSizeAxes = Axes.None,\r\n                AutoSizeAxes = Axes.Both,\r\n            },\r\n        };\r\n    }\r\n\r\n    private ScoringStatusMode statusMode;\r\n\r\n    public ScoringStatusMode ScoringStatusMode\r\n    {\r\n        get => statusMode;\r\n        set\r\n        {\r\n            statusMode = value;\r\n\r\n            Schedule(() =>\r\n            {\r\n                bool scorable = statusMode == ScoringStatusMode.Scoring;\r\n                icon.Icon = scorable ? FontAwesome.Regular.DotCircle : FontAwesome.Regular.PauseCircle;\r\n                icon.Colour = scorable ? Color4.Red : Color4.LightGray;\r\n\r\n                string text = getScoringStatusText(statusMode).ToString();\r\n                var block = Markdown.Parse(text).OfType<ParagraphBlock>().FirstOrDefault();\r\n\r\n                messageText.Clear();\r\n                if (block != null)\r\n                    messageText.AddInlineText(block.Inline);\r\n            });\r\n        }\r\n    }\r\n\r\n    private static LocalisableString getScoringStatusText(ScoringStatusMode statusMode)\r\n    {\r\n        return statusMode switch\r\n        {\r\n            ScoringStatusMode.AndroidMicrophonePermissionDeclined => \"Go to setting to open permission for lazer.\",\r\n            ScoringStatusMode.AndroidDoesNotSupported => \"Android device haven't support scoring system yet :(\",\r\n            ScoringStatusMode.IOSMicrophonePermissionDeclined => \"Go to setting to open permission for lazer.\",\r\n            ScoringStatusMode.IOSDoesNotSupported => \"iOS device haven't support scoring system yet :(\",\r\n            ScoringStatusMode.OSXMicrophonePermissionDeclined => \"Go to setting to open permission for lazer.\",\r\n            ScoringStatusMode.OSXDoesNotSupported => \"Osx device haven't support scoring system yet :(\",\r\n            ScoringStatusMode.WindowsMicrophonePermissionDeclined => \"Open lazer with admin permission to enable scoring system.\",\r\n            ScoringStatusMode.NotScoring => \"This beatmap is not scorable.\",\r\n            ScoringStatusMode.AutoPlay => \"Auto play mode.\",\r\n            ScoringStatusMode.Edit => \"Edit mode.\",\r\n            ScoringStatusMode.Scoring => \"Scoring...\",\r\n            ScoringStatusMode.NotInitialized => \"Seems microphone device is not ready.\",\r\n            _ => \"Weird... Should not goes to here either :oops:\",\r\n        };\r\n    }\r\n}\r\n\r\npublic enum ScoringStatusMode\r\n{\r\n    /// <summary>\r\n    /// Due to android device does not authorize microphone access.\r\n    /// </summary>\r\n    [Description(\"Android permission declined.\")]\r\n    AndroidMicrophonePermissionDeclined,\r\n\r\n    /// <summary>\r\n    /// Scoring system does not support android device.\r\n    /// Will throw this if osu.framework.microphone does not support it yet.\r\n    /// Or official client does not open this permission.\r\n    /// </summary>\r\n    [Description(\"Android target not supported.\")]\r\n    AndroidDoesNotSupported,\r\n\r\n    /// <summary>\r\n    /// Due to iOS device does not authorize microphone access.\r\n    /// </summary>\r\n    [Description(\"iOS permission declined.\")]\r\n    IOSMicrophonePermissionDeclined,\r\n\r\n    /// <summary>\r\n    /// Scoring system does not support iOS device.\r\n    /// Will throw this if osu.framework.microphone does not support it yet.\r\n    /// Or official client does not open this permission.\r\n    /// </summary>\r\n    [Description(\"iOS target not supported.\")]\r\n    IOSDoesNotSupported,\r\n\r\n    /// <summary>\r\n    /// Due to osx device does not authorize microphone access.\r\n    /// </summary>\r\n    [Description(\"osx permission declined.\")]\r\n    OSXMicrophonePermissionDeclined,\r\n\r\n    /// <summary>\r\n    /// Scoring system does not support osx device.\r\n    /// Will throw this if osu.framework.microphone does not support it yet.\r\n    /// Or official client does not open this permission.\r\n    /// </summary>\r\n    [Description(\"osx target not supported.\")]\r\n    OSXDoesNotSupported,\r\n\r\n    /// <summary>\r\n    /// Due to windows device does not authorize microphone access.\r\n    /// Windows client don't need to ask permission.\r\n    /// Open lazer client with admin permission can solve that.\r\n    /// </summary>\r\n    [Description(\"Windows permission declined.\")]\r\n    WindowsMicrophonePermissionDeclined,\r\n\r\n    /// <summary>\r\n    /// No microphone device in this computer/macbook.\r\n    /// </summary>\r\n    [Description(\"No microphone device.\")]\r\n    NoMicrophoneDevice,\r\n\r\n    /// <summary>\r\n    /// Beatmap is not scoring.\r\n    /// </summary>\r\n    [Description(\"No scoring.\")]\r\n    NotScoring,\r\n\r\n    /// <summary>\r\n    /// Beatmap is not scoring.\r\n    /// </summary>\r\n    [Description(\"Autoplay.\")]\r\n    AutoPlay,\r\n\r\n    /// <summary>\r\n    /// In edit mode.\r\n    /// </summary>\r\n    [Description(\"Edit mode.\")]\r\n    Edit,\r\n\r\n    /// <summary>\r\n    /// Everything works well.\r\n    /// </summary>\r\n    [Description(\"Scoring...\")]\r\n    Scoring,\r\n\r\n    /// <summary>\r\n    /// Microphone scoring is not initialized.\r\n    /// </summary>\r\n    [Description(\"Not initialized.\")]\r\n    NotInitialized,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/UI/Components/VoiceVisualization.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Caching;\r\nusing osu.Framework.Extensions.IEnumerableExtensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Lines;\r\nusing osu.Framework.Graphics.Performance;\r\nusing osu.Framework.Layout;\r\nusing osu.Framework.Threading;\r\nusing osu.Game.Rulesets.UI.Scrolling;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.UI.Components;\r\n\r\npublic abstract partial class VoiceVisualization<T> : LifetimeManagementContainer\r\n{\r\n    private const float safe_lifetime_end_multiplier = 1;\r\n\r\n    private readonly IBindable<double> timeRange = new BindableDouble();\r\n    private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();\r\n\r\n    [Resolved]\r\n    private IScrollingInfo scrollingInfo { get; set; } = null!;\r\n\r\n    private readonly LayoutValue initialStateCache = new(Invalidation.RequiredParentSizeToFit | Invalidation.DrawInfo);\r\n\r\n    private readonly IDictionary<ScoringPath, IList<T>> frames = new Dictionary<ScoringPath, IList<T>>();\r\n    private readonly IDictionary<ScoringPath, Cached> pathInitialStateCache = new Dictionary<ScoringPath, Cached>();\r\n\r\n    protected IEnumerable<ScoringPath> Paths => InternalChildren.OfType<ScoringPath>();\r\n    protected IEnumerable<ScoringPath> AlivePaths => AliveInternalChildren.OfType<ScoringPath>();\r\n\r\n    protected virtual float PathRadius => 2;\r\n\r\n    protected VoiceVisualization()\r\n    {\r\n        AddLayout(initialStateCache);\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        direction.BindTo(scrollingInfo.Direction);\r\n        timeRange.BindTo(scrollingInfo.TimeRange);\r\n\r\n        direction.ValueChanged += _ => initialStateCache.Invalidate();\r\n        timeRange.ValueChanged += _ => initialStateCache.Invalidate();\r\n    }\r\n\r\n    protected abstract double GetTime(T frame);\r\n\r\n    protected abstract float GetPosition(T frame);\r\n\r\n    protected void CreateNew(T point)\r\n    {\r\n        var path = new ScoringPath\r\n        {\r\n            PathRadius = PathRadius,\r\n        };\r\n        frames.Add(path, new List<T> { point });\r\n        pathInitialStateCache.Add(path, new Cached());\r\n\r\n        AddInternal(path);\r\n    }\r\n\r\n    protected void Append(T point)\r\n    {\r\n        frames.LastOrDefault().Value.Add(point);\r\n    }\r\n\r\n    public void Clear()\r\n    {\r\n        frames.Clear();\r\n        pathInitialStateCache.Clear();\r\n        ClearInternal();\r\n    }\r\n\r\n    private float scrollLength;\r\n\r\n    protected override void Update()\r\n    {\r\n        base.Update();\r\n\r\n        scrollLength = DrawSize.X;\r\n\r\n        // If change the speed or direction, mark all the cache is invalid and re-calculate life time\r\n        if (!initialStateCache.IsValid)\r\n        {\r\n            // Reset scroll info\r\n            scrollingInfo.Algorithm.Value.Reset();\r\n\r\n            foreach (var cached in pathInitialStateCache.Values)\r\n                cached.Invalidate();\r\n\r\n            foreach (var path in Paths)\r\n                computeLifetime(path);\r\n\r\n            // Mark all the state is valid\r\n            initialStateCache.Validate();\r\n        }\r\n\r\n        // Re-calculate alive path\r\n        AlivePaths.ForEach(computePath);\r\n    }\r\n\r\n    protected void MarkAsInvalid(ScoringPath path) => pathInitialStateCache[path].Invalidate();\r\n\r\n    protected void Invalid() => initialStateCache.Invalidate();\r\n\r\n    private void computeLifetime(ScoringPath path)\r\n    {\r\n        var firstFrameInPath = frames[path].FirstOrDefault();\r\n        var lastFrameInPath = frames[path].LastOrDefault();\r\n\r\n        if (firstFrameInPath == null || lastFrameInPath == null)\r\n            return;\r\n\r\n        double startTime = GetTime(firstFrameInPath);\r\n        double endTime = GetTime(lastFrameInPath);\r\n\r\n        float originAdjustment = direction.Value switch\r\n        {\r\n            ScrollingDirection.Left => path.OriginPosition.X,\r\n            ScrollingDirection.Right => path.DrawWidth - path.OriginPosition.X,\r\n            _ => 0.0f,\r\n        };\r\n\r\n        path.LifetimeStart = scrollingInfo.Algorithm.Value.GetDisplayStartTime(startTime, originAdjustment, timeRange.Value, scrollLength);\r\n        path.LifetimeEnd = scrollingInfo.Algorithm.Value.TimeAt(scrollLength * safe_lifetime_end_multiplier, endTime, timeRange.Value, scrollLength);\r\n    }\r\n\r\n    // Cant use AddOnce() since the delegate is re-constructed every invocation\r\n    private void computePath(ScoringPath path) => path.Schedule(() =>\r\n    {\r\n        var firstFrameInPath = frames[path].FirstOrDefault();\r\n        if (firstFrameInPath == null)\r\n            return;\r\n\r\n        double startTime = GetTime(firstFrameInPath);\r\n        if (pathInitialStateCache[path].IsValid)\r\n            return;\r\n\r\n        pathInitialStateCache[path].Validate();\r\n\r\n        // Calculate path\r\n        var frameList = frames[path];\r\n        if (frameList.Count <= 1)\r\n            return;\r\n\r\n        path.ClearVertices();\r\n\r\n        bool left = direction.Value == ScrollingDirection.Left;\r\n        path.Anchor = path.Origin = left ? Anchor.TopLeft : Anchor.TopRight;\r\n\r\n        foreach (var frame in frameList)\r\n        {\r\n            float x = scrollingInfo.Algorithm.Value.GetLength(startTime, GetTime(frame), timeRange.Value, scrollLength);\r\n            path.AddVertex(new Vector2(left ? x : -x, GetPosition(frame)));\r\n        }\r\n    });\r\n\r\n    protected override void UpdateAfterChildrenLife()\r\n    {\r\n        base.UpdateAfterChildrenLife();\r\n\r\n        // We need to calculate hit object positions as soon as possible after lifetimes so that hitobjects get the final say in their positions\r\n        foreach (var path in AlivePaths)\r\n            updatePosition(path, Time.Current);\r\n    }\r\n\r\n    protected override void OnChildLifetimeBoundaryCrossed(LifetimeBoundaryCrossedEvent e)\r\n    {\r\n        // Recalculate path if appear\r\n        if (e.Kind == LifetimeBoundaryKind.Start && e.Child is ScoringPath path)\r\n            computePath(path);\r\n\r\n        base.OnChildLifetimeBoundaryCrossed(e);\r\n    }\r\n\r\n    protected virtual float Offset => 0;\r\n\r\n    private void updatePosition(ScoringPath path, double currentTime)\r\n    {\r\n        var firstFrameInPath = frames[path].FirstOrDefault();\r\n        if (firstFrameInPath == null)\r\n            return;\r\n\r\n        double startTime = GetTime(firstFrameInPath);\r\n        int multiple = direction.Value == ScrollingDirection.Left ? 1 : -1;\r\n        float x = scrollingInfo.Algorithm.Value.PositionAt(startTime, currentTime, timeRange.Value, scrollLength);\r\n        path.X = (x + Offset) * multiple;\r\n    }\r\n\r\n    protected partial class ScoringPath : Path\r\n    {\r\n        public override bool RemoveWhenNotAlive => false;\r\n\r\n        /// <summary>\r\n        /// Schedules an <see cref=\"Action\"/> to this <see cref=\"ScoringPath\"/>.\r\n        /// todo : might move this?\r\n        /// </summary>\r\n        protected internal new ScheduledDelegate Schedule(Action action) => base.Schedule(action);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/UI/DefaultHitExplosion.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Extensions.Color4Extensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Effects;\r\nusing osu.Framework.Utils;\r\nusing osu.Game.Rulesets.UI.Scrolling;\r\nusing osuTK;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.UI;\r\n\r\npublic partial class DefaultHitExplosion : CompositeDrawable\r\n{\r\n    // need to check about what is this.\r\n    public const float EXPLOSION_SIZE = 15;\r\n\r\n    public override bool RemoveWhenNotAlive => true;\r\n\r\n    private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();\r\n\r\n    private readonly CircularContainer largeFaint;\r\n    private readonly CircularContainer mainGlow1;\r\n\r\n    public DefaultHitExplosion(Color4 objectColour, bool isSmall = false)\r\n    {\r\n        Origin = Anchor.Centre;\r\n        Width = Height = EXPLOSION_SIZE;\r\n\r\n        // scale roughly in-line with visual appearance of notes\r\n        Scale = new Vector2(0.6f, 1f);\r\n\r\n        if (isSmall)\r\n            Scale *= 0.5f;\r\n\r\n        const float angle_variangle = 15; // should be less than 45\r\n\r\n        const float roundness = 80;\r\n\r\n        const float initial_height = 10;\r\n\r\n        var colour = Interpolation.ValueAt(0.4f, objectColour, Color4.White, 0, 1);\r\n\r\n        InternalChildren = new Drawable[]\r\n        {\r\n            largeFaint = new CircularContainer\r\n            {\r\n                Anchor = Anchor.Centre,\r\n                Origin = Anchor.Centre,\r\n                RelativeSizeAxes = Axes.Both,\r\n                Masking = true,\r\n                // we want our size to be very small so the glow dominates it.\r\n                Size = new Vector2(0.8f),\r\n                Blending = BlendingParameters.Additive,\r\n                EdgeEffect = new EdgeEffectParameters\r\n                {\r\n                    Type = EdgeEffectType.Glow,\r\n                    Colour = Interpolation.ValueAt(0.1f, objectColour, Color4.White, 0, 1).Opacity(0.3f),\r\n                    Roundness = 160,\r\n                    Radius = 200,\r\n                },\r\n            },\r\n            mainGlow1 = new CircularContainer\r\n            {\r\n                Anchor = Anchor.Centre,\r\n                Origin = Anchor.Centre,\r\n                RelativeSizeAxes = Axes.Both,\r\n                Masking = true,\r\n                Blending = BlendingParameters.Additive,\r\n                EdgeEffect = new EdgeEffectParameters\r\n                {\r\n                    Type = EdgeEffectType.Glow,\r\n                    Colour = Interpolation.ValueAt(0.6f, objectColour, Color4.White, 0, 1),\r\n                    Roundness = 20,\r\n                    Radius = 50,\r\n                },\r\n            },\r\n            new CircularContainer\r\n            {\r\n                Anchor = Anchor.Centre,\r\n                Origin = Anchor.Centre,\r\n                RelativeSizeAxes = Axes.Both,\r\n                Masking = true,\r\n                Size = new Vector2(0.1f, initial_height),\r\n                Blending = BlendingParameters.Additive,\r\n                Rotation = RNG.NextSingle(-angle_variangle, angle_variangle),\r\n                EdgeEffect = new EdgeEffectParameters\r\n                {\r\n                    Type = EdgeEffectType.Glow,\r\n                    Colour = colour,\r\n                    Roundness = roundness,\r\n                    Radius = 40,\r\n                },\r\n            },\r\n            new CircularContainer\r\n            {\r\n                Anchor = Anchor.Centre,\r\n                Origin = Anchor.Centre,\r\n                RelativeSizeAxes = Axes.Both,\r\n                Masking = true,\r\n                Size = new Vector2(initial_height),\r\n                Blending = BlendingParameters.Additive,\r\n                Rotation = RNG.NextSingle(-angle_variangle, angle_variangle),\r\n                EdgeEffect = new EdgeEffectParameters\r\n                {\r\n                    Type = EdgeEffectType.Glow,\r\n                    Colour = colour,\r\n                    Roundness = roundness,\r\n                    Radius = 40,\r\n                },\r\n            },\r\n        };\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(IScrollingInfo scrollingInfo)\r\n    {\r\n        direction.BindTo(scrollingInfo.Direction);\r\n        direction.BindValueChanged(onDirectionChanged, true);\r\n    }\r\n\r\n    protected override void LoadComplete()\r\n    {\r\n        const double duration = 200;\r\n\r\n        base.LoadComplete();\r\n\r\n        largeFaint\r\n            .ResizeTo(largeFaint.Size * new Vector2(5, 1), duration, Easing.OutQuint)\r\n            .FadeOut(duration * 2);\r\n\r\n        mainGlow1.ScaleTo(1.4f, duration, Easing.OutQuint);\r\n\r\n        this.FadeOut(duration, Easing.Out);\r\n    }\r\n\r\n    private void onDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)\r\n    {\r\n        Anchor = direction.NewValue == ScrollingDirection.Left ? Anchor.CentreLeft : Anchor.CentreRight;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/UI/DrawableKaraokeRuleset.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Input;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Input.Handlers;\r\nusing osu.Game.Replays;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Replays;\r\nusing osu.Game.Rulesets.Karaoke.Skinning.Fonts;\r\nusing osu.Game.Rulesets.Karaoke.UI.Position;\r\nusing osu.Game.Rulesets.Mods;\r\nusing osu.Game.Rulesets.Objects;\r\nusing osu.Game.Rulesets.Objects.Drawables;\r\nusing osu.Game.Rulesets.UI;\r\nusing osu.Game.Rulesets.UI.Scrolling;\r\nusing osu.Game.Scoring;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.UI;\r\n\r\npublic partial class DrawableKaraokeRuleset : DrawableScrollingRuleset<KaraokeHitObject>\r\n{\r\n    public KaraokeSessionStatics Session { get; private set; } = null!;\r\n    public new KaraokePlayfield Playfield => (KaraokePlayfield)base.Playfield;\r\n\r\n    public new KaraokeRulesetConfigManager Config => (KaraokeRulesetConfigManager)base.Config;\r\n\r\n    public new KaraokeInputManager KeyBindingInputManager => (KaraokeInputManager)base.KeyBindingInputManager;\r\n\r\n    private readonly Bindable<KaraokeScrollingDirection> configDirection = new();\r\n\r\n    [Cached(typeof(INotePositionInfo))]\r\n    private readonly NotePositionInfo positionCalculator;\r\n\r\n    [Cached]\r\n    private readonly FontManager fontManager;\r\n\r\n    [Cached(typeof(IKaraokeBeatmapResourcesProvider))]\r\n    private readonly KaraokeBeatmapResourcesProvider karaokeBeatmapResourcesProvider;\r\n\r\n    public new KaraokeBeatmap Beatmap => (KaraokeBeatmap)base.Beatmap;\r\n\r\n    protected virtual bool DisplayNotePlayfield => Beatmap.IsScorable();\r\n\r\n    public DrawableKaraokeRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod>? mods)\r\n        : base(ruleset, beatmap, mods)\r\n    {\r\n        AddInternal(positionCalculator = new NotePositionInfo());\r\n        AddInternal(fontManager = new FontManager());\r\n        AddInternal(karaokeBeatmapResourcesProvider = new KaraokeBeatmapResourcesProvider());\r\n    }\r\n\r\n    protected override Playfield CreatePlayfield() => new KaraokePlayfield();\r\n\r\n    protected override PassThroughInputManager CreateInputManager() =>\r\n        new KaraokeInputManager(Ruleset.RulesetInfo);\r\n\r\n    protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)\r\n    {\r\n        var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));\r\n        dependencies.Cache(Session = new KaraokeSessionStatics(Config, Beatmap));\r\n        return dependencies;\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        // TODO : it should be moved into NotePlayfield\r\n        new BarLineGenerator<BarLine>(Beatmap).BarLines.ForEach(bar => base.Playfield.Add(bar));\r\n\r\n        Config.BindWith(KaraokeRulesetSetting.ScrollDirection, configDirection);\r\n        configDirection.BindValueChanged(direction => Direction.Value = (ScrollingDirection)direction.NewValue, true);\r\n\r\n        Config.BindWith(KaraokeRulesetSetting.ScrollTime, TimeRange);\r\n\r\n        // Hide note playfield.\r\n        if (!DisplayNotePlayfield)\r\n            Playfield.NotePlayfield.Hide();\r\n    }\r\n\r\n    public override DrawableHitObject<KaraokeHitObject>? CreateDrawableRepresentation(KaraokeHitObject h) => null;\r\n\r\n    protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new KaraokeFramedReplayInputHandler(replay);\r\n\r\n    protected override ReplayRecorder CreateReplayRecorder(Score score) => new KaraokeReplayRecorder(score);\r\n\r\n    // todo : for now get the fonts in here, might move to better place.\r\n    public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new KaraokePlayfieldAdjustmentContainer();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/UI/DrawableNoteJudgement.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Rulesets.Judgements;\r\nusing osu.Game.Rulesets.Scoring;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.UI;\r\n\r\npublic partial class DrawableNoteJudgement : DrawableJudgement\r\n{\r\n    protected override Drawable CreateDefaultJudgement(HitResult result) => new DefaultKaraokeJudgementPiece(result);\r\n\r\n    private partial class DefaultKaraokeJudgementPiece : DefaultJudgementPiece\r\n    {\r\n        public DefaultKaraokeJudgementPiece(HitResult result)\r\n            : base(result)\r\n        {\r\n        }\r\n\r\n        protected override void LoadComplete()\r\n        {\r\n            base.LoadComplete();\r\n\r\n            JudgementText.Font = JudgementText.Font.With(size: 25);\r\n        }\r\n\r\n        public override void PlayAnimation()\r\n        {\r\n            switch (Result)\r\n            {\r\n                case HitResult.None:\r\n                case HitResult.Miss:\r\n                    base.PlayAnimation();\r\n                    break;\r\n\r\n                default:\r\n                    this.ScaleTo(0.8f);\r\n                    this.ScaleTo(1, 250, Easing.OutElastic);\r\n\r\n                    this.Delay(50)\r\n                        .ScaleTo(0.75f, 250)\r\n                        .FadeOut(200);\r\n\r\n                    // karaoke uses a custom fade length, so the base call is intentionally omitted.\r\n                    break;\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/UI/HUD/BindableNumberExtension.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Bindables;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.UI.HUD;\r\n\r\n/// <summary>\r\n/// Will move into framework layer\r\n/// </summary>\r\npublic static class BindableNumberExtension\r\n{\r\n    public static void TriggerIncrease(this BindableInt bindableInt)\r\n    {\r\n        bindableInt.Value += bindableInt.Precision;\r\n    }\r\n\r\n    public static void TriggerDecrease(this BindableInt bindableInt)\r\n    {\r\n        bindableInt.Value -= bindableInt.Precision;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/UI/HUD/GeneralSettingOverlay.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Input.Bindings;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Game.Input.Bindings;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.UI.PlayerSettings;\r\nusing osu.Game.Screens.Play.PlayerSettings;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.UI.HUD;\r\n\r\npublic partial class GeneralSettingOverlay : SettingOverlay, IKeyBindingHandler<KaraokeAction>\r\n{\r\n    private readonly BindableInt bindablePitch = new();\r\n    private readonly BindableInt bindableVocalPitch = new();\r\n    private readonly BindableInt bindableScoringPitch = new();\r\n\r\n    protected override OverlayColourScheme OverlayColourScheme => OverlayColourScheme.Blue;\r\n\r\n    public GeneralSettingOverlay()\r\n    {\r\n        Children = new Drawable[]\r\n        {\r\n            new VisualSettings\r\n            {\r\n                Expanded =\r\n                {\r\n                    Value = false,\r\n                },\r\n            },\r\n            new PitchSettings\r\n            {\r\n                Expanded =\r\n                {\r\n                    Value = false,\r\n                },\r\n            },\r\n        };\r\n    }\r\n\r\n    protected override SettingButton CreateButton() => new()\r\n    {\r\n        Name = \"Toggle setting button\",\r\n        Text = \"Settings\",\r\n        TooltipText = \"Open/Close setting\",\r\n        Action = ToggleVisibility,\r\n    };\r\n\r\n    // should be able to get the key event.\r\n    protected override bool BlockNonPositionalInput => false;\r\n\r\n    // should get key event even it's hide.\r\n    public override bool PropagateNonPositionalInputSubTree => true;\r\n\r\n    // on press should return false to prevent handle the back key action.\r\n    public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)\r\n        => false;\r\n\r\n    public virtual bool OnPressed(KeyBindingPressEvent<KaraokeAction> e)\r\n    {\r\n        switch (e.Action)\r\n        {\r\n            // Pitch\r\n            case KaraokeAction.IncreasePitch:\r\n                bindablePitch.TriggerIncrease();\r\n                break;\r\n\r\n            case KaraokeAction.DecreasePitch:\r\n                bindablePitch.TriggerDecrease();\r\n                break;\r\n\r\n            case KaraokeAction.ResetPitch:\r\n                bindablePitch.SetDefault();\r\n                break;\r\n\r\n            // Vocal pitch\r\n            case KaraokeAction.IncreaseVocalPitch:\r\n                bindableVocalPitch.TriggerIncrease();\r\n                break;\r\n\r\n            case KaraokeAction.DecreaseVocalPitch:\r\n                bindableVocalPitch.TriggerDecrease();\r\n                break;\r\n\r\n            case KaraokeAction.ResetVocalPitch:\r\n                bindableVocalPitch.SetDefault();\r\n                break;\r\n\r\n            // Scoring pitch\r\n            case KaraokeAction.IncreaseScoringPitch:\r\n                bindableScoringPitch.TriggerIncrease();\r\n                break;\r\n\r\n            case KaraokeAction.DecreaseScoringPitch:\r\n                bindableScoringPitch.TriggerDecrease();\r\n                break;\r\n\r\n            case KaraokeAction.ResetScoringPitch:\r\n                bindableScoringPitch.SetDefault();\r\n                break;\r\n\r\n            default:\r\n                return false;\r\n        }\r\n\r\n        return true;\r\n    }\r\n\r\n    public virtual void OnReleased(KeyBindingReleaseEvent<KaraokeAction> e)\r\n    {\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(KaraokeSessionStatics session)\r\n    {\r\n        session.BindWith(KaraokeRulesetSession.Pitch, bindablePitch);\r\n        session.BindWith(KaraokeRulesetSession.VocalPitch, bindableVocalPitch);\r\n        session.BindWith(KaraokeRulesetSession.ScoringPitch, bindableScoringPitch);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/UI/HUD/ISettingHUDOverlay.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Screens.Play.PlayerSettings;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.UI.HUD;\r\n\r\npublic interface ISettingHUDOverlay\r\n{\r\n    void AddSettingsGroup(PlayerSettingsGroup group);\r\n\r\n    void AddExtraOverlay(SettingOverlay overlay);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/UI/HUD/PracticeOverlay.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.UI.PlayerSettings;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.UI.HUD;\r\n\r\npublic partial class PracticeOverlay : SettingOverlay\r\n{\r\n    protected override OverlayColourScheme OverlayColourScheme => OverlayColourScheme.Purple;\r\n\r\n    public PracticeOverlay()\r\n    {\r\n        Children = new[]\r\n        {\r\n            new PracticeSettings\r\n            {\r\n                Expanded =\r\n                {\r\n                    Value = true,\r\n                },\r\n                Width = 400,\r\n            },\r\n        };\r\n    }\r\n\r\n    protected override SettingButton CreateButton() => new()\r\n    {\r\n        Name = \"Toggle Practice\",\r\n        Text = \"Practice\",\r\n        TooltipText = \"Open/Close practice overlay\",\r\n        Action = ToggleVisibility,\r\n    };\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/UI/HUD/SettingButton.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics.Cursor;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Graphics.UserInterface;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.UI.HUD;\r\n\r\npublic partial class SettingButton : OsuButton, IHasTooltip\r\n{\r\n    public LocalisableString TooltipText { get; set; }\r\n\r\n    public SettingButton()\r\n    {\r\n        Width = 90;\r\n        Height = 45;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/UI/HUD/SettingButtonsDisplay.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Layout;\r\nusing osu.Game.Configuration;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.Shapes;\r\nusing osu.Game.Screens.Play;\r\nusing osu.Game.Skinning;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.UI.HUD;\r\n\r\npublic partial class SettingButtonsDisplay : CompositeDrawable, ISerialisableDrawable\r\n{\r\n    private readonly CornerBackground background;\r\n    private readonly FillFlowContainer<SettingButton> triggerButtons;\r\n\r\n    public bool UsesFixedAnchor { get; set; }\r\n\r\n    [SettingSource(\"Alpha\", \"The alpha value of this box\")]\r\n    public BindableNumber<float> BoxAlpha { get; } = new(1)\r\n    {\r\n        MinValue = 0,\r\n        MaxValue = 1,\r\n        Precision = 0.01f,\r\n    };\r\n\r\n    public SettingButtonsDisplay()\r\n    {\r\n        AutoSizeAxes = Axes.Both;\r\n        InternalChildren = new Drawable[]\r\n        {\r\n            background = new CornerBackground\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n            },\r\n            triggerButtons = new FillFlowContainer<SettingButton>\r\n            {\r\n                Anchor = Anchor.CentreRight,\r\n                Origin = Anchor.CentreRight,\r\n                AutoSizeAxes = Axes.Both,\r\n                Spacing = new Vector2(10),\r\n                Margin = new MarginPadding(10),\r\n                Direction = FillDirection.Vertical,\r\n                AlwaysPresent = true,\r\n            },\r\n        };\r\n    }\r\n\r\n    protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source)\r\n    {\r\n        // trying to change relative position in here.\r\n        if ((invalidation & Invalidation.MiscGeometry) != 0)\r\n        {\r\n            var overlayDirection = Anchor.HasFlag(Anchor.x0) ? OverlayDirection.Left : OverlayDirection.Right;\r\n            settingOverlayContainer?.ChangeOverlayDirection(overlayDirection);\r\n        }\r\n\r\n        return base.OnInvalidate(invalidation, source);\r\n    }\r\n\r\n    private SettingOverlayContainer? settingOverlayContainer;\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OsuColour colours, HUDOverlay hud, Player player)\r\n    {\r\n        background.Colour = colours.ContextMenuGray;\r\n\r\n        var rulesetInfo = player.Ruleset.Value;\r\n        Schedule(() =>\r\n        {\r\n            hud.Add(new KaraokeControlInputManager(rulesetInfo)\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n                Child = settingOverlayContainer = new SettingOverlayContainer\r\n                {\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    OnNewOverlayAdded = overlay =>\r\n                    {\r\n                        var button = overlay.CreateToggleButton();\r\n                        triggerButtons.Add(button);\r\n                    },\r\n                },\r\n            });\r\n        });\r\n\r\n        BoxAlpha.BindValueChanged(alpha => triggerButtons.Alpha = alpha.NewValue, true);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/UI/HUD/SettingOverlay.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Game.Graphics.Containers;\r\nusing osu.Game.Overlays;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.UI.HUD;\r\n\r\n/// <summary>\r\n/// Present setting at right side\r\n/// </summary>\r\npublic abstract partial class SettingOverlay : OsuFocusedOverlayContainer\r\n{\r\n    public const float SETTING_MARGIN = 20;\r\n    public const float SETTING_SPACING = 20;\r\n    public const float TRANSITION_LENGTH = 600;\r\n\r\n    protected override bool DimMainContent => false;\r\n\r\n    protected override Container<Drawable> Content => content;\r\n\r\n    private readonly FillFlowContainer<Drawable> content;\r\n\r\n    protected abstract OverlayColourScheme OverlayColourScheme { get; }\r\n\r\n    [Cached]\r\n    private readonly OverlayColourProvider colourProvider;\r\n\r\n    protected SettingOverlay()\r\n    {\r\n        RelativeSizeAxes = Axes.Y;\r\n\r\n        colourProvider = new OverlayColourProvider(OverlayColourScheme);\r\n\r\n        InternalChildren = new Drawable[]\r\n        {\r\n            new Box\r\n            {\r\n                Name = \"Background\",\r\n                RelativeSizeAxes = Axes.Both,\r\n                Colour = colourProvider.Background4,\r\n            },\r\n            new Container\r\n            {\r\n                RelativeSizeAxes = Axes.Y,\r\n                AutoSizeAxes = Axes.X,\r\n                Child = content = new FillFlowContainer<Drawable>\r\n                {\r\n                    AutoSizeAxes = Axes.Both,\r\n                    Direction = FillDirection.Vertical,\r\n                    Spacing = new Vector2(SETTING_SPACING),\r\n                    Margin = new MarginPadding(SETTING_MARGIN),\r\n                },\r\n            },\r\n        };\r\n    }\r\n\r\n    private OverlayDirection direction;\r\n\r\n    public OverlayDirection Direction\r\n    {\r\n        get => direction;\r\n        set\r\n        {\r\n            if (direction == value)\r\n                return;\r\n\r\n            direction = value;\r\n\r\n            switch (direction)\r\n            {\r\n                case OverlayDirection.Left:\r\n                    Anchor = Anchor.CentreLeft;\r\n                    Origin = Anchor.CentreLeft;\r\n                    break;\r\n\r\n                case OverlayDirection.Right:\r\n                    Anchor = Anchor.CentreRight;\r\n                    Origin = Anchor.CentreRight;\r\n                    break;\r\n\r\n                default:\r\n                    throw new ArgumentOutOfRangeException(nameof(direction));\r\n            }\r\n\r\n            if (State.Value == Visibility.Hidden)\r\n            {\r\n                X = getHideXPosition();\r\n            }\r\n        }\r\n    }\r\n\r\n    protected override void LoadComplete()\r\n    {\r\n        base.LoadComplete();\r\n\r\n        // todo : fix the case that should not affect by other overlay.\r\n        OverlayActivationMode.UnbindAll();\r\n\r\n        // Use lazy way to force open overlay\r\n        // Will create ruleset own overlay eventually.\r\n        ((Bindable<OverlayActivation>)OverlayActivationMode).Value = OverlayActivation.All;\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        AutoSizeAxes = Axes.X;\r\n    }\r\n\r\n    protected override void PopIn()\r\n    {\r\n        this.MoveToX(0, TRANSITION_LENGTH, Easing.OutQuint);\r\n        this.FadeTo(1, TRANSITION_LENGTH, Easing.OutQuint);\r\n    }\r\n\r\n    protected override void PopOut()\r\n    {\r\n        base.PopOut();\r\n\r\n        float width = getHideXPosition();\r\n        this.MoveToX(width, TRANSITION_LENGTH, Easing.OutQuint);\r\n        this.FadeTo(0, TRANSITION_LENGTH, Easing.OutQuint);\r\n    }\r\n\r\n    private float getHideXPosition() =>\r\n        direction switch\r\n        {\r\n            OverlayDirection.Left => -DrawWidth,\r\n            OverlayDirection.Right => DrawWidth,\r\n            _ => throw new ArgumentOutOfRangeException(),\r\n        };\r\n\r\n    public SettingButton CreateToggleButton()\r\n        => CreateButton().With(x =>\r\n        {\r\n            x.BackgroundColour = colourProvider.Colour1;\r\n        });\r\n\r\n    protected abstract SettingButton CreateButton();\r\n}\r\n\r\npublic enum OverlayDirection\r\n{\r\n    Left,\r\n\r\n    Right,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/UI/HUD/SettingOverlayContainer.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Input.Bindings;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Game.Rulesets.Karaoke.Mods;\r\nusing osu.Game.Rulesets.Mods;\r\nusing osu.Game.Rulesets.UI;\r\nusing osu.Game.Screens.Play.PlayerSettings;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.UI.HUD;\r\n\r\npublic partial class SettingOverlayContainer : CompositeDrawable, IKeyBindingHandler<KaraokeAction>, ISettingHUDOverlay\r\n{\r\n    private GeneralSettingOverlay generalSettingsOverlay = null!;\r\n\r\n    public Action<SettingOverlay>? OnNewOverlayAdded;\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(IBindable<IReadOnlyList<Mod>>? mods)\r\n    {\r\n        AddExtraOverlay(generalSettingsOverlay = new GeneralSettingOverlay());\r\n\r\n        if (mods == null)\r\n            return;\r\n\r\n        foreach (var mod in mods.Value.OfType<IApplicableToSettingHUDOverlay>())\r\n            mod.ApplyToOverlay(this);\r\n    }\r\n\r\n    public void ToggleGeneralSettingsOverlay() => generalSettingsOverlay.ToggleVisibility();\r\n\r\n    public virtual bool OnPressed(KeyBindingPressEvent<KaraokeAction> e)\r\n    {\r\n        switch (e.Action)\r\n        {\r\n            // Open adjustment overlay\r\n            case KaraokeAction.OpenPanel:\r\n                ToggleGeneralSettingsOverlay();\r\n                return true;\r\n\r\n            default:\r\n                return false;\r\n        }\r\n    }\r\n\r\n    public virtual void OnReleased(KeyBindingReleaseEvent<KaraokeAction> e)\r\n    {\r\n    }\r\n\r\n    public void AddSettingsGroup(PlayerSettingsGroup group)\r\n    {\r\n        generalSettingsOverlay.Add(group);\r\n    }\r\n\r\n    public void AddExtraOverlay(SettingOverlay overlay)\r\n    {\r\n        AddInternal(overlay);\r\n        OnNewOverlayAdded?.Invoke(overlay);\r\n    }\r\n\r\n    public void ChangeOverlayDirection(OverlayDirection direction)\r\n    {\r\n        foreach (var settingOverlay in InternalChildren.OfType<SettingOverlay>())\r\n        {\r\n            settingOverlay.Direction = direction;\r\n        }\r\n    }\r\n\r\n    protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)\r\n    {\r\n        var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));\r\n\r\n        // use tricky way to get session from karaoke ruleset.\r\n        object drawableRuleset = dependencies.Get(typeof(DrawableRuleset));\r\n\r\n        if (drawableRuleset is not DrawableKaraokeRuleset drawableKaraokeRuleset)\r\n            return dependencies;\r\n\r\n        dependencies.CacheAs(drawableKaraokeRuleset.Config);\r\n        dependencies.CacheAs(drawableKaraokeRuleset.Session);\r\n\r\n        return dependencies;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/UI/KaraokePlayfield.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Drawables;\r\nusing osu.Game.Rulesets.Karaoke.UI.Scrolling;\r\nusing osu.Game.Rulesets.Objects;\r\nusing osu.Game.Rulesets.Objects.Drawables;\r\nusing osu.Game.Rulesets.UI.Scrolling;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.UI;\r\n\r\npublic partial class KaraokePlayfield : ScrollingPlayfield\r\n{\r\n    [Resolved]\r\n    private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;\r\n\r\n    public WorkingBeatmap WorkingBeatmap => beatmap.Value;\r\n\r\n    public LyricPlayfield LyricPlayfield { get; }\r\n\r\n    public ScrollingNotePlayfield NotePlayfield { get; }\r\n\r\n    public BindableBool DisplayCursor { get; set; } = new();\r\n    public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => !DisplayCursor.Value && base.ReceivePositionalInputAt(screenSpacePos);\r\n\r\n    private readonly BindableInt bindablePitch = new();\r\n    private readonly BindableInt bindableVocalPitch = new();\r\n    private readonly BindableInt bindablePlayback = new();\r\n    private readonly BindableDouble notePlayfieldAlpha = new();\r\n    private readonly BindableDouble lyricPlayfieldAlpha = new();\r\n\r\n    public KaraokePlayfield()\r\n    {\r\n        AddInternal(LyricPlayfield = CreateLyricPlayfield().With(x =>\r\n        {\r\n            x.RelativeSizeAxes = Axes.Both;\r\n        }));\r\n\r\n        AddInternal(NotePlayfield = CreateNotePlayfield(9).With(x =>\r\n        {\r\n            x.RelativeSizeAxes = Axes.X;\r\n        }));\r\n\r\n        AddNested(LyricPlayfield);\r\n        AddNested(NotePlayfield);\r\n\r\n        bindablePitch.BindValueChanged(value =>\r\n        {\r\n            // Convert between -10 and 10 into 0.5 and 1.5\r\n            float newValue = 1.0f + (float)value.NewValue / 40;\r\n            WorkingBeatmap.Track.Frequency.Value = newValue;\r\n        });\r\n\r\n        bindableVocalPitch.BindValueChanged(value =>\r\n        {\r\n            // TODO : implement until has vocal track\r\n        });\r\n\r\n        bindablePlayback.BindValueChanged(value =>\r\n        {\r\n            // Convert between -10 and 10 into 0.5 and 1.5\r\n            float newValue = 1.0f + (float)value.NewValue / 40;\r\n            WorkingBeatmap.Track.Tempo.Value = newValue;\r\n        });\r\n\r\n        // Alpha\r\n        notePlayfieldAlpha.BindValueChanged(x =>\r\n        {\r\n            // todo : how to check is there any notes in this playfield?\r\n            double alpha = WorkingBeatmap.Beatmap.IsScorable() ? x.NewValue : 0;\r\n            NotePlayfield.Alpha = (float)alpha;\r\n        });\r\n        lyricPlayfieldAlpha.BindValueChanged(x => LyricPlayfield.Alpha = (float)x.NewValue);\r\n    }\r\n\r\n    protected virtual LyricPlayfield CreateLyricPlayfield() => new();\r\n\r\n    protected virtual ScrollingNotePlayfield CreateNotePlayfield(int columns) => new NotePlayfield(columns);\r\n\r\n    #region Pooling support\r\n\r\n    public override void Add(HitObject hitObject)\r\n    {\r\n        switch (hitObject)\r\n        {\r\n            case Lyric:\r\n                LyricPlayfield.Add(hitObject);\r\n                break;\r\n\r\n            case Note:\r\n            case BarLine:\r\n                NotePlayfield.Add(hitObject);\r\n\r\n                break;\r\n\r\n            default:\r\n                throw new ArgumentException($\"Unsupported {nameof(HitObject)} type: {hitObject.GetType()}\");\r\n        }\r\n    }\r\n\r\n    public override bool Remove(HitObject hitObject)\r\n    {\r\n        switch (hitObject)\r\n        {\r\n            case Lyric:\r\n                return LyricPlayfield.Remove(hitObject);\r\n\r\n            case Note:\r\n            case BarLine:\r\n                return NotePlayfield.Remove(hitObject);\r\n\r\n            default:\r\n                throw new ArgumentException($\"Unsupported {nameof(HitObject)} type: {hitObject.GetType()}\");\r\n        }\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Non-pooling support\r\n\r\n    public override void Add(DrawableHitObject h)\r\n    {\r\n        switch (h)\r\n        {\r\n            case DrawableLyric:\r\n                LyricPlayfield.Add(h);\r\n                break;\r\n\r\n            case DrawableNote:\r\n                NotePlayfield.Add(h);\r\n\r\n                break;\r\n\r\n            default:\r\n                base.Add(h);\r\n                break;\r\n        }\r\n    }\r\n\r\n    public override bool Remove(DrawableHitObject h) =>\r\n        h switch\r\n        {\r\n            DrawableLyric => LyricPlayfield.Remove(h),\r\n            DrawableNote => NotePlayfield.Remove(h),\r\n            _ => base.Remove(h),\r\n        };\r\n\r\n    #endregion\r\n\r\n    public override void PostProcess()\r\n    {\r\n        base.PostProcess();\r\n\r\n        // trigger again to update note playfield alpha.\r\n        notePlayfieldAlpha.TriggerChange();\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(KaraokeRulesetConfigManager rulesetConfig, KaraokeSessionStatics session)\r\n    {\r\n        // Cursor\r\n        rulesetConfig.BindWith(KaraokeRulesetSetting.ShowCursor, DisplayCursor);\r\n\r\n        // Alpha\r\n        rulesetConfig.BindWith(KaraokeRulesetSetting.NoteAlpha, notePlayfieldAlpha);\r\n        rulesetConfig.BindWith(KaraokeRulesetSetting.LyricAlpha, lyricPlayfieldAlpha);\r\n\r\n        // Pitch\r\n        session.BindWith(KaraokeRulesetSession.Pitch, bindablePitch);\r\n        session.BindWith(KaraokeRulesetSession.VocalPitch, bindableVocalPitch);\r\n        session.BindWith(KaraokeRulesetSession.PlaybackSpeed, bindablePlayback);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/UI/KaraokePlayfieldAdjustmentContainer.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Rendering;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.IO.Stores;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.IO.Stores;\r\nusing osu.Game.Rulesets.Karaoke.Skinning.Fonts;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Drawables;\r\nusing osu.Game.Rulesets.UI;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.UI;\r\n\r\n/// <summary>\r\n/// Having a place to get all user customize font.\r\n/// todo : need to check will have better place or not.\r\n/// </summary>\r\npublic partial class KaraokePlayfieldAdjustmentContainer : PlayfieldAdjustmentContainer\r\n{\r\n    [Resolved]\r\n    private FontStore fontStore { get; set; } = null!;\r\n\r\n    private KaraokeLocalFontStore localFontStore = null!;\r\n\r\n    protected override Container<Drawable> Content => content;\r\n    private readonly DrawableStage content;\r\n\r\n    public KaraokePlayfieldAdjustmentContainer()\r\n    {\r\n        InternalChild = content = new DrawableStage\r\n        {\r\n            RelativeSizeAxes = Axes.Both,\r\n        };\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(FontManager fontManager, IRenderer renderer, KaraokeRulesetConfigManager manager)\r\n    {\r\n        // get all font usage which wants to import.\r\n        var targetImportFonts = new[]\r\n        {\r\n            manager.Get<FontUsage>(KaraokeRulesetSetting.MainFont),\r\n            manager.Get<FontUsage>(KaraokeRulesetSetting.RubyFont),\r\n            manager.Get<FontUsage>(KaraokeRulesetSetting.RomanisationFont),\r\n            manager.Get<FontUsage>(KaraokeRulesetSetting.TranslationFont),\r\n            manager.Get<FontUsage>(KaraokeRulesetSetting.NoteFont),\r\n        };\r\n\r\n        var fontInfos = targetImportFonts\r\n                        .Distinct()\r\n                        .ToArray();\r\n\r\n        if (!fontInfos.Any())\r\n            return;\r\n\r\n        // create local font store and import those files\r\n        localFontStore = new KaraokeLocalFontStore(fontManager, renderer);\r\n        fontStore.AddStore(localFontStore);\r\n\r\n        foreach (var fontInfo in fontInfos)\r\n        {\r\n            localFontStore.AddFont(fontInfo);\r\n        }\r\n    }\r\n\r\n    protected override void Dispose(bool isDisposing)\r\n    {\r\n        base.Dispose(isDisposing);\r\n\r\n        fontStore.RemoveStore(localFontStore);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/UI/KaraokeReplayRecorder.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Game.Rulesets.Karaoke.Replays;\r\nusing osu.Game.Rulesets.Replays;\r\nusing osu.Game.Rulesets.UI;\r\nusing osu.Game.Scoring;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.UI;\r\n\r\npublic partial class KaraokeReplayRecorder : ReplayRecorder<KaraokeScoringAction>\r\n{\r\n    public KaraokeReplayRecorder(Score score)\r\n        : base(score)\r\n    {\r\n    }\r\n\r\n    protected override ReplayFrame HandleFrame(Vector2 mousePosition, List<KaraokeScoringAction> actions, ReplayFrame previousFrame)\r\n    {\r\n        if (actions.Any())\r\n            return new KaraokeReplayFrame(Time.Current, actions.FirstOrDefault().Scale);\r\n\r\n        return new KaraokeReplayFrame(Time.Current);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/UI/KaraokeScrollingDirection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.UI.Scrolling;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.UI;\r\n\r\npublic enum KaraokeScrollingDirection\r\n{\r\n    Left = ScrollingDirection.Left,\r\n    Right = ScrollingDirection.Right,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/UI/KaraokeSettingsSubsection.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Localisation;\r\nusing osu.Framework.Screens;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Overlays.Settings;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Extensions;\r\nusing osu.Game.Rulesets.Karaoke.Localisation;\r\nusing osu.Game.Rulesets.Karaoke.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Settings;\r\nusing osu.Game.Screens;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.UI;\r\n\r\npublic partial class KaraokeSettingsSubsection : RulesetSettingsSubsection\r\n{\r\n    protected override LocalisableString Header => CommonStrings.RulesetName;\r\n\r\n    public KaraokeSettingsSubsection(Ruleset ruleset)\r\n        : base(ruleset)\r\n    {\r\n    }\r\n\r\n    private KaraokeChangelogOverlay? changelogOverlay;\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OsuGame game, IPerformFromScreenRunner performer)\r\n    {\r\n        var config = (KaraokeRulesetConfigManager)Config;\r\n\r\n        Children = new Drawable[]\r\n        {\r\n            // Scrolling\r\n            new SettingsEnumDropdown<KaraokeScrollingDirection>\r\n            {\r\n                ClassicDefault = KaraokeScrollingDirection.Left,\r\n                LabelText = KaraokeSettingsSubsectionStrings.ScrollingDirection,\r\n                TooltipText = KaraokeSettingsSubsectionStrings.ScrollingDirectionTooltip,\r\n                Current = config.GetBindable<KaraokeScrollingDirection>(KaraokeRulesetSetting.ScrollDirection),\r\n            },\r\n            new SettingsSlider<double, TimeSlider>\r\n            {\r\n                LabelText = KaraokeSettingsSubsectionStrings.ScrollSpeed,\r\n                Current = config.GetBindable<double>(KaraokeRulesetSetting.ScrollTime),\r\n            },\r\n            // Gameplay\r\n            new SettingsCheckbox\r\n            {\r\n                LabelText = KaraokeSettingsSubsectionStrings.ShowCursorWhilePlaying,\r\n                TooltipText = KaraokeSettingsSubsectionStrings.ShowCursorWhilePlayingTooltip,\r\n                Current = config.GetBindable<bool>(KaraokeRulesetSetting.ShowCursor),\r\n            },\r\n            // Device\r\n            new SettingsMicrophoneDeviceDropdown\r\n            {\r\n                ClassicDefault = string.Empty,\r\n                LabelText = KaraokeSettingsSubsectionStrings.MicrophoneDevice,\r\n                Current = config.GetBindable<string>(KaraokeRulesetSetting.MicrophoneDevice),\r\n            },\r\n            // Practice\r\n            new DangerousSettingsButton\r\n            {\r\n                Text = KaraokeSettingsSubsectionStrings.OpenRulesetSettings,\r\n                TooltipText = KaraokeSettingsSubsectionStrings.OpenRulesetSettingsTooltip,\r\n                Action = () => performer.PerformFromScreen(menu => menu.Push(new KaraokeSettings())),\r\n            },\r\n            new SettingsButton\r\n            {\r\n                Text = KaraokeSettingsSubsectionStrings.ChangeLog,\r\n                TooltipText = KaraokeSettingsSubsectionStrings.ChangeLogTooltip,\r\n                Action = () =>\r\n                {\r\n                    try\r\n                    {\r\n                        var displayContainer = game.GetChangelogPlacementContainer();\r\n                        var settingOverlay = game.GetSettingsOverlay();\r\n                        if (displayContainer == null)\r\n                            return;\r\n\r\n                        if (changelogOverlay == null && !displayContainer.Children.OfType<KaraokeChangelogOverlay>().Any())\r\n                            displayContainer.Add(changelogOverlay = new KaraokeChangelogOverlay(\"karaoke-dev\"));\r\n\r\n                        changelogOverlay?.Show();\r\n                        settingOverlay?.Hide();\r\n                    }\r\n                    catch\r\n                    {\r\n                        // maybe this overlay has been moved into internal.\r\n                    }\r\n                },\r\n            },\r\n        };\r\n    }\r\n\r\n    private partial class TimeSlider : RoundedSliderBar<double>\r\n    {\r\n        public override LocalisableString TooltipText => Current.Value.ToString(\"N0\") + \"ms\";\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/UI/LyricPlayfield.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Drawables;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Drawables;\r\nusing osu.Game.Rulesets.Objects;\r\nusing osu.Game.Rulesets.Objects.Drawables;\r\nusing osu.Game.Rulesets.UI;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.UI;\r\n\r\npublic partial class LyricPlayfield : Playfield\r\n{\r\n    [Resolved]\r\n    private IStageHitObjectRunner? stageRunner { get; set; }\r\n\r\n    private readonly Bindable<Lyric[]> singingLyrics = new();\r\n\r\n    protected override void OnNewDrawableHitObject(DrawableHitObject drawableHitObject)\r\n    {\r\n        if (drawableHitObject is DrawableLyric drawableLyric)\r\n        {\r\n            drawableLyric.OnLyricStart += onLyricStart;\r\n            drawableLyric.OnLyricEnd += onLyricEnd;\r\n        }\r\n\r\n        base.OnNewDrawableHitObject(drawableHitObject);\r\n    }\r\n\r\n    private void onLyricStart(DrawableLyric drawableLyric)\r\n    {\r\n        var lyrics = singingLyrics.Value ?? Array.Empty<Lyric>();\r\n        var lyric = drawableLyric.HitObject;\r\n\r\n        if (lyrics.Contains(lyric))\r\n            return;\r\n\r\n        singingLyrics.Value = lyrics.Concat(new[] { lyric }).ToArray();\r\n    }\r\n\r\n    private void onLyricEnd(DrawableLyric drawableLyric)\r\n    {\r\n        var lyrics = singingLyrics.Value ?? Array.Empty<Lyric>();\r\n        var lyric = drawableLyric.HitObject;\r\n\r\n        if (!lyrics.Contains(lyric))\r\n            return;\r\n\r\n        singingLyrics.Value = lyrics.Where(x => x != lyric).ToArray();\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(KaraokeSessionStatics session)\r\n    {\r\n        // Practice\r\n        session.BindWith(KaraokeRulesetSession.SingingLyrics, singingLyrics);\r\n\r\n        RegisterPool<Lyric, DrawableLyric>(50);\r\n    }\r\n\r\n    protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new LyricHitObjectLifetimeEntry(hitObject, stageRunner);\r\n\r\n    private class LyricHitObjectLifetimeEntry : HitObjectLifetimeEntry\r\n    {\r\n        private readonly IStageHitObjectRunner? stageRunner;\r\n\r\n        public LyricHitObjectLifetimeEntry(HitObject hitObject, IStageHitObjectRunner? runner)\r\n            : base(hitObject)\r\n        {\r\n            stageRunner = runner;\r\n            if (stageRunner == null)\r\n                return;\r\n\r\n            // Manually set to reduce the number of future alive objects to a bare minimum.\r\n            updateLifetime();\r\n\r\n            stageRunner.OnCommandUpdated += updateLifetime;\r\n        }\r\n\r\n        private void updateLifetime()\r\n        {\r\n            if (stageRunner == null)\r\n                throw new InvalidOperationException();\r\n\r\n            // follow the same event as SetInitialLifetime() in the base class.\r\n            LifetimeStart = HitObject.StartTime - InitialLifetimeOffset;\r\n            LifetimeEnd = lyric.EndTime + stageRunner.GetEndTimeOffset(lyric);\r\n        }\r\n\r\n        private Lyric lyric => (Lyric)HitObject;\r\n\r\n        protected override double InitialLifetimeOffset\r\n        {\r\n            get\r\n            {\r\n                if (stageRunner == null)\r\n                    return base.InitialLifetimeOffset;\r\n\r\n                return stageRunner.GetStartTimeOffset(lyric) + stageRunner.GetPreemptTime(HitObject);\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/UI/NotePlayfield.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Input.Bindings;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Game.Rulesets.Judgements;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Drawables;\r\nusing osu.Game.Rulesets.Karaoke.Replays;\r\nusing osu.Game.Rulesets.Karaoke.Scoring;\r\nusing osu.Game.Rulesets.Karaoke.UI.Components;\r\nusing osu.Game.Rulesets.Karaoke.UI.Position;\r\nusing osu.Game.Rulesets.Karaoke.UI.Scrolling;\r\nusing osu.Game.Rulesets.Objects.Drawables;\r\nusing osu.Game.Rulesets.Scoring;\r\nusing osu.Game.Rulesets.UI;\r\nusing osu.Game.Skinning;\r\nusing osuTK;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.UI;\r\n\r\npublic partial class NotePlayfield : ScrollingNotePlayfield, IKeyBindingHandler<KaraokeScoringAction>\r\n{\r\n    private readonly BindableInt scoringPitch = new();\r\n\r\n    private readonly CenterLine centerLine;\r\n\r\n    private readonly Container judgementArea;\r\n    private readonly JudgementContainer<DrawableNoteJudgement> judgements;\r\n    private readonly JudgementPooler<DrawableNoteJudgement> judgementPooler;\r\n    private readonly Drawable judgementLine;\r\n    private readonly ScoringMarker scoringMarker;\r\n\r\n    private readonly RealTimeScoringVisualization realTimeScoringVisualization;\r\n    private readonly ReplayScoringVisualization replayScoringVisualization;\r\n\r\n    private readonly ScoringStatus scoringStatus;\r\n\r\n    // Note playfield should be present even being hidden.\r\n    public override bool IsPresent => true;\r\n\r\n    [Resolved]\r\n    private INotePositionInfo notePositionInfo { get; set; } = null!;\r\n\r\n    public NotePlayfield(int columns)\r\n        : base(columns)\r\n    {\r\n        if (InternalChildren.FirstOrDefault() is Container container)\r\n        {\r\n            // add padding to first children.\r\n            container.Padding = new MarginPadding { Top = 30, Bottom = 30 };\r\n        }\r\n\r\n        BackgroundLayer.AddRange(new Drawable[]\r\n        {\r\n            new SkinnableDrawable(new KaraokeSkinComponentLookup(KaraokeSkinComponents.StageBackground))\r\n            {\r\n                Depth = 2,\r\n                RelativeSizeAxes = Axes.Both,\r\n            },\r\n            new Box\r\n            {\r\n                Depth = 1,\r\n                Name = \"Background\",\r\n                RelativeSizeAxes = Axes.Both,\r\n                Colour = Color4.Black,\r\n                Alpha = 0.5f,\r\n            },\r\n            centerLine = new CenterLine\r\n            {\r\n                Anchor = Anchor.Centre,\r\n                Origin = Anchor.Centre,\r\n            },\r\n        });\r\n\r\n        HitObjectLayer.Add(judgementArea = new Container\r\n        {\r\n            RelativeSizeAxes = Axes.Both,\r\n            RelativePositionAxes = Axes.X,\r\n            Children = new[]\r\n            {\r\n                judgements = new JudgementContainer<DrawableNoteJudgement>\r\n                {\r\n                    Anchor = Anchor.CentreLeft,\r\n                    Origin = Anchor.CentreLeft,\r\n                    AutoSizeAxes = Axes.Both,\r\n                    BypassAutoSizeAxes = Axes.Both,\r\n                },\r\n                judgementLine = new SkinnableDrawable(new KaraokeSkinComponentLookup(KaraokeSkinComponents.JudgementLine), _ => new DefaultJudgementLine())\r\n                {\r\n                    RelativeSizeAxes = Axes.Y,\r\n                    Anchor = Anchor.Centre,\r\n                    Origin = Anchor.Centre,\r\n                },\r\n                scoringMarker = new ScoringMarker\r\n                {\r\n                    Alpha = 0,\r\n                },\r\n            },\r\n        });\r\n\r\n        HitObjectArea.AddRange(new Drawable[]\r\n        {\r\n            // todo : generate this only if in auto-play mode.\r\n            replayScoringVisualization = new ReplayScoringVisualization(null)\r\n            {\r\n                Name = \"Replay scoring Visualization\",\r\n                RelativeSizeAxes = Axes.Both,\r\n                Alpha = 0.6f,\r\n            },\r\n            realTimeScoringVisualization = new RealTimeScoringVisualization\r\n            {\r\n                Name = \"Scoring Visualization\",\r\n                RelativeSizeAxes = Axes.Both,\r\n            },\r\n        });\r\n\r\n        AddInternal(scoringStatus = new ScoringStatus(ScoringStatusMode.NotInitialized));\r\n\r\n        var hitWindows = new KaraokeNoteHitWindows();\r\n        AddInternal(judgementPooler = new JudgementPooler<DrawableNoteJudgement>(Enum.GetValues<HitResult>().Where(r => hitWindows.IsHitResultAllowed(r))));\r\n    }\r\n\r\n    protected override void OnDirectionChanged(KaraokeScrollingDirection direction, float judgementAreaPercentage)\r\n    {\r\n        base.OnDirectionChanged(direction, judgementAreaPercentage);\r\n\r\n        bool left = direction == KaraokeScrollingDirection.Left;\r\n\r\n        //TODO : will apply in skin\r\n        int judgementPadding = 10;\r\n\r\n        judgementArea.Size = new Vector2(judgementAreaPercentage, 1);\r\n        judgementArea.X = left ? 0 : 1 - judgementAreaPercentage;\r\n\r\n        judgementLine.Anchor = left ? Anchor.CentreRight : Anchor.CentreLeft;\r\n        scoringMarker.Anchor = scoringMarker.Origin = left ? Anchor.CentreRight : Anchor.CentreLeft;\r\n        scoringMarker.Scale = left ? new Vector2(1, 1) : new Vector2(-1, 1);\r\n\r\n        judgements.Anchor = judgements.Origin = left ? Anchor.CentreRight : Anchor.CentreLeft;\r\n        judgements.X = left ? -judgementPadding : judgementPadding;\r\n\r\n        realTimeScoringVisualization.Anchor = left ? Anchor.CentreLeft : Anchor.CentreRight;\r\n        realTimeScoringVisualization.Origin = left ? Anchor.CentreRight : Anchor.CentreLeft;\r\n    }\r\n\r\n    protected override void LoadComplete()\r\n    {\r\n        base.LoadComplete();\r\n\r\n        NewResult += OnNewResult;\r\n\r\n        scoringPitch.BindValueChanged(value =>\r\n        {\r\n            int newValue = value.NewValue;\r\n            var targetTone = new Tone((newValue < 0 ? newValue - 1 : newValue) / 2, newValue % 2 != 0);\r\n            float targetY = notePositionInfo.Calculator.YPositionAt(targetTone);\r\n            float targetHeight = targetTone.Half ? 5 : DefaultColumnBackground.COLUMN_HEIGHT;\r\n            float alpha = targetTone.Half ? 0.6f : 0.2f;\r\n\r\n            centerLine.MoveToY(targetY, 100);\r\n            centerLine.ResizeHeightTo(targetHeight, 100);\r\n            centerLine.Alpha = alpha;\r\n        }, true);\r\n    }\r\n\r\n    public void ClearReplay()\r\n    {\r\n        replayScoringVisualization.Clear();\r\n    }\r\n\r\n    public void AddReplay(KaraokeReplayFrame frame)\r\n    {\r\n        replayScoringVisualization.Add(frame);\r\n    }\r\n\r\n    internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result)\r\n    {\r\n        if (!judgedObject.DisplayResult || !DisplayJudgements.Value)\r\n            return;\r\n\r\n        if (judgedObject is not DrawableNote note)\r\n            return;\r\n\r\n        judgements.Clear(false);\r\n        judgements.Add(judgementPooler.Get(result.Type, j =>\r\n        {\r\n            j.Apply(result, judgedObject);\r\n\r\n            j.Y = notePositionInfo.Calculator.YPositionAt(note.HitObject.Tone + 2);\r\n            j.Anchor = Anchor.Centre;\r\n            j.Origin = Anchor.Centre;\r\n        })!);\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(KaraokeSessionStatics? session)\r\n    {\r\n        session?.BindWith(KaraokeRulesetSession.ScoringPitch, scoringPitch);\r\n\r\n        session?.GetBindable<ScoringStatusMode>(KaraokeRulesetSession.ScoringStatus).BindValueChanged(e => { scoringStatus.ScoringStatusMode = e.NewValue; });\r\n    }\r\n\r\n    public bool OnPressed(KeyBindingPressEvent<KaraokeScoringAction> e)\r\n    {\r\n        // TODO : appear marker and move position with delay time\r\n        scoringMarker.Y = notePositionInfo.Calculator.YPositionAt(e.Action);\r\n        scoringMarker.Alpha = 1;\r\n\r\n        // Mark as singing\r\n        realTimeScoringVisualization.AddAction(e.Action);\r\n\r\n        return true;\r\n    }\r\n\r\n    public void OnReleased(KeyBindingReleaseEvent<KaraokeScoringAction> e)\r\n    {\r\n        // TODO : disappear marker\r\n        scoringMarker.Alpha = 0;\r\n\r\n        // Stop singing\r\n        realTimeScoringVisualization.Release();\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/UI/PlayerSettings/ClickablePlayerSliderBar.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Cursor;\r\nusing osu.Framework.Localisation;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Overlays.Settings;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.UI.PlayerSettings;\r\n\r\npublic partial class ClickablePlayerSliderBar : SettingsSlider<int>\r\n{\r\n    protected const int BUTTON_SIZE = 25;\r\n    protected const int BUTTON_SPACING = 15;\r\n\r\n    private ClickableSliderBar bar => (ClickableSliderBar)Control;\r\n\r\n    protected override Drawable CreateControl() => new ClickableSliderBar\r\n    {\r\n        Margin = new MarginPadding { Top = 5, Bottom = 5 },\r\n        RelativeSizeAxes = Axes.X,\r\n    };\r\n\r\n    public ClickablePlayerSliderBar()\r\n    {\r\n        Padding = new MarginPadding { Left = BUTTON_SPACING * 2, Right = BUTTON_SPACING * 2 };\r\n    }\r\n\r\n    public void ResetToDefaultValue() => bar.ResetToDefaultValue();\r\n\r\n    public void TriggerDecrease() => bar.TriggerDecrease();\r\n\r\n    public void TriggerIncrease() => bar.TriggerIncrease();\r\n\r\n    private partial class ClickableSliderBar : RoundedSliderBar<int>\r\n    {\r\n        private readonly ToolTipButton decreaseButton;\r\n        private readonly ToolTipButton increaseButton;\r\n\r\n        public override LocalisableString TooltipText => (Current.Value >= 0 ? \"+\" : string.Empty) + Current.Value.ToString(\"N0\");\r\n\r\n        public ClickableSliderBar()\r\n        {\r\n            KeyboardStep = 1;\r\n\r\n            Add(decreaseButton = new ToolTipButton\r\n            {\r\n                Position = new Vector2(-BUTTON_SPACING, 0),\r\n                Origin = Anchor.CentreRight,\r\n                Anchor = Anchor.CentreLeft,\r\n                Width = BUTTON_SIZE,\r\n                Height = BUTTON_SIZE,\r\n                Text = \"-\",\r\n                TooltipText = \"Decrease\",\r\n                Action = () => Current.Value -= (int)KeyboardStep,\r\n            });\r\n\r\n            Add(increaseButton = new ToolTipButton\r\n            {\r\n                Position = new Vector2(BUTTON_SPACING, 0),\r\n                Origin = Anchor.CentreLeft,\r\n                Anchor = Anchor.CentreRight,\r\n                Width = BUTTON_SIZE,\r\n                Height = BUTTON_SIZE,\r\n                Text = \"+\",\r\n                TooltipText = \"Increase\",\r\n                Action = () => Current.Value += (int)KeyboardStep,\r\n            });\r\n        }\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(OsuColour colours)\r\n        {\r\n            AccentColour = colours.Yellow;\r\n            Nub.AccentColour = colours.Yellow;\r\n            Nub.GlowingAccentColour = colours.YellowLighter;\r\n            Nub.GlowColour = colours.YellowDarker;\r\n        }\r\n\r\n        public void ResetToDefaultValue() => Current.SetDefault();\r\n\r\n        public void TriggerDecrease() => decreaseButton.Action?.Invoke();\r\n\r\n        public void TriggerIncrease() => increaseButton.Action?.Invoke();\r\n    }\r\n\r\n    private partial class ToolTipButton : OsuButton, IHasTooltip\r\n    {\r\n        public LocalisableString TooltipText { get; init; }\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(OsuColour colours)\r\n        {\r\n            BackgroundColour = colours.Blue;\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/UI/PlayerSettings/ILyricNavigator.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.UI.PlayerSettings;\r\n\r\npublic interface ILyricNavigator\r\n{\r\n    void SeekTimeByLyric(Lyric target);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/UI/PlayerSettings/LyricsPreview.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Extensions.IEnumerableExtensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Containers;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Mods;\r\nusing osuTK;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.UI.PlayerSettings;\r\n\r\npublic partial class LyricsPreview : CompositeDrawable\r\n{\r\n    private readonly Bindable<Lyric[]> singingLyrics = new();\r\n\r\n    [Resolved]\r\n    private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;\r\n\r\n    [Resolved]\r\n    private ILyricNavigator lyricNavigator { get; set; } = null!;\r\n\r\n    public LyricsPreview()\r\n    {\r\n        FillFlowContainer<ClickableLyric> lyricTable;\r\n\r\n        InternalChild = new OsuScrollContainer\r\n        {\r\n            RelativeSizeAxes = Axes.Both,\r\n            Child = lyricTable = new FillFlowContainer<ClickableLyric>\r\n            {\r\n                AutoSizeAxes = Axes.Y,\r\n                RelativeSizeAxes = Axes.X,\r\n                Direction = FillDirection.Vertical,\r\n                Spacing = new Vector2(15),\r\n            },\r\n        };\r\n\r\n        singingLyrics.BindValueChanged(value =>\r\n        {\r\n            var oldValue = value.OldValue;\r\n            if (oldValue != null)\r\n                lyricTable.Where(x => oldValue.Contains(x.Lyric)).ForEach(x => { x.Selected = false; });\r\n\r\n            var newValue = value.NewValue;\r\n            if (newValue != null)\r\n                lyricTable.Where(x => newValue.Contains(x.Lyric)).ForEach(x => { x.Selected = true; });\r\n        });\r\n\r\n        Schedule(() =>\r\n        {\r\n            var lyrics = beatmap.Value.Beatmap.HitObjects.OfType<Lyric>().ToList();\r\n            lyricTable.Children = lyrics.Select(x => createLyricContainer(x).With(c =>\r\n            {\r\n                c.Selected = false;\r\n                c.Action = () => triggerLyric(x);\r\n            })).ToList();\r\n        });\r\n    }\r\n\r\n    private ClickableLyric createLyricContainer(Lyric lyric) => new(lyric);\r\n\r\n    private void triggerLyric(Lyric lyric)\r\n    {\r\n        lyricNavigator.SeekTimeByLyric(lyric);\r\n\r\n        // because playback might not clear singing lyrics, so we should re-assign the lyric here.\r\n        // todo: find a better place.\r\n        singingLyrics.Value = new[] { lyric };\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(IBindable<IReadOnlyList<Mod>> mods, KaraokeSessionStatics session)\r\n    {\r\n        session.BindWith(KaraokeRulesetSession.SingingLyrics, singingLyrics);\r\n    }\r\n\r\n    private partial class ClickableLyric : ClickableContainer\r\n    {\r\n        private const float fade_duration = 100;\r\n\r\n        private Color4 hoverTextColour;\r\n        private Color4 idolTextColour;\r\n\r\n        private readonly Box background;\r\n        private readonly Drawable icon;\r\n        private readonly DrawableLyricSpriteText drawableLyric;\r\n\r\n        public readonly Lyric Lyric;\r\n\r\n        public ClickableLyric(Lyric lyric)\r\n        {\r\n            Lyric = lyric;\r\n\r\n            AutoSizeAxes = Axes.Y;\r\n            RelativeSizeAxes = Axes.X;\r\n            Masking = true;\r\n            CornerRadius = 5;\r\n            Children = new[]\r\n            {\r\n                background = new Box\r\n                {\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    Alpha = 0,\r\n                },\r\n                icon = new SpriteIcon\r\n                {\r\n                    Anchor = Anchor.CentreLeft,\r\n                    Origin = Anchor.CentreLeft,\r\n                    Size = new Vector2(15),\r\n                    Icon = FontAwesome.Solid.Play,\r\n                    Margin = new MarginPadding { Left = 5 },\r\n                    Alpha = 0,\r\n                },\r\n                drawableLyric = new DrawableLyricSpriteText(lyric)\r\n                {\r\n                    Font = new FontUsage(size: 25),\r\n                    TopTextFont = new FontUsage(size: 10),\r\n                    BottomTextFont = new FontUsage(size: 10),\r\n                    Margin = new MarginPadding { Left = 25 },\r\n                },\r\n            };\r\n        }\r\n\r\n        private bool selected;\r\n\r\n        public bool Selected\r\n        {\r\n            get => selected;\r\n            set\r\n            {\r\n                if (value == selected) return;\r\n\r\n                selected = value;\r\n\r\n                background.FadeTo(Selected ? 1 : 0, fade_duration);\r\n                icon.FadeTo(Selected ? 1 : 0, fade_duration);\r\n                drawableLyric.FadeColour(Selected ? hoverTextColour : idolTextColour, fade_duration);\r\n            }\r\n        }\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(OsuColour colours)\r\n        {\r\n            hoverTextColour = colours.Yellow;\r\n            idolTextColour = colours.Gray9;\r\n\r\n            drawableLyric.Colour = idolTextColour;\r\n            background.Colour = colours.Blue;\r\n            icon.Colour = hoverTextColour;\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/UI/PlayerSettings/PitchSettings.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Screens.Play.PlayerSettings;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.UI.PlayerSettings;\r\n\r\npublic partial class PitchSettings : PlayerSettingsGroup\r\n{\r\n    private readonly ClickablePlayerSliderBar pitchSliderBar;\r\n    private readonly ClickablePlayerSliderBar vocalPitchSliderBar;\r\n    private readonly ClickablePlayerSliderBar scoringPitchSliderBar;\r\n\r\n    public PitchSettings()\r\n        : base(\"Pitch\")\r\n    {\r\n        Children = new Drawable[]\r\n        {\r\n            new OsuSpriteText\r\n            {\r\n                Text = \"Pitch:\",\r\n            },\r\n            pitchSliderBar = new ClickablePlayerSliderBar(),\r\n            new OsuSpriteText\r\n            {\r\n                Text = \"Vocal pitch:\",\r\n            },\r\n            vocalPitchSliderBar = new ClickablePlayerSliderBar(),\r\n            new OsuSpriteText\r\n            {\r\n                Text = \"Scoring pitch:\",\r\n            },\r\n            scoringPitchSliderBar = new ClickablePlayerSliderBar(),\r\n        };\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(KaraokeSessionStatics session)\r\n    {\r\n        pitchSliderBar.Current = session.GetBindable<int>(KaraokeRulesetSession.Pitch);\r\n        vocalPitchSliderBar.Current = session.GetBindable<int>(KaraokeRulesetSession.VocalPitch);\r\n        scoringPitchSliderBar.Current = session.GetBindable<int>(KaraokeRulesetSession.ScoringPitch);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/UI/PlayerSettings/PlaybackSettings.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Input.Bindings;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Screens.Play.PlayerSettings;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.UI.PlayerSettings;\r\n\r\npublic partial class PlaybackSettings : PlayerSettingsGroup, IKeyBindingHandler<KaraokeAction>\r\n{\r\n    private readonly ClickablePlayerSliderBar playBackSliderBar;\r\n\r\n    public PlaybackSettings()\r\n        : base(\"Playback\")\r\n    {\r\n        Children = new Drawable[]\r\n        {\r\n            new OsuSpriteText\r\n            {\r\n                Text = \"Playback:\",\r\n            },\r\n            playBackSliderBar = new ClickablePlayerSliderBar(),\r\n        };\r\n    }\r\n\r\n    public bool OnPressed(KeyBindingPressEvent<KaraokeAction> e)\r\n    {\r\n        switch (e.Action)\r\n        {\r\n            case KaraokeAction.IncreaseTempo:\r\n                playBackSliderBar.TriggerIncrease();\r\n                break;\r\n\r\n            case KaraokeAction.DecreaseTempo:\r\n                playBackSliderBar.TriggerDecrease();\r\n                break;\r\n\r\n            case KaraokeAction.ResetTempo:\r\n                playBackSliderBar.ResetToDefaultValue();\r\n                break;\r\n\r\n            default:\r\n                return false;\r\n        }\r\n\r\n        return true;\r\n    }\r\n\r\n    public void OnReleased(KeyBindingReleaseEvent<KaraokeAction> e)\r\n    {\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(KaraokeSessionStatics session)\r\n    {\r\n        playBackSliderBar.Current = session.GetBindable<int>(KaraokeRulesetSession.PlaybackSpeed);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/UI/PlayerSettings/PlayerDropdown.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Overlays.Settings;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.UI.PlayerSettings;\r\n\r\npublic partial class PlayerEnumDropdown<T> : SettingsDropdown<T> where T : struct, Enum\r\n{\r\n    protected override OsuDropdown<T> CreateDropdown() => new EnumDropdown();\r\n\r\n    protected partial class EnumDropdown : OsuEnumDropdown<T>\r\n    {\r\n        public EnumDropdown()\r\n        {\r\n            RelativeSizeAxes = Axes.X;\r\n        }\r\n\r\n        [BackgroundDependencyLoader]\r\n        private void load(OsuColour colours)\r\n        {\r\n            // todo: adjust the style.\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/UI/PlayerSettings/PracticeSettings.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Input.Bindings;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Mods;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Mods;\r\nusing osu.Game.Screens.Play.PlayerSettings;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.UI.PlayerSettings;\r\n\r\n[Cached(typeof(ILyricNavigator))]\r\npublic partial class PracticeSettings : PlayerSettingsGroup, IKeyBindingHandler<KaraokeAction>, ILyricNavigator\r\n{\r\n    [Resolved]\r\n    private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;\r\n\r\n    private readonly PlayerSliderBar<double> preemptTimeSliderBar;\r\n\r\n    public PracticeSettings()\r\n        : base(\"Practice\")\r\n    {\r\n        Children = new Drawable[]\r\n        {\r\n            new OsuSpriteText\r\n            {\r\n                Text = \"Practice preempt time:\",\r\n            },\r\n            preemptTimeSliderBar = new PlayerSliderBar<double>(),\r\n            new OsuSpriteText\r\n            {\r\n                Text = \"Lyric:\",\r\n            },\r\n            new LyricsPreview\r\n            {\r\n                Height = 580,\r\n                RelativeSizeAxes = Axes.X,\r\n            },\r\n        };\r\n    }\r\n\r\n    public bool OnPressed(KeyBindingPressEvent<KaraokeAction> e)\r\n    {\r\n        switch (e.Action)\r\n        {\r\n            case KaraokeAction.FirstLyric:\r\n                // TODO : switch to first lyric\r\n                break;\r\n\r\n            case KaraokeAction.PreviousLyric:\r\n                // TODO : switch to previous lyric\r\n                break;\r\n\r\n            case KaraokeAction.NextLyric:\r\n                // TODO : switch to next lyric\r\n                break;\r\n\r\n            case KaraokeAction.PlayAndPause:\r\n                // TODO : pause\r\n                break;\r\n\r\n            default:\r\n                return false;\r\n        }\r\n\r\n        return true;\r\n    }\r\n\r\n    public void OnReleased(KeyBindingReleaseEvent<KaraokeAction> e)\r\n    {\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(IBindable<IReadOnlyList<Mod>> mods)\r\n    {\r\n        var practiceMod = mods.Value.OfType<KaraokeModPractice>().First();\r\n        preemptTimeSliderBar.Current.Value = practiceMod.LyricPreemptTime.Value;\r\n    }\r\n\r\n    public void SeekTimeByLyric(Lyric target)\r\n    {\r\n        double? time = target.StartTime - preemptTimeSliderBar.Current.Value;\r\n        if (time != null)\r\n            beatmap.Value.Track.Seek(time.Value);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/UI/Position/INotePositionInfo.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Bindables;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.UI.Position;\r\n\r\npublic interface INotePositionInfo\r\n{\r\n    IBindable<NotePositionCalculator> Position { get; }\r\n\r\n    NotePositionCalculator Calculator { get; }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/UI/Position/NotePositionCalculator.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Replays;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.UI.Position;\r\n\r\npublic class NotePositionCalculator\r\n{\r\n    private readonly int columns;\r\n    private readonly float columnSpacing;\r\n    private readonly Tone offset;\r\n\r\n    public float ColumnHeight { get; }\r\n\r\n    public NotePositionCalculator(int columns, float columnHeight, float columnSpacing, Tone offset = new())\r\n    {\r\n        ColumnHeight = columnHeight;\r\n\r\n        // todo : not sure should column can be even.\r\n        this.columns = columns;\r\n        this.columnSpacing = columnSpacing;\r\n        this.offset = offset;\r\n    }\r\n\r\n    public float YPositionAt(Note note) => YPositionAt(note.Tone);\r\n\r\n    public float YPositionAt(Tone tone) => YPositionAt(toFloat(tone));\r\n\r\n    public float YPositionAt(KaraokeScoringAction action) => YPositionAt(action.Scale);\r\n\r\n    public float YPositionAt(KaraokeReplayFrame frame) => YPositionAt(frame.Scale);\r\n\r\n    public float YPositionAt(float scale) => -(columnSpacing + ColumnHeight) * Math.Clamp(scale, toFloat(MinTone), toFloat(MaxTone));\r\n\r\n    public Tone MaxTone =>\r\n        new()\r\n        {\r\n            Scale = columns / 2,\r\n        };\r\n\r\n    public Tone MinTone => -MaxTone;\r\n\r\n    private float toFloat(Tone tone)\r\n        => tone.Scale + (tone.Half ? 0.5f : 0);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/UI/Position/NotePositionInfo.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Skinning;\r\nusing osu.Game.Rulesets.Karaoke.UI.Components;\r\nusing osu.Game.Rulesets.Karaoke.UI.Scrolling;\r\nusing osu.Game.Skinning;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.UI.Position;\r\n\r\npublic partial class NotePositionInfo : SkinReloadableDrawable, INotePositionInfo\r\n{\r\n    private const int columns = 9;\r\n\r\n    private readonly Bindable<NotePositionCalculator> position = new();\r\n    public new IBindable<NotePositionCalculator> Position => position;\r\n    public NotePositionCalculator Calculator => Position.Value;\r\n\r\n    private readonly IBindable<int> bindableColumns = new Bindable<int>(columns);\r\n    private readonly IBindable<float> bindableColumnHeight = new Bindable<float>(DefaultColumnBackground.COLUMN_HEIGHT);\r\n    private readonly IBindable<float> bindableColumnSpacing = new Bindable<float>(ScrollingNotePlayfield.COLUMN_SPACING);\r\n\r\n    public NotePositionInfo()\r\n    {\r\n        bindableColumnHeight.BindValueChanged(_ => updatePositionCalculator());\r\n        bindableColumnSpacing.BindValueChanged(_ => updatePositionCalculator());\r\n\r\n        updatePositionCalculator();\r\n    }\r\n\r\n    protected override void SkinChanged(ISkinSource skin)\r\n    {\r\n        base.SkinChanged(skin);\r\n\r\n        bindableColumnHeight.UnbindBindings();\r\n        bindableColumnSpacing.UnbindBindings();\r\n\r\n        var columnHeight = skin.GetConfig<KaraokeSkinConfigurationLookup, float>(new KaraokeSkinConfigurationLookup(columns, LegacyKaraokeSkinConfigurationLookups.ColumnHeight));\r\n        if (columnHeight == null)\r\n            return;\r\n\r\n        var columnSpacing = skin.GetConfig<KaraokeSkinConfigurationLookup, float>(new KaraokeSkinConfigurationLookup(columns, LegacyKaraokeSkinConfigurationLookups.ColumnSpacing));\r\n        if (columnSpacing == null)\r\n            return;\r\n\r\n        bindableColumnHeight.BindTo(columnHeight);\r\n        bindableColumnSpacing.BindTo(columnSpacing);\r\n    }\r\n\r\n    private void updatePositionCalculator()\r\n        => position.Value = new NotePositionCalculator(bindableColumns.Value, bindableColumnHeight.Value, bindableColumnSpacing.Value);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/UI/Scrolling/ScrollingNotePlayfield.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Extensions.IEnumerableExtensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Drawables;\r\nusing osu.Game.Rulesets.Karaoke.Skinning;\r\nusing osu.Game.Rulesets.Karaoke.UI.Components;\r\nusing osu.Game.Rulesets.Karaoke.UI.Position;\r\nusing osu.Game.Rulesets.UI.Scrolling;\r\nusing osu.Game.Skinning;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.UI.Scrolling;\r\n\r\npublic abstract partial class ScrollingNotePlayfield : ScrollingPlayfield\r\n{\r\n    public const float COLUMN_SPACING = 1;\r\n\r\n    private readonly FillFlowContainer<DefaultColumnBackground> columnFlow;\r\n\r\n    protected readonly Container BackgroundLayer;\r\n    protected readonly Container HitObjectLayer;\r\n    protected readonly Container HitObjectArea;\r\n\r\n    private readonly IBindable<NotePositionCalculator> calculator = new Bindable<NotePositionCalculator>();\r\n\r\n    public int Columns { get; }\r\n\r\n    protected ScrollingNotePlayfield(int columns)\r\n    {\r\n        Columns = columns;\r\n\r\n        RelativeSizeAxes = Axes.X;\r\n        AutoSizeAxes = Axes.Y;\r\n        InternalChildren = new Drawable[]\r\n        {\r\n            new Container\r\n            {\r\n                Anchor = Anchor.CentreLeft,\r\n                Origin = Anchor.CentreLeft,\r\n                RelativeSizeAxes = Axes.X,\r\n                AutoSizeAxes = Axes.Y,\r\n                Masking = true,\r\n                CornerRadius = 5,\r\n                Children = new Drawable[]\r\n                {\r\n                    BackgroundLayer = new Container\r\n                    {\r\n                        Name = \"Background mask\",\r\n                        RelativeSizeAxes = Axes.X,\r\n                        AutoSizeAxes = Axes.Y,\r\n                        Masking = true,\r\n                        CornerRadius = 5,\r\n                        Children = new Drawable[]\r\n                        {\r\n                            // background\r\n                            columnFlow = new FillFlowContainer<DefaultColumnBackground>\r\n                            {\r\n                                Name = \"Columns\",\r\n                                RelativeSizeAxes = Axes.X,\r\n                                AutoSizeAxes = Axes.Y,\r\n                                Direction = FillDirection.Vertical,\r\n                                Padding = new MarginPadding { Top = COLUMN_SPACING, Bottom = COLUMN_SPACING },\r\n                                Spacing = new Vector2(0, COLUMN_SPACING),\r\n                            },\r\n                            // center line\r\n                        },\r\n                    },\r\n                    HitObjectLayer = new Container\r\n                    {\r\n                        RelativeSizeAxes = Axes.Both,\r\n                        Children = new Drawable[]\r\n                        {\r\n                            // judgement\r\n                            HitObjectArea = new Container\r\n                            {\r\n                                Depth = 1,\r\n                                RelativeSizeAxes = Axes.Both,\r\n                                RelativePositionAxes = Axes.X,\r\n                                Children = new Drawable[]\r\n                                {\r\n                                    new Container\r\n                                    {\r\n                                        Name = \"Hit objects\",\r\n                                        RelativeSizeAxes = Axes.Both,\r\n                                        Child = HitObjectContainer,\r\n                                    },\r\n                                    // scoring visualization\r\n                                },\r\n                            },\r\n                        },\r\n                    },\r\n                },\r\n            },\r\n            // other things like microphone status\r\n        };\r\n\r\n        for (int i = 0; i < columns; i++)\r\n        {\r\n            var column = new DefaultColumnBackground(i)\r\n            {\r\n                IsSpecial = i % 2 == 0,\r\n            };\r\n\r\n            columnFlow.Add(column);\r\n        }\r\n\r\n        RegisterPool<Note, DrawableNote>(50);\r\n        RegisterPool<BarLine, DrawableBarLine>(15);\r\n    }\r\n\r\n    protected virtual void OnDirectionChanged(KaraokeScrollingDirection direction, float judgementAreaPercentage)\r\n    {\r\n        bool left = direction == KaraokeScrollingDirection.Left;\r\n\r\n        HitObjectArea.Size = new Vector2(1 - judgementAreaPercentage, 1);\r\n        HitObjectArea.X = left ? judgementAreaPercentage : 0;\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OsuColour colours, ISkinSource skin, INotePositionInfo notePositionInfo)\r\n    {\r\n        columnFlow.Children.ForEach(x => x.Colour = x.IsSpecial ? colours.Gray9 : colours.Gray0);\r\n\r\n        Direction.BindValueChanged(dir =>\r\n        {\r\n            float judgementAreaPercentage = skin.GetConfig<KaraokeSkinConfigurationLookup, float>(\r\n                                                    new KaraokeSkinConfigurationLookup(Columns, LegacyKaraokeSkinConfigurationLookups.JudgementAresPercentage, 0))\r\n                                                ?.Value ?? 0.4f;\r\n\r\n            var newDirection = dir.NewValue;\r\n\r\n            switch (newDirection)\r\n            {\r\n                case ScrollingDirection.Left:\r\n                    OnDirectionChanged(KaraokeScrollingDirection.Left, judgementAreaPercentage);\r\n                    break;\r\n\r\n                case ScrollingDirection.Right:\r\n                    OnDirectionChanged(KaraokeScrollingDirection.Right, judgementAreaPercentage);\r\n                    break;\r\n\r\n                default:\r\n                    throw new ArgumentOutOfRangeException(nameof(newDirection));\r\n            }\r\n        });\r\n\r\n        calculator.BindTo(notePositionInfo.Position);\r\n        calculator.BindValueChanged(e =>\r\n        {\r\n            float columnHeight = e.NewValue.ColumnHeight;\r\n\r\n            for (int i = 0; i < Columns; i++)\r\n            {\r\n                columnFlow[i].Height = columnHeight;\r\n            }\r\n        }, true);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Utils/ActivatorUtils.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Utils;\r\n\r\npublic static class ActivatorUtils\r\n{\r\n    public static TObject CreateInstance<TObject>(params object?[]? args)\r\n    {\r\n        var algorithm = (TObject?)Activator.CreateInstance(typeof(TObject), args);\r\n        if (algorithm == null)\r\n            throw new InvalidOperationException();\r\n\r\n        return algorithm;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Utils/AssemblyUtils.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing System.Reflection;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Utils;\r\n\r\npublic static class AssemblyUtils\r\n{\r\n    public static Assembly? GetAssemblyByName(string name)\r\n    {\r\n        var defaultAssembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(x => x.GetName().Name == name);\r\n\r\n        if (defaultAssembly != null)\r\n            return defaultAssembly;\r\n\r\n        // Note: because multiple assembly might be wrapped into single one by ILRepack, so should find by main dll again if not found.\r\n        return AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(x => x.FullName?.Contains(\"osu.Game.Rulesets.Karaoke\") ?? false);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Utils/BindablesUtils.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing osu.Framework.Bindables;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Utils;\r\n\r\npublic static class BindablesUtils\r\n{\r\n    public static void Sync<T1, T2>(BindableList<T1> firstBindableList, BindableList<T2> secondBindableList)\r\n    {\r\n        OnyWaySync(firstBindableList, secondBindableList);\r\n        OnyWaySync(secondBindableList, firstBindableList);\r\n    }\r\n\r\n    public static void OnyWaySync<T1, T2>(BindableList<T1> firstBindableList, BindableList<T2> secondBindableList)\r\n    {\r\n        // add objects to second list if has default value in first list.\r\n        var defaultItems = firstBindableList.OfType<T2>().Except(secondBindableList);\r\n        secondBindableList.AddRange(defaultItems);\r\n\r\n        firstBindableList.CollectionChanged += (_, args) =>\r\n        {\r\n            var newItems = args.NewItems?.OfType<T2>().Except(secondBindableList).ToArray();\r\n            var oldItems = args.OldItems;\r\n\r\n            if (newItems != null && newItems.Any())\r\n            {\r\n                // add objects to second list if new items have been added.\r\n                secondBindableList.AddRange(newItems);\r\n            }\r\n\r\n            if (oldItems != null && oldItems.Count > 0)\r\n            {\r\n                // remove objects from second list if exist items has been removed.\r\n                secondBindableList.RemoveAll(x => oldItems.Contains(x));\r\n            }\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Utils/CharUtils.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Text.Unicode;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Utils;\r\n\r\npublic static class CharUtils\r\n{\r\n    public static bool IsSpacing(char c)\r\n        => c is ' ' or '　';\r\n\r\n    /// <summary>\r\n    /// Check this char is kana\r\n    /// </summary>\r\n    /// <param name=\"c\"></param>\r\n    /// <returns></returns>\r\n    public static bool IsKana(char c) =>\r\n        (c >= '\\u3041' && c <= '\\u309F') | // ひらがなwith゛゜\r\n        (c >= '\\u30A0' && c <= '\\u30FF') | // カタカナwith゠・ー\r\n        (c >= '\\u31F0' && c <= '\\u31FF') | // Katakana Phonetic Extensions\r\n        (c >= '\\uFF65' && c <= '\\uFF9F');\r\n\r\n    /// <summary>\r\n    /// Check this character is English latter or not.\r\n    /// </summary>\r\n    /// <param name=\"c\"></param>\r\n    /// <returns></returns>\r\n    public static bool IsEnglish(char c) =>\r\n        (c >= 'A' && c <= 'Z') ||\r\n        (c >= 'a' && c <= 'z') ||\r\n        (c >= 'Ａ' && c <= 'Ｚ') ||\r\n        (c >= 'ａ' && c <= 'ｚ');\r\n\r\n    /// <summary>\r\n    /// Check this char is symbol\r\n    /// </summary>\r\n    /// <param name=\"c\"></param>\r\n    /// <returns></returns>\r\n    public static bool IsAsciiSymbol(char c) =>\r\n        (c >= ' ' && c <= '/') ||\r\n        (c >= ':' && c <= '@') ||\r\n        (c >= '[' && c <= '`') ||\r\n        (c >= '{' && c <= '~');\r\n\r\n    /// <summary>\r\n    /// Check this char is chinese character\r\n    /// </summary>\r\n    /// <param name=\"c\"></param>\r\n    /// <returns></returns>\r\n    public static bool IsChinese(char c)\r\n    {\r\n        // From : https://stackoverflow.com/a/61738863/4105113\r\n        int minValue = UnicodeRanges.CjkUnifiedIdeographs.FirstCodePoint;\r\n        int maxValue = minValue + UnicodeRanges.CjkUnifiedIdeographs.Length;\r\n        return c >= minValue && c < maxValue;\r\n    }\r\n\r\n    /// <summary>\r\n    /// Check this char is latin alphabet or not.\r\n    /// Usually, this is used to check the romanisation result.\r\n    /// </summary>\r\n    /// <param name=\"c\"></param>\r\n    /// <returns></returns>\r\n    public static bool IsLatin(char c)\r\n    {\r\n        switch (c)\r\n        {\r\n            case >= 'A' and <= 'Z':\r\n            case >= 'a' and <= 'z':\r\n            // another romanised characters\r\n            // see: https://www.unicode.org/charts/PDF/U1E00.pdf\r\n            case >= '\\u1E00' and <= '\\u1EFF':\r\n                return true;\r\n\r\n            default:\r\n                return false;\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Utils/ComparableUtils.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections;\r\nusing System.Linq;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Utils;\r\n\r\npublic static class ComparableUtils\r\n{\r\n    public static int Compare<T>(T? x, T? y, params Func<T, T, int>[] comparators)\r\n    {\r\n        if (x == null || y == null)\r\n            throw new InvalidOperationException(\"This utils does not support null cases\");\r\n\r\n        return comparators.Select(cmp => cmp(x, y))\r\n                          .FirstOrDefault(result => result != 0);\r\n    }\r\n\r\n    public static int CompareByProperty<T>(T? x, T? y, params Func<T, object>[] comparators)\r\n    {\r\n        var comparerResults = comparators.Select(comparer =>\r\n        {\r\n            return (Func<T, T, int>)compareFunction;\r\n\r\n            int compareFunction(T aa, T bb)\r\n            {\r\n                object xPropertyValue = comparer(aa);\r\n                object yPropertyValue = comparer(bb);\r\n                return Comparer.Default.Compare(xPropertyValue, yPropertyValue);\r\n            }\r\n        }).ToArray();\r\n\r\n        return Compare(x, y, comparerResults);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Utils/CultureInfoUtils.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Globalization;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Utils;\r\n\r\npublic static class CultureInfoUtils\r\n{\r\n    /// <summary>\r\n    /// Get all the languages that are not related to the country\r\n    /// </summary>\r\n    /// <returns></returns>\r\n    public static CultureInfo[] GetAvailableLanguages()\r\n    {\r\n        // todo: should make sure that all the language's LCID or ISC code are not duplicated.\r\n        return CultureInfo.GetCultures(CultureTypes.NeutralCultures);\r\n    }\r\n\r\n    public static bool IsLanguage(CultureInfo cultureInfo)\r\n        => (cultureInfo.CultureTypes & CultureTypes.NeutralCultures) != 0;\r\n\r\n    public static string GetLanguageDisplayText(CultureInfo? cultureInfo)\r\n        => cultureInfo?.NativeName ?? \"None\";\r\n\r\n    public static int GetSaveCultureInfoId(CultureInfo cultureInfo)\r\n        => cultureInfo.LCID;\r\n\r\n    public static CultureInfo CreateLoadCultureInfoById(int lcid) => new(lcid);\r\n\r\n    public static string GetSaveCultureInfoCode(CultureInfo cultureInfo)\r\n        => cultureInfo.ToString();\r\n\r\n    public static CultureInfo CreateLoadCultureInfoByCode(string code) => new(code);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Utils/EnumUtils.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Utils;\r\n\r\npublic static class EnumUtils\r\n{\r\n    public static T GetPreviousValue<T>(T v) where T : struct, Enum\r\n        => Enum.GetValues<T>().Concat(new[] { default(T) }).Reverse().SkipWhile(e => !EqualityComparer<T>.Default.Equals(v, e)).Skip(1).First();\r\n\r\n    public static T GetNextValue<T>(T v) where T : struct, Enum\r\n        => Enum.GetValues<T>().Concat(new[] { default(T) }).SkipWhile(e => !EqualityComparer<T>.Default.Equals(v, e)).Skip(1).First();\r\n\r\n    public static T Casting<T>(Enum mode)\r\n        => (T)(object)mode;\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Utils/FontUsageUtils.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Skinning.Fonts;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Utils;\r\n\r\npublic static class FontUsageUtils\r\n{\r\n    public static FontInfo ToFontInfo(FontUsage fontUsage, FontFormat fontFormat)\r\n        => new(fontUsage.FontName, fontFormat);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Utils/FontUtils.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Utils;\r\n\r\npublic static class FontUtils\r\n{\r\n    public const float DEFAULT_FONT_SIZE = 28;\r\n    public const float DEFAULT_FONT_SIZE_IN_COMPOSER = 36;\r\n\r\n    /// <summary>\r\n    /// For selecting size\r\n    /// </summary>\r\n    /// <returns></returns>\r\n    public static float[] DefaultFontSize()\r\n        => new float[] { 8, 9, 10, 11, 12, 14, 16, 18, 20, 22, 24, 26, 28, 36, 48, 72 };\r\n\r\n    public static float[] DefaultFontSize(float minSize, float maxSize)\r\n        => DefaultFontSize().Where(x => x >= minSize && x <= maxSize).ToArray();\r\n\r\n    /// <summary>\r\n    /// For selecting preview size in editor.\r\n    /// </summary>\r\n    /// <returns></returns>\r\n    public static float[] DefaultPreviewFontSize()\r\n        => new float[] { 12, 14, 16, 18, 20, 22, 24, 26, 28, 32, 36, 40, 48 };\r\n\r\n    /// <summary>\r\n    /// For selecting preview size in editor.\r\n    /// </summary>\r\n    /// <returns></returns>\r\n    public static float[] ComposerFontSize()\r\n        => new float[] { 20, 24, 28, 32, 36, 40, 48, 64, 72 };\r\n\r\n    public static string GetText(float fontSize)\r\n        => $\"{fontSize} px\";\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Utils/JpStringUtils.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing WanaKanaSharp;\r\nusing Zipangu;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Utils;\r\n\r\npublic static class JpStringUtils\r\n{\r\n    public static string ToHiragana(string text)\r\n    {\r\n        return text.KatakanaToHiragana();\r\n    }\r\n\r\n    public static string ToKatakana(string text)\r\n    {\r\n        return text.HiraganaToKatakana();\r\n    }\r\n\r\n    public static string ToRomaji(string text)\r\n    {\r\n        return RomajiConverter.ToRomaji(text, false, null);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Utils/RectangleFUtils.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing osu.Framework.Graphics.Primitives;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Utils;\r\n\r\npublic static class RectangleFUtils\r\n{\r\n    /// <summary>Creates the smallest possible third rectangle that can contain both of multi rectangles that form a union.</summary>\r\n    /// <returns>A third <see cref=\"RectangleF\"/> structure that contains both of the multi rectangles that form the union.</returns>\r\n    /// <param name=\"rectangles\">All the rectangles to union.</param>\r\n    /// <filterpriority>1</filterpriority>\r\n    public static RectangleF Union(params RectangleF[] rectangles)\r\n    {\r\n        if (rectangles.Length == 0)\r\n            return new RectangleF();\r\n\r\n        var result = rectangles.FirstOrDefault();\r\n\r\n        return rectangles.Aggregate(result, RectangleF.Union);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Utils/StackTraceUtils.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Utils;\r\n\r\npublic class StackTraceUtils\r\n{\r\n    public static bool IsStackTraceContains(string text)\r\n    {\r\n        string? stackTrace = Environment.StackTrace;\r\n        return stackTrace.Contains(text);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Utils/TextIndexUtils.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Graphics.Sprites;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Utils;\r\n\r\npublic static class TextIndexUtils\r\n{\r\n    public static int ToGapIndex(TextIndex index)\r\n        => GetValueByState(index, index.Index, index.Index + 1);\r\n\r\n    public static int ToCharIndex(TextIndex index)\r\n        => index.Index;\r\n\r\n    public static TextIndex FromStringIndex(int index, bool end)\r\n    {\r\n        return end ? new TextIndex(index - 1, TextIndex.IndexState.End) : new TextIndex(index);\r\n    }\r\n\r\n    public static TextIndex.IndexState ReverseState(TextIndex.IndexState state)\r\n    {\r\n        return GetValueByState(state, TextIndex.IndexState.End, TextIndex.IndexState.Start);\r\n    }\r\n\r\n    public static TextIndex GetPreviousIndex(TextIndex originIndex)\r\n    {\r\n        int previousIndex = ToGapIndex(originIndex) - 1;\r\n        var previousState = ReverseState(originIndex.State);\r\n        return new TextIndex(previousIndex, previousState);\r\n    }\r\n\r\n    public static TextIndex GetNextIndex(TextIndex originIndex)\r\n    {\r\n        int nextIndex = ToGapIndex(originIndex);\r\n        var nextState = ReverseState(originIndex.State);\r\n        return new TextIndex(nextIndex, nextState);\r\n    }\r\n\r\n    public static TextIndex ShiftingIndex(TextIndex originIndex, int offset)\r\n        => new(originIndex.Index + offset, originIndex.State);\r\n\r\n    public static bool OutOfRange(TextIndex index, string lyric)\r\n    {\r\n        if (string.IsNullOrEmpty(lyric))\r\n            return true;\r\n\r\n        return index.Index < 0 || index.Index >= lyric.Length;\r\n    }\r\n\r\n    public static T GetValueByState<T>(TextIndex index, T startValue, T endValue) =>\r\n        GetValueByState(index.State, startValue, endValue);\r\n\r\n    public static T GetValueByState<T>(TextIndex.IndexState state, T startValue, T endValue) =>\r\n        state switch\r\n        {\r\n            TextIndex.IndexState.Start => startValue,\r\n            TextIndex.IndexState.End => endValue,\r\n            _ => throw new ArgumentOutOfRangeException(nameof(state)),\r\n        };\r\n\r\n    public static T GetValueByState<T>(TextIndex index, Func<T> startValue, Func<T> endValue) =>\r\n        GetValueByState(index.State, startValue, endValue);\r\n\r\n    public static T GetValueByState<T>(TextIndex.IndexState state, Func<T> startValue, Func<T> endValue) =>\r\n        state switch\r\n        {\r\n            TextIndex.IndexState.Start => startValue(),\r\n            TextIndex.IndexState.End => endValue(),\r\n            _ => throw new ArgumentOutOfRangeException(nameof(state)),\r\n        };\r\n\r\n    /// <summary>\r\n    /// Display string with position format\r\n    /// </summary>\r\n    /// <example>\r\n    /// 3<br/>\r\n    /// 4(end)<br/>\r\n    /// </example>\r\n    /// <param name=\"textIndex\"></param>\r\n    /// <returns></returns>\r\n    public static string PositionFormattedString(TextIndex textIndex)\r\n    {\r\n        int index = textIndex.Index;\r\n        string state = GetValueByState(textIndex, string.Empty, \"(end)\");\r\n        return $\"{index}{state}\";\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Utils/TypeUtils.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Diagnostics.CodeAnalysis;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Utils;\r\n\r\npublic static class TypeUtils\r\n{\r\n    [return: NotNullIfNotNull(\"value\")]\r\n    public static TType? ChangeType<TType>(object? value)\r\n    {\r\n        if (value == null)\r\n            return default;\r\n\r\n        var type = typeof(TType);\r\n        return (TType)Convert.ChangeType(value, Nullable.GetUnderlyingType(type) ?? type);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/Utils/VersionUtils.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Development;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Utils;\r\n\r\npublic static class VersionUtils\r\n{\r\n    public static Version GetVersion()\r\n        => AssemblyUtils.GetAssemblyByName(\"osu.Game.Rulesets.Karaoke\")?.GetName().Version ?? new Version();\r\n\r\n    /// <summary>\r\n    /// Get the major version of this ruleset.\r\n    /// Will be a noun or word.\r\n    /// </summary>\r\n    /// <returns>Major version name</returns>\r\n    public static string MajorVersionName => \"UwU\";\r\n\r\n    public static bool IsDeployedBuild => GetVersion().Major > 1;\r\n\r\n    public static string DisplayVersion\r\n    {\r\n        get\r\n        {\r\n            var assemblyVersion = GetVersion();\r\n            bool isDeployedBuild = assemblyVersion.Major > 0;\r\n            if (!isDeployedBuild)\r\n                return \"local \" + (DebugUtils.IsDebugBuild ? \"debug\" : \"release\");\r\n\r\n            return $\"{assemblyVersion.Major}.{assemblyVersion.Minor}.{assemblyVersion.Build}-{MajorVersionName}\";\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke/osu.Game.Rulesets.Karaoke.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\r\n  <PropertyGroup Label=\"Project\">\r\n    <TargetFramework>net8.0</TargetFramework>\r\n    <AssemblyTitle>osu.Game.Rulesets.Karaoke</AssemblyTitle>\r\n    <OutputType>Library</OutputType>\r\n    <PlatformTarget>AnyCPU</PlatformTarget>\r\n    <RootNamespace>osu.Game.Rulesets.Karaoke</RootNamespace>\r\n  </PropertyGroup>\r\n  <ItemGroup>\r\n    <PackageReference Include=\"osu.Game.Rulesets.Karaoke.Resources\" Version=\"2022.611.0\" />\r\n    <PackageReference Include=\"LanguageDetection.karaoke-dev\" Version=\"1.3.3-alpha\" />\r\n    <PackageReference Include=\"LrcParser\" Version=\"2025.623.0\" />\r\n    <PackageReference Include=\"Octokit\" Version=\"14.0.0\" />\r\n    <PackageReference Include=\"osu.Framework.KaraokeFont\" Version=\"2025.607.0\" />\r\n    <PackageReference Include=\"osu.Framework.Microphone\" Version=\"2025.614.2\" />\r\n    <PackageReference Include=\"ppy.LocalisationAnalyser\" Version=\"2025.1208.0\">\r\n      <PrivateAssets>all</PrivateAssets>\r\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\r\n    </PackageReference>\r\n    <PackageReference Include=\"ppy.osu.Game\" Version=\"2026.408.0\" />\r\n    <PackageReference Include=\"Lucene.Net\" Version=\"4.8.0-beta00016\" />\r\n    <PackageReference Include=\"Lucene.Net.Analysis.Kuromoji\" Version=\"4.8.0-beta00016\" />\r\n    <PackageReference Include=\"SixLabors.Fonts\" Version=\"2.1.3\" />\r\n    <PackageReference Include=\"SixLabors.ImageSharp.Drawing\" Version=\"2.1.7\" />\r\n    <!--install because it might cause \"Could not load file or assembly\" error, might be removed eventually-->\r\n    <PackageReference Include=\"System.Text.Encodings.Web\" Version=\"10.0.6\" />\r\n    <PackageReference Include=\"WanaKanaSharp\" Version=\"0.2.0\" />\r\n    <PackageReference Include=\"Zipangu\" Version=\"1.1.8\" />\r\n  </ItemGroup>\r\n  <ItemGroup>\r\n    <Folder Include=\"Resources\\Samples\\Gameplay\" />\r\n  </ItemGroup>\r\n  <ItemGroup>\r\n    <AssemblyAttribute Include=\"System.Runtime.CompilerServices.InternalsVisibleTo\">\r\n      <_Parameter1>$(AssemblyName).Tests</_Parameter1>\r\n    </AssemblyAttribute>\r\n  </ItemGroup>\r\n\r\n  <!--We need to copy framework assembly to the output, else will not able to copy the file into DLLs folder.-->\r\n  <PropertyGroup Condition=\" '$(Configuration)'=='Release' \">\r\n    <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>\r\n  </PropertyGroup>\r\n\r\n  <Target Name=\"CopyCustomContent\" Condition=\" '$(Configuration)'=='Release' \" AfterTargets=\"AfterBuild\">\r\n    <ItemGroup>\r\n      <InputAssemblies Include=\"$(OutputPath)osu.Game.Rulesets.Karaoke.dll\" />\r\n      <InputAssemblies Include=\"$(OutputPath)osu.Game.Rulesets.Karaoke.Resources.dll\" />\r\n      <InputAssemblies Include=\"$(OutputPath)*/osu.Game.Rulesets.Karaoke.Resources.resources.dll\" Exclude=\"$(OutputPath)/DLLs/\" />\r\n      <InputAssemblies Include=\"$(OutputPath)LanguageDetection.dll\" />\r\n      <InputAssemblies Include=\"$(OutputPath)LrcParser.dll\" />\r\n      <InputAssemblies Include=\"$(OutputPath)Octokit.dll\" />\r\n      <InputAssemblies Include=\"$(OutputPath)osu.Framework.KaraokeFont.dll\" />\r\n      <InputAssemblies Include=\"$(OutputPath)osu.Framework.Microphone.dll\" />\r\n      <InputAssemblies Include=\"$(OutputPath)NWaves.dll\" />\r\n      <InputAssemblies Include=\"$(OutputPath)Lucene.Net.dll\" />\r\n      <InputAssemblies Include=\"$(OutputPath)Lucene.Net.Analysis.Common.dll\" />\r\n      <InputAssemblies Include=\"$(OutputPath)Lucene.Net.Analysis.Kuromoji.dll\" />\r\n      <InputAssemblies Include=\"$(OutputPath)J2N.dll\" />\r\n      <InputAssemblies Include=\"$(OutputPath)SixLabors.Fonts.dll\" />\r\n      <InputAssemblies Include=\"$(OutputPath)SixLabors.ImageSharp.Drawing.dll\" />\r\n      <InputAssemblies Include=\"$(OutputPath)WanaKanaSharp.dll\" />\r\n      <InputAssemblies Include=\"$(OutputPath)Zipangu.dll\" />\r\n    </ItemGroup>\r\n    \r\n    <Copy SourceFiles=\"@(InputAssemblies)\" DestinationFiles=\"$(OutDir)/DLLs/%(RecursiveDir)%(Filename)%(Extension)\" />\r\n  </Target>\r\n\r\n</Project>\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Architectures/BaseTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing ArchUnitNET.Domain;\r\nusing ArchUnitNET.Loader;\r\nusing NUnit.Framework;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Architectures;\r\n\r\npublic abstract class BaseTest\r\n{\r\n    private Project.ProjectAttribute? executeProject;\r\n\r\n    [SetUp]\r\n    public void SetUp()\r\n    {\r\n        executeProject = null;\r\n    }\r\n\r\n    #region Utility\r\n\r\n    protected Project.ProjectAttribute GetExecuteProject()\r\n    {\r\n        return executeProject ??= MethodUtils.GetExecuteProject();\r\n    }\r\n\r\n    protected Architecture GetProjectArchitecture(params Project.ProjectAttribute[] extraProjects)\r\n    {\r\n        // trying to get the callstack with test attribute\r\n        var rootObject = GetExecuteProject().RootObjectType;\r\n        var assembly = rootObject.Assembly;\r\n\r\n        // note:\r\n        // 1. only load the test assembly because loading too much assembly will cause the test to be slow.\r\n        // 2. should not filter the namespace in here because it will cause inner class or inherit class cannot be found.\r\n        return new ArchLoader()\r\n               .LoadAssembly(assembly)\r\n               .LoadAssemblies(extraProjects.Select(x => x.RootObjectType.Assembly).ToArray())\r\n               .Build();\r\n    }\r\n\r\n    protected void Assertion(Action assert)\r\n    {\r\n        // check the execute project type\r\n        var executeType = GetExecuteProject().ExecuteType;\r\n\r\n        switch (executeType)\r\n        {\r\n            case Project.ExecuteType.Check:\r\n            {\r\n                assert();\r\n                break;\r\n            }\r\n\r\n            case Project.ExecuteType.Report:\r\n            {\r\n                Assert.Multiple(() =>\r\n                {\r\n                    assert();\r\n\r\n                    int totalCount = TestContext.CurrentContext.AssertCount;\r\n                    int failedCount = TestContext.CurrentContext.Result.Assertions.Count();\r\n                    Console.WriteLine(\"=================================\");\r\n                    Console.WriteLine($\"There are {failedCount} failed in {totalCount} test step.\");\r\n                });\r\n\r\n                break;\r\n            }\r\n\r\n            default:\r\n                throw new ArgumentOutOfRangeException();\r\n        }\r\n    }\r\n\r\n    #endregion\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Architectures/Edit/Checks/TestCheck.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing ArchUnitNET.Domain.Extensions;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Architectures.Edit.Checks;\r\n\r\npublic class TestCheck : BaseTest\r\n{\r\n    [Test]\r\n    [Project.Karaoke(true)]\r\n    public void CheckCheckClassNamingAndInherit()\r\n    {\r\n        var architecture = GetProjectArchitecture();\r\n\r\n        var baseIssue = architecture.GetClassOfType(typeof(Issue));\r\n        var issues = architecture.Classes.Where(x => x.Namespace.RelativeNameMatches(GetExecuteProject(), \"Edit.Checks.Issues\")).ToArray();\r\n\r\n        var checkOrIssueTemplate = architecture.Classes.Where(x => x.Namespace.RelativeNameMatches(GetExecuteProject(), \"Edit.Checks\")).ToArray();\r\n\r\n        var baseCheck = architecture.GetInterfaceOfType(typeof(ICheck));\r\n        var checks = checkOrIssueTemplate.Where(x => x.IsNested == false).ToArray();\r\n\r\n        var baseIssueTemplate = architecture.GetClassOfType(typeof(IssueTemplate));\r\n        var issueTemplates = checkOrIssueTemplate.Where(x => x.IsNested).ToArray();\r\n\r\n        Assertion(() =>\r\n        {\r\n            // issues.\r\n            Assert.That(issues.Length, Is.Not.Zero, $\"{nameof(Issue)} amount is weird.\");\r\n\r\n            foreach (var checkClass in issues)\r\n            {\r\n                Assert.That(checkClass.InheritedClasses.Contains(baseIssue), $\"Class inherit is invalid: {checkClass}\");\r\n                Assert.That(checkClass.NameEndsWith(\"Issue\"), $\"Class name is invalid: {checkClass}\");\r\n            }\r\n\r\n            // checks\r\n            Assert.That(checks.Length, Is.Not.Zero, $\"{nameof(ICheck)} amount is weird.\");\r\n\r\n            foreach (var check in checks)\r\n            {\r\n                Assert.That(check.ImplementsInterface(baseCheck), $\"Class inherit is invalid: {check}\");\r\n                Assert.That(check.NameStartsWith(\"Check\"), $\"Class name is invalid: {check}\");\r\n            }\r\n\r\n            // issue templates.\r\n            Assert.That(issueTemplates.Length != 0, $\"{nameof(IssueTemplate)} amount is weird\");\r\n\r\n            foreach (var checkClass in issueTemplates)\r\n            {\r\n                Assert.That(checkClass.InheritedClasses.Contains(baseIssueTemplate), $\"Class inherit is invalid: {checkClass}\");\r\n                Assert.That(checkClass.NameStartsWith(\"IssueTemplate\"), $\"Class name is invalid: {checkClass}\");\r\n            }\r\n        });\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Architectures/Edit/Checks/TestCheckTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing ArchUnitNET.Domain;\r\nusing ArchUnitNET.Domain.Extensions;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Editor.Checks;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Architectures.Edit.Checks;\r\n\r\npublic class TestCheckTest : BaseTest\r\n{\r\n    [Test]\r\n    [Project.KaraokeTest(true)]\r\n    public void CheckShouldContainsTest()\r\n    {\r\n        var architecture = GetProjectArchitecture(new Project.KaraokeAttribute());\r\n\r\n        var baseCheckTest = architecture.GetClassOfType(typeof(BaseCheckTest<>));\r\n        var allChecks = architecture.Classes\r\n                                    .Where(x => x.Namespace.RelativeNameMatches(new Project.KaraokeAttribute(), \"Edit.Checks\"))\r\n                                    .Where(x => x.IsNested == false && x.IsAbstract == false)\r\n                                    .ToArray();\r\n        var allCheckTests = architecture.Classes.Where(x => x.InheritedClasses.Contains(baseCheckTest)).ToArray();\r\n\r\n        Assertion(() =>\r\n        {\r\n            Assert.That(allChecks.Length, Is.Not.Zero, \"No check found\");\r\n\r\n            foreach (var check in allChecks)\r\n            {\r\n                // need to make sure that all checks have a test class.\r\n                var matchedTest = allCheckTests.FirstOrDefault(x => x.NameContains(check.Name));\r\n                Assert.That(matchedTest, Is.Not.Null, $\"Check {check} should have a test class.\");\r\n\r\n                // need to make sure that all issue template should be tested.\r\n                var innerIssueTemplates = check.GetInnerClasses();\r\n\r\n                foreach (var issueTemplate in innerIssueTemplates)\r\n                {\r\n                    Assert.That(check.GetTypeDependencies().Contains(issueTemplate), $\"Seems {issueTemplate} is not tested.\");\r\n                }\r\n            }\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    [Project.KaraokeTest(true)]\r\n    public void CheckTestClassAndMethod()\r\n    {\r\n        var architecture = GetProjectArchitecture(new Project.KaraokeAttribute());\r\n        var baseCheck = architecture.GetInterfaceOfType(typeof(ICheck));\r\n        var baseCheckTest = architecture.GetClassOfType(typeof(BaseCheckTest<>));\r\n\r\n        var assertOkMethod = baseCheckTest.GetMethodMembersContainsName(\"AssertOk\").FirstOrDefault();\r\n        var assertNotOkMethod = baseCheckTest.GetMethodMembersContainsName(\"AssertNotOk\").FirstOrDefault();\r\n\r\n        var allCheckTests = architecture.Classes.Where(x => x.InheritedClasses.Contains(baseCheckTest) && x.IsAbstract == false).ToArray();\r\n\r\n        Assertion(() =>\r\n        {\r\n            Assert.That(assertOkMethod, Is.Not.Null, \"AssertOk method not found\");\r\n            Assert.That(assertNotOkMethod, Is.Not.Null, \"AssertNotOk method not found\");\r\n\r\n            Assert.That(allCheckTests.Length, Is.Not.Zero, \"No check test found\");\r\n\r\n            foreach (var checkTest in allCheckTests)\r\n            {\r\n                // check the class naming.\r\n                Assert.That(isTestClassValid(checkTest, baseCheck), $\"Test class {checkTest} should have correct naming\");\r\n\r\n                // check the test method naming in the test case.\r\n                var testMethods = checkTest.GetAllTestMembers(architecture).ToArray();\r\n                Assert.That(testMethods.Length, Is.Not.Zero, $\"No test method in the {checkTest}\");\r\n\r\n                foreach (var testMethod in testMethods)\r\n                {\r\n                    Assert.That(isTestNamingValid(testMethod), $\"Test method {testMethod} should have correct naming\");\r\n                    Assert.That(isTestMethod(testMethod), $\"Test method {testMethod} should call {assertOkMethod} or {assertNotOkMethod} method.\");\r\n                }\r\n            }\r\n\r\n            return;\r\n\r\n            static bool isTestClassValid(Class testClass, Interface baseCheck)\r\n            {\r\n                var testCheck = testClass.GetGenericTypes().OfType<Class>().First(x => x.ImplementsInterface(baseCheck));\r\n                return testClass.NameStartsWith(testCheck.Name);\r\n            }\r\n\r\n            static bool isTestNamingValid(IMember testMethod)\r\n            {\r\n                var calledMethods = testMethod.GetMethodCallDependencies().FirstOrDefault(x => x.TargetMember.NameStartsWith(\"AssertNotOk\"));\r\n\r\n                if (calledMethods != null)\r\n                {\r\n                    // todo: should get the generic type from the AssertNotOk method. the end of the method should be the issue template.\r\n                    return testMethod.NameStartsWith(\"TestCheck\");\r\n                }\r\n\r\n                return true;\r\n            }\r\n\r\n            static bool isTestMethod(IMember testMethod)\r\n            {\r\n                var calledMethods = testMethod.GetCalledMethods().ToArray();\r\n                return calledMethods.Any(x => x.NameStartsWith(\"AssertOk\") || x.NameStartsWith(\"AssertNotOk\"));\r\n            }\r\n        });\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Architectures/Extensions.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing System.Text.RegularExpressions;\r\nusing ArchUnitNET.Domain;\r\nusing ArchUnitNET.Domain.Extensions;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Tests;\r\nusing Attribute = ArchUnitNET.Domain.Attribute;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Architectures;\r\n\r\npublic static class Extensions\r\n{\r\n    #region Architecture\r\n\r\n    public static IEnumerable<Class> GetAllTestClass(this Architecture architecture)\r\n    {\r\n        var testProject = new Project.KaraokeTestAttribute();\r\n        var testClasses = architecture.Classes.Where(x =>\r\n                                      {\r\n                                          if (x.Namespace.RelativeNameStartsWith(testProject, \"Helper\"))\r\n                                              return false;\r\n\r\n                                          return x.Namespace.RelativeNameStartsWith(testProject, \"\");\r\n                                      })\r\n                                      .Except(new[]\r\n                                      {\r\n                                          architecture.GetClassOfType(typeof(KaraokeTestBrowser)),\r\n                                          architecture.GetClassOfType(typeof(VisualTestRunner)),\r\n                                      }).ToArray();\r\n\r\n        if (testClasses.Length == 0)\r\n            throw new InvalidOperationException(\"No test class found in the project.\");\r\n\r\n        return testClasses;\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Class\r\n\r\n    public static IEnumerable<Class> GetInnerClasses(this Class @class)\r\n    {\r\n        // do the same things as type.GetNestedTypes();\r\n        // see: https://learn.microsoft.com/en-us/dotnet/api/system.type.getnestedtypes?view=net-8.0&redirectedfrom=MSDN#overloads\r\n        // note: cannot use @class.GetNestedTypes(); because it will return the nested class in the same file.\r\n        return @class.GetTypeDependencies().Where(x => x.IsNested).OfType<Class>().Distinct();\r\n    }\r\n\r\n    public static IEnumerable<IMember> GetAllTestMembers(this Class @class, Architecture architecture)\r\n    {\r\n        var testAttribute = architecture.GetAttributeOfType(typeof(TestAttribute));\r\n        var testCaseAttribute = architecture.GetAttributeOfType(typeof(TestCaseAttribute));\r\n        return @class.MembersIncludingInherited.Where(x => x.Attributes.Contains(testAttribute) || x.Attributes.Contains(testCaseAttribute));\r\n    }\r\n\r\n    public static bool HasAttributeInSelfOrChild(this Class @class, Attribute attribute)\r\n    {\r\n        return @class.Attributes.Contains(attribute) || @class.InheritedClasses.Any(c => c.Attributes.Contains(attribute));\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Name\r\n\r\n    public static IEnumerable<IType> GetGenericTypes(this Class @class)\r\n    {\r\n        return @class.GetInheritsBaseClassDependencies().SelectMany(x => x.TargetGenericArguments.Select(arg => arg.Type));\r\n    }\r\n\r\n    private static readonly string[] ingored_namespace_list =\r\n    [\r\n        \"Sentry.Generated\",\r\n    ];\r\n\r\n    public static bool RelativeNameStartsWith(\r\n        this IHasName cls,\r\n        Project.ProjectAttribute project,\r\n        string pattern,\r\n        StringComparison stringComparison = StringComparison.CurrentCulture)\r\n    {\r\n        string? fullNamespace = cls.FullName;\r\n        if (ingored_namespace_list.Contains(fullNamespace))\r\n            return false;\r\n\r\n        string relativeNamespace = getRelativeNamespace(project,fullNamespace);\r\n        return relativeNamespace.StartsWith(pattern, stringComparison);\r\n    }\r\n\r\n    public static bool RelativeNameMatches(this IHasName cls,\r\n                                           Project.ProjectAttribute project,\r\n                                           string pattern,\r\n                                           bool useRegularExpressions = false)\r\n    {\r\n        string? fullNamespace = cls.FullName;\r\n        if (ingored_namespace_list.Contains(fullNamespace))\r\n            return false;\r\n\r\n        string relativeNamespace = getRelativeNamespace(project, fullNamespace);\r\n\r\n        if (!useRegularExpressions)\r\n            return string.Equals(relativeNamespace, pattern, StringComparison.OrdinalIgnoreCase);\r\n\r\n        return Regex.IsMatch(relativeNamespace, pattern);\r\n    }\r\n\r\n    private static string getRelativeNamespace(Project.ProjectAttribute project, string fullNamespace)\r\n    {\r\n        string? rootObjectNamespace = project.RootObjectType.Namespace;\r\n        if (rootObjectNamespace == null)\r\n            throw new NotSupportedException(\"Root object namespace should not be null.\");\r\n\r\n        if (!fullNamespace.StartsWith(rootObjectNamespace, StringComparison.Ordinal))\r\n            throw new NotSupportedException(\"The namespace of the class is not in the root object namespace.\");\r\n\r\n        // remove the start namespace with dot.\r\n        return fullNamespace == rootObjectNamespace\r\n            ? string.Empty\r\n            : fullNamespace[$\"{rootObjectNamespace}.\".Length..];\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Type\r\n\r\n    public static IEnumerable<MethodMember> GetMethodMembersContainsName(this IType type, string name)\r\n    {\r\n        return type.GetMethodMembers().WhereNameContains(name);\r\n    }\r\n\r\n    public static IEnumerable<TType> WhereNameContains<TType>(this IEnumerable<TType> source, string name) where TType : IHasName\r\n    {\r\n        return source.Where(hasName => hasName.Name.Contains(name));\r\n    }\r\n\r\n    #endregion\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Architectures/MethodUtils.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Diagnostics;\r\nusing System.Linq;\r\nusing System.Reflection;\r\nusing NUnit.Framework;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Architectures;\r\n\r\npublic class MethodUtils\r\n{\r\n    public static Project.ProjectAttribute GetExecuteProject()\r\n    {\r\n        // trying to get the callstack with test attribute\r\n        var stackTrace = new StackTrace();\r\n        var frames = stackTrace.GetFrames();\r\n\r\n        foreach (var frame in frames)\r\n        {\r\n            var method = frame.GetMethod();\r\n            if (method == null)\r\n                continue;\r\n\r\n            var attributes = method.CustomAttributes;\r\n            if (attributes.All(x => x.AttributeType != typeof(TestAttribute)))\r\n                continue;\r\n\r\n            return getDefaultAttributeByMethod(method);\r\n        }\r\n\r\n        throw new InvalidOperationException(\"Test method is not in the callstack.\");\r\n\r\n        static Project.ProjectAttribute getDefaultAttributeByMethod(MethodBase method)\r\n        {\r\n            var projects = method.CustomAttributes\r\n                                 .Where(x => x.AttributeType.BaseType == typeof(Project.ProjectAttribute))\r\n                                 .Select(x =>\r\n                                 {\r\n                                     object?[] constructorParams = x.ConstructorArguments.Select(args => args.Value).ToArray();\r\n                                     object? instance = Activator.CreateInstance(x.AttributeType, constructorParams);\r\n\r\n                                     if (instance is not Project.ProjectAttribute propertyAttribute)\r\n                                         throw new InvalidOperationException(\"The attribute is not found.\");\r\n\r\n                                     return propertyAttribute;\r\n                                 });\r\n\r\n            return projects.Single(x => x.Execute);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Architectures/Project.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Tests;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Architectures;\r\n\r\npublic class Project\r\n{\r\n    public sealed class KaraokeAttribute : ProjectAttribute\r\n    {\r\n        public override ExecuteType ExecuteType => ExecuteType.Check;\r\n\r\n        public override Type RootObjectType => typeof(KaraokeRuleset);\r\n\r\n        public KaraokeAttribute(bool execute = false)\r\n            : base(execute)\r\n        {\r\n        }\r\n    }\r\n\r\n    public sealed class KaraokeTestAttribute : ProjectAttribute\r\n    {\r\n        public override ExecuteType ExecuteType => ExecuteType.Check;\r\n\r\n        public override Type RootObjectType => typeof(VisualTestRunner);\r\n\r\n        public KaraokeTestAttribute(bool execute = false)\r\n            : base(execute)\r\n        {\r\n        }\r\n    }\r\n\r\n    public sealed class OsuGameAttribute : ProjectAttribute\r\n    {\r\n        public override ExecuteType ExecuteType => ExecuteType.Report;\r\n\r\n        public override Type RootObjectType => typeof(OsuGame);\r\n\r\n        public OsuGameAttribute(bool execute = false)\r\n            : base(execute)\r\n        {\r\n        }\r\n    }\r\n\r\n    public sealed class OsuFrameworkAttribute : ProjectAttribute\r\n    {\r\n        public override ExecuteType ExecuteType => ExecuteType.Report;\r\n\r\n        public override Type RootObjectType => typeof(Host);\r\n\r\n        public OsuFrameworkAttribute(bool execute = false)\r\n            : base(execute)\r\n        {\r\n        }\r\n    }\r\n\r\n    [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]\r\n    public abstract class ProjectAttribute : Attribute\r\n    {\r\n        public bool Execute { get; }\r\n\r\n        public abstract ExecuteType ExecuteType { get; }\r\n\r\n        public abstract Type RootObjectType { get; }\r\n\r\n        protected ProjectAttribute(bool execute)\r\n        {\r\n            Execute = execute;\r\n        }\r\n    }\r\n\r\n    public enum ExecuteType\r\n    {\r\n        /// <summary>\r\n        /// Make sure the project follow the architecture rule.\r\n        /// </summary>\r\n        Check,\r\n\r\n        /// <summary>\r\n        /// Get the percentage of follow/not follow the architecture rule.\r\n        /// </summary>\r\n        Report,\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Architectures/TestClass.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing ArchUnitNET.Domain;\r\nusing ArchUnitNET.Domain.Extensions;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.Containers;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Containers;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Timeline;\r\nusing osu.Game.Rulesets.Karaoke.UI.Scrolling;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Architectures;\r\n\r\npublic class TestClass : BaseTest\r\n{\r\n    [Test]\r\n    [Project.Karaoke(true)]\r\n    [Project.KaraokeTest]\r\n    [Project.OsuFramework]\r\n    [Project.OsuGame]\r\n    public void CheckAbstractClassLocation()\r\n    {\r\n        var architecture = GetProjectArchitecture();\r\n        var abstractClasses = GetExecuteProject() switch\r\n        {\r\n            Project.KaraokeAttribute => architecture.Classes.Where(x => x.IsAbstract == true).Except(new[]\r\n            {\r\n                architecture.GetClassOfType(typeof(ScrollingNotePlayfield)),\r\n                architecture.GetClassOfType(typeof(BindableScrollContainer)),\r\n                architecture.GetClassOfType(typeof(OrderRearrangeableListContainer<>)),\r\n                architecture.GetClassOfType(typeof(EditableTimelineSelectionBlueprint<>)),\r\n            }),\r\n            _ => architecture.Classes.Where(x => x.IsAbstract == true),\r\n        };\r\n\r\n        Assertion(() =>\r\n        {\r\n            foreach (var abstractClass in abstractClasses)\r\n            {\r\n                var allChildClasses = architecture.Classes.Where(x => x.InheritedClasses.Contains(abstractClass)).ToArray();\r\n                if (allChildClasses.Length == 0)\r\n                    continue;\r\n\r\n                Assert.That(isAllowAbstractClassPosition(abstractClass, allChildClasses), $\"Those child class: \\n{string.Join('\\n', allChildClasses.Select(x => x.ToString()))}\\n\\n is not in the child namespace of: \\n{abstractClass}\");\r\n            }\r\n        });\r\n        return;\r\n\r\n        static bool isAllowAbstractClassPosition(IType abstractClass, Class[] allChildClasses)\r\n        {\r\n            // follow the ModelBackedDrawable in the osu.framework, we allow the abstract class in here.\r\n            if (abstractClass.Namespace.NameContains(\"Graphics\"))\r\n                return true;\r\n\r\n            // should only check the case if all child class's namespace is not related to the parent class.\r\n            int childClassInValidNamespace = allChildClasses.Select(x => x.Namespace.FullName).Count(x => x.Contains(abstractClass.Namespace.FullName));\r\n            int childClassInInvalidNamespace = allChildClasses.Select(x => x.Namespace.FullName).Count(x => !x.Contains(abstractClass.Namespace.FullName));\r\n\r\n            if (childClassInInvalidNamespace <= childClassInValidNamespace)\r\n                return true;\r\n\r\n            return false;\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Architectures/TestTestClass.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing ArchUnitNET.Domain.Extensions;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Testing;\r\nusing osu.Game.Tests.Visual;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Architectures;\r\n\r\npublic class TestTestClass : BaseTest\r\n{\r\n    [Test]\r\n    [Project.KaraokeTest(true)]\r\n    public void CheckTestClassAndMethodNaming()\r\n    {\r\n        var architecture = GetProjectArchitecture();\r\n        var baseTestScenes = new[]\r\n        {\r\n            architecture.GetClassOfType(typeof(EditorClockTestScene)),\r\n            architecture.GetClassOfType(typeof(ModTestScene)),\r\n            architecture.GetClassOfType(typeof(ModFailConditionTestScene)),\r\n            architecture.GetClassOfType(typeof(OsuGridTestScene)),\r\n            architecture.GetClassOfType(typeof(OsuManualInputManagerTestScene)),\r\n            architecture.GetClassOfType(typeof(OsuTestScene)),\r\n            architecture.GetClassOfType(typeof(PlayerTestScene)),\r\n            architecture.GetClassOfType(typeof(ScreenTestScene)),\r\n            architecture.GetClassOfType(typeof(SkinnableTestScene)),\r\n        };\r\n        var headlessTestScene = architecture.GetAttributeOfType(typeof(HeadlessTestAttribute));\r\n\r\n        // get all test classes\r\n        var testClasses = architecture.GetAllTestClass().ToArray();\r\n\r\n        Assertion(() =>\r\n        {\r\n            foreach (var testClass in testClasses)\r\n            {\r\n                var testMethods = testClass.GetAllTestMembers(architecture).ToArray();\r\n\r\n                // skip test class without test method. it might not be the test class.\r\n                if (testMethods.Length == 0)\r\n                    continue;\r\n\r\n                // test the test class.\r\n                if (testClass.InheritedClasses.Any(x => baseTestScenes.Contains(x)))\r\n                {\r\n                    if (testClass.HasAttributeInSelfOrChild(headlessTestScene))\r\n                    {\r\n                        Assert.That(testClass.NameEndsWith(\"Test\"), $\"Test class {testClass} should end with 'Test'.\");\r\n                    }\r\n                    else if (testClass.IsAbstract == true)\r\n                    {\r\n                        bool testEndWithTest = testClass.NameEndsWith(\"TestScene\") || testClass.NameEndsWith(\"TestScene`2\");\r\n                        Assert.That(testEndWithTest, $\"Test class {testClass} should end with 'TestScene'.\");\r\n                    }\r\n                    else\r\n                    {\r\n                        Assert.That(testClass.NameStartsWith(\"TestScene\"), $\"Test class {testClass} should start with 'TestScene'.\");\r\n                    }\r\n                }\r\n                else\r\n                {\r\n                    bool testEndWithTest = testClass.NameEndsWith(\"Test\") || testClass.NameEndsWith(\"Test`1\") || testClass.NameEndsWith(\"Test`2\");\r\n                    Assert.That(testEndWithTest, $\"Test class {testClass} should end with 'Test'.\");\r\n                }\r\n\r\n                // test all test methods in the test class.\r\n                foreach (var testMethod in testMethods)\r\n                {\r\n                    Assert.That(testMethod.NameStartsWith(\"Test\"), $\"Test method {testMethod} should start with 'Test'.\");\r\n                }\r\n            }\r\n        });\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Architectures/osu.Game.Rulesets.Karaoke.Architectures.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\r\n  <PropertyGroup Label=\"Project\">\r\n    <OutputType>WinExe</OutputType>\r\n    <RootNamespace>osu.Game.Rulesets.Karaoke.Architectures</RootNamespace>\r\n    <TargetFramework>net8.0</TargetFramework>\r\n  </PropertyGroup>\r\n  <PropertyGroup>\r\n    <GenerateProgramFile>false</GenerateProgramFile>\r\n  </PropertyGroup>\r\n  <ItemGroup>\r\n    <ProjectReference Include=\"..\\osu.Game.Rulesets.Karaoke.Tests\\osu.Game.Rulesets.Karaoke.Tests.csproj\" />\r\n  </ItemGroup>\r\n  <ItemGroup>\r\n    <PackageReference Include=\"Microsoft.NET.Test.Sdk\" Version=\"18.0.1\" />\r\n    <PackageReference Include=\"TngTech.ArchUnitNET\" Version=\"0.13.1\" />\r\n  </ItemGroup>\r\n</Project>\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/.vscode/launch.json",
    "content": "{\n    \"version\": \"0.2.0\",\n    \"configurations\": [\n        {\n            \"name\": \"VisualTests (Debug)\",\n            \"type\": \"coreclr\",\n            \"request\": \"launch\",\n            \"program\": \"dotnet\",\n            \"args\": [\n                \"${workspaceRoot}/bin/Debug/netcoreapp2.2/osu.Game.Rulesets.KaraokeRuleset.Tests.dll\"\n            ],\n            \"cwd\": \"${workspaceRoot}\",\n            \"preLaunchTask\": \"Build (Debug)\",\n            \"env\": {},\n            \"console\": \"internalConsole\"\n        },\n        {\n            \"name\": \"VisualTests (Release)\",\n            \"type\": \"coreclr\",\n            \"request\": \"launch\",\n            \"program\": \"dotnet\",\n            \"args\": [\n                \"${workspaceRoot}/bin/Release/netcoreapp2.2/osu.Game.Rulesets.KaraokeRuleset.Tests.dll\"\n            ],\n            \"cwd\": \"${workspaceRoot}\",\n            \"preLaunchTask\": \"Build (Release)\",\n            \"env\": {},\n            \"console\": \"internalConsole\"\n        }\n    ]\n}"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/.vscode/tasks.json",
    "content": "{\n    // See https://go.microsoft.com/fwlink/?LinkId=733558\n    // for the documentation about the tasks.json format\n    \"version\": \"2.0.0\",\n    \"tasks\": [\n        {\n            \"label\": \"Build (Debug)\",\n            \"type\": \"shell\",\n            \"command\": \"dotnet\",\n            \"args\": [\n                \"build\",\n                \"--no-restore\",\n                \"osu.Game.Rulesets.KaraokeRuleset.Tests.csproj\",\n                \"/p:GenerateFullPaths=true\",\n                \"/m\",\n                \"/verbosity:m\"\n            ],\n            \"group\": \"build\",\n            \"problemMatcher\": \"$msCompile\"\n        },\n        {\n            \"label\": \"Build (Release)\",\n            \"type\": \"shell\",\n            \"command\": \"dotnet\",\n            \"args\": [\n                \"build\",\n                \"--no-restore\",\n                \"osu.Game.Rulesets.KaraokeRuleset.Tests.csproj\",\n                \"/p:Configuration=Release\",\n                \"/p:GenerateFullPaths=true\",\n                \"/m\",\n                \"/verbosity:m\"\n            ],\n            \"group\": \"build\",\n            \"problemMatcher\": \"$msCompile\"\n        },\n        {\n            \"label\": \"Restore\",\n            \"type\": \"shell\",\n            \"command\": \"dotnet\",\n            \"args\": [\n                \"restore\"\n            ],\n            \"problemMatcher\": []\n        }\n    ]\n}"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Asserts/ObjectAssert.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing Newtonsoft.Json;\r\nusing Newtonsoft.Json.Serialization;\r\nusing NUnit.Framework;\r\nusing osu.Game.IO.Serialization;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Asserts;\r\n\r\npublic class ObjectAssert : Assert\r\n{\r\n    public static void ArePropertyEqual<T>(T expected, T actual) where T : class\r\n    {\r\n        var settings = JsonSerializableExtensions.CreateGlobalSettings();\r\n        settings.ContractResolver = new WritablePropertiesOnlyResolver();\r\n\r\n        string expectJsonString = JsonConvert.SerializeObject(expected, settings);\r\n        string actualJsonString = JsonConvert.SerializeObject(actual, settings);\r\n\r\n        That(expectJsonString, Is.EqualTo(actualJsonString));\r\n    }\r\n\r\n    private class WritablePropertiesOnlyResolver : DefaultContractResolver\r\n    {\r\n        // we only wants to save properties that only writable.\r\n        protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)\r\n        {\r\n            var props = base.CreateProperties(type, memberSerialization);\r\n            return props.Where(p => p.Writable).ToList();\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Asserts/RubyTagAssert.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Asserts;\r\n\r\npublic class RubyTagAssert : Assert\r\n{\r\n    public static void ArePropertyEqual(IList<RubyTag> expected, IList<RubyTag> actual)\r\n    {\r\n        That(expected.Count, Is.EqualTo(actual.Count));\r\n\r\n        for (int i = 0; i < expected.Count; i++)\r\n        {\r\n            ArePropertyEqual(expected[i], actual[i]);\r\n            ArePropertyEqual(expected[i], actual[i]);\r\n        }\r\n    }\r\n\r\n    public static void ArePropertyEqual(RubyTag expected, RubyTag actual)\r\n    {\r\n        That(expected.Text, Is.EqualTo(actual.Text));\r\n        That(expected.StartIndex, Is.EqualTo(actual.StartIndex));\r\n        That(expected.EndIndex, Is.EqualTo(actual.EndIndex));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Asserts/TimeTagAssert.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Asserts;\r\n\r\npublic class TimeTagAssert : Assert\r\n{\r\n    public static void ArePropertyEqual(IList<TimeTag> expected, IList<TimeTag> actual)\r\n    {\r\n        That(expected.Count, Is.EqualTo(actual.Count));\r\n\r\n        for (int i = 0; i < expected.Count; i++)\r\n        {\r\n            ArePropertyEqual(expected[i], actual[i]);\r\n        }\r\n    }\r\n\r\n    public static void ArePropertyEqual(TimeTag expect, TimeTag actually)\r\n    {\r\n        That(expect.Index, Is.EqualTo(actually.Index));\r\n        That(expect.Time, Is.EqualTo(actually.Time));\r\n        That(expect.FirstSyllable, Is.EqualTo(actually.FirstSyllable));\r\n        That(expect.RomanisedSyllable, Is.EqualTo(actually.RomanisedSyllable));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Beatmaps/ElementIdTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Beatmaps;\r\n\r\npublic class ElementIdTest\r\n{\r\n    [Test]\r\n    public void TestEmptyElementId()\r\n    {\r\n        var emptyElementId = ElementId.Empty;\r\n\r\n        // Should get empty string if the element is empty.\r\n        Assert.That(emptyElementId.ToString(), Is.Empty);\r\n\r\n        // Should be equal to empty element id.\r\n        Assert.That(emptyElementId, Is.EqualTo(ElementId.Empty));\r\n        Assert.That(emptyElementId, Is.Not.EqualTo(ElementId.NewElementId()));\r\n    }\r\n\r\n    [TestCase(\"1234567\", true)] // number is OK\r\n    [TestCase(\"0abcdef\", true)] // alphabet is OK\r\n    [TestCase(\"0ABCDEF\", false)] // upper case is not allowed.\r\n    [TestCase(\"abcdefg\", false)] // alphabet should be within a~f\r\n    [TestCase(\"xyz7890\", false)] // alphabet should be within a~f\r\n    [TestCase(\"123456\", false)] // should be 7 digits\r\n    [TestCase(\"abcdefgh\", false)] // should be 7 digits\r\n    [TestCase(\"\", false)] // should be 7 digits\r\n    public void TestCreateElementId(string id, bool created)\r\n    {\r\n        if (created)\r\n        {\r\n            Assert.DoesNotThrow(() =>\r\n            {\r\n                _ = new ElementId(id);\r\n            });\r\n        }\r\n        else\r\n        {\r\n            Assert.Throws<ArgumentException>(() =>\r\n            {\r\n                _ = new ElementId(id);\r\n            });\r\n        }\r\n    }\r\n\r\n    [Test]\r\n    public void TestNewElementIdShouldNotDuplicated()\r\n    {\r\n        const int create_amount = 1000;\r\n\r\n        // Arrange\r\n        HashSet<ElementId> idSet = new HashSet<ElementId>();\r\n\r\n        for (int i = 0; i < create_amount; i++)\r\n        {\r\n            var elementId = ElementId.NewElementId();\r\n            idSet.Add(elementId);\r\n        }\r\n\r\n        Assert.That(idSet.Count, Is.EqualTo(create_amount));\r\n    }\r\n\r\n    [TestCase(\"1234567\", \"1234567\", 0)]\r\n    [TestCase(\"1234567\", \"2345678\", -1)]\r\n    [TestCase(\"2345678\", \"1234567\", 1)]\r\n    public void TestCompareTo(string a, string b, int expected)\r\n    {\r\n        var elementIdA = new ElementId(a);\r\n        var elementIdB = new ElementId(b);\r\n\r\n        int actual = elementIdA.CompareTo(elementIdB);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [Test]\r\n    public void TestCompareToOther()\r\n    {\r\n        var elementId = new ElementId(\"1234567\");\r\n\r\n        Assert.That(elementId.CompareTo(null), Is.EqualTo(1));\r\n        Assert.Throws<ArgumentException>(() =>\r\n        {\r\n            int _ = elementId.CompareTo(3);\r\n        });\r\n        Assert.Throws<ArgumentException>(() =>\r\n        {\r\n            // should not compare to other type\r\n            int _ = elementId.CompareTo(\"123\");\r\n        });\r\n        Assert.DoesNotThrow(() =>\r\n        {\r\n            // should not compare to the string also.\r\n            int _ = elementId.CompareTo(new ElementId(\"1234567\"));\r\n        });\r\n    }\r\n\r\n    [TestCase(\"1234567\", \"1234567\", true)]\r\n    [TestCase(\"1234567\", \"7654321\", false)]\r\n    public void TestEqual(string a, string b, bool expected)\r\n    {\r\n        var elementIdA = new ElementId(a);\r\n        var elementIdB = new ElementId(b);\r\n\r\n        bool actual = elementIdA.Equals(elementIdB);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(\"1234567\", \"1234567\", true)]\r\n    [TestCase(\"1234567\", \"7654321\", false)]\r\n    public void TestEqualWithEqualityComparer(string a, string b, bool expected)\r\n    {\r\n        var elementIdA = new ElementId(a);\r\n        var elementIdB = new ElementId(b);\r\n\r\n        bool actual = EqualityComparer<object>.Default.Equals(elementIdA, elementIdB);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(\"1234567\", \"1234567\", true)]\r\n    [TestCase(\"1234567\", \"7654321\", false)]\r\n    public void TestEqualOperator(string a, string b, bool expected)\r\n    {\r\n        var elementIdA = new ElementId(a);\r\n        var elementIdB = new ElementId(b);\r\n\r\n        bool actual = elementIdA == elementIdB;\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(\"1234567\", \"1234567\", false)]\r\n    [TestCase(\"1234567\", \"7654321\", true)]\r\n    public void TestNotEqualOperator(string a, string b, bool expected)\r\n    {\r\n        var elementIdA = new ElementId(a);\r\n        var elementIdB = new ElementId(b);\r\n\r\n        bool actual = elementIdA != elementIdB;\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(\"1234567\", \"1234567\")]\r\n    [TestCase(\"7654321\", \"7654321\")]\r\n    public void TestToString(string a, string expected)\r\n    {\r\n        var elementId = new ElementId(a);\r\n\r\n        Assert.That(expected, Is.EqualTo(elementId.ToString()));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Beatmaps/Formats/KaraokeLegacyBeatmapDecoderTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Beatmaps.Formats;\r\nusing osu.Game.IO;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Formats;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Resources;\r\nusing osu.Game.Rulesets.Mods;\r\nusing osu.Game.Tests.Beatmaps;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Beatmaps.Formats;\r\n\r\n[TestFixture]\r\npublic class KaraokeLegacyBeatmapDecoderTest\r\n{\r\n    public KaraokeLegacyBeatmapDecoderTest()\r\n    {\r\n        // It's a tricky to let osu! to read karaoke testing beatmap\r\n        KaraokeLegacyBeatmapDecoder.Register();\r\n    }\r\n\r\n    [Test]\r\n    public void TestDecodeBeatmapLyric()\r\n    {\r\n        using var resStream = TestResources.OpenBeatmapResource(\"karaoke-file-samples\");\r\n        using var stream = new LineBufferedReader(resStream);\r\n\r\n        var decoder = Decoder.GetDecoder<Beatmap>(stream);\r\n        Assert.That(decoder.GetType(), Is.EqualTo(typeof(KaraokeLegacyBeatmapDecoder)));\r\n\r\n        var working = new TestWorkingBeatmap(decoder.Decode(stream));\r\n\r\n        Assert.That(working.Beatmap.BeatmapVersion, Is.EqualTo(1));\r\n        Assert.That(working.GetPlayableBeatmap(new KaraokeRuleset().RulesetInfo, Array.Empty<Mod>()).BeatmapVersion, Is.EqualTo(1));\r\n\r\n        // Test lyric part decode result\r\n        var lyrics = working.Beatmap.HitObjects.OfType<Lyric>();\r\n        Assert.That(lyrics.Count(), Is.EqualTo(54));\r\n\r\n        // Test note decode part\r\n        var notes = working.Beatmap.HitObjects.OfType<Note>().Where(x => x.Display).ToList();\r\n        Assert.That(notes.Count, Is.EqualTo(36));\r\n\r\n        testNote(\"た\", 0, actualNote: notes[0]);\r\n        testNote(\"だ\", 0, actualNote: notes[1]);\r\n        testNote(\"か\", 0, actualNote: notes[2]); // 風,か\r\n        testNote(\"ぜ\", 0, actualNote: notes[3]); // 風,ぜ\r\n        testNote(\"に\", 1, actualNote: notes[4]);\r\n        testNote(\"揺\", 2, actualNote: notes[5]);\r\n        testNote(\"ら\", 3, actualNote: notes[6]);\r\n        testNote(\"れ\", 4, actualNote: notes[7]);\r\n        testNote(\"て\", 3, actualNote: notes[8]);\r\n    }\r\n\r\n    [Test]\r\n    public void TestDecodeNotes()\r\n    {\r\n        // Karaoke beatmap\r\n        var beatmap = decodeBeatmap(\"karaoke-note-samples\");\r\n\r\n        // Get notes\r\n        var notes = beatmap.HitObjects.OfType<Note>().ToList();\r\n\r\n        testNote(\"か\", 1, actualNote: notes[0]);\r\n        testNote(\"ら\", 2, true, notes[1]);\r\n        testNote(\"お\", 3, actualNote: notes[2]);\r\n        testNote(\"け\", 3, true, notes[3]);\r\n        testNote(\"け\", 4, actualNote: notes[4]);\r\n    }\r\n\r\n    [Test]\r\n    public void TestDecodeTranslations()\r\n    {\r\n        // Karaoke beatmap\r\n        var beatmap = decodeBeatmap(\"karaoke-translation-samples\");\r\n\r\n        // Get translations\r\n        var translations = beatmap.AvailableTranslationLanguages();\r\n        var lyrics = beatmap.HitObjects.OfType<Lyric>().ToList();\r\n\r\n        // Check is not null\r\n        Assert.That(translations, Is.Not.Null);\r\n\r\n        // Check translations count\r\n        Assert.That(translations.Count, Is.EqualTo(2));\r\n\r\n        // All lyric should have two translations\r\n        Assert.That(lyrics[0].Translations.Count, Is.EqualTo(2));\r\n        Assert.That(lyrics[1].Translations.Count, Is.EqualTo(2));\r\n\r\n        // Check chinese translations\r\n        var chineseLanguageId = translations[0];\r\n        Assert.That(lyrics[0].Translations[chineseLanguageId], Is.EqualTo(\"卡拉OK\"));\r\n        Assert.That(lyrics[1].Translations[chineseLanguageId], Is.EqualTo(\"喜歡\"));\r\n\r\n        // Check english translations\r\n        var englishLanguageId = translations[1];\r\n        Assert.That(lyrics[0].Translations[englishLanguageId], Is.EqualTo(\"karaoke\"));\r\n        Assert.That(lyrics[1].Translations[englishLanguageId], Is.EqualTo(\"like it\"));\r\n    }\r\n\r\n    private static KaraokeBeatmap decodeBeatmap(string fileName)\r\n    {\r\n        using var resStream = TestResources.OpenBeatmapResource(fileName);\r\n        using var stream = new LineBufferedReader(resStream);\r\n\r\n        // Create karaoke beatmap decoder\r\n        var decoder = new KaraokeLegacyBeatmapDecoder();\r\n\r\n        // Create initial beatmap\r\n        var beatmap = decoder.Decode(stream);\r\n\r\n        // Convert to karaoke beatmap\r\n        return (KaraokeBeatmap)new KaraokeBeatmapConverter(beatmap, new KaraokeRuleset()).Convert();\r\n    }\r\n\r\n    private static void testNote(string expectedText, int expectedTone, bool expectedHalf = false, Note actualNote = default!)\r\n    {\r\n        Assert.That(actualNote.Text, Is.EqualTo(expectedText));\r\n        Assert.That(actualNote.Tone.Scale, Is.EqualTo(expectedTone));\r\n        Assert.That(actualNote.Tone.Half, Is.EqualTo(expectedHalf));\r\n    }\r\n\r\n    [TestCase(0, 1, new double[] { 1000, 3000 })]\r\n    [TestCase(0, 0.5, new double[] { 1000, 1500 })]\r\n    [TestCase(0.5, 0.5, new double[] { 2500, 1500 })]\r\n    [TestCase(0.3, 1, null)] // start + duration should not exceed 1\r\n    public void TestSliceNoteTime(double startPercentage, double durationPercentage, double[]? expected)\r\n    {\r\n        var referencedLyric = new Lyric\r\n        {\r\n            TimeTags = TestCaseTagHelper.ParseTimeTags(new[] { \"[0,start]:1000\", \"[1,start]:4000\" }),\r\n        };\r\n\r\n        // start time will be 1000, and duration will be 3000.\r\n        var note = new Note\r\n        {\r\n            ReferenceLyricId = referencedLyric.ID,\r\n            ReferenceLyric = referencedLyric,\r\n            ReferenceTimeTagIndex = 0,\r\n        };\r\n\r\n        if (expected != null)\r\n        {\r\n            var sliceNote = KaraokeLegacyBeatmapDecoder.SliceNote(note, startPercentage, durationPercentage);\r\n            Assert.That(sliceNote.StartTime, Is.EqualTo(expected[0]));\r\n            Assert.That(sliceNote.Duration, Is.EqualTo(expected[1]));\r\n        }\r\n        else\r\n        {\r\n            Assert.That(() => KaraokeLegacyBeatmapDecoder.SliceNote(note, startPercentage, durationPercentage), Throws.Exception);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Beatmaps/Formats/KaraokeLegacyBeatmapEncoderTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.IO;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Formats;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Beatmaps.Formats;\r\n\r\n[TestFixture]\r\npublic class KaraokeLegacyBeatmapEncoderTest\r\n{\r\n    [Test]\r\n    public void TestEncodeBeatmapLyric()\r\n    {\r\n        // Because encoder is not fully implemented, so just test not crash during encoding.\r\n        const int start_time = 1000;\r\n\r\n        var beatmap = new Beatmap\r\n        {\r\n            HitObjects = new List<HitObject>\r\n            {\r\n                new Lyric\r\n                {\r\n                    Text = \"カラオケ！\",\r\n                    TimeTags = new List<TimeTag>\r\n                    {\r\n                        new(new TextIndex(0), start_time + 500),\r\n                        new(new TextIndex(1), start_time + 600)\r\n                        {\r\n                            FirstSyllable = true,\r\n                            RomanisedSyllable = \"ra\",\r\n                        },\r\n                        new(new TextIndex(2), start_time + 1000),\r\n                        new(new TextIndex(3), start_time + 1500)\r\n                        {\r\n                            FirstSyllable = true,\r\n                            RomanisedSyllable = \"ke\",\r\n                        },\r\n                        new(new TextIndex(4), start_time + 2000),\r\n                    },\r\n                    RubyTags = new[]\r\n                    {\r\n                        new RubyTag\r\n                        {\r\n                            StartIndex = 0,\r\n                            EndIndex = 0,\r\n                            Text = \"か\",\r\n                        },\r\n                        new RubyTag\r\n                        {\r\n                            StartIndex = 2,\r\n                            EndIndex = 2,\r\n                            Text = \"お\",\r\n                        },\r\n                    },\r\n                },\r\n            },\r\n        };\r\n\r\n        using var ms = new MemoryStream();\r\n        using var sw = new StreamWriter(ms);\r\n\r\n        var encoder = new KaraokeLegacyBeatmapEncoder();\r\n        string encodeResult = encoder.Encode(beatmap);\r\n        sw.WriteLine(encodeResult);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Beatmaps/KaraokeBeatmapConversionTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Formats;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Objects;\r\nusing osu.Game.Tests.Beatmaps;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Beatmaps;\r\n\r\n[TestFixture]\r\npublic class KaraokeBeatmapConversionTest : BeatmapConversionTest<ConvertValue>\r\n{\r\n    protected override string ResourceAssembly => \"osu.Game.Rulesets.Karaoke.Tests\";\r\n\r\n    public KaraokeBeatmapConversionTest()\r\n    {\r\n        // It's a tricky to let osu! to read karaoke testing beatmap\r\n        KaraokeLegacyBeatmapDecoder.Register();\r\n    }\r\n\r\n    [Ignore(\"Fix this test case after.\")]\r\n    [TestCase(\"karaoke-file-samples\")]\r\n    public void Test(string name) => base.Test(name);\r\n\r\n    protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject)\r\n    {\r\n        switch (hitObject)\r\n        {\r\n            case Lyric line:\r\n                yield return createConvertValue(line);\r\n\r\n                break;\r\n        }\r\n\r\n        static ConvertValue createConvertValue(Lyric obj) => new()\r\n        {\r\n            StartTime = obj.StartTime,\r\n            EndTime = obj.EndTime,\r\n            Lyric = obj.Text,\r\n        };\r\n    }\r\n\r\n    protected override Ruleset CreateRuleset() => new KaraokeRuleset();\r\n}\r\n\r\npublic struct ConvertValue : IEquatable<ConvertValue>\r\n{\r\n    /// <summary>\r\n    /// A sane value to account for osu!stable using <see cref=\"int\"/>s everywhere.\r\n    /// </summary>\r\n    private const double conversion_lenience = 2;\r\n\r\n    public double StartTime;\r\n    public double EndTime;\r\n    public string Lyric;\r\n\r\n    public bool Equals(ConvertValue other)\r\n        => Precision.AlmostEquals(StartTime, other.StartTime, conversion_lenience)\r\n           && Precision.AlmostEquals(EndTime, other.EndTime, conversion_lenience)\r\n           && Lyric == other.Lyric;\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Beatmaps/Metadatas/PageInfoTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Beatmaps.Metadatas;\r\n\r\npublic class PageInfoTest\r\n{\r\n    [TestCase(new double[] { 1000, 2000, 3000, 4000 }, 999, null)]\r\n    [TestCase(new double[] { 1000, 2000, 3000, 4000 }, 1000, 1000)]\r\n    [TestCase(new double[] { 1000, 2000, 3000, 4000 }, 1001, 1000)]\r\n    [TestCase(new double[] { 1000, 2000, 3000, 4000 }, 4000, 4000)]\r\n    [TestCase(new double[] { 1000, 2000, 3000, 4000 }, 4001, null)]\r\n    [TestCase(new double[] { }, 0, null)]\r\n    [TestCase(new double[] { 1000 }, 999, null)] // should be able to get the time only if time is between two pages.\r\n    [TestCase(new double[] { 1000 }, 1000, null)]\r\n    [TestCase(new double[] { 1000 }, 1002, null)]\r\n    [TestCase(new double[] { 4000, 3000, 2000, 1000 }, 1000, 1000)] // should works even not sorting.\r\n    public void TestGetPageAt(double[] times, double time, double? expectedTime)\r\n    {\r\n        var pageInfo = new PageInfo();\r\n        pageInfo.Pages.AddRange(createPages(times));\r\n\r\n        var actualPage = pageInfo.GetPageAt(time);\r\n\r\n        Assert.That(actualPage?.Time, Is.EqualTo(expectedTime));\r\n    }\r\n\r\n    [TestCase(new double[] { 1000, 2000, 3000, 4000 }, 999, null)]\r\n    [TestCase(new double[] { 1000, 2000, 3000, 4000 }, 1000, 0)]\r\n    [TestCase(new double[] { 1000, 2000, 3000, 4000 }, 1001, 0)]\r\n    [TestCase(new double[] { 1000, 2000, 3000, 4000 }, 4000, 3)]\r\n    [TestCase(new double[] { 1000, 2000, 3000, 4000 }, 4001, null)]\r\n    [TestCase(new double[] { }, 0, null)]\r\n    [TestCase(new double[] { 1000 }, 999, null)] // should be able to get the time only if time is between two pages.\r\n    [TestCase(new double[] { 1000 }, 1000, null)]\r\n    [TestCase(new double[] { 1000 }, 1002, null)]\r\n    [TestCase(new double[] { 4000, 3000, 2000, 1000 }, 1000, 0)] // should works even not sorting.\r\n    public void TestGetPageIndexAt(double[] times, double time, int? expectedIndex)\r\n    {\r\n        var pageInfo = new PageInfo();\r\n        pageInfo.Pages.AddRange(createPages(times));\r\n\r\n        int? actualPageIndex = pageInfo.GetPageIndexAt(time);\r\n\r\n        Assert.That(actualPageIndex, Is.EqualTo(expectedIndex));\r\n    }\r\n\r\n    [Test]\r\n    public void TestGetPageOrder()\r\n    {\r\n        var pageInfo = new PageInfo();\r\n        pageInfo.Pages.AddRange(createPages(new double[] { 1000 }));\r\n\r\n        var existPage = pageInfo.Pages.First();\r\n        int? existPageOrder = pageInfo.GetPageOrder(existPage);\r\n        Assert.That(existPageOrder, Is.EqualTo(1));\r\n\r\n        var notExistPage = new Page { Time = 1000 };\r\n        int? notExistPageOrder = pageInfo.GetPageOrder(notExistPage);\r\n        Assert.That(notExistPageOrder, Is.Null);\r\n    }\r\n\r\n    private static IEnumerable<Page> createPages(IEnumerable<double> times)\r\n        => times.Select(x => new Page { Time = x });\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Beatmaps/Metadatas/SingerInfoTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Beatmaps.Metadatas;\r\n\r\npublic class SingerInfoTest\r\n{\r\n    [Test]\r\n    public void TestSingers()\r\n    {\r\n        var singerInfo = new SingerInfo();\r\n        var singer = singerInfo.AddSinger();\r\n        var singerState = singerInfo.AddSingerState(singer);\r\n\r\n        Assert.That(singerInfo.Singers.Count, Is.EqualTo(1));\r\n        Assert.That(singerInfo.SingerState.Count, Is.EqualTo(1));\r\n        Assert.That(singerInfo.Singers[0], Is.EqualTo(singer));\r\n        Assert.That(singerInfo.SingerState[0], Is.EqualTo(singerState));\r\n    }\r\n\r\n    [Test]\r\n    public void TestGetAllSingers()\r\n    {\r\n        var singerInfo = new SingerInfo();\r\n        var singer = singerInfo.AddSinger();\r\n        singerInfo.AddSingerState(singer);\r\n\r\n        var allSingers = singerInfo.GetAllSingers().ToArray();\r\n        Assert.That(allSingers.Length, Is.EqualTo(1));\r\n        Assert.That(allSingers[0], Is.EqualTo(singer));\r\n    }\r\n\r\n    [Test]\r\n    public void TestGetAllAvailableSingerState()\r\n    {\r\n        var singerInfo = new SingerInfo();\r\n        var singer = singerInfo.AddSinger();\r\n        var singerState = singerInfo.AddSingerState(singer);\r\n\r\n        var singerStates = singerInfo.GetAllAvailableSingerStates(singer).ToArray();\r\n        Assert.That(singerStates.Length, Is.EqualTo(1));\r\n        Assert.That(singerStates[0], Is.EqualTo(singerState));\r\n    }\r\n\r\n    [Test]\r\n    public void TestGetSingerByIds()\r\n    {\r\n        var singerInfo = new SingerInfo();\r\n        var singer = singerInfo.AddSinger();\r\n        var singerState = singerInfo.AddSingerState(singer);\r\n\r\n        // the case if contains singer and sub-singer\r\n        var singerMapWithSingerAndStates = singerInfo.GetSingerByIds(new[] { singer.ID, singerState.ID });\r\n        Assert.That(singerMapWithSingerAndStates.Count, Is.EqualTo(1));\r\n        Assert.That(singerMapWithSingerAndStates.Keys.FirstOrDefault(), Is.EqualTo(singer));\r\n        Assert.That(singerMapWithSingerAndStates.Values.FirstOrDefault(), Is.EqualTo(new[] { singerState }));\r\n\r\n        // the case with main singer id.\r\n        var singerMapWithSingerOnly = singerInfo.GetSingerByIds(new[] { singer.ID });\r\n        Assert.That(singerMapWithSingerOnly.Count, Is.EqualTo(1));\r\n        Assert.That(singerMapWithSingerOnly.Keys.FirstOrDefault(), Is.EqualTo(singer));\r\n        Assert.That(singerMapWithSingerOnly.Values.FirstOrDefault(), Is.EqualTo(Array.Empty<SingerState>()));\r\n\r\n        // the case with sub-singer only.\r\n        // technically should not happened.\r\n        var singerMap = singerInfo.GetSingerByIds(new[] { singerState.ID });\r\n        Assert.That(singerMap.Count, Is.EqualTo(0));\r\n    }\r\n\r\n    [Test]\r\n    public void TestGetSingerMap()\r\n    {\r\n        var singerInfo = new SingerInfo();\r\n        var singer = singerInfo.AddSinger();\r\n        var singerState = singerInfo.AddSingerState(singer);\r\n\r\n        var singerMap = singerInfo.GetSingerMap();\r\n        Assert.That(singerMap.Count, Is.EqualTo(1));\r\n        Assert.That(singerMap.Keys.FirstOrDefault(), Is.EqualTo(singer));\r\n        Assert.That(singerMap.Values.FirstOrDefault(), Is.EqualTo(new[] { singerState }));\r\n    }\r\n\r\n    [Test]\r\n    public void TestAddSinger()\r\n    {\r\n        var singerInfo = new SingerInfo();\r\n        var singer = singerInfo.AddSinger();\r\n\r\n        Assert.That(singerInfo.Singers.Count, Is.EqualTo(1));\r\n        Assert.That(singer.ID.ToString(), Is.Not.Empty);\r\n    }\r\n\r\n    [Test]\r\n    public void TestAddSingerState()\r\n    {\r\n        var singerInfo = new SingerInfo();\r\n        var singer = singerInfo.AddSinger();\r\n        var singerState = singerInfo.AddSingerState(singer);\r\n\r\n        Assert.That(singerInfo.Singers.Count, Is.EqualTo(1));\r\n        Assert.That(singerInfo.SingerState.Count, Is.EqualTo(1));\r\n        Assert.That(singerState.ID.ToString(), Is.Not.Empty);\r\n        Assert.That(singerState.MainSingerId.ToString(), Is.Not.Empty);\r\n    }\r\n\r\n    [Test]\r\n    public void TestRemoveSinger()\r\n    {\r\n        var singerInfo = new SingerInfo();\r\n        var singer = singerInfo.AddSinger();\r\n        var singerState = singerInfo.AddSingerState(singer);\r\n\r\n        // should remove all the related sub-singer\r\n        singerInfo.RemoveSinger(singer);\r\n        Assert.That(singerInfo.Singers.Count, Is.EqualTo(0));\r\n\r\n        // should ignore the sub-singer\r\n        singerInfo.RemoveSinger(singerState);\r\n        Assert.That(singerInfo.Singers.Count, Is.EqualTo(0));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Beatmaps/TestKaraokeBeatmap.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Diagnostics;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Beatmaps.Formats;\r\nusing osu.Game.IO;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Resources;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Beatmaps;\r\n\r\npublic class TestKaraokeBeatmap : Beatmap\r\n{\r\n    public TestKaraokeBeatmap(RulesetInfo ruleset)\r\n    {\r\n        var baseBeatmap = createTestBeatmap();\r\n\r\n        BeatmapInfo = baseBeatmap.BeatmapInfo;\r\n        ControlPointInfo = baseBeatmap.ControlPointInfo;\r\n        Breaks = baseBeatmap.Breaks;\r\n        HitObjects = baseBeatmap.HitObjects;\r\n\r\n        BeatmapInfo.Ruleset = ruleset;\r\n\r\n        Debug.Assert(BeatmapInfo.BeatmapSet != null);\r\n\r\n        BeatmapInfo.BeatmapSet.Beatmaps.Add(BeatmapInfo);\r\n    }\r\n\r\n    private static Beatmap createTestBeatmap()\r\n    {\r\n        using var stream = TestResources.OpenBeatmapResource(\"karaoke-file-samples\");\r\n        using var reader = new LineBufferedReader(stream);\r\n\r\n        return Decoder.GetDecoder<Beatmap>(reader).Decode(reader);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Beatmaps/Utils/SingerUtilsTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Beatmaps.Utils;\r\n\r\n[TestFixture]\r\npublic class SingerUtilsTest\r\n{\r\n    [TestCase(new int[] { }, 0)]\r\n    [TestCase(new[] { 1 }, 1)]\r\n    [TestCase(new[] { 1, 2, 3 }, 7)]\r\n    [TestCase(new[] { 1, 4, 5 }, 25)]\r\n    public void TestGetShiftingStyleIndex(int[] singerIndexes, int expected)\r\n    {\r\n        int actual = SingerUtils.GetShiftingStyleIndex(singerIndexes);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(-1, new int[] { })]\r\n    [TestCase(0, new int[] { })]\r\n    [TestCase(1, new[] { 1 })]\r\n    [TestCase(7, new[] { 1, 2, 3 })]\r\n    [TestCase(25, new[] { 1, 4, 5 })]\r\n    public void TestGetSingersIndex(int styleIndex, int[] expected)\r\n    {\r\n        int[] actual = SingerUtils.GetSingersIndex(styleIndex);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Bindables/BindableCultureInfoTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Globalization;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Bindables;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Bindables;\r\n\r\npublic class BindableCultureInfoTest\r\n{\r\n    [TestCase(\"ar\", 1)]\r\n    [TestCase(\"en-US\", 1033)]\r\n    [TestCase(\"ja-JP\", 1041)]\r\n    public void TestParsingString(string value, int expectedLcid)\r\n    {\r\n        var bindable = new BindableCultureInfo();\r\n        bindable.Parse(value, CultureInfo.InvariantCulture);\r\n\r\n        var expected = new CultureInfo(expectedLcid);\r\n        var actual = bindable.Value;\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(1, 1)]\r\n    [TestCase(1033, 1033)]\r\n    [TestCase(1041, 1041)]\r\n    public void TestParsingNumber(int value, int expectedLcid)\r\n    {\r\n        var bindable = new BindableCultureInfo();\r\n        bindable.Parse(value, CultureInfo.InvariantCulture);\r\n\r\n        var expected = new CultureInfo(expectedLcid);\r\n        var actual = bindable.Value;\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(1, 1)]\r\n    [TestCase(1033, 1033)]\r\n    [TestCase(1041, 1041)]\r\n    public void TestParsingCultureInfo(int value, int expectedLcid)\r\n    {\r\n        var bindable = new BindableCultureInfo();\r\n        bindable.Parse(new CultureInfo(value), CultureInfo.InvariantCulture);\r\n\r\n        var expected = new CultureInfo(expectedLcid);\r\n        var actual = bindable.Value;\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(\"中文（简体）\")]\r\n    public void TestParsingNotSupportedCultureInfo(string value)\r\n    {\r\n        var bindable = new BindableCultureInfo();\r\n        Assert.DoesNotThrow(() => bindable.Parse(value, CultureInfo.InvariantCulture));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Bindables/BindableFontUsageTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Globalization;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Bindables;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Bindables;\r\n\r\npublic class BindableFontUsageTest\r\n{\r\n    [TestCase(\"family=f weight=w size=10 italics=true fixedWidth=true\", \"f\", 10, \"w\", true, true)]\r\n    [TestCase(\"Font=f-w Size=10 Italics=true FixedWidth=true\", \"f\", 10, \"w\", true, true)]\r\n    public void TestParsingString(string value, string family, float size, string weight = null!, bool italics = false, bool fixedWidth = false)\r\n    {\r\n        var bindable = new BindableFontUsage();\r\n        bindable.Parse(value, CultureInfo.InvariantCulture);\r\n\r\n        var expected = new FontUsage(family, size, weight, italics, fixedWidth);\r\n        var actual = bindable.Value;\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Difficulty/DifficultyCalculatorTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.IO;\r\nusing System.Reflection;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Extensions.ObjectExtensions;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Beatmaps.Formats;\r\nusing osu.Game.IO;\r\nusing osu.Game.Rulesets.Difficulty;\r\nusing osu.Game.Rulesets.Mods;\r\nusing osu.Game.Tests.Beatmaps;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Difficulty;\r\n\r\n/// <summary>\r\n/// Copy the class from the osu.Game because this project and osu.game use different version nUnit, which might cause \"method not found\" exception.\r\n/// todo: this class can be removed once osu.game upgrade the nUnit to 4.x\r\n/// </summary>\r\n[TestFixture]\r\npublic abstract class DifficultyCalculatorTest\r\n{\r\n    private const string resource_namespace = \"Testing.Beatmaps\";\r\n\r\n    protected abstract string ResourceAssembly { get; }\r\n\r\n    protected void Test(double expectedStarRating, int expectedMaxCombo, string name, params Mod[] mods)\r\n    {\r\n        var attributes = CreateDifficultyCalculator(getBeatmap(name)).Calculate(mods);\r\n\r\n        // Platform-dependent math functions (Pow, Cbrt, Exp, etc) may result in minute differences.\r\n        Assert.That(attributes.StarRating, Is.EqualTo(expectedStarRating).Within(0.00001));\r\n        Assert.That(attributes.MaxCombo, Is.EqualTo(expectedMaxCombo));\r\n    }\r\n\r\n    private IWorkingBeatmap getBeatmap(string name)\r\n    {\r\n        using (var resStream = openResource($\"{resource_namespace}.{name}.osu\"))\r\n        using (var stream = new LineBufferedReader(resStream))\r\n        {\r\n            var decoder = Decoder.GetDecoder<Beatmap>(stream);\r\n\r\n            ((LegacyBeatmapDecoder)decoder).ApplyOffsets = false;\r\n\r\n            return new TestWorkingBeatmap(decoder.Decode(stream))\r\n            {\r\n                BeatmapInfo =\r\n                {\r\n                    Ruleset = CreateRuleset().RulesetInfo\r\n                }\r\n            };\r\n        }\r\n    }\r\n\r\n    private Stream openResource(string name)\r\n    {\r\n        string localPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location).AsNonNull();\r\n        return Assembly.LoadFrom(Path.Combine(localPath, $\"{ResourceAssembly}.dll\")).GetManifestResourceStream($@\"{ResourceAssembly}.Resources.{name}\")!;\r\n    }\r\n\r\n    protected abstract DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap);\r\n\r\n    protected abstract Ruleset CreateRuleset();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Difficulty/KaraokeDifficultyCalculatorTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Rulesets.Difficulty;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Formats;\r\nusing osu.Game.Rulesets.Karaoke.Difficulty;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Difficulty;\r\n\r\n[TestFixture]\r\npublic class KaraokeDifficultyCalculatorTest : DifficultyCalculatorTest\r\n{\r\n    public KaraokeDifficultyCalculatorTest()\r\n    {\r\n        // It's a tricky to let osu! to read karaoke testing beatmap\r\n        KaraokeLegacyBeatmapDecoder.Register();\r\n    }\r\n\r\n    protected override string ResourceAssembly => \"osu.Game.Rulesets.Karaoke.Tests\";\r\n\r\n    [TestCase(1.7535600395779332d, 936, \"karaoke-file-samples\")]\r\n    [TestCase(1.6991673944010039d, 924, \"karaoke-file-samples-without-note\")]\r\n    public void Test(double expectedStarRating, int expectedMaxCombo, string name)\r\n        => base.Test(expectedStarRating, expectedMaxCombo, name);\r\n\r\n    protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new KaraokeDifficultyCalculator(new KaraokeRuleset().RulesetInfo, beatmap);\r\n\r\n    protected override Ruleset CreateRuleset() => new KaraokeRuleset();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/ChangeHandlers/BaseChangeHandlerTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Testing;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Objects;\r\nusing osu.Game.Screens.Edit;\r\nusing osu.Game.Tests.Visual;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.ChangeHandlers;\r\n\r\n/// <summary>\r\n/// it's a base class for testing all change handler.\r\n/// Should inherit <see cref=\"OsuTestScene\"/> because all change handler need the injecting to get the value.\r\n/// </summary>\r\n[HeadlessTest]\r\npublic abstract partial class BaseChangeHandlerTest<TChangeHandler> : EditorClockTestScene where TChangeHandler : Component\r\n{\r\n    private TChangeHandler changeHandler = null!;\r\n\r\n    private int transactionCount;\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        var beatmap = new KaraokeBeatmap\r\n        {\r\n            BeatmapInfo =\r\n            {\r\n                Ruleset = new KaraokeRuleset().RulesetInfo,\r\n            },\r\n        };\r\n        var editorBeatmap = new EditorBeatmap(beatmap);\r\n        var editorChangeHandler = new MockEditorChangeHandler(editorBeatmap);\r\n        Dependencies.Cache(editorBeatmap);\r\n        Dependencies.CacheAs<IEditorChangeHandler>(editorChangeHandler);\r\n        editorChangeHandler.TransactionEnded += () =>\r\n        {\r\n            transactionCount++;\r\n        };\r\n\r\n        Children = new Drawable[]\r\n        {\r\n            editorBeatmap,\r\n            changeHandler = CreateChangeHandler(),\r\n        };\r\n    }\r\n\r\n    protected virtual TChangeHandler CreateChangeHandler()\r\n        => Activator.CreateInstance<TChangeHandler>();\r\n\r\n    [SetUp]\r\n    public virtual void SetUp()\r\n    {\r\n        AddStep(\"Setup\", () =>\r\n        {\r\n            var editorBeatmap = Dependencies.Get<EditorBeatmap>();\r\n            editorBeatmap.Clear();\r\n            editorBeatmap.SelectedHitObjects.Clear();\r\n        });\r\n\r\n        // Should set-up karaoke beatmap before testing.\r\n        // still able to call the SetUpKaraokeBeatmap or SetUpEditorBeatmap in the test case.\r\n        SetUpKaraokeBeatmap(_ => { });\r\n    }\r\n\r\n    protected virtual bool IncludeAutoGenerator => false;\r\n\r\n    protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)\r\n    {\r\n        if (!IncludeAutoGenerator)\r\n        {\r\n            return base.CreateChildDependencies(parent);\r\n        }\r\n\r\n        var baseDependencies = new DependencyContainer(base.CreateChildDependencies(parent));\r\n        baseDependencies.Cache(new KaraokeRulesetEditGeneratorConfigManager());\r\n        return baseDependencies;\r\n    }\r\n\r\n    protected virtual void SetUpEditorBeatmap(Action<EditorBeatmap> action)\r\n    {\r\n        AddStep(\"Prepare testing beatmap\", () =>\r\n        {\r\n            var editorBeatmap = Dependencies.Get<EditorBeatmap>();\r\n            action(editorBeatmap);\r\n        });\r\n    }\r\n\r\n    protected virtual void SetUpKaraokeBeatmap(Action<KaraokeBeatmap> action)\r\n    {\r\n        SetUpEditorBeatmap(editorBeatmap =>\r\n        {\r\n            var karaokeBeatmap = EditorBeatmapUtils.GetPlayableBeatmap(editorBeatmap);\r\n            action(karaokeBeatmap);\r\n        });\r\n    }\r\n\r\n    protected void TriggerHandlerChanged(Action<TChangeHandler> c)\r\n    {\r\n        AddStep(\"Trigger change handler\", () =>\r\n        {\r\n            // should reset transaction number in here because it will increase if load testing object.\r\n            transactionCount = 0;\r\n            c(changeHandler);\r\n        });\r\n    }\r\n\r\n    protected void TriggerHandlerChangedWithException<T>(Action<TChangeHandler> c) where T : Exception\r\n    {\r\n        TriggerHandlerChanged(ch =>\r\n        {\r\n            Assert.Throws<T>(() => c(ch));\r\n        });\r\n    }\r\n\r\n    protected void AssertEditorBeatmap(Action<EditorBeatmap> assert)\r\n    {\r\n        AddStep(\"Is result matched\", () =>\r\n        {\r\n            var editorBeatmap = Dependencies.Get<EditorBeatmap>();\r\n            assert(editorBeatmap);\r\n        });\r\n\r\n        AssertStatus();\r\n    }\r\n\r\n    protected void AssertKaraokeBeatmap(Action<KaraokeBeatmap> assert)\r\n    {\r\n        AssertEditorBeatmap(editorBeatmap =>\r\n        {\r\n            var karaokeBeatmap = EditorBeatmapUtils.GetPlayableBeatmap(editorBeatmap);\r\n            assert(karaokeBeatmap);\r\n        });\r\n    }\r\n\r\n    protected void PrepareHitObject(Func<HitObject> hitObject, bool selected = true)\r\n        => PrepareHitObjects(() => new[] { hitObject() }, selected);\r\n\r\n    protected void PrepareHitObjects(Func<IEnumerable<HitObject>> selectedHitObjects, bool selected = true)\r\n    {\r\n        AddStep(\"Prepare testing hit objects\", () =>\r\n        {\r\n            var hitobjects = selectedHitObjects().ToList();\r\n            var editorBeatmap = Dependencies.Get<EditorBeatmap>();\r\n\r\n            editorBeatmap.AddRange(hitobjects);\r\n\r\n            if (selected)\r\n            {\r\n                editorBeatmap.SelectedHitObjects.AddRange(hitobjects);\r\n            }\r\n        });\r\n    }\r\n\r\n    protected void AssertHitObject<THitObject>(Action<THitObject> assert) where THitObject : HitObject\r\n    {\r\n        AssertHitObjects<THitObject>(hitObjects =>\r\n        {\r\n            foreach (var hitObject in hitObjects)\r\n            {\r\n                assert(hitObject);\r\n            }\r\n        });\r\n    }\r\n\r\n    protected void AssertHitObjects<THitObject>(Action<IEnumerable<THitObject>> assert) where THitObject : HitObject\r\n    {\r\n        AddStep(\"Is result matched\", () =>\r\n        {\r\n            var editorBeatmap = Dependencies.Get<EditorBeatmap>();\r\n            assert(editorBeatmap.HitObjects.OfType<THitObject>());\r\n        });\r\n\r\n        AssertStatus();\r\n    }\r\n\r\n    protected void AssertStatus()\r\n    {\r\n        // even if there's no property changed in the lyric editor, should still trigger the change handler.\r\n        // because every change handler call should cause one undo step.\r\n        // also, technically should not call the change handler if there's no possible to change the properties.\r\n        AssertTransactionOnlyTriggerOnce();\r\n\r\n        // We should make sure that if the working property is changed by the change handler.\r\n        // Should trigger the beatmap editor to run the beatmap processor to re-fill the working property.\r\n        AssertWorkingPropertyInHitObjectValid();\r\n    }\r\n\r\n    protected void AssertTransactionOnlyTriggerOnce()\r\n    {\r\n        AddStep(\"Transaction should be only triggered once.\", () =>\r\n        {\r\n            Assert.That(transactionCount, Is.EqualTo(1));\r\n        });\r\n    }\r\n\r\n    protected void AssertWorkingPropertyInHitObjectValid()\r\n    {\r\n        AddWaitStep(\"Waiting for working property being re-filled in the beatmap processor.\", 1);\r\n        AddAssert(\"Check if working property in the hit object is valid\", () =>\r\n        {\r\n            var editorBeatmap = Dependencies.Get<EditorBeatmap>();\r\n\r\n            return editorBeatmap.HitObjects.OfType<KaraokeHitObject>().All(hitObject => hitObject switch\r\n            {\r\n                Lyric lyric => lyric.GetAllInvalidWorkingProperties().Length == 0,\r\n                Note note => note.GetAllInvalidWorkingProperties().Length == 0,\r\n                _ => throw new NotSupportedException(),\r\n            });\r\n        });\r\n    }\r\n\r\n    private partial class MockEditorChangeHandler : TransactionalCommitComponent, IEditorChangeHandler\r\n    {\r\n        public event Action? OnStateChange;\r\n\r\n        public MockEditorChangeHandler(EditorBeatmap editorBeatmap)\r\n        {\r\n            editorBeatmap.TransactionBegan += BeginChange;\r\n            editorBeatmap.TransactionEnded += EndChange;\r\n            editorBeatmap.SaveStateTriggered += SaveState;\r\n        }\r\n\r\n        public void RestoreState(int direction)\r\n        {\r\n        }\r\n\r\n        protected override void UpdateState()\r\n        {\r\n            OnStateChange?.Invoke();\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/ChangeHandlers/BaseHitObjectChangeHandlerTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Framework.Allocation;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers;\r\nusing osu.Game.Rulesets.Objects;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.ChangeHandlers;\r\n\r\npublic abstract partial class BaseHitObjectChangeHandlerTest<TChangeHandler, THitObject> : BaseChangeHandlerTest<TChangeHandler>\r\n    where TChangeHandler : HitObjectChangeHandler<THitObject>, new() where THitObject : HitObject\r\n{\r\n    protected void AssertHitObject(Action<THitObject> assert)\r\n    {\r\n        AssertHitObject<THitObject>(assert);\r\n    }\r\n\r\n    protected void AssertHitObjects(Action<IEnumerable<THitObject>> assert)\r\n    {\r\n        AssertHitObjects<THitObject>(assert);\r\n    }\r\n\r\n    protected void AssertSelectedHitObject(Action<THitObject> assert)\r\n    {\r\n        AssertSelectedHitObjects(hitObjects =>\r\n        {\r\n            foreach (var hitObject in hitObjects)\r\n            {\r\n                assert(hitObject);\r\n            }\r\n        });\r\n    }\r\n\r\n    protected void AssertSelectedHitObjects(Action<IEnumerable<THitObject>> assert)\r\n    {\r\n        AddStep(\"Is result matched\", () =>\r\n        {\r\n            var editorBeatmap = Dependencies.Get<EditorBeatmap>();\r\n            assert(editorBeatmap.SelectedHitObjects.OfType<THitObject>());\r\n        });\r\n\r\n        AssertStatus();\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/ChangeHandlers/BaseHitObjectPropertyChangeHandlerTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers;\r\nusing osu.Game.Rulesets.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.ChangeHandlers;\r\n\r\npublic abstract partial class BaseHitObjectPropertyChangeHandlerTest<TChangeHandler, THitObject> : BaseHitObjectChangeHandlerTest<TChangeHandler, THitObject>\r\n    where TChangeHandler : HitObjectPropertyChangeHandler<THitObject>, new() where THitObject : HitObject;\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/ChangeHandlers/Beatmaps/BeatmapPagesChangeHandlerTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.ChangeHandlers.Beatmaps;\r\n\r\npublic partial class BeatmapPagesChangeHandlerTest : BaseChangeHandlerTest<BeatmapPagesChangeHandler>\r\n{\r\n    protected override bool IncludeAutoGenerator => true;\r\n\r\n    [Test]\r\n    public void TestGeneratePage()\r\n    {\r\n        PrepareHitObject(() => TestCaseTagHelper.ParseLyric(\"[1000,3000]:karaoke\"), false);\r\n\r\n        TriggerHandlerChanged(c => c.AutoGenerate());\r\n\r\n        AssertKaraokeBeatmap(karaokeBeatmap =>\r\n        {\r\n            var pages = karaokeBeatmap.PageInfo.SortedPages;\r\n\r\n            Assert.That(pages.Count, Is.EqualTo(2));\r\n            Assert.That(pages[0].Time, Is.EqualTo(1000));\r\n            Assert.That(pages[1].Time, Is.EqualTo(3000));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestGeneratePageWithInvalidCase()\r\n    {\r\n        // there's no time-info inside.\r\n        PrepareHitObject(() => new Lyric(), false);\r\n\r\n        TriggerHandlerChangedWithException<GeneratorNotSupportedException>(c => c.AutoGenerate());\r\n    }\r\n\r\n    [Test]\r\n    public void TestAdd()\r\n    {\r\n        Page page = new Page();\r\n\r\n        TriggerHandlerChanged(c =>\r\n        {\r\n            c.Add(page);\r\n        });\r\n\r\n        AssertKaraokeBeatmap(karaokeBeatmap =>\r\n        {\r\n            var pages = karaokeBeatmap.PageInfo.Pages;\r\n            Assert.That(pages.Count, Is.EqualTo(1));\r\n            Assert.That(pages[0], Is.EqualTo(page));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestRemove()\r\n    {\r\n        Page firstPage = new Page { Time = 1000 };\r\n        Page secondPage = new Page { Time = 2000 };\r\n\r\n        SetUpKaraokeBeatmap(karaokeBeatmap =>\r\n        {\r\n            var pages = karaokeBeatmap.PageInfo.Pages;\r\n            pages.Add(firstPage);\r\n            pages.Add(secondPage);\r\n        });\r\n\r\n        TriggerHandlerChanged(c =>\r\n        {\r\n            c.Remove(firstPage);\r\n        });\r\n\r\n        AssertKaraokeBeatmap(karaokeBeatmap =>\r\n        {\r\n            var pages = karaokeBeatmap.PageInfo.Pages;\r\n            Assert.That(pages.Count, Is.EqualTo(1));\r\n            Assert.That(pages[0], Is.EqualTo(secondPage));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestRemoveRange()\r\n    {\r\n        Page firstPage = new Page { Time = 1000 };\r\n        Page secondPage = new Page { Time = 2000 };\r\n\r\n        SetUpKaraokeBeatmap(karaokeBeatmap =>\r\n        {\r\n            var pages = karaokeBeatmap.PageInfo.Pages;\r\n            pages.Add(firstPage);\r\n            pages.Add(secondPage);\r\n        });\r\n\r\n        TriggerHandlerChanged(c =>\r\n        {\r\n            c.RemoveRange(new[] { firstPage });\r\n        });\r\n\r\n        AssertKaraokeBeatmap(karaokeBeatmap =>\r\n        {\r\n            var pages = karaokeBeatmap.PageInfo.Pages;\r\n            Assert.That(pages.Count, Is.EqualTo(1));\r\n            Assert.That(pages[0], Is.EqualTo(secondPage));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestShiftingPageTime()\r\n    {\r\n        Page firstPage = new Page();\r\n        Page secondPage = new Page();\r\n\r\n        SetUpKaraokeBeatmap(karaokeBeatmap =>\r\n        {\r\n            var pages = karaokeBeatmap.PageInfo.Pages;\r\n            pages.Add(firstPage);\r\n            pages.Add(secondPage);\r\n        });\r\n\r\n        TriggerHandlerChanged(c =>\r\n        {\r\n            c.ShiftingPageTime(new[] { firstPage }, 1000);\r\n        });\r\n\r\n        AssertKaraokeBeatmap(_ =>\r\n        {\r\n            Assert.That(firstPage.Time, Is.EqualTo(1000));\r\n            Assert.That(secondPage.Time, Is.EqualTo(0));\r\n        });\r\n    }\r\n\r\n    protected override void SetUpKaraokeBeatmap(Action<KaraokeBeatmap> action)\r\n    {\r\n        base.SetUpKaraokeBeatmap(karaokeBeatmap =>\r\n        {\r\n            karaokeBeatmap.PageInfo = new PageInfo();\r\n\r\n            action(karaokeBeatmap);\r\n        });\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/ChangeHandlers/Beatmaps/BeatmapSingersChangeHandlerTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.ChangeHandlers.Beatmaps;\r\n\r\npublic partial class BeatmapSingersChangeHandlerTest : BaseChangeHandlerTest<BeatmapSingersChangeHandler>\r\n{\r\n    [Test]\r\n    [Ignore(\"Not working because singer in karaoke beatmap only sync to change handler once.\")]\r\n    public void TestChangeOrder()\r\n    {\r\n        Singer firstSinger = null!;\r\n        Singer secondSinger = null!;\r\n\r\n        SetUpKaraokeBeatmap(karaokeBeatmap =>\r\n        {\r\n            firstSinger = karaokeBeatmap.SingerInfo.AddSinger(s => s.Order = 1);\r\n            secondSinger = karaokeBeatmap.SingerInfo.AddSinger(s => s.Order = 2);\r\n        });\r\n\r\n        TriggerHandlerChanged(c =>\r\n        {\r\n            c.ChangeOrder(firstSinger, 2);\r\n        });\r\n\r\n        AssertKaraokeBeatmap(karaokeBeatmap =>\r\n        {\r\n            Assert.That(firstSinger.ID, Is.EqualTo(1));\r\n            Assert.That(firstSinger.Order, Is.EqualTo(2));\r\n            Assert.That(secondSinger.ID, Is.EqualTo(2));\r\n            Assert.That(secondSinger.Order, Is.EqualTo(1));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    [Ignore(\"It's hard to test this because it needs lots of dependencies.\")]\r\n    public void TestChangeSingerAvatar()\r\n    {\r\n    }\r\n\r\n    [Test]\r\n    [Ignore(\"Not working because singer in karaoke beatmap only sync to change handler once.\")]\r\n    public void TestAdd()\r\n    {\r\n        SetUpKaraokeBeatmap(karaokeBeatmap =>\r\n        {\r\n            karaokeBeatmap.SingerInfo.AddSinger(s => s.Order = 1);\r\n            karaokeBeatmap.SingerInfo.AddSinger(s => s.Order = 2);\r\n        });\r\n\r\n        TriggerHandlerChanged(c =>\r\n        {\r\n            c.Add();\r\n        });\r\n\r\n        AssertKaraokeBeatmap(karaokeBeatmap =>\r\n        {\r\n            var singers = karaokeBeatmap.SingerInfo.Singers;\r\n            Assert.That(singers.Count, Is.EqualTo(3));\r\n            var lastSinger = singers.Last();\r\n            Assert.That(lastSinger.ID, Is.EqualTo(2));\r\n            Assert.That(lastSinger.Order, Is.EqualTo(3));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    [Ignore(\"Not working because singer in karaoke beatmap only sync to change handler once.\")]\r\n    public void TestRemove()\r\n    {\r\n        Singer firstSinger = null!;\r\n        Singer secondSinger = null!;\r\n\r\n        SetUpKaraokeBeatmap(karaokeBeatmap =>\r\n        {\r\n            firstSinger = karaokeBeatmap.SingerInfo.AddSinger(s => s.Order = 1);\r\n            secondSinger = karaokeBeatmap.SingerInfo.AddSinger(s => s.Order = 2);\r\n\r\n            karaokeBeatmap.HitObjects = new List<KaraokeHitObject>\r\n            {\r\n                new Lyric\r\n                {\r\n                    SingerIds = { firstSinger.ID },\r\n                },\r\n            };\r\n        });\r\n\r\n        TriggerHandlerChanged(c =>\r\n        {\r\n            c.Remove(firstSinger);\r\n        });\r\n\r\n        AssertKaraokeBeatmap(karaokeBeatmap =>\r\n        {\r\n            var singers = karaokeBeatmap.SingerInfo.Singers;\r\n            Assert.That(singers.Count, Is.EqualTo(1));\r\n            Assert.That(secondSinger.ID, Is.EqualTo(1));\r\n            Assert.That(secondSinger.Order, Is.EqualTo(1));\r\n            var lyrics = karaokeBeatmap.HitObjects.OfType<Lyric>().Where(x => x.SingerIds.Contains(firstSinger.ID));\r\n            Assert.That(lyrics, Is.Empty);\r\n        });\r\n    }\r\n\r\n    protected override void SetUpKaraokeBeatmap(Action<KaraokeBeatmap> action)\r\n    {\r\n        base.SetUpKaraokeBeatmap(karaokeBeatmap =>\r\n        {\r\n            karaokeBeatmap.SingerInfo = new SingerInfo();\r\n\r\n            action(karaokeBeatmap);\r\n        });\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/ChangeHandlers/Beatmaps/BeatmapTranslationsChangeHandlerTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Globalization;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.ChangeHandlers.Beatmaps;\r\n\r\npublic partial class BeatmapTranslationsChangeHandlerTest : BaseChangeHandlerTest<BeatmapTranslationsChangeHandler>\r\n{\r\n    [Test]\r\n    public void TestAdd()\r\n    {\r\n        SetUpKaraokeBeatmap(karaokeBeatmap =>\r\n        {\r\n            karaokeBeatmap.AvailableTranslationLanguages = new List<CultureInfo>\r\n            {\r\n                new(\"zh-TW\"),\r\n            };\r\n        });\r\n\r\n        TriggerHandlerChanged(c =>\r\n        {\r\n            c.Add(new CultureInfo(\"Ja-jp\"));\r\n        });\r\n\r\n        AssertKaraokeBeatmap(karaokeBeatmap =>\r\n        {\r\n            Assert.That(karaokeBeatmap.AvailableTranslationLanguages.Count, Is.EqualTo(2));\r\n            Assert.That(karaokeBeatmap.AvailableTranslationLanguages[0], Is.EqualTo(new CultureInfo(\"zh-TW\")));\r\n            Assert.That(karaokeBeatmap.AvailableTranslationLanguages[1], Is.EqualTo(new CultureInfo(\"Ja-jp\")));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestRemove()\r\n    {\r\n        SetUpKaraokeBeatmap(karaokeBeatmap =>\r\n        {\r\n            karaokeBeatmap.AvailableTranslationLanguages = new List<CultureInfo>\r\n            {\r\n                new(\"zh-TW\"),\r\n                new(\"Ja-jp\"),\r\n            };\r\n        });\r\n\r\n        TriggerHandlerChanged(c =>\r\n        {\r\n            c.Remove(new CultureInfo(\"Ja-jp\"));\r\n        });\r\n\r\n        AssertKaraokeBeatmap(karaokeBeatmap =>\r\n        {\r\n            Assert.That(karaokeBeatmap.AvailableTranslationLanguages.Count, Is.EqualTo(1));\r\n            Assert.That(karaokeBeatmap.AvailableTranslationLanguages[0], Is.EqualTo(new CultureInfo(\"zh-TW\")));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestIsLanguageContainsTranslation()\r\n    {\r\n        SetUpKaraokeBeatmap(karaokeBeatmap =>\r\n        {\r\n            karaokeBeatmap.AvailableTranslationLanguages = new List<CultureInfo>\r\n            {\r\n                new(\"zh-TW\"),\r\n                new(\"Ja-jp\"),\r\n            };\r\n        });\r\n\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Translations = new Dictionary<CultureInfo, string>\r\n            {\r\n                {\r\n                    new(\"zh-TW\"), \"卡拉 OK\"\r\n                },\r\n            },\r\n        });\r\n\r\n        TriggerHandlerChanged(c =>\r\n        {\r\n            Assert.That(c.IsLanguageContainsTranslation(new CultureInfo(\"Ja-jp\")), Is.False);\r\n            Assert.That(c.IsLanguageContainsTranslation(new CultureInfo(\"zh-TW\")), Is.True);\r\n        });\r\n    }\r\n\r\n    protected override void SetUpKaraokeBeatmap(Action<KaraokeBeatmap> action)\r\n    {\r\n        base.SetUpKaraokeBeatmap(karaokeBeatmap =>\r\n        {\r\n            karaokeBeatmap.AvailableTranslationLanguages = new List<CultureInfo>();\r\n\r\n            action(karaokeBeatmap);\r\n        });\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/ChangeHandlers/ImportBeatmapChangeHandlerTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.ChangeHandlers;\r\n\r\npublic partial class ImportBeatmapChangeHandlerTest : BaseChangeHandlerTest<ImportBeatmapChangeHandler>\r\n{\r\n    [Test]\r\n    public void TestImport()\r\n    {\r\n        SetUpKaraokeBeatmap(karaokeBeatmap =>\r\n        {\r\n            karaokeBeatmap.NoteInfo.Columns = 10;\r\n        });\r\n\r\n        TriggerHandlerChanged(c =>\r\n        {\r\n            var beatmap = new KaraokeBeatmap\r\n            {\r\n                HitObjects = new List<KaraokeHitObject>\r\n                {\r\n                    new Lyric(),\r\n                    new Lyric(),\r\n                    new Note(),\r\n                },\r\n            };\r\n            c.Import(beatmap);\r\n        });\r\n\r\n        AssertKaraokeBeatmap(karaokeBeatmap =>\r\n        {\r\n            // should not change the property in the karaoke beatmap.\r\n            Assert.That(karaokeBeatmap.NoteInfo.Columns, Is.EqualTo(10));\r\n\r\n            // check the hit objects.\r\n            // and notice that we only import the lyric from other beatmap.\r\n            Assert.That(karaokeBeatmap.HitObjects.Count, Is.EqualTo(2));\r\n        });\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/ChangeHandlers/LockChangeHandlerTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Properties;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Types;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.ChangeHandlers;\r\n\r\npublic partial class LockChangeHandlerTest : BaseHitObjectPropertyChangeHandlerTest<LockChangeHandler, KaraokeHitObject>\r\n{\r\n    [Test]\r\n    public void TestLock()\r\n    {\r\n        var referencedLyric = new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            Lock = LockState.None,\r\n        };\r\n\r\n        PrepareHitObject(() => referencedLyric);\r\n\r\n        PrepareHitObject(() => new Note\r\n        {\r\n            Text = \"カラオケ\",\r\n            ReferenceLyricId = referencedLyric.ID,\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.Lock(LockState.Full));\r\n\r\n        AssertSelectedHitObject(h =>\r\n        {\r\n            if (h is IHasLock hasLock)\r\n                Assert.That(hasLock.Lock, Is.EqualTo(LockState.Full));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestUnlock()\r\n    {\r\n        var referencedLyric = new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            Lock = LockState.Full,\r\n        };\r\n\r\n        PrepareHitObject(() => referencedLyric);\r\n\r\n        PrepareHitObject(() => new Note\r\n        {\r\n            Text = \"カラオケ\",\r\n            ReferenceLyricId = referencedLyric.ID,\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.Unlock());\r\n\r\n        AssertSelectedHitObject(h =>\r\n        {\r\n            if (h is IHasLock hasLock)\r\n                Assert.That(hasLock.Lock, Is.EqualTo(LockState.None));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestLockToReferenceLyric()\r\n    {\r\n        var referencedLyric = new Lyric();\r\n        PrepareHitObject(() => referencedLyric, false);\r\n\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            ReferenceLyricId = referencedLyric.ID,\r\n            ReferenceLyric = referencedLyric,\r\n            ReferenceLyricConfig = new SyncLyricConfig(),\r\n        });\r\n\r\n        TriggerHandlerChangedWithException<ChangeForbiddenException>(c => c.Lock(LockState.Full));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/ChangeHandlers/Lyrics/LyricLanguageChangeHandlerTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Globalization;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.ChangeHandlers.Lyrics;\r\n\r\npublic partial class LyricLanguageChangeHandlerTest : LyricPropertyChangeHandlerTest<LyricLanguageChangeHandler>\r\n{\r\n    [Test]\r\n    public void TestSetLanguageToJapanese()\r\n    {\r\n        var language = new CultureInfo(\"ja\");\r\n        PrepareHitObject(() => new Lyric());\r\n\r\n        TriggerHandlerChanged(c => c.SetLanguage(language));\r\n\r\n        AssertSelectedHitObject(h =>\r\n        {\r\n            Assert.That(h.Language, Is.EqualTo(language));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestSetLanguageToNull()\r\n    {\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"???\",\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.SetLanguage(null));\r\n\r\n        AssertSelectedHitObject(h =>\r\n        {\r\n            Assert.That(h.Language, Is.Null);\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestSetLanguageWithReferenceLyric()\r\n    {\r\n        PrepareLyricWithSyncConfig(new Lyric());\r\n        TriggerHandlerChangedWithException<ChangeForbiddenException>(c => c.SetLanguage(new CultureInfo(\"ja\")));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/ChangeHandlers/Lyrics/LyricPropertyAutoGenerateChangeHandlerTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Globalization;\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Properties;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Extensions;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.ChangeHandlers.Lyrics;\r\n\r\n/// <summary>\r\n/// This test is focus on make sure that:\r\n/// If the <see cref=\"Lyric.ReferenceLyric\"/> in the <see cref=\"Lyric\"/> is not empty.\r\n/// <see cref=\"ILyricPropertyAutoGenerateChangeHandler\"/> should be able to change the property.\r\n/// </summary>\r\npublic partial class LyricPropertyAutoGenerateChangeHandlerTest : LyricPropertyChangeHandlerTest<LyricPropertyAutoGenerateChangeHandler>\r\n{\r\n    protected override bool IncludeAutoGenerator => true;\r\n\r\n    #region Reference lyric\r\n\r\n    [Test]\r\n    public void TestDetectReferenceLyric()\r\n    {\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n        }, false);\r\n\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.AutoGenerate(AutoGenerateType.DetectReferenceLyric));\r\n\r\n        AssertSelectedHitObject(h =>\r\n        {\r\n            Assert.That(h.ReferenceLyric, Is.Not.Null);\r\n            Assert.That(h.ReferenceLyricConfig, Is.InstanceOf<SyncLyricConfig>());\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestDetectReferenceLyricWithNonSupportedLyric()\r\n    {\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n        }, false);\r\n\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"???\",\r\n        });\r\n\r\n        TriggerHandlerChangedWithException<DetectorNotSupportedException>(c => c.AutoGenerate(AutoGenerateType.DetectReferenceLyric));\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Language\r\n\r\n    [Test]\r\n    public void TestDetectLanguage()\r\n    {\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.AutoGenerate(AutoGenerateType.DetectLanguage));\r\n\r\n        AssertSelectedHitObject(h =>\r\n        {\r\n            Assert.That(h.Language, Is.EqualTo(new CultureInfo(\"ja\")));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestDetectLanguageWithNonSupportedLyric()\r\n    {\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"???\",\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.AutoGenerate(AutoGenerateType.DetectLanguage));\r\n\r\n        AssertSelectedHitObject(h =>\r\n        {\r\n            Assert.That(h.Language, Is.Null);\r\n        });\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Ruby\r\n\r\n    [Test]\r\n    public void TestAutoGenerateRubyTags()\r\n    {\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"風\",\r\n            Language = new CultureInfo(17),\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.AutoGenerate(AutoGenerateType.AutoGenerateRubyTags));\r\n\r\n        AssertSelectedHitObject(h =>\r\n        {\r\n            var rubyTags = h.RubyTags;\r\n            Assert.That(rubyTags.Count, Is.EqualTo(1));\r\n            Assert.That(rubyTags[0].Text, Is.EqualTo(\"かぜ\"));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestAutoGenerateRubyTagsWithNonSupportedLyric()\r\n    {\r\n        PrepareHitObjects(() => new[]\r\n        {\r\n            new Lyric\r\n            {\r\n                Text = \"風\",\r\n            },\r\n            new Lyric\r\n            {\r\n                Text = string.Empty,\r\n            },\r\n            new Lyric\r\n            {\r\n                Text = string.Empty,\r\n                Language = new CultureInfo(17),\r\n            },\r\n        });\r\n\r\n        TriggerHandlerChangedWithException<GeneratorNotSupportedException>(c => c.AutoGenerate(AutoGenerateType.AutoGenerateRubyTags));\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Time-tag\r\n\r\n    [Test]\r\n    public void TestAutoGenerateTimeTags()\r\n    {\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            Language = new CultureInfo(17),\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.AutoGenerate(AutoGenerateType.AutoGenerateTimeTags));\r\n\r\n        AssertSelectedHitObject(h =>\r\n        {\r\n            Assert.That(h.TimeTags.Count, Is.EqualTo(5));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestAutoGenerateTimeTagsWithNonSupportedLyric()\r\n    {\r\n        PrepareHitObjects(() => new[]\r\n        {\r\n            new Lyric\r\n            {\r\n                Text = \"カラオケ\",\r\n            },\r\n            new Lyric\r\n            {\r\n                Text = string.Empty,\r\n            },\r\n            new Lyric\r\n            {\r\n                Text = string.Empty,\r\n                Language = new CultureInfo(17),\r\n            },\r\n        });\r\n\r\n        TriggerHandlerChangedWithException<GeneratorNotSupportedException>(c => c.AutoGenerate(AutoGenerateType.AutoGenerateTimeTags));\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Romanisation\r\n\r\n    [Test]\r\n    public void TestAutoGenerateRomanisation()\r\n    {\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            Language = new CultureInfo(17),\r\n            TimeTags = TestCaseTagHelper.ParseTimeTags(new[] { \"[0,start]\", \"[3,end]\" }),\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.AutoGenerate(AutoGenerateType.AutoGenerateRomanisation));\r\n\r\n        AssertSelectedHitObject(h =>\r\n        {\r\n            Assert.That(h.TimeTags[0].RomanisedSyllable, Is.EqualTo(\"karaoke\"));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestAutoGenerateRomanisationWithNonSupportedLyric()\r\n    {\r\n        PrepareHitObjects(() => new[]\r\n        {\r\n            new Lyric\r\n            {\r\n                Text = \"カラオケ\",\r\n                Language = new CultureInfo(17),\r\n                // with no time-tag.\r\n            },\r\n        });\r\n\r\n        TriggerHandlerChangedWithException<GeneratorNotSupportedException>(c => c.AutoGenerate(AutoGenerateType.AutoGenerateRomanisation));\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Note\r\n\r\n    [Test]\r\n    public void TestAutoGenerateNotes()\r\n    {\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            TimeTags = new[]\r\n            {\r\n                new TimeTag(new TextIndex(0), 0),\r\n                new TimeTag(new TextIndex(1), 1000),\r\n                new TimeTag(new TextIndex(2), 2000),\r\n                new TimeTag(new TextIndex(3), 3000),\r\n                new TimeTag(new TextIndex(3, TextIndex.IndexState.End), 4000),\r\n            },\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.AutoGenerate(AutoGenerateType.AutoGenerateNotes));\r\n\r\n        AssertSelectedHitObject(h =>\r\n        {\r\n            var actualNotes = getMatchedNotes(h);\r\n            Assert.That(actualNotes.Length, Is.EqualTo(4));\r\n            Assert.That(actualNotes[0].Text, Is.EqualTo(\"カ\"));\r\n            Assert.That(actualNotes[1].Text, Is.EqualTo(\"ラ\"));\r\n            Assert.That(actualNotes[2].Text, Is.EqualTo(\"オ\"));\r\n            Assert.That(actualNotes[3].Text, Is.EqualTo(\"ケ\"));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestAutoGenerateNotesWithNonSupportedLyric()\r\n    {\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n        });\r\n\r\n        TriggerHandlerChangedWithException<GeneratorNotSupportedException>(c => c.AutoGenerate(AutoGenerateType.AutoGenerateNotes));\r\n    }\r\n\r\n    private Note[] getMatchedNotes(Lyric lyric)\r\n    {\r\n        var editorBeatmap = Dependencies.Get<EditorBeatmap>();\r\n        return EditorBeatmapUtils.GetNotesByLyric(editorBeatmap, lyric).ToArray();\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Shared tests\r\n\r\n    [Test]\r\n    [Description(\"Should be able to generate the property if the lyric is not reference to other lyric.\")]\r\n    public void TestChangeWithNormalLyric([Values] AutoGenerateType type)\r\n    {\r\n        // for detect reference lyric.\r\n        if (isLyricReferenceChangeHandler(type))\r\n        {\r\n            PrepareHitObject(() => new Lyric\r\n            {\r\n                Text = \"karaoke\",\r\n            }, false);\r\n        }\r\n\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"karaoke\",\r\n            Language = new CultureInfo(17), // for auto-generate ruby and romanisation.\r\n            TimeTags = new[] // for auto-generate notes.\r\n            {\r\n                new TimeTag(new TextIndex(0), 0),\r\n                new TimeTag(new TextIndex(1), 1000),\r\n                new TimeTag(new TextIndex(2), 2000),\r\n                new TimeTag(new TextIndex(3), 3000),\r\n                new TimeTag(new TextIndex(3, TextIndex.IndexState.End), 4000),\r\n            },\r\n        });\r\n\r\n        TriggerHandlerChanged(c =>\r\n        {\r\n            Assert.That(c.CanGenerate(type), Is.True);\r\n        });\r\n\r\n        TriggerHandlerChanged(c =>\r\n        {\r\n            Assert.That(c.GetGeneratorNotSupportedLyrics(type), Is.Empty);\r\n        });\r\n\r\n        TriggerHandlerChanged(c =>\r\n        {\r\n            Assert.That(() => c.AutoGenerate(type), Throws.Nothing);\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    [Description(\"Should not be able to generate the property if the lyric is missing detectable property.\")]\r\n    public void TestChangeWithMissingPropertyLyric([Values] AutoGenerateType type)\r\n    {\r\n        PrepareHitObject(() => new Lyric());\r\n\r\n        TriggerHandlerChanged(c =>\r\n        {\r\n            Assert.That(c.CanGenerate(type), Is.False);\r\n        });\r\n\r\n        TriggerHandlerChanged(c =>\r\n        {\r\n            Assert.That(c.GetGeneratorNotSupportedLyrics(type), Is.Not.Empty);\r\n        });\r\n\r\n        TriggerHandlerChanged(c =>\r\n        {\r\n            var exception = Assert.Catch(() => c.AutoGenerate(type));\r\n            Assert.That(new[] { typeof(GeneratorNotSupportedException), typeof(DetectorNotSupportedException) }, Does.Contain(exception?.GetType()));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    [Description(\"Should not be able to generate the property if the lyric is reference to other lyric.\")]\r\n    public void TestCheckWithReferencedLyric([Values] AutoGenerateType type)\r\n    {\r\n        if (isLyricReferenceChangeHandler(type))\r\n            return;\r\n\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"karaoke\",\r\n            Language = new CultureInfo(17), // for auto-generate ruby and romanisation.\r\n            TimeTags = new[] // for auto-generate notes.\r\n            {\r\n                new TimeTag(new TextIndex(0), 0),\r\n                new TimeTag(new TextIndex(1), 1000),\r\n                new TimeTag(new TextIndex(2), 2000),\r\n                new TimeTag(new TextIndex(3), 3000),\r\n                new TimeTag(new TextIndex(3, TextIndex.IndexState.End), 4000),\r\n            },\r\n            // has reference lyric.\r\n            ReferenceLyricId = TestCaseElementIdHelper.CreateElementIdByNumber(1),\r\n            ReferenceLyric = new Lyric().ChangeId(1),\r\n            ReferenceLyricConfig = new SyncLyricConfig(),\r\n        });\r\n\r\n        TriggerHandlerChanged(c =>\r\n        {\r\n            Assert.That(c.CanGenerate(type), Is.False);\r\n        });\r\n\r\n        TriggerHandlerChanged(c =>\r\n        {\r\n            Assert.That(c.GetGeneratorNotSupportedLyrics(type), Is.Not.Empty);\r\n        });\r\n\r\n        TriggerHandlerChangedWithException<ChangeForbiddenException>(c => c.AutoGenerate(type));\r\n    }\r\n\r\n    private bool isLyricReferenceChangeHandler(AutoGenerateType type)\r\n        => type == AutoGenerateType.DetectReferenceLyric;\r\n\r\n    #endregion\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/ChangeHandlers/Lyrics/LyricPropertyChangeHandlerTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Properties;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.ChangeHandlers.Lyrics;\r\n\r\npublic abstract partial class LyricPropertyChangeHandlerTest<TChangeHandler> : BaseHitObjectPropertyChangeHandlerTest<TChangeHandler, Lyric>\r\n    where TChangeHandler : LyricPropertyChangeHandler, ILyricPropertyChangeHandler, new()\r\n{\r\n    protected Lyric PrepareLyricWithSyncConfig(Lyric referencedLyric, IReferenceLyricPropertyConfig? config = null, bool selected = true)\r\n    {\r\n        var lyric = new Lyric\r\n        {\r\n            ReferenceLyricId = referencedLyric.ID,\r\n            ReferenceLyric = referencedLyric,\r\n            ReferenceLyricConfig = config ?? new SyncLyricConfig(),\r\n        };\r\n\r\n        PrepareHitObjects(() => new[] { lyric }, selected);\r\n\r\n        return lyric;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/ChangeHandlers/Lyrics/LyricReferenceChangeHandlerTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Properties;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.ChangeHandlers.Lyrics;\r\n\r\npublic partial class LyricReferenceChangeHandlerTest : LyricPropertyChangeHandlerTest<LyricReferenceChangeHandler>\r\n{\r\n    [Test]\r\n    public void TestUpdateReferenceLyric()\r\n    {\r\n        var referencedLyric = new Lyric\r\n        {\r\n            Text = \"Referenced lyric\",\r\n        };\r\n\r\n        PrepareHitObject(() => referencedLyric, false);\r\n\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"I need the reference lyric.\",\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.UpdateReferenceLyric(referencedLyric));\r\n\r\n        AssertSelectedHitObject(h =>\r\n        {\r\n            Assert.That(h.ReferenceLyric, Is.EqualTo(referencedLyric));\r\n            Assert.That(h.ReferenceLyricConfig, Is.InstanceOf<ReferenceLyricConfig>());\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestSwitchToReferenceLyricConfig()\r\n    {\r\n        var referencedLyric = new Lyric\r\n        {\r\n            Text = \"Referenced lyric\",\r\n        };\r\n\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"Lyric\",\r\n            ReferenceLyricId = referencedLyric.ID,\r\n            ReferenceLyric = referencedLyric,\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.SwitchToReferenceLyricConfig());\r\n\r\n        AssertSelectedHitObject(h =>\r\n        {\r\n            Assert.That(h.ReferenceLyric, Is.EqualTo(referencedLyric));\r\n            Assert.That(h.ReferenceLyricConfig, Is.InstanceOf<ReferenceLyricConfig>());\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestSwitchToSyncLyricConfig()\r\n    {\r\n        var referencedLyric = new Lyric\r\n        {\r\n            Text = \"Referenced lyric\",\r\n        };\r\n\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"Lyric\",\r\n            ReferenceLyricId = referencedLyric.ID,\r\n            ReferenceLyric = referencedLyric,\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.SwitchToSyncLyricConfig());\r\n\r\n        AssertSelectedHitObject(h =>\r\n        {\r\n            Assert.That(h.ReferenceLyric, Is.EqualTo(referencedLyric));\r\n            Assert.That(h.ReferenceLyricConfig, Is.InstanceOf<SyncLyricConfig>());\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestAdjustLyricConfig()\r\n    {\r\n        var referencedLyric = new Lyric\r\n        {\r\n            Text = \"Referenced lyric\",\r\n        };\r\n\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"Lyric\",\r\n            ReferenceLyricId = referencedLyric.ID,\r\n            ReferenceLyric = referencedLyric,\r\n            ReferenceLyricConfig = new SyncLyricConfig(),\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.AdjustLyricConfig<SyncLyricConfig>(x =>\r\n        {\r\n            x.OffsetTime = 100;\r\n            x.SyncSingerProperty = false;\r\n        }));\r\n\r\n        AssertSelectedHitObject(h =>\r\n        {\r\n            var config = (h.ReferenceLyricConfig as SyncLyricConfig)!;\r\n            Assert.That(config.OffsetTime, Is.EqualTo(100));\r\n            Assert.That(config.SyncSingerProperty, Is.False);\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestWithReferenceLyric()\r\n    {\r\n        var lyric = new Lyric\r\n        {\r\n            Text = \"Referenced lyric\",\r\n        };\r\n\r\n        PrepareHitObject(() => lyric, false);\r\n        PrepareLyricWithSyncConfig(new Lyric());\r\n\r\n        // should not block the reference language change.\r\n        TriggerHandlerChanged(c => c.UpdateReferenceLyric(lyric));\r\n        TriggerHandlerChanged(c => c.SwitchToReferenceLyricConfig());\r\n        TriggerHandlerChanged(c => c.SwitchToSyncLyricConfig());\r\n        TriggerHandlerChanged(c => c.AdjustLyricConfig<SyncLyricConfig>(syncLyricConfig => syncLyricConfig.OffsetTime = 1000));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/ChangeHandlers/Lyrics/LyricRubyTagsChangeHandlerTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Globalization;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.ChangeHandlers.Lyrics;\r\n\r\npublic partial class LyricRubyTagsChangeHandlerTest : LyricPropertyChangeHandlerTest<LyricRubyTagsChangeHandler>\r\n{\r\n    [Test]\r\n    public void TestAdd()\r\n    {\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"風\",\r\n            Language = new CultureInfo(17),\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.Add(new RubyTag\r\n        {\r\n            StartIndex = 0,\r\n            EndIndex = 0,\r\n            Text = \"かぜ\",\r\n        }));\r\n\r\n        AssertSelectedHitObject(h =>\r\n        {\r\n            var rubyTags = h.RubyTags;\r\n            Assert.That(rubyTags.Count, Is.EqualTo(1));\r\n            Assert.That(rubyTags[0].Text, Is.EqualTo(\"かぜ\"));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestAddRange()\r\n    {\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"風\",\r\n            Language = new CultureInfo(17),\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.AddRange(new[]\r\n        {\r\n            new RubyTag\r\n            {\r\n                StartIndex = 0,\r\n                EndIndex = 0,\r\n                Text = \"かぜ\",\r\n            },\r\n        }));\r\n\r\n        AssertSelectedHitObject(h =>\r\n        {\r\n            var rubyTags = h.RubyTags;\r\n            Assert.That(rubyTags.Count, Is.EqualTo(1));\r\n            Assert.That(rubyTags[0].Text, Is.EqualTo(\"かぜ\"));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestRemove()\r\n    {\r\n        var removedTag = new RubyTag\r\n        {\r\n            StartIndex = 0,\r\n            EndIndex = 0,\r\n            Text = \"かぜ\",\r\n        };\r\n\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"風\",\r\n            Language = new CultureInfo(17),\r\n            RubyTags = new List<RubyTag>\r\n            {\r\n                removedTag,\r\n            },\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.Remove(removedTag));\r\n\r\n        AssertSelectedHitObject(h =>\r\n        {\r\n            Assert.That(h.RubyTags, Is.Empty);\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestRemoveRange()\r\n    {\r\n        var removedTag = new RubyTag\r\n        {\r\n            StartIndex = 0,\r\n            EndIndex = 0,\r\n            Text = \"か\",\r\n        };\r\n\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            Language = new CultureInfo(17),\r\n            RubyTags = new List<RubyTag>\r\n            {\r\n                removedTag,\r\n                new()\r\n                {\r\n                    StartIndex = 1,\r\n                    EndIndex = 1,\r\n                    Text = \"ら\",\r\n                },\r\n            },\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.RemoveRange(new[] { removedTag }));\r\n\r\n        AssertSelectedHitObject(h =>\r\n        {\r\n            Assert.That(h.RubyTags.Count, Is.EqualTo(1));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestSetIndex()\r\n    {\r\n        var targetTag = new RubyTag\r\n        {\r\n            StartIndex = 0,\r\n            EndIndex = 0,\r\n            Text = \"か\",\r\n        };\r\n\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            Language = new CultureInfo(17),\r\n            RubyTags = new List<RubyTag>\r\n            {\r\n                targetTag,\r\n            },\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.SetIndex(targetTag, 1, 2));\r\n\r\n        AssertSelectedHitObject(h =>\r\n        {\r\n            Assert.That(targetTag.StartIndex, Is.EqualTo(1));\r\n            Assert.That(targetTag.EndIndex, Is.EqualTo(2));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestShiftingIndex()\r\n    {\r\n        var targetTag = new RubyTag\r\n        {\r\n            StartIndex = 0,\r\n            EndIndex = 0,\r\n            Text = \"か\",\r\n        };\r\n\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            Language = new CultureInfo(17),\r\n            RubyTags = new List<RubyTag>\r\n            {\r\n                targetTag,\r\n            },\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.ShiftingIndex(new[] { targetTag }, 1));\r\n\r\n        AssertSelectedHitObject(h =>\r\n        {\r\n            Assert.That(targetTag.StartIndex, Is.EqualTo(1));\r\n            Assert.That(targetTag.EndIndex, Is.EqualTo(1));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestSetText()\r\n    {\r\n        var targetTag = new RubyTag\r\n        {\r\n            StartIndex = 0,\r\n            EndIndex = 0,\r\n            Text = \"か\",\r\n        };\r\n\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            Language = new CultureInfo(17),\r\n            RubyTags = new List<RubyTag>\r\n            {\r\n                targetTag,\r\n            },\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.SetText(targetTag, \"からおけ\"));\r\n\r\n        AssertSelectedHitObject(h =>\r\n        {\r\n            Assert.That(targetTag.Text, Is.EqualTo(\"からおけ\"));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestWithReferenceLyric()\r\n    {\r\n        PrepareLyricWithSyncConfig(new Lyric\r\n        {\r\n            Text = \"風\",\r\n            Language = new CultureInfo(17),\r\n        });\r\n\r\n        TriggerHandlerChangedWithException<ChangeForbiddenException>(c => c.Add(new RubyTag\r\n        {\r\n            StartIndex = 0,\r\n            EndIndex = 0,\r\n            Text = \"かぜ\",\r\n        }));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/ChangeHandlers/Lyrics/LyricSingerChangeHandlerTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Properties;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.ChangeHandlers.Lyrics;\r\n\r\npublic partial class LyricSingerChangeHandlerTest : LyricPropertyChangeHandlerTest<LyricSingerChangeHandler>\r\n{\r\n    [Test]\r\n    public void TestAdd()\r\n    {\r\n        Singer singer = null!;\r\n        SetUpKaraokeBeatmap(karaokeBeatmap =>\r\n        {\r\n            singer = karaokeBeatmap.SingerInfo.AddSinger(s =>\r\n            {\r\n                s.Name = \"Singer1\";\r\n            });\r\n        });\r\n        PrepareHitObject(() => new Lyric());\r\n\r\n        TriggerHandlerChanged(c => c.Add(singer));\r\n\r\n        AssertSelectedHitObject(h =>\r\n        {\r\n            var singers = h.SingerIds;\r\n            Assert.That(singers.Count, Is.EqualTo(1));\r\n            Assert.That(singers.FirstOrDefault(), Is.EqualTo(singer.ID));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestAddRange()\r\n    {\r\n        Singer singer = null!;\r\n        SetUpKaraokeBeatmap(karaokeBeatmap =>\r\n        {\r\n            singer = karaokeBeatmap.SingerInfo.AddSinger(s =>\r\n            {\r\n                s.Name = \"Singer1\";\r\n            });\r\n        });\r\n        PrepareHitObject(() => new Lyric());\r\n\r\n        TriggerHandlerChanged(c => c.AddRange(new[] { singer }));\r\n\r\n        AssertSelectedHitObject(h =>\r\n        {\r\n            var singers = h.SingerIds;\r\n            Assert.That(singers.Count, Is.EqualTo(1));\r\n            Assert.That(singers.FirstOrDefault(), Is.EqualTo(singer.ID));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestRemove()\r\n    {\r\n        Singer singer = null!;\r\n        Singer anotherSinger = null!;\r\n        SetUpKaraokeBeatmap(karaokeBeatmap =>\r\n        {\r\n            singer = karaokeBeatmap.SingerInfo.AddSinger(s =>\r\n            {\r\n                s.Name = \"Singer1\";\r\n            });\r\n\r\n            anotherSinger = karaokeBeatmap.SingerInfo.AddSinger(s =>\r\n            {\r\n                s.Name = \"Another singer\";\r\n            });\r\n        });\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            SingerIds = new[]\r\n            {\r\n                singer.ID,\r\n                anotherSinger.ID,\r\n            },\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.Remove(singer));\r\n\r\n        AssertSelectedHitObject(h =>\r\n        {\r\n            var singers = h.SingerIds;\r\n\r\n            // should not contains removed singer.\r\n            Assert.That(singers.Contains(singer.ID), Is.False);\r\n            // should only contain remain singer。\r\n            Assert.That(singers.Count, Is.EqualTo(1));\r\n            Assert.That(singers.Contains(anotherSinger.ID), Is.True);\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestRemoveRange()\r\n    {\r\n        Singer singer = null!;\r\n        Singer anotherSinger = null!;\r\n        SetUpKaraokeBeatmap(karaokeBeatmap =>\r\n        {\r\n            singer = karaokeBeatmap.SingerInfo.AddSinger(s =>\r\n            {\r\n                s.Name = \"Singer1\";\r\n            });\r\n\r\n            anotherSinger = karaokeBeatmap.SingerInfo.AddSinger(s =>\r\n            {\r\n                s.Name = \"Another singer\";\r\n            });\r\n        });\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            SingerIds = new[]\r\n            {\r\n                singer.ID,\r\n                anotherSinger.ID,\r\n            },\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.RemoveRange(new[] { singer }));\r\n\r\n        AssertSelectedHitObject(h =>\r\n        {\r\n            var singers = h.SingerIds;\r\n\r\n            // should not contains removed singer.\r\n            Assert.That(singers.Contains(singer.ID), Is.False);\r\n            // should only contain remain singer。\r\n            Assert.That(singers.Count, Is.EqualTo(1));\r\n            Assert.That(singers.Contains(anotherSinger.ID), Is.True);\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestClear()\r\n    {\r\n        Singer singer = null!;\r\n        SetUpKaraokeBeatmap(karaokeBeatmap =>\r\n        {\r\n            singer = karaokeBeatmap.SingerInfo.AddSinger(s =>\r\n            {\r\n                s.Name = \"Singer1\";\r\n            });\r\n        });\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            SingerIds = new[]\r\n            {\r\n                singer.ID,\r\n            },\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.Clear());\r\n\r\n        AssertSelectedHitObject(h =>\r\n        {\r\n            Assert.That(h.SingerIds, Is.Empty);\r\n        });\r\n    }\r\n\r\n    [TestCase(true)]\r\n    [TestCase(false)]\r\n    public void TestWithReferenceLyric(bool syncSinger)\r\n    {\r\n        Singer singer = null!;\r\n        SetUpKaraokeBeatmap(karaokeBeatmap =>\r\n        {\r\n            singer = karaokeBeatmap.SingerInfo.AddSinger(s =>\r\n            {\r\n                s.Name = \"Singer1\";\r\n            });\r\n        });\r\n        PrepareLyricWithSyncConfig(new Lyric(), new SyncLyricConfig\r\n        {\r\n            SyncSingerProperty = syncSinger,\r\n        });\r\n\r\n        if (syncSinger)\r\n        {\r\n            TriggerHandlerChangedWithException<ChangeForbiddenException>(c => c.Add(singer));\r\n        }\r\n        else\r\n        {\r\n            TriggerHandlerChanged(c => c.Add(singer));\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/ChangeHandlers/Lyrics/LyricTextChangeHandlerTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.ChangeHandlers.Lyrics;\r\n\r\npublic partial class LyricTextChangeHandlerTest : LyricPropertyChangeHandlerTest<LyricTextChangeHandler>\r\n{\r\n    [Test]\r\n    public void TestInsertText()\r\n    {\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"カラ\",\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.InsertText(2, \"オケ\"));\r\n\r\n        AssertSelectedHitObject(h =>\r\n        {\r\n            Assert.That(h.Text, Is.EqualTo(\"カラオケ\"));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestDeleteLyricText()\r\n    {\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.DeleteLyricText(4));\r\n\r\n        AssertSelectedHitObject(h =>\r\n        {\r\n            Assert.That(h.Text, Is.EqualTo(\"カラオ\"));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestDeleteAllLyricText()\r\n    {\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"カ\",\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.DeleteLyricText(1));\r\n\r\n        AssertHitObjects(x => Assert.That(x, Is.Empty));\r\n    }\r\n\r\n    [Test]\r\n    public void TestWithReferenceLyric()\r\n    {\r\n        PrepareLyricWithSyncConfig(new Lyric\r\n        {\r\n            Text = \"カラ\",\r\n        });\r\n\r\n        TriggerHandlerChangedWithException<ChangeForbiddenException>(c => c.InsertText(2, \"オケ\"));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/ChangeHandlers/Lyrics/LyricTimeTagsChangeHandlerTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Properties;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.ChangeHandlers.Lyrics;\r\n\r\npublic partial class LyricTimeTagsChangeHandlerTest : LyricPropertyChangeHandlerTest<LyricTimeTagsChangeHandler>\r\n{\r\n    [Test]\r\n    public void TestSetTimeTagTime()\r\n    {\r\n        var timeTag = new TimeTag(new TextIndex(), 1000);\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            TimeTags = new[]\r\n            {\r\n                timeTag,\r\n            },\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.SetTimeTagTime(timeTag, 2000));\r\n\r\n        AssertSelectedHitObject(_ =>\r\n        {\r\n            Assert.That(timeTag.Time, Is.EqualTo(2000));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestSetTimeTagFirstSyllable()\r\n    {\r\n        var timeTag = new TimeTag(new TextIndex(), 1000);\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            TimeTags = new[]\r\n            {\r\n                timeTag,\r\n            },\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.SetTimeTagFirstSyllable(timeTag, true));\r\n\r\n        AssertSelectedHitObject(_ =>\r\n        {\r\n            Assert.That(timeTag.FirstSyllable, Is.True);\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestSetTimeTagRomanisedSyllable()\r\n    {\r\n        var timeTag = new TimeTag(new TextIndex(), 1000);\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            TimeTags = new[]\r\n            {\r\n                timeTag,\r\n            },\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.SetTimeTagRomanisedSyllable(timeTag, \"karaoke\"));\r\n\r\n        AssertSelectedHitObject(_ =>\r\n        {\r\n            Assert.That(timeTag.RomanisedSyllable, Is.EqualTo(\"karaoke\"));\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.SetTimeTagRomanisedSyllable(timeTag, \"  \"));\r\n\r\n        AssertSelectedHitObject(_ =>\r\n        {\r\n            Assert.That(timeTag.RomanisedSyllable, Is.EqualTo(string.Empty));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestShiftingTimeTagTime()\r\n    {\r\n        var timeTag = new TimeTag(new TextIndex());\r\n        var timeTagWithTime = new TimeTag(new TextIndex(), 1000);\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            TimeTags = new[]\r\n            {\r\n                timeTag,\r\n                timeTagWithTime,\r\n            },\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.ShiftingTimeTagTime(new[] { timeTag, timeTagWithTime }, 2000));\r\n\r\n        // use this temp way to trigger transaction count increase.\r\n        AddStep(\"Prepare testing beatmap\", () =>\r\n        {\r\n            var editorBeatmap = Dependencies.Get<EditorBeatmap>();\r\n            editorBeatmap.PerformOnSelection(h =>\r\n            {\r\n                editorBeatmap.Update(h);\r\n            });\r\n        });\r\n\r\n        AssertSelectedHitObject(_ =>\r\n        {\r\n            Assert.That(timeTag.Time, Is.Null);\r\n            Assert.That(timeTagWithTime.Time, Is.EqualTo(3000));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestClearTimeTagTime()\r\n    {\r\n        var timeTag = new TimeTag(new TextIndex(), 1000);\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            TimeTags = new[]\r\n            {\r\n                timeTag,\r\n            },\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.ClearTimeTagTime(timeTag));\r\n\r\n        AssertSelectedHitObject(_ =>\r\n        {\r\n            Assert.That(timeTag.Time, Is.Null);\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestClearAllTimeTagTime()\r\n    {\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            TimeTags = new[]\r\n            {\r\n                new TimeTag(new TextIndex()), // without time.\r\n                new TimeTag(new TextIndex(), 1000),\r\n                new TimeTag(new TextIndex(3, TextIndex.IndexState.End), 3000),\r\n            },\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.ClearAllTimeTagTime());\r\n\r\n        AssertSelectedHitObject(h =>\r\n        {\r\n            Assert.That(h.TimeTags.All(x => x.Time == null));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestAdd()\r\n    {\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.Add(new TimeTag(new TextIndex(), 1000)));\r\n\r\n        AssertSelectedHitObject(h =>\r\n        {\r\n            Assert.That(h.TimeTags.Count, Is.EqualTo(1));\r\n            Assert.That(h.TimeTags[0].Time, Is.EqualTo(1000));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestAddRange()\r\n    {\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.AddRange(new[] { new TimeTag(new TextIndex(), 1000) }));\r\n\r\n        AssertSelectedHitObject(h =>\r\n        {\r\n            Assert.That(h.TimeTags.Count, Is.EqualTo(1));\r\n            Assert.That(h.TimeTags[0].Time, Is.EqualTo(1000));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestRemove()\r\n    {\r\n        var removedTag = new TimeTag(new TextIndex(), 1000);\r\n\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            TimeTags = new[]\r\n            {\r\n                removedTag,\r\n            },\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.Remove(removedTag));\r\n\r\n        AssertSelectedHitObject(h =>\r\n        {\r\n            Assert.That(h.TimeTags, Is.Empty);\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestRemoveRange()\r\n    {\r\n        var removedTag = new TimeTag(new TextIndex(), 1000);\r\n\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            TimeTags = new[]\r\n            {\r\n                removedTag,\r\n            },\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.RemoveRange(new[] { removedTag }));\r\n\r\n        AssertSelectedHitObject(h =>\r\n        {\r\n            Assert.That(h.TimeTags, Is.Empty);\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestAddByPosition()\r\n    {\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.AddByPosition(new TextIndex(3, TextIndex.IndexState.End)));\r\n\r\n        AssertSelectedHitObject(h =>\r\n        {\r\n            Assert.That(h.TimeTags.Count, Is.EqualTo(1));\r\n\r\n            var actualTimeTag = h.TimeTags[0];\r\n            Assert.That(actualTimeTag.Index, Is.EqualTo(new TextIndex(3, TextIndex.IndexState.End)));\r\n            Assert.That(actualTimeTag.Time, Is.Null);\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestRemoveByPosition()\r\n    {\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            TimeTags = new[]\r\n            {\r\n                new TimeTag(new TextIndex(3, TextIndex.IndexState.End), 4000),\r\n                new TimeTag(new TextIndex(3, TextIndex.IndexState.End)),\r\n                new TimeTag(new TextIndex(3, TextIndex.IndexState.End), 5000),\r\n            },\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.RemoveByPosition(new TextIndex(3, TextIndex.IndexState.End)));\r\n\r\n        AssertSelectedHitObject(h =>\r\n        {\r\n            Assert.That(h.TimeTags.Count, Is.EqualTo(2));\r\n\r\n            // should delete the min time of the time-tag\r\n            var actualTimeTag = h.TimeTags[0];\r\n            Assert.That(actualTimeTag.Index, Is.EqualTo(new TextIndex(3, TextIndex.IndexState.End)));\r\n            Assert.That(actualTimeTag.Time, Is.EqualTo(4000));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestRemoveByPositionCase2()\r\n    {\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            TimeTags = new[]\r\n            {\r\n                new TimeTag(new TextIndex(3, TextIndex.IndexState.End), 5000),\r\n                new TimeTag(new TextIndex(3, TextIndex.IndexState.End), 4000),\r\n            },\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.RemoveByPosition(new TextIndex(3, TextIndex.IndexState.End)));\r\n\r\n        AssertSelectedHitObject(h =>\r\n        {\r\n            Assert.That(h.TimeTags.Count, Is.EqualTo(1));\r\n\r\n            // should delete the min time of the time-tag\r\n            var actualTimeTag = h.TimeTags[0];\r\n            Assert.That(actualTimeTag.Index, Is.EqualTo(new TextIndex(3, TextIndex.IndexState.End)));\r\n            Assert.That(actualTimeTag.Time, Is.EqualTo(5000));\r\n        });\r\n    }\r\n\r\n    [TestCase(ShiftingDirection.Left, ShiftingType.Index, 1)]\r\n    [TestCase(ShiftingDirection.Left, ShiftingType.State, 2)]\r\n    [TestCase(ShiftingDirection.Right, ShiftingType.State, 2)]\r\n    [TestCase(ShiftingDirection.Right, ShiftingType.Index, 3)]\r\n    public void TestShifting(ShiftingDirection direction, ShiftingType type, int expectedIndex)\r\n    {\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            TimeTags = new[]\r\n            {\r\n                new TimeTag(new TextIndex(1)),\r\n                new TimeTag(new TextIndex(1, TextIndex.IndexState.End)),\r\n                new TimeTag(new TextIndex(2), 4000), // target.\r\n                new TimeTag(new TextIndex(2, TextIndex.IndexState.End)),\r\n                new TimeTag(new TextIndex(3)),\r\n            },\r\n        });\r\n\r\n        TriggerHandlerChanged(c =>\r\n        {\r\n            var lyric = Dependencies.Get<EditorBeatmap>().HitObjects.OfType<Lyric>().First();\r\n            var targetTimeTag = lyric.TimeTags[2];\r\n            var actualTimeTag = c.Shifting(targetTimeTag, direction, type);\r\n\r\n            Assert.That(lyric.TimeTags.IndexOf(actualTimeTag), Is.EqualTo(expectedIndex));\r\n\r\n            // the property should be the same\r\n            Assert.That(actualTimeTag.Time, Is.EqualTo(targetTimeTag.Time));\r\n        });\r\n    }\r\n\r\n    [TestCase(ShiftingDirection.Left, ShiftingType.Index, 0)]\r\n    [TestCase(ShiftingDirection.Left, ShiftingType.State, 0)]\r\n    [TestCase(ShiftingDirection.Right, ShiftingType.State, 0)]\r\n    [TestCase(ShiftingDirection.Right, ShiftingType.Index, 0)]\r\n    public void TestShiftingToFirst(ShiftingDirection direction, ShiftingType type, int expectedIndex)\r\n    {\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            TimeTags = new[]\r\n            {\r\n                new TimeTag(new TextIndex(1)), // target.\r\n                new TimeTag(new TextIndex(3)),\r\n            },\r\n        });\r\n\r\n        TriggerHandlerChanged(c =>\r\n        {\r\n            var lyric = Dependencies.Get<EditorBeatmap>().HitObjects.OfType<Lyric>().First();\r\n            var targetTimeTag = lyric.TimeTags[0];\r\n            var actualTimeTag = c.Shifting(targetTimeTag, direction, type);\r\n\r\n            Assert.That(lyric.TimeTags.IndexOf(actualTimeTag), Is.EqualTo(expectedIndex));\r\n\r\n            // the property should be the same\r\n            Assert.That(actualTimeTag.Time, Is.EqualTo(targetTimeTag.Time));\r\n        });\r\n    }\r\n\r\n    [TestCase(ShiftingDirection.Left, ShiftingType.Index, 1)]\r\n    [TestCase(ShiftingDirection.Left, ShiftingType.State, 1)]\r\n    [TestCase(ShiftingDirection.Right, ShiftingType.State, 1)]\r\n    [TestCase(ShiftingDirection.Right, ShiftingType.Index, 1)]\r\n    public void TestShiftingToLast(ShiftingDirection direction, ShiftingType type, int expectedIndex)\r\n    {\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            TimeTags = new[]\r\n            {\r\n                new TimeTag(new TextIndex(0)),\r\n                new TimeTag(new TextIndex(2)), // target.\r\n            },\r\n        });\r\n\r\n        TriggerHandlerChanged(c =>\r\n        {\r\n            var lyric = Dependencies.Get<EditorBeatmap>().HitObjects.OfType<Lyric>().First();\r\n            var targetTimeTag = lyric.TimeTags[1];\r\n            var actualTimeTag = c.Shifting(targetTimeTag, direction, type);\r\n\r\n            Assert.That(lyric.TimeTags.IndexOf(actualTimeTag), Is.EqualTo(expectedIndex));\r\n\r\n            // the property should be the same\r\n            Assert.That(actualTimeTag.Time, Is.EqualTo(targetTimeTag.Time));\r\n        });\r\n    }\r\n\r\n    [TestCase(ShiftingDirection.Left, ShiftingType.Index, 1)]\r\n    [TestCase(ShiftingDirection.Left, ShiftingType.State, 1)]\r\n    [TestCase(ShiftingDirection.Right, ShiftingType.State, 1)]\r\n    [TestCase(ShiftingDirection.Right, ShiftingType.Index, 1)]\r\n    public void TestShiftingWithNoDuplicatedTimeTag(ShiftingDirection direction, ShiftingType type, int expectedIndex)\r\n    {\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            TimeTags = new[]\r\n            {\r\n                new TimeTag(new TextIndex(0)),\r\n                new TimeTag(new TextIndex(2), 4000), // target.\r\n                new TimeTag(new TextIndex(3, TextIndex.IndexState.End)),\r\n            },\r\n        });\r\n\r\n        TriggerHandlerChanged(c =>\r\n        {\r\n            var lyric = Dependencies.Get<EditorBeatmap>().HitObjects.OfType<Lyric>().First();\r\n            var targetTimeTag = lyric.TimeTags[1];\r\n            var actualTimeTag = c.Shifting(targetTimeTag, direction, type);\r\n\r\n            Assert.That(lyric.TimeTags.IndexOf(actualTimeTag), Is.EqualTo(expectedIndex));\r\n\r\n            // the property should be the same\r\n            Assert.That(actualTimeTag.Time, Is.EqualTo(targetTimeTag.Time));\r\n        });\r\n    }\r\n\r\n    [TestCase(ShiftingDirection.Left, ShiftingType.Index, 0)]\r\n    [TestCase(ShiftingDirection.Left, ShiftingType.State, 0)]\r\n    [TestCase(ShiftingDirection.Right, ShiftingType.State, 0)]\r\n    [TestCase(ShiftingDirection.Right, ShiftingType.Index, 0)]\r\n    public void TestShiftingWithOneTimeTag(ShiftingDirection direction, ShiftingType type, int expectedIndex)\r\n    {\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            TimeTags = new[]\r\n            {\r\n                new TimeTag(new TextIndex(2), 4000), // target.\r\n            },\r\n        });\r\n\r\n        TriggerHandlerChanged(c =>\r\n        {\r\n            var lyric = Dependencies.Get<EditorBeatmap>().HitObjects.OfType<Lyric>().First();\r\n            var targetTimeTag = lyric.TimeTags[0];\r\n            var actualTimeTag = c.Shifting(targetTimeTag, direction, type);\r\n\r\n            Assert.That(lyric.TimeTags.IndexOf(actualTimeTag), Is.EqualTo(expectedIndex));\r\n\r\n            // the property should be the same\r\n            Assert.That(actualTimeTag.Time, Is.EqualTo(targetTimeTag.Time));\r\n        });\r\n    }\r\n\r\n    [Ignore(\"Will be implement if it will increase better UX.\")]\r\n    [TestCase(ShiftingDirection.Left, ShiftingType.State, 1)]\r\n    [TestCase(ShiftingDirection.Left, ShiftingType.Index, 1)]\r\n    [TestCase(ShiftingDirection.Right, ShiftingType.State, 3)]\r\n    [TestCase(ShiftingDirection.Right, ShiftingType.Index, 3)]\r\n    public void TestShiftingWithSameTimeTag(ShiftingDirection direction, ShiftingType type, int expectedIndex)\r\n    {\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            TimeTags = new[]\r\n            {\r\n                new TimeTag(new TextIndex(2), 3000),\r\n                new TimeTag(new TextIndex(2), 4000), // target.\r\n                new TimeTag(new TextIndex(2), 5000),\r\n            },\r\n        });\r\n\r\n        TriggerHandlerChanged(c =>\r\n        {\r\n            var lyric = Dependencies.Get<EditorBeatmap>().HitObjects.OfType<Lyric>().First();\r\n            var targetTimeTag = lyric.TimeTags[1];\r\n            var actualTimeTag = c.Shifting(targetTimeTag, direction, type);\r\n\r\n            Assert.That(lyric.TimeTags.IndexOf(actualTimeTag), Is.EqualTo(expectedIndex));\r\n\r\n            // the property should be the same\r\n            Assert.That(actualTimeTag.Time, Is.EqualTo(targetTimeTag.Time));\r\n        });\r\n    }\r\n\r\n    [TestCase(TextIndex.IndexState.Start, ShiftingDirection.Left, ShiftingType.Index)]\r\n    [TestCase(TextIndex.IndexState.Start, ShiftingDirection.Left, ShiftingType.State)]\r\n    [TestCase(TextIndex.IndexState.Start, ShiftingDirection.Right, ShiftingType.Index)]\r\n    [TestCase(TextIndex.IndexState.End, ShiftingDirection.Left, ShiftingType.Index)]\r\n    [TestCase(TextIndex.IndexState.End, ShiftingDirection.Right, ShiftingType.State)]\r\n    [TestCase(TextIndex.IndexState.End, ShiftingDirection.Right, ShiftingType.Index)]\r\n    public void TestShiftingException(TextIndex.IndexState state, ShiftingDirection direction, ShiftingType type)\r\n    {\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"-\",\r\n            TimeTags = new[]\r\n            {\r\n                new TimeTag(new TextIndex(0, state), 5000), // target.\r\n            },\r\n        });\r\n\r\n        // will have exception because the time-tag cannot move right.\r\n        TriggerHandlerChanged(c =>\r\n        {\r\n            Assert.Throws<ArgumentOutOfRangeException>(() =>\r\n            {\r\n                var lyric = Dependencies.Get<EditorBeatmap>().HitObjects.OfType<Lyric>().First();\r\n                var targetTimeTag = lyric.TimeTags[0];\r\n                c.Shifting(targetTimeTag, direction, type);\r\n            });\r\n        });\r\n    }\r\n\r\n    [TestCase(true)]\r\n    [TestCase(false)]\r\n    public void TestWithReferenceLyric(bool syncTimeTag)\r\n    {\r\n        var lyric = PrepareLyricWithSyncConfig(new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            TimeTags = new[]\r\n            {\r\n                new TimeTag(new TextIndex(), 1000),\r\n            },\r\n        }, new SyncLyricConfig\r\n        {\r\n            SyncTimeTagProperty = syncTimeTag,\r\n        });\r\n\r\n        // should add the time-tag by hand because it does not sync from thr referenced lyric.\r\n        if (!syncTimeTag)\r\n        {\r\n            lyric.TimeTags = new[]\r\n            {\r\n                new TimeTag(new TextIndex(), 2000),\r\n            };\r\n        }\r\n\r\n        var timeTag = lyric.TimeTags.First();\r\n\r\n        if (syncTimeTag)\r\n        {\r\n            TriggerHandlerChangedWithException<ChangeForbiddenException>(c => c.SetTimeTagTime(timeTag, 2000));\r\n        }\r\n        else\r\n        {\r\n            TriggerHandlerChanged(c => c.SetTimeTagTime(timeTag, 2000));\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/ChangeHandlers/Lyrics/LyricTranslationChangeHandlerTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Globalization;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.ChangeHandlers.Lyrics;\r\n\r\npublic partial class LyricTranslationChangeHandlerTest : LyricPropertyChangeHandlerTest<LyricTranslationChangeHandler>\r\n{\r\n    [Test]\r\n    public void TestUpdateTranslationWithNewLanguage()\r\n    {\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.UpdateTranslation(new CultureInfo(17), \"からおけ\"));\r\n\r\n        AssertSelectedHitObject(h =>\r\n        {\r\n            Assert.That(h.Translations.Count, Is.EqualTo(1));\r\n            Assert.That(h.Translations[new CultureInfo(17)], Is.EqualTo(\"からおけ\"));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestUpdateTranslationWithExistLanguage()\r\n    {\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            Translations = new Dictionary<CultureInfo, string>\r\n            {\r\n                { new CultureInfo(17), \"からおけ\" },\r\n            },\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.UpdateTranslation(new CultureInfo(17), \"karaoke\"));\r\n\r\n        AssertSelectedHitObject(h =>\r\n        {\r\n            Assert.That(h.Translations.Count, Is.EqualTo(1));\r\n            Assert.That(h.Translations[new CultureInfo(17)], Is.EqualTo(\"karaoke\"));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestUpdateTranslationWithEmptyText()\r\n    {\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            Translations = new Dictionary<CultureInfo, string>\r\n            {\r\n                { new CultureInfo(17), \"からおけ\" },\r\n            },\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.UpdateTranslation(new CultureInfo(17), string.Empty));\r\n\r\n        AssertSelectedHitObject(h =>\r\n        {\r\n            Assert.That(h.Translations, Is.Empty);\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestUpdateTranslationWithNullText()\r\n    {\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            Translations = new Dictionary<CultureInfo, string>\r\n            {\r\n                { new CultureInfo(17), \"からおけ\" },\r\n            },\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.UpdateTranslation(new CultureInfo(17), string.Empty));\r\n\r\n        AssertSelectedHitObject(h =>\r\n        {\r\n            Assert.That(h.Translations, Is.Empty);\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestWithReferenceLyric()\r\n    {\r\n        PrepareLyricWithSyncConfig(new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n        });\r\n\r\n        TriggerHandlerChangedWithException<ChangeForbiddenException>(c => c.UpdateTranslation(new CultureInfo(17), \"からおけ\"));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/ChangeHandlers/Lyrics/LyricsChangeHandlerTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.ChangeHandlers.Lyrics;\r\n\r\npublic partial class LyricsChangeHandlerTest : BaseHitObjectChangeHandlerTest<LyricsChangeHandler, Lyric>\r\n{\r\n    [Test]\r\n    public void TestSplit()\r\n    {\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.Split(2));\r\n\r\n        AssertHitObjects(objects =>\r\n        {\r\n            var lyrics = objects.ToArray();\r\n            var firstLyric = lyrics.First();\r\n            var secondLyric = lyrics.Last();\r\n\r\n            // test property in the first lyric.\r\n            Assert.That(firstLyric.Text, Is.EqualTo(\"カラ\"));\r\n            Assert.That(firstLyric.Order, Is.EqualTo(0));\r\n\r\n            // test property in the second lyric.\r\n            Assert.That(secondLyric.Text, Is.EqualTo(\"オケ\"));\r\n            Assert.That(secondLyric.Order, Is.EqualTo(1));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestCombine()\r\n    {\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"カラ\",\r\n            Order = 1,\r\n        }, false);\r\n\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"オケ\",\r\n            Order = 2,\r\n        });\r\n\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"karaoke\",\r\n            Order = 3,\r\n        }, false);\r\n\r\n        TriggerHandlerChanged(c => c.Combine());\r\n\r\n        AssertHitObjects(objects =>\r\n        {\r\n            var lyrics = objects.ToArray();\r\n            Assert.That(lyrics.Length, Is.EqualTo(2));\r\n\r\n            var combinedLyric = lyrics.First(x => x.Text == \"カラオケ\");\r\n            Assert.That(combinedLyric.Order, Is.EqualTo(1));\r\n\r\n            var notAffectedLyric = lyrics.First(x => x.Text == \"karaoke\");\r\n            Assert.That(notAffectedLyric.Order, Is.EqualTo(2));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestCreateAtPosition()\r\n    {\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            Order = 1,\r\n        });\r\n\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"karaoke\",\r\n            Order = 2,\r\n        }, false);\r\n\r\n        TriggerHandlerChanged(c => c.CreateAtPosition());\r\n\r\n        AssertHitObjects(hitObjects =>\r\n        {\r\n            var lyrics = hitObjects.ToArray();\r\n            Assert.That(lyrics.Length, Is.EqualTo(3));\r\n\r\n            var firstLyric = lyrics.First(x => x.Text == \"カラオケ\");\r\n            Assert.That(firstLyric.Order, Is.EqualTo(1));\r\n\r\n            var secondLyric = lyrics.First(x => x.Text == \"New lyric\");\r\n            Assert.That(secondLyric.Order, Is.EqualTo(2));\r\n\r\n            var thirdLyric = lyrics.First(x => x.Text == \"karaoke\");\r\n            Assert.That(thirdLyric.Order, Is.EqualTo(3));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestCreateAtLast()\r\n    {\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            Order = 1,\r\n        });\r\n\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"karaoke\",\r\n            Order = 2,\r\n        }, false);\r\n\r\n        TriggerHandlerChanged(c => c.CreateAtLast());\r\n\r\n        AssertHitObjects(hitObjects =>\r\n        {\r\n            var lyrics = hitObjects.ToArray();\r\n            Assert.That(lyrics.Length, Is.EqualTo(3));\r\n\r\n            var firstLyric = lyrics.First(x => x.Text == \"カラオケ\");\r\n            Assert.That(firstLyric.Order, Is.EqualTo(1));\r\n\r\n            var secondLyric = lyrics.First(x => x.Text == \"karaoke\");\r\n            Assert.That(secondLyric.Order, Is.EqualTo(2));\r\n\r\n            var thirdLyric = lyrics.First(x => x.Text == \"New lyric\");\r\n            Assert.That(thirdLyric.Order, Is.EqualTo(3));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestCreateAtLastWithEmptyBeatmap()\r\n    {\r\n        TriggerHandlerChanged(c => c.CreateAtLast());\r\n\r\n        AssertHitObjects(hitObjects =>\r\n        {\r\n            var lyrics = hitObjects.ToArray();\r\n            Assert.That(lyrics.Length, Is.EqualTo(1));\r\n\r\n            var addedLyric = lyrics.First(x => x.Text == \"New lyric\");\r\n            Assert.That(addedLyric.Order, Is.EqualTo(1));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestAddBelowToSelection()\r\n    {\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            Order = 1,\r\n        });\r\n\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"Last lyric\",\r\n            Order = 2,\r\n        }, false);\r\n\r\n        TriggerHandlerChanged(c => c.AddBelowToSelection(new Lyric\r\n        {\r\n            Text = \"New lyric\",\r\n        }));\r\n\r\n        AssertHitObjects(hitObjects =>\r\n        {\r\n            var lyrics = hitObjects.ToArray();\r\n            Assert.That(lyrics.Length, Is.EqualTo(3));\r\n\r\n            var addedLyric = lyrics.First(x => x.Text == \"New lyric\");\r\n            Assert.That(addedLyric.Order, Is.EqualTo(2));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestAddRangeBelowToSelection()\r\n    {\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            Order = 1,\r\n        });\r\n\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"Last lyric\",\r\n            Order = 2,\r\n        }, false);\r\n\r\n        TriggerHandlerChanged(c => c.AddRangeBelowToSelection(new[]\r\n        {\r\n            new Lyric\r\n            {\r\n                Text = \"New lyric\",\r\n            },\r\n        }));\r\n\r\n        AssertHitObjects(hitObjects =>\r\n        {\r\n            var lyrics = hitObjects.ToArray();\r\n            Assert.That(lyrics.Length, Is.EqualTo(3));\r\n\r\n            var addedLyric = lyrics.First(x => x.Text == \"New lyric\");\r\n            Assert.That(addedLyric.Order, Is.EqualTo(2));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestRemove()\r\n    {\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            Order = 1,\r\n        });\r\n\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"karaoke\",\r\n            Order = 2,\r\n        }, false);\r\n\r\n        TriggerHandlerChanged(c => c.Remove());\r\n\r\n        AssertHitObjects(hitObjects =>\r\n        {\r\n            var lyrics = hitObjects.ToArray();\r\n            Assert.That(lyrics.Length, Is.EqualTo(1));\r\n\r\n            var secondLyric = lyrics.First(x => x.Text == \"karaoke\");\r\n            Assert.That(secondLyric.Order, Is.EqualTo(1));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestChangeOrder()\r\n    {\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            Order = 1,\r\n        });\r\n\r\n        PrepareHitObject(() => new Lyric\r\n        {\r\n            Text = \"karaoke\",\r\n            Order = 2,\r\n        }, false);\r\n\r\n        // move the \"カラオケ\" lyric next of the \"karaoke\" lyric.\r\n        TriggerHandlerChanged(c => c.ChangeOrder(1));\r\n\r\n        AssertHitObjects(hitObjects =>\r\n        {\r\n            var lyrics = hitObjects.ToArray();\r\n            Assert.That(lyrics.Length, Is.EqualTo(2));\r\n\r\n            var firstLyric = lyrics.First(x => x.Text == \"karaoke\");\r\n            Assert.That(firstLyric.Order, Is.EqualTo(1));\r\n\r\n            var secondLyric = lyrics.First(x => x.Text == \"カラオケ\");\r\n            Assert.That(secondLyric.Order, Is.EqualTo(2));\r\n        });\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/ChangeHandlers/Notes/NotePropertyChangeHandlerTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Notes;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Properties;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.ChangeHandlers.Notes;\r\n\r\npublic partial class NotePropertyChangeHandlerTest : BaseHitObjectPropertyChangeHandlerTest<NotePropertyChangeHandler, Note>\r\n{\r\n    private static readonly ElementId referenced_lyric_id = TestCaseElementIdHelper.CreateElementIdByNumber(1);\r\n\r\n    [Test]\r\n    public void TestChangeText()\r\n    {\r\n        PrepareHitObject(() => new Note\r\n        {\r\n            ReferenceLyricId = referenced_lyric_id,\r\n            Text = \"カラオケ\",\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.ChangeText(\"からおけ\"));\r\n\r\n        AssertSelectedHitObject(h =>\r\n        {\r\n            Assert.That(h.Text, Is.EqualTo(\"からおけ\"));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestChangeRubyText()\r\n    {\r\n        PrepareHitObject(() => new Note\r\n        {\r\n            ReferenceLyricId = referenced_lyric_id,\r\n            RubyText = \"からおけ\",\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.ChangeRubyText(\"カラオケ\"));\r\n\r\n        AssertSelectedHitObject(h =>\r\n        {\r\n            Assert.That(h.RubyText, Is.EqualTo(\"カラオケ\"));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestChangeDisplayStateToVisible()\r\n    {\r\n        PrepareHitObject(() => new Note\r\n        {\r\n            ReferenceLyricId = referenced_lyric_id,\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.ChangeDisplayState(true));\r\n\r\n        AssertSelectedHitObject(h =>\r\n        {\r\n            Assert.That(h.Display);\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestChangeDisplayStateToNonVisible()\r\n    {\r\n        PrepareHitObject(() => new Note\r\n        {\r\n            ReferenceLyricId = referenced_lyric_id,\r\n            Display = true,\r\n            Tone = new Tone(3),\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.ChangeDisplayState(false));\r\n\r\n        AssertSelectedHitObject(h =>\r\n        {\r\n            Assert.That(h.Display, Is.False);\r\n            Assert.That(h.Tone, Is.EqualTo(new Tone()));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    [Ignore(\"Waiting to implement the lock rules.\")]\r\n    public void TestWithReferenceLyric()\r\n    {\r\n        PrepareHitObject(() => new Note\r\n        {\r\n            Text = \"カラオケ\",\r\n            ReferenceLyric = new Lyric\r\n            {\r\n                ReferenceLyric = new Lyric(),\r\n                ReferenceLyricConfig = new ReferenceLyricConfig(),\r\n            },\r\n        });\r\n\r\n        TriggerHandlerChangedWithException<ChangeForbiddenException>(c => c.ChangeText(\"からおけ\"));\r\n    }\r\n\r\n    [Test]\r\n    public void TestOffsetTone()\r\n    {\r\n        PrepareHitObject(() => new Note\r\n        {\r\n            ReferenceLyricId = referenced_lyric_id,\r\n            Display = true,\r\n            Tone = new Tone(3),\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.OffsetTone(new Tone(-3)));\r\n\r\n        AssertSelectedHitObject(h =>\r\n        {\r\n            Assert.That(h.Tone, Is.EqualTo(new Tone()));\r\n            Assert.That(h.Display);\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestOffsetToneWithZeroValue()\r\n    {\r\n        PrepareHitObject(() => new Note\r\n        {\r\n            ReferenceLyricId = referenced_lyric_id,\r\n            Display = true,\r\n            Tone = new Tone(3),\r\n        });\r\n\r\n        // offset value should not be zero.\r\n        TriggerHandlerChangedWithException<InvalidOperationException>(c => c.OffsetTone(new Tone()));\r\n    }\r\n\r\n    protected override void SetUpEditorBeatmap(Action<EditorBeatmap> action)\r\n    {\r\n        base.SetUpEditorBeatmap(editorBeatmap =>\r\n        {\r\n            action(editorBeatmap);\r\n\r\n            editorBeatmap.Add(new Lyric\r\n            {\r\n                Text = \"Referenced lyric\",\r\n            }.ChangeId(referenced_lyric_id));\r\n        });\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/ChangeHandlers/Notes/NotesChangeHandlerTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Notes;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.ChangeHandlers.Notes;\r\n\r\npublic partial class NotesChangeHandlerTest : BaseHitObjectChangeHandlerTest<NotesChangeHandler, Note>\r\n{\r\n    [Test]\r\n    public void TestSplit()\r\n    {\r\n        var referencedLyric = TestCaseNoteHelper.CreateLyricForNote(2, \"カラオケ\", 1000, 1000);\r\n\r\n        PrepareHitObject(() => new Note\r\n        {\r\n            Text = \"カラオケ\",\r\n            ReferenceLyricId = referencedLyric.ID,\r\n            ReferenceLyric = referencedLyric,\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.Split());\r\n\r\n        AssertHitObjects(notes =>\r\n        {\r\n            var actualNotes = notes.ToArray();\r\n            Assert.That(actualNotes.Length, Is.EqualTo(2));\r\n            var firstNote = actualNotes[0];\r\n            var secondNote = actualNotes[1];\r\n            Assert.That(firstNote.ReferenceLyric, Is.SameAs(secondNote.ReferenceLyric));\r\n            Assert.That(firstNote.Text, Is.EqualTo(\"カラオケ\"));\r\n            Assert.That(firstNote.StartTime, Is.EqualTo(1000));\r\n            Assert.That(firstNote.Duration, Is.EqualTo(500));\r\n            Assert.That(secondNote.Text, Is.EqualTo(\"カラオケ\"));\r\n            Assert.That(secondNote.StartTime, Is.EqualTo(1500));\r\n            Assert.That(secondNote.Duration, Is.EqualTo(500));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestCombine()\r\n    {\r\n        var referencedLyric = TestCaseNoteHelper.CreateLyricForNote(2, \"カラオケ\", 1000, 1000);\r\n\r\n        // note that lyric and notes should in the selection.\r\n        PrepareHitObject(() => referencedLyric);\r\n        PrepareHitObjects(() => new[]\r\n        {\r\n            new Note\r\n            {\r\n                Text = \"カラ\",\r\n                RubyText = \"から\",\r\n                ReferenceLyricId = referencedLyric.ID,\r\n                ReferenceLyric = referencedLyric,\r\n                ReferenceTimeTagIndex = 0,\r\n            },\r\n            new Note\r\n            {\r\n                Text = \"オケ\",\r\n                RubyText = \"おけ\",\r\n                ReferenceLyricId = referencedLyric.ID,\r\n                ReferenceLyric = referencedLyric,\r\n                ReferenceTimeTagIndex = 0,\r\n            },\r\n        });\r\n\r\n        TriggerHandlerChanged(c => c.Combine());\r\n\r\n        AssertHitObjects(notes =>\r\n        {\r\n            var actualNotes = notes.ToArray();\r\n            Assert.That(actualNotes.Length, Is.EqualTo(1));\r\n            var combinedNote = actualNotes.First();\r\n            Assert.That(combinedNote.Text, Is.EqualTo(\"カラ\"));\r\n            Assert.That(combinedNote.RubyText, Is.EqualTo(\"から\"));\r\n            Assert.That(combinedNote.StartTime, Is.EqualTo(1000));\r\n            Assert.That(combinedNote.Duration, Is.EqualTo(1000));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestClear()\r\n    {\r\n        var referencedLyric = new Lyric();\r\n\r\n        // note that lyric and notes should in the selection.\r\n        PrepareHitObject(() => referencedLyric);\r\n        PrepareHitObjects(() => new[]\r\n        {\r\n            new Note\r\n            {\r\n                Text = \"カラ\",\r\n                RubyText = \"から\",\r\n                ReferenceLyricId = referencedLyric.ID,\r\n                ReferenceLyric = referencedLyric,\r\n            },\r\n            new Note\r\n            {\r\n                Text = \"オケ\",\r\n                RubyText = \"おけ\",\r\n                ReferenceLyricId = referencedLyric.ID,\r\n                ReferenceLyric = referencedLyric,\r\n            },\r\n        }, false);\r\n\r\n        TriggerHandlerChanged(c => c.Clear());\r\n\r\n        AssertHitObjects(x => Assert.That(x, Is.Empty));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/ChangeHandlers/Stages/BaseStageInfoChangeHandlerTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.ChangeHandlers.Stages;\r\n\r\npublic abstract partial class BaseStageInfoChangeHandlerTest<TChangeHandler> : BaseChangeHandlerTest<TChangeHandler>\r\n    where TChangeHandler : Component\r\n{\r\n    protected virtual void SetUpStageInfo<TStageInfo>(Action<TStageInfo>? action = null)\r\n        => throw new NotImplementedException();\r\n\r\n    public void AssertStageInfos(Action<IList<StageInfo>> assert)\r\n        => throw new NotImplementedException();\r\n\r\n    public void AssertStageInfo<TStageInfo>(Action<TStageInfo> assert) where TStageInfo : StageInfo\r\n        => throw new NotImplementedException();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/ChangeHandlers/Stages/ClassicStageChangeHandlerTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Stages;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos.Classic;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.ChangeHandlers.Stages;\r\n\r\n[Ignore(\"Ignore all stage-related change handler test until able to edit the stage info.\")]\r\npublic partial class ClassicStageChangeHandlerTest : BaseStageInfoChangeHandlerTest<ClassicStageChangeHandler>\r\n{\r\n    #region Layout definition\r\n\r\n    [Test]\r\n    public void TestEditLayoutDefinition()\r\n    {\r\n        SetUpStageInfo<ClassicStageInfo>();\r\n\r\n        TriggerHandlerChanged(c =>\r\n        {\r\n            c.EditLayoutDefinition(x =>\r\n            {\r\n                x.LineHeight = 12;\r\n            });\r\n        });\r\n\r\n        AssertStageInfo<ClassicStageInfo>(stageInfo =>\r\n        {\r\n            Assert.That(stageInfo, Is.Not.Null);\r\n            // assert definition。\r\n            var definition = stageInfo.StageDefinition;\r\n            Assert.That(definition.LineHeight, Is.EqualTo(12));\r\n        });\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Timing info\r\n\r\n    [Test]\r\n    public void TestAddTimingPoint()\r\n    {\r\n        SetUpStageInfo<ClassicStageInfo>();\r\n\r\n        TriggerHandlerChanged(c =>\r\n        {\r\n            c.AddTimingPoint(x =>\r\n            {\r\n                x.Time = 1000;\r\n            });\r\n        });\r\n\r\n        AssertStageInfo<ClassicStageInfo>(stageInfo =>\r\n        {\r\n            var timingInfo = stageInfo.LyricTimingInfo;\r\n            Assert.That(timingInfo, Is.Not.Null);\r\n            // assert timing。\r\n            var timingPoint = timingInfo.Timings.FirstOrDefault();\r\n            Assert.That(timingPoint, Is.Not.Null);\r\n            Assert.That(timingPoint!.Time, Is.EqualTo(1000));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestRemoveTimingPoint()\r\n    {\r\n        ClassicLyricTimingPoint removedTimingPoint = null!;\r\n\r\n        SetUpStageInfo<ClassicStageInfo>(stageInfo =>\r\n        {\r\n            var timingInfo = stageInfo.LyricTimingInfo;\r\n            timingInfo.Timings.Add(new ClassicLyricTimingPoint\r\n            {\r\n                Time = 1000,\r\n            });\r\n\r\n            timingInfo.Timings.Add(removedTimingPoint = new ClassicLyricTimingPoint\r\n            {\r\n                Time = 2000,\r\n            });\r\n        });\r\n\r\n        TriggerHandlerChanged(c =>\r\n        {\r\n            c.RemoveTimingPoint(removedTimingPoint);\r\n        });\r\n\r\n        AssertStageInfo<ClassicStageInfo>(stageInfo =>\r\n        {\r\n            var timingInfo = stageInfo.LyricTimingInfo;\r\n            Assert.That(timingInfo, Is.Not.Null);\r\n            // assert timing。\r\n            var timingPoint = timingInfo.Timings.FirstOrDefault();\r\n            Assert.That(timingPoint, Is.Not.Null);\r\n            Assert.That(timingPoint!.Time, Is.EqualTo(1000));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestRemoveRangeOfTimingPoints()\r\n    {\r\n        ClassicLyricTimingPoint removedTimingPoint = null!;\r\n\r\n        SetUpStageInfo<ClassicStageInfo>(stageInfo =>\r\n        {\r\n            var timingInfo = stageInfo.LyricTimingInfo;\r\n            timingInfo.Timings.Add(new ClassicLyricTimingPoint\r\n            {\r\n                Time = 1000,\r\n            });\r\n\r\n            timingInfo.Timings.Add(removedTimingPoint = new ClassicLyricTimingPoint\r\n            {\r\n                Time = 2000,\r\n            });\r\n        });\r\n\r\n        TriggerHandlerChanged(c =>\r\n        {\r\n            c.RemoveRangeOfTimingPoints(new[] { removedTimingPoint });\r\n        });\r\n\r\n        AssertStageInfo<ClassicStageInfo>(stageInfo =>\r\n        {\r\n            var timingInfo = stageInfo.LyricTimingInfo;\r\n            Assert.That(timingInfo, Is.Not.Null);\r\n            // assert timing。\r\n            var timingPoint = timingInfo.Timings.FirstOrDefault();\r\n            Assert.That(timingPoint, Is.Not.Null);\r\n            Assert.That(timingPoint!.Time, Is.EqualTo(1000));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestShiftingTimingPoints()\r\n    {\r\n        ClassicLyricTimingPoint shiftingTimingPoint1 = null!;\r\n        ClassicLyricTimingPoint shiftingTimingPoint2 = null!;\r\n\r\n        SetUpStageInfo<ClassicStageInfo>(stageInfo =>\r\n        {\r\n            var timingInfo = stageInfo.LyricTimingInfo;\r\n            timingInfo.Timings.Add(shiftingTimingPoint1 = new ClassicLyricTimingPoint\r\n            {\r\n                Time = 1000,\r\n            });\r\n\r\n            timingInfo.Timings.Add(shiftingTimingPoint2 = new ClassicLyricTimingPoint\r\n            {\r\n                Time = 2000,\r\n            });\r\n        });\r\n\r\n        TriggerHandlerChanged(c =>\r\n        {\r\n            c.ShiftingTimingPoints(new[] { shiftingTimingPoint1, shiftingTimingPoint2 }, 100);\r\n        });\r\n\r\n        AssertStageInfo<ClassicStageInfo>(stageInfo =>\r\n        {\r\n            var timingInfo = stageInfo.LyricTimingInfo;\r\n            Assert.That(timingInfo, Is.Not.Null);\r\n            // assert timing。\r\n            var timingPoint = timingInfo.Timings;\r\n            Assert.That(timingPoint.Count, Is.EqualTo(2));\r\n            Assert.That(timingPoint[0].Time, Is.EqualTo(1100));\r\n            Assert.That(timingPoint[1].Time, Is.EqualTo(2100));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestAddLyricIntoTimingPoint()\r\n    {\r\n        ClassicLyricTimingPoint timingPoint = null!;\r\n\r\n        SetUpStageInfo<ClassicStageInfo>(stageInfo =>\r\n        {\r\n            var timingInfo = stageInfo.LyricTimingInfo;\r\n            timingInfo.Timings.Add(timingPoint = new ClassicLyricTimingPoint\r\n            {\r\n                Time = 1000,\r\n            });\r\n        });\r\n\r\n        Lyric lyric1 = null!;\r\n        Lyric lyric2 = null!;\r\n\r\n        PrepareHitObject(() => lyric1 = new Lyric());\r\n        PrepareHitObject(() => lyric2 = new Lyric(), false);\r\n\r\n        TriggerHandlerChanged(c =>\r\n        {\r\n            c.AddLyricIntoTimingPoint(timingPoint);\r\n        });\r\n\r\n        AssertStageInfo<ClassicStageInfo>(stageInfo =>\r\n        {\r\n            var timingInfo = stageInfo.LyricTimingInfo;\r\n            Assert.That(timingInfo, Is.Not.Null);\r\n            // assert mapping status。\r\n            Assert.That(timingInfo.GetLyricTimingPoints(lyric1), Is.EqualTo(new[] { timingPoint }));\r\n            Assert.That(timingInfo.GetLyricTimingPoints(lyric2), Is.Empty);\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestRemoveLyricFromTimingPoint()\r\n    {\r\n        ClassicLyricTimingPoint timingPoint = null!;\r\n\r\n        Lyric lyric1 = null!;\r\n        Lyric lyric2 = null!;\r\n\r\n        PrepareHitObject(() => lyric1 = new Lyric());\r\n        PrepareHitObject(() => lyric2 = new Lyric(), false);\r\n\r\n        SetUpStageInfo<ClassicStageInfo>(stageInfo =>\r\n        {\r\n            var timingInfo = stageInfo.LyricTimingInfo;\r\n            timingInfo.Timings.Add(timingPoint = new ClassicLyricTimingPoint\r\n            {\r\n                Time = 1000,\r\n            });\r\n            timingInfo.AddToMapping(timingPoint, lyric1);\r\n            timingInfo.AddToMapping(timingPoint, lyric2);\r\n        });\r\n\r\n        TriggerHandlerChanged(c =>\r\n        {\r\n            c.RemoveLyricFromTimingPoint(timingPoint);\r\n        });\r\n\r\n        AssertStageInfo<ClassicStageInfo>(stageInfo =>\r\n        {\r\n            var timingInfo = stageInfo.LyricTimingInfo;\r\n            Assert.That(timingInfo, Is.Not.Null);\r\n            // assert mapping status。\r\n            Assert.That(timingInfo.GetLyricTimingPoints(lyric1), Is.Empty); // should clear the mapping in the lyric1 because it's being selected。\r\n            Assert.That(timingInfo.GetLyricTimingPoints(lyric2), Is.EqualTo(new[] { timingPoint }));\r\n        });\r\n    }\r\n\r\n    #endregion\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/ChangeHandlers/Stages/StageElementCategoryChangeHandlerTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Stages;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Stages;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Commands;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\nusing osu.Game.Rulesets.Objects.Drawables;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.ChangeHandlers.Stages;\r\n\r\n[Ignore(\"Ignore all stage-related change handler test until able to edit the stage info.\")]\r\npublic partial class StageElementCategoryChangeHandlerTest : BaseStageInfoChangeHandlerTest<StageElementCategoryChangeHandlerTest.TestStageElementCategoryChangeHandler>\r\n{\r\n    protected override TestStageElementCategoryChangeHandler CreateChangeHandler()\r\n        => new(x => x.OfType<TestStageInfo>().First().Category);\r\n\r\n    [Test]\r\n    public void TestAddElement()\r\n    {\r\n        TriggerHandlerChanged(c =>\r\n        {\r\n            c.AddElement(x =>\r\n            {\r\n                x.Name = \"Element 1\";\r\n            });\r\n        });\r\n\r\n        AssertStageInfo<TestStageInfo>(stageInfo =>\r\n        {\r\n            var category = stageInfo.Category;\r\n            var firstElement = category.AvailableElements.First();\r\n\r\n            Assert.That(firstElement.Name, Is.EqualTo(\"Element 1\"));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestEditElement()\r\n    {\r\n        TestStageElement element = null!;\r\n\r\n        SetUpStageInfo<TestStageInfo>(stageInfo =>\r\n        {\r\n            var category = stageInfo.Category;\r\n            element = category.AddElement();\r\n        });\r\n\r\n        TriggerHandlerChanged(c =>\r\n        {\r\n            c.EditElement(element.ID, x =>\r\n            {\r\n                x.Name = \"Edit Element 1\";\r\n            });\r\n        });\r\n\r\n        AssertStageInfo<TestStageInfo>(stageInfo =>\r\n        {\r\n            var category = stageInfo.Category;\r\n            var firstElement = category.AvailableElements.First();\r\n\r\n            Assert.That(firstElement.Name, Is.EqualTo(\"Edit Element 1\"));\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestRemoveElement()\r\n    {\r\n        TestStageElement element = null!;\r\n\r\n        SetUpStageInfo<TestStageInfo>(stageInfo =>\r\n        {\r\n            var category = stageInfo.Category;\r\n            element = category.AddElement();\r\n        });\r\n\r\n        TriggerHandlerChanged(c =>\r\n        {\r\n            c.RemoveElement(element);\r\n        });\r\n\r\n        AssertStageInfo<TestStageInfo>(stageInfo =>\r\n        {\r\n            var category = stageInfo.Category;\r\n\r\n            Assert.That(category.AvailableElements, Is.Empty);\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestAddToMapping()\r\n    {\r\n        TestStageElement element = null!;\r\n\r\n        SetUpStageInfo<TestStageInfo>(stageInfo =>\r\n        {\r\n            var category = stageInfo.Category;\r\n            element = category.AddElement();\r\n        });\r\n\r\n        PrepareHitObject(() => new Lyric());\r\n\r\n        TriggerHandlerChanged(c =>\r\n        {\r\n            c.AddToMapping(element);\r\n        });\r\n\r\n        AssertStageInfo<TestStageInfo>(stageInfo =>\r\n        {\r\n            var category = stageInfo.Category;\r\n\r\n            Assert.That(category.Mappings, Is.Not.Empty);\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestRemoveFromMapping()\r\n    {\r\n        Lyric lyric = new Lyric();\r\n\r\n        SetUpStageInfo<TestStageInfo>(stageInfo =>\r\n        {\r\n            var category = stageInfo.Category;\r\n            var element = category.AddElement();\r\n\r\n            // Add to Mapping\r\n            category.AddToMapping(element, lyric);\r\n        });\r\n\r\n        PrepareHitObject(() => lyric);\r\n\r\n        TriggerHandlerChanged(c =>\r\n        {\r\n            c.RemoveFromMapping();\r\n        });\r\n\r\n        AssertStageInfo<TestStageInfo>(stageInfo =>\r\n        {\r\n            var category = stageInfo.Category;\r\n\r\n            Assert.That(category.Mappings, Is.Empty);\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestClearUnusedMapping()\r\n    {\r\n        SetUpStageInfo<TestStageInfo>(stageInfo =>\r\n        {\r\n            var category = stageInfo.Category;\r\n            var element = category.AddElement();\r\n\r\n            // Add to Mapping\r\n            category.AddToMapping(element, new Lyric());\r\n        });\r\n\r\n        TriggerHandlerChanged(c =>\r\n        {\r\n            c.ClearUnusedMapping();\r\n        });\r\n\r\n        AssertStageInfo<TestStageInfo>(stageInfo =>\r\n        {\r\n            var category = stageInfo.Category;\r\n\r\n            Assert.That(category.Mappings, Is.Empty);\r\n        });\r\n    }\r\n\r\n    public partial class TestStageElementCategoryChangeHandler : StageElementCategoryChangeHandler<TestStageElement, Lyric>\r\n    {\r\n        public TestStageElementCategoryChangeHandler(Func<IEnumerable<StageInfo>, StageElementCategory<TestStageElement, Lyric>> stageCategoryAction)\r\n            : base(stageCategoryAction)\r\n        {\r\n        }\r\n    }\r\n\r\n    private class TestStageInfo : StageInfo\r\n    {\r\n        #region Category\r\n\r\n        /// <summary>\r\n        /// Category to save the <see cref=\"Lyric\"/>'s and <see cref=\"Note\"/>'s style.\r\n        /// </summary>\r\n        public TestCategory Category { get; } = new();\r\n\r\n        #endregion\r\n\r\n        #region Stage element\r\n\r\n        protected override IEnumerable<StageElement> GetLyricStageElements(Lyric lyric)\r\n        {\r\n            return Array.Empty<StageElement>();\r\n        }\r\n\r\n        protected override IEnumerable<StageElement> GetNoteStageElements(Note note)\r\n        {\r\n            return Array.Empty<StageElement>();\r\n        }\r\n\r\n        #endregion\r\n\r\n        #region Provider\r\n\r\n        public override IPlayfieldCommandProvider CreatePlayfieldCommandProvider(bool displayNotePlayfield)\r\n            => throw new NotImplementedException();\r\n\r\n        public override IStageElementProvider? CreateStageElementProvider(bool displayNotePlayfield)\r\n            => throw new NotImplementedException();\r\n\r\n        public override IHitObjectCommandProvider? CreateHitObjectCommandProvider<TObject>() =>\r\n            typeof(TObject) switch\r\n            {\r\n                Type type when type == typeof(Lyric) => new TestCommandProvider(this),\r\n                Type type when type == typeof(Note) => null,\r\n                _ => null\r\n            };\r\n\r\n        #endregion\r\n    }\r\n\r\n    private class TestCategory : StageElementCategory<TestStageElement, Lyric>\r\n    {\r\n        protected override TestStageElement CreateDefaultElement()\r\n            => new();\r\n    }\r\n\r\n    public class TestStageElement : StageElement, IComparable<TestStageElement>\r\n    {\r\n        public int CompareTo(TestStageElement? other)\r\n        {\r\n            return ComparableUtils.CompareByProperty(this, other,\r\n                x => x.Name,\r\n                x => x.ID);\r\n        }\r\n    }\r\n\r\n    private class TestCommandProvider : HitObjectCommandProvider<TestStageInfo, Lyric>\r\n    {\r\n        public TestCommandProvider(TestStageInfo stageInfo)\r\n            : base(stageInfo)\r\n        {\r\n        }\r\n\r\n        protected override double GeneratePreemptTime(Lyric hitObject)\r\n            => 0;\r\n\r\n        protected override Tuple<double?, double?> GetStartAndEndTime(Lyric lyric)\r\n        {\r\n            if (!lyric.TimeValid)\r\n                return new Tuple<double?, double?>(null, null);\r\n\r\n            return new Tuple<double?, double?>(lyric.StartTime, lyric.EndTime);\r\n        }\r\n\r\n        protected override IEnumerable<IStageCommand> GetInitialCommands(Lyric hitObject)\r\n        {\r\n            throw new NotImplementedException();\r\n        }\r\n\r\n        protected override IEnumerable<IStageCommand> GetStartTimeStateCommands(Lyric hitObject)\r\n        {\r\n            throw new NotImplementedException();\r\n        }\r\n\r\n        protected override IEnumerable<IStageCommand> GetHitStateCommands(Lyric hitObject, ArmedState state)\r\n        {\r\n            throw new NotImplementedException();\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/ChangeHandlers/Stages/StagesChangeHandlerTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Stages;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos.Classic;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.ChangeHandlers.Stages;\r\n\r\n[Ignore(\"Ignore all stage-related change handler test until able to edit the stage info.\")]\r\npublic partial class StagesChangeHandlerTest : BaseStageInfoChangeHandlerTest<StagesChangeHandler>\r\n{\r\n    protected override bool IncludeAutoGenerator => true;\r\n\r\n    [Test]\r\n    public void TestAutoGenerate()\r\n    {\r\n        PrepareHitObject(() => new Lyric(), false);\r\n        PrepareHitObject(() => new Lyric(), false);\r\n\r\n        TriggerHandlerChanged(c =>\r\n        {\r\n            c.AutoGenerate<ClassicStageInfo>();\r\n        });\r\n\r\n        AssertStageInfos(stageInfos =>\r\n        {\r\n            Assert.That(stageInfos.Count, Is.EqualTo(1));\r\n            Assert.That(stageInfos[0].GetType(), Is.EqualTo(typeof(ClassicStageInfo)));\r\n        });\r\n\r\n        // Should not add the same stage again.\r\n        TriggerHandlerChangedWithException<InvalidOperationException>(c =>\r\n        {\r\n            c.AutoGenerate<ClassicStageInfo>();\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestRemove()\r\n    {\r\n        SetUpStageInfo<ClassicStageInfo>();\r\n\r\n        TriggerHandlerChanged(c =>\r\n        {\r\n            c.Remove<ClassicStageInfo>();\r\n        });\r\n\r\n        AssertStageInfos(stageInfos =>\r\n        {\r\n            Assert.That(stageInfos.Count, Is.EqualTo(0));\r\n        });\r\n\r\n        // Should not remove if there's no matched stage info type.\r\n        TriggerHandlerChangedWithException<InvalidOperationException>(c =>\r\n        {\r\n            c.Remove<ClassicStageInfo>();\r\n        });\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Checks/BaseCheckTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Edit;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Checks;\r\n\r\npublic abstract class BaseCheckTest<TCheck> where TCheck : class, ICheck, new()\r\n{\r\n    private TCheck check = null!;\r\n\r\n    [SetUp]\r\n    public void Setup()\r\n    {\r\n        check = new TCheck();\r\n\r\n        // check template in the list should not be duplicated.\r\n        var possibleTemplates = check.PossibleTemplates;\r\n        Assert.That(possibleTemplates.Count(), Is.EqualTo(possibleTemplates.Select(x => x.GetType()).Distinct().Count()));\r\n    }\r\n\r\n    protected void AssertOk(BeatmapVerifierContext context)\r\n    {\r\n        Assert.That(Run(context), Is.Empty);\r\n    }\r\n\r\n    protected void AssertNotOk<TIssueTemplate>(BeatmapVerifierContext context)\r\n        where TIssueTemplate : IssueTemplate\r\n    {\r\n        AssertNotOk<Issue, TIssueTemplate>(context);\r\n    }\r\n\r\n    protected void AssertNotOk<TIssue, TIssueTemplate>(BeatmapVerifierContext context)\r\n        where TIssue : Issue\r\n        where TIssueTemplate : IssueTemplate\r\n    {\r\n        var issues = Run(context).ToList();\r\n\r\n        // should make sure that only has one issue.\r\n        Assert.That(issues.Single().GetType(), Is.EqualTo(typeof(TIssue)));\r\n        Assert.That(issues.Single().Template.GetType(), Is.EqualTo(typeof(TIssueTemplate)));\r\n\r\n        // should make sure that issue template is in the list。\r\n        Assert.That(check.PossibleTemplates.OfType<TIssueTemplate>().Single(), Is.Not.Null);\r\n    }\r\n\r\n    protected IEnumerable<Issue> Run(BeatmapVerifierContext context)\r\n    {\r\n        return check.Run(context);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Checks/BeatmapPropertyCheckTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Checks;\r\n\r\npublic abstract class BeatmapPropertyCheckTest<TCheck> : BaseCheckTest<TCheck> where TCheck : class, ICheck, new();\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Checks/CheckBeatmapAvailableTranslationsTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Globalization;\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Rulesets.Edit;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Screens.Edit;\r\nusing osu.Game.Tests.Beatmaps;\r\nusing static osu.Game.Rulesets.Karaoke.Edit.Checks.CheckBeatmapAvailableTranslations;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Checks;\r\n\r\n[TestFixture]\r\npublic class CheckBeatmapAvailableTranslationsTest : BeatmapPropertyCheckTest<CheckBeatmapAvailableTranslations>\r\n{\r\n    [Test]\r\n    public void TestNoLyricAndNoLanguage()\r\n    {\r\n        // test no lyric and no default language. (should not show alert)\r\n        var beatmap = createTestingBeatmap(null, null);\r\n\r\n        AssertOk(getContext(beatmap));\r\n    }\r\n\r\n    [Test]\r\n    public void TestNoLyricButHaveLanguage()\r\n    {\r\n        // test no lyric and have language. (should not show alert)\r\n        var translationLanguages = new List<CultureInfo> { new(\"Ja-jp\") };\r\n        var beatmap = createTestingBeatmap(translationLanguages, null);\r\n\r\n        AssertOk(getContext(beatmap));\r\n    }\r\n\r\n    [Test]\r\n    public void TestHaveLyricButNoLanguage()\r\n    {\r\n        // test have lyric and no language. (should not show alert)\r\n        var lyrics = new[] { new Lyric() };\r\n        var beatmap = createTestingBeatmap(null, lyrics);\r\n\r\n        AssertOk(getContext(beatmap));\r\n    }\r\n\r\n    [Test]\r\n    public void TestEveryLyricContainsTranslation()\r\n    {\r\n        var translationLanguages = new List<CultureInfo> { new(\"Ja-jp\") };\r\n        var beatmap = createTestingBeatmap(translationLanguages, new[]\r\n        {\r\n            createLyric(new CultureInfo(\"Ja-jp\"), \"translation1\"),\r\n            createLyric(new CultureInfo(\"Ja-jp\"), \"translation2\"),\r\n        });\r\n\r\n        AssertOk(getContext(beatmap));\r\n    }\r\n\r\n    [Test]\r\n    public void TestCheckMissingTranslation()\r\n    {\r\n        // no lyric with translation string. (should have issue)\r\n        var translationLanguages = new List<CultureInfo> { new(\"Ja-jp\") };\r\n        var beatmap = createTestingBeatmap(translationLanguages, new[]\r\n        {\r\n            createLyric(),\r\n            createLyric(),\r\n        });\r\n        AssertNotOk<IssueTemplateMissingTranslation>(getContext(beatmap));\r\n\r\n        // no lyric with translation string. (should have issue)\r\n        var beatmap2 = createTestingBeatmap(translationLanguages, new[]\r\n        {\r\n            createLyric(new CultureInfo(\"Ja-jp\")),\r\n            createLyric(),\r\n        });\r\n        AssertNotOk<IssueTemplateMissingTranslation>(getContext(beatmap2));\r\n\r\n        // no lyric with translation string. (should have issue)\r\n        var beatmap3 = createTestingBeatmap(translationLanguages, new[]\r\n        {\r\n            createLyric(new CultureInfo(\"Ja-jp\")),\r\n            createLyric(new CultureInfo(\"Ja-jp\"), string.Empty),\r\n        });\r\n        AssertNotOk<IssueTemplateMissingTranslation>(getContext(beatmap3));\r\n    }\r\n\r\n    [Test]\r\n    public void TestCheckMissingPartialTranslation()\r\n    {\r\n        // some lyric with translation string. (should have issue)\r\n        var translationLanguages = new List<CultureInfo> { new(\"Ja-jp\") };\r\n        var beatmap4 = createTestingBeatmap(translationLanguages, new[]\r\n        {\r\n            createLyric(new CultureInfo(\"Ja-jp\"), \"translation1\"),\r\n            createLyric(new CultureInfo(\"Ja-jp\")),\r\n        });\r\n        AssertNotOk<IssueTemplateMissingPartialTranslation>(getContext(beatmap4));\r\n    }\r\n\r\n    [Test]\r\n    public void TestCheckTranslationNotInListedLanguage()\r\n    {\r\n        // lyric translation not listed. (should have issue)\r\n        var beatmap6 = createTestingBeatmap(null, new[]\r\n        {\r\n            createLyric(new CultureInfo(\"en-US\"), \"translation1\"),\r\n        });\r\n        AssertNotOk<IssueTemplateTranslationNotInListedLanguage>(getContext(beatmap6));\r\n    }\r\n\r\n    private static IBeatmap createTestingBeatmap(List<CultureInfo>? translationLanguages, IEnumerable<Lyric>? lyrics)\r\n    {\r\n        var karaokeBeatmap = new KaraokeBeatmap\r\n        {\r\n            BeatmapInfo =\r\n            {\r\n                Ruleset = new KaraokeRuleset().RulesetInfo,\r\n            },\r\n            AvailableTranslationLanguages = translationLanguages ?? new List<CultureInfo>(),\r\n            HitObjects = lyrics?.OfType<KaraokeHitObject>().ToList() ?? new List<KaraokeHitObject>(),\r\n        };\r\n        return new EditorBeatmap(karaokeBeatmap);\r\n    }\r\n\r\n    private static BeatmapVerifierContext getContext(IBeatmap beatmap)\r\n        => new(beatmap, new TestWorkingBeatmap(beatmap));\r\n\r\n    private static Lyric createLyric(CultureInfo? cultureInfo = null, string translation = null!)\r\n    {\r\n        var lyric = new Lyric();\r\n        if (cultureInfo == null)\r\n            return lyric;\r\n\r\n        lyric.Translations.Add(cultureInfo, translation);\r\n        return lyric;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Checks/CheckBeatmapNoteInfoTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Rulesets.Edit;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks.Issues;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Screens.Edit;\r\nusing osu.Game.Tests.Beatmaps;\r\nusing static osu.Game.Rulesets.Karaoke.Edit.Checks.CheckBeatmapNoteInfo;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Checks;\r\n\r\npublic class CheckBeatmapNoteInfoTest : BeatmapPropertyCheckTest<CheckBeatmapNoteInfo>\r\n{\r\n    [Test]\r\n    public void TestCheckColumnNotEnough()\r\n    {\r\n        var beatmap = createTestingBeatmap(x => x.Columns = MIN_COLUMNS - 1, null);\r\n        AssertNotOk<IssueTemplateColumnNotEnough>(getContext(beatmap));\r\n    }\r\n\r\n    [Test]\r\n    public void TestCheckColumnExceed()\r\n    {\r\n        var beatmap = createTestingBeatmap(x => x.Columns = MAX_COLUMNS + 1, null);\r\n        AssertNotOk<IssueTemplateColumnExceed>(getContext(beatmap));\r\n    }\r\n\r\n    [Test]\r\n    public void TestCheckNoteToneTooLow()\r\n    {\r\n        var beatmap = createTestingBeatmap(x => x.Columns = MIN_COLUMNS, new[]\r\n        {\r\n            new Note\r\n            {\r\n                Tone = new Tone(-MIN_COLUMNS),\r\n            },\r\n        });\r\n        AssertNotOk<NoteIssue, IssueTemplateNoteToneTooLow>(getContext(beatmap));\r\n    }\r\n\r\n    [Test]\r\n    public void TestCheckNoteToneTooHigh()\r\n    {\r\n        var beatmap = createTestingBeatmap(x => x.Columns = MIN_COLUMNS, new[]\r\n        {\r\n            new Note\r\n            {\r\n                Tone = new Tone(MIN_COLUMNS),\r\n            },\r\n        });\r\n        AssertNotOk<NoteIssue, IssueTemplateNoteToneTooHigh>(getContext(beatmap));\r\n    }\r\n\r\n    private static IBeatmap createTestingBeatmap(Action<NoteInfo> action, IEnumerable<Note>? notes)\r\n    {\r\n        var karaokeBeatmap = new KaraokeBeatmap\r\n        {\r\n            BeatmapInfo =\r\n            {\r\n                Ruleset = new KaraokeRuleset().RulesetInfo,\r\n            },\r\n            HitObjects = notes?.OfType<KaraokeHitObject>().ToList() ?? new List<KaraokeHitObject>(),\r\n        };\r\n\r\n        action(karaokeBeatmap.NoteInfo);\r\n\r\n        return new EditorBeatmap(karaokeBeatmap);\r\n    }\r\n\r\n    private static BeatmapVerifierContext getContext(IBeatmap beatmap)\r\n        => new(beatmap, new TestWorkingBeatmap(beatmap));\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Checks/CheckBeatmapPageInfoTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Rulesets.Edit;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks.Issues;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Screens.Edit;\r\nusing osu.Game.Tests.Beatmaps;\r\nusing static osu.Game.Rulesets.Karaoke.Edit.Checks.CheckBeatmapPageInfo;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Checks;\r\n\r\npublic class CheckBeatmapPageInfoTest : BeatmapPropertyCheckTest<CheckBeatmapPageInfo>\r\n{\r\n    [Test]\r\n    public void TestCheckLessThanTwoPages()\r\n    {\r\n        var pages = new List<Page>\r\n        {\r\n            new(),\r\n        };\r\n\r\n        var lyrics = new List<Lyric>\r\n        {\r\n            new(),\r\n        };\r\n\r\n        // should have at least two pages.\r\n        var beatmap = createTestingBeatmap(null, null);\r\n        AssertNotOk<IssueTemplateLessThanTwoPages>(getContext(beatmap));\r\n\r\n        // should have at least two pages.\r\n        var beatmap2 = createTestingBeatmap(pages, null);\r\n        AssertNotOk<IssueTemplateLessThanTwoPages>(getContext(beatmap2));\r\n\r\n        // should have at least two pages.\r\n        var beatmap3 = createTestingBeatmap(null, lyrics);\r\n        AssertNotOk<IssueTemplateLessThanTwoPages>(getContext(beatmap3));\r\n\r\n        // should have at least two pages.\r\n        var beatmap4 = createTestingBeatmap(pages, lyrics);\r\n        AssertNotOk<IssueTemplateLessThanTwoPages>(getContext(beatmap4));\r\n    }\r\n\r\n    [Test]\r\n    public void TestCheckPageIntervalTooShort()\r\n    {\r\n        var pages = new List<Page>\r\n        {\r\n            new()\r\n            {\r\n                Time = 0,\r\n            },\r\n            new()\r\n            {\r\n                Time = MIN_INTERVAL - 1,\r\n            },\r\n        };\r\n\r\n        var lyrics = new List<Lyric>\r\n        {\r\n            // create a lyric that between two time-tags\r\n            new()\r\n            {\r\n                TimeTags = new List<TimeTag>\r\n                {\r\n                    new(new TextIndex(), 500),\r\n                },\r\n            },\r\n        };\r\n\r\n        var beatmap = createTestingBeatmap(pages, lyrics);\r\n        AssertNotOk<BeatmapPageIssue, IssueTemplatePageIntervalTooShort>(getContext(beatmap));\r\n    }\r\n\r\n    [Test]\r\n    public void TestCheckPageIntervalTooLong()\r\n    {\r\n        var pages = new List<Page>\r\n        {\r\n            new()\r\n            {\r\n                Time = 0,\r\n            },\r\n            new()\r\n            {\r\n                Time = MAX_INTERVAL + 1,\r\n            },\r\n        };\r\n\r\n        var lyrics = new List<Lyric>\r\n        {\r\n            // create a lyric that between two time-tags\r\n            new()\r\n            {\r\n                TimeTags = new List<TimeTag>\r\n                {\r\n                    new(new TextIndex(), 1000),\r\n                },\r\n            },\r\n        };\r\n\r\n        var beatmap = createTestingBeatmap(pages, lyrics);\r\n        AssertNotOk<BeatmapPageIssue, IssueTemplatePageIntervalTooLong>(getContext(beatmap));\r\n    }\r\n\r\n    [Test]\r\n    public void TestCheckPageIntervalShouldHaveAtLeastOneLyric()\r\n    {\r\n        var pages = new List<Page>\r\n        {\r\n            new()\r\n            {\r\n                Time = 0,\r\n            },\r\n            new()\r\n            {\r\n                Time = MIN_INTERVAL,\r\n            },\r\n        };\r\n\r\n        var beatmap = createTestingBeatmap(pages, null);\r\n        AssertNotOk<BeatmapPageIssue, IssueTemplatePageIntervalShouldHaveAtLeastOneLyric>(getContext(beatmap));\r\n    }\r\n\r\n    [Test]\r\n    public void TestCheckLyricNotWrapIntoTime()\r\n    {\r\n        var pages = new List<Page>\r\n        {\r\n            new()\r\n            {\r\n                Time = 0,\r\n            },\r\n            new()\r\n            {\r\n                Time = MIN_INTERVAL,\r\n            },\r\n        };\r\n\r\n        var timeTag = new TimeTag(new TextIndex(), 2000);\r\n\r\n        var lyrics = new List<Lyric>\r\n        {\r\n            // create a lyric that between two time-tags\r\n            new()\r\n            {\r\n                TimeTags = new List<TimeTag>\r\n                {\r\n                    new(new TextIndex(), MIN_INTERVAL - 1),\r\n                },\r\n            },\r\n            // another lyric's time is adjustable.\r\n            new()\r\n            {\r\n                TimeTags = new List<TimeTag>\r\n                {\r\n                    timeTag,\r\n                },\r\n            },\r\n        };\r\n\r\n        // should be OK\r\n        var beatmap = createTestingBeatmap(pages, lyrics);\r\n        AssertOk(getContext(beatmap));\r\n\r\n        // out of range.\r\n        timeTag.Time = -1;\r\n        var beatmap2 = createTestingBeatmap(pages, lyrics);\r\n        AssertNotOk<LyricIssue, IssueTemplateLyricNotWrapIntoTime>(getContext(beatmap2));\r\n\r\n        // out of range.\r\n        timeTag.Time = MIN_INTERVAL + 1;\r\n        var beatmap3 = createTestingBeatmap(pages, lyrics);\r\n        AssertNotOk<LyricIssue, IssueTemplateLyricNotWrapIntoTime>(getContext(beatmap3));\r\n    }\r\n\r\n    private static IBeatmap createTestingBeatmap(IEnumerable<Page>? pages, IEnumerable<Lyric>? lyrics)\r\n    {\r\n        var karaokeBeatmap = new KaraokeBeatmap\r\n        {\r\n            BeatmapInfo =\r\n            {\r\n                Ruleset = new KaraokeRuleset().RulesetInfo,\r\n            },\r\n            HitObjects = lyrics?.OfType<KaraokeHitObject>().ToList() ?? new List<KaraokeHitObject>(),\r\n        };\r\n        karaokeBeatmap.PageInfo.Pages.AddRange(pages ?? new List<Page>());\r\n        return new EditorBeatmap(karaokeBeatmap);\r\n    }\r\n\r\n    private static BeatmapVerifierContext getContext(IBeatmap beatmap)\r\n        => new(beatmap, new TestWorkingBeatmap(beatmap));\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Checks/CheckClassicStageInfoTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Rulesets.Edit;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks.Issues;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos.Classic;\r\nusing osu.Game.Screens.Edit;\r\nusing osu.Game.Tests.Beatmaps;\r\nusing static osu.Game.Rulesets.Karaoke.Edit.Checks.CheckClassicStageInfo;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Checks;\r\n\r\n[Ignore(\"Disable this test until able to get the stage info from the resource file.\")]\r\npublic class CheckClassicStageInfoTest : BaseCheckTest<CheckClassicStageInfo>\r\n{\r\n    #region stage definition\r\n\r\n    [Test]\r\n    public void TestCheckInvalidRowHeight()\r\n    {\r\n        var beatmap = createTestingBeatmap(Array.Empty<Lyric>());\r\n        var stageInfo = createTestingStageInfo(stage =>\r\n        {\r\n            stage.StageDefinition.LineHeight = MIN_ROW_HEIGHT - 1;\r\n        });\r\n        AssertNotOk<IssueTemplateInvalidRowHeight>(getContext(beatmap, stageInfo));\r\n\r\n        var beatmap2 = createTestingBeatmap(Array.Empty<Lyric>());\r\n        var stageInfo2 = createTestingStageInfo(stage =>\r\n        {\r\n            stage.StageDefinition.LineHeight = MAX_ROW_HEIGHT + 1;\r\n        });\r\n        AssertNotOk<IssueTemplateInvalidRowHeight>(getContext(beatmap2, stageInfo2));\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region timing info\r\n\r\n    [Test]\r\n    public void TestCheckLessThanTwoTimingPoints()\r\n    {\r\n        var lyrics = new List<Lyric>\r\n        {\r\n            new(),\r\n        };\r\n\r\n        // test with 0 lyric and 0 timing points.\r\n        var beatmap = createTestingBeatmap(null);\r\n        var stageInfo = createTestingStageInfo(info => info.LyricTimingInfo.Timings.Clear());\r\n        AssertNotOk<IssueTemplateLessThanTwoTimingPoints>(getContext(beatmap, stageInfo));\r\n\r\n        // test with 0 lyric and 1 timing points.\r\n        var beatmap2 = createTestingBeatmap(null);\r\n        var stageInfo2 = createTestingStageInfo(timingInfos => timingInfos.AddTimingPoint());\r\n        AssertNotOk<IssueTemplateLessThanTwoTimingPoints>(getContext(beatmap2, stageInfo2));\r\n\r\n        // test with 1 lyric and 0 timing points.\r\n        var beatmap3 = createTestingBeatmap(lyrics);\r\n        var stageInfo3 = createTestingStageInfo(timingInfos => timingInfos.Timings.Clear());\r\n        AssertNotOk<IssueTemplateLessThanTwoTimingPoints>(getContext(beatmap3, stageInfo3));\r\n\r\n        // test with 1 lyric and 1 timing points.\r\n        var beatmap4 = createTestingBeatmap(lyrics);\r\n        var stageInfo4 = createTestingStageInfo(timingInfos => timingInfos.AddTimingPoint());\r\n        AssertNotOk<IssueTemplateLessThanTwoTimingPoints>(getContext(beatmap4, stageInfo4));\r\n    }\r\n\r\n    [Test]\r\n    public void TestCheckTimingIntervalTooShort()\r\n    {\r\n        var beatmap = createTestingBeatmap(null);\r\n        var stageInfo = createTestingStageInfo(timingInfos =>\r\n        {\r\n            timingInfos.Timings.Clear();\r\n            timingInfos.AddTimingPoint(x => x.Time = 0);\r\n            timingInfos.AddTimingPoint(x => x.Time = MIN_TIMING_INTERVAL - 1);\r\n        });\r\n        AssertNotOk<BeatmapClassicLyricTimingPointIssue, IssueTemplateTimingIntervalTooShort>(getContext(beatmap, stageInfo));\r\n    }\r\n\r\n    [Test]\r\n    public void TestCheckTimingIntervalTooLong()\r\n    {\r\n        var beatmap = createTestingBeatmap(null);\r\n        var stageInfo = createTestingStageInfo(timingInfos =>\r\n        {\r\n            timingInfos.Timings.Clear();\r\n            timingInfos.AddTimingPoint(x => x.Time = 0);\r\n            timingInfos.AddTimingPoint(x => x.Time = MAX_TIMING_INTERVAL + 1);\r\n        });\r\n        AssertNotOk<BeatmapClassicLyricTimingPointIssue, IssueTemplateTimingIntervalTooLong>(getContext(beatmap, stageInfo));\r\n    }\r\n\r\n    [TestCase(true)]\r\n    [TestCase(false)]\r\n    public void TestCheckTimingInfoHitObjectNotExist(bool hasHitObjectsInBeatmap)\r\n    {\r\n        var lyrics = new List<Lyric>\r\n        {\r\n            new(),\r\n        };\r\n\r\n        var beatmap = createTestingBeatmap(hasHitObjectsInBeatmap ? lyrics : null);\r\n        var stageInfo = createTestingStageInfo(timingInfos =>\r\n        {\r\n            timingInfos.Timings.Clear();\r\n            var timingPoint = timingInfos.AddTimingPoint(x => x.Time = 0);\r\n            timingInfos.AddTimingPoint(x => x.Time = MIN_TIMING_INTERVAL + 1);\r\n\r\n            var lyric = new Lyric();\r\n\r\n            // should have error because lyric is not in the beatmap.\r\n            timingInfos.AddToMapping(timingPoint, lyric);\r\n        });\r\n        AssertNotOk<IssueTemplateTimingInfoHitObjectNotExist>(getContext(beatmap, stageInfo));\r\n    }\r\n\r\n    [Test]\r\n    public void TestCheckTimingInfoMappingHasNoTiming()\r\n    {\r\n        var lyrics = new List<Lyric>\r\n        {\r\n            new(),\r\n        };\r\n\r\n        var beatmap = createTestingBeatmap(lyrics);\r\n        var stageInfo = createTestingStageInfo(timingInfos =>\r\n        {\r\n            timingInfos.Timings.Clear();\r\n            timingInfos.AddTimingPoint(x => x.Time = 0);\r\n            timingInfos.AddTimingPoint(x => x.Time = MIN_TIMING_INTERVAL + 1);\r\n\r\n            // should have error because mapping value is empty.\r\n            timingInfos.Mappings.Add(lyrics.First().ID, Array.Empty<ElementId>());\r\n        });\r\n        AssertNotOk<IssueTemplateTimingInfoMappingHasNoTiming>(getContext(beatmap, stageInfo));\r\n    }\r\n\r\n    [Test]\r\n    public void TestCheckTimingInfoTimingNotExist()\r\n    {\r\n        var lyrics = new List<Lyric>\r\n        {\r\n            new(),\r\n        };\r\n\r\n        var beatmap = createTestingBeatmap(lyrics);\r\n        var stageInfo = createTestingStageInfo(timingInfos =>\r\n        {\r\n            timingInfos.Timings.Clear();\r\n            timingInfos.AddTimingPoint(x => x.Time = 0);\r\n            timingInfos.AddTimingPoint(x => x.Time = MIN_TIMING_INTERVAL + 1);\r\n\r\n            // should have error because mapping value is not exist.\r\n            timingInfos.Mappings.Add(lyrics.First().ID, new[] { ElementId.NewElementId() });\r\n        });\r\n        AssertNotOk<IssueTemplateTimingInfoTimingNotExist>(getContext(beatmap, stageInfo));\r\n    }\r\n\r\n    [Test]\r\n    public void TestCheckTimingInfoLyricNotHaveTwoTiming()\r\n    {\r\n        var lyrics = new List<Lyric>\r\n        {\r\n            new(),\r\n        };\r\n\r\n        var beatmap = createTestingBeatmap(lyrics);\r\n        var stageInfo = createTestingStageInfo(timingInfos =>\r\n        {\r\n            timingInfos.Timings.Clear();\r\n            timingInfos.AddTimingPoint(x => x.Time = 0);\r\n            timingInfos.AddTimingPoint(x => x.Time = MIN_TIMING_INTERVAL + 1);\r\n            timingInfos.AddTimingPoint(x => x.Time = MIN_TIMING_INTERVAL * 2 + 1);\r\n\r\n            // should have error because mapping value is not exactly 2.\r\n            timingInfos.Mappings.Add(lyrics.First().ID, timingInfos.Timings.Select(x => x.ID).ToArray());\r\n        });\r\n        AssertNotOk<IssueTemplateTimingInfoLyricNotHaveTwoTiming>(getContext(beatmap, stageInfo));\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region element\r\n\r\n    [Test]\r\n    public void TestCheckLyricLayoutInvalidLineNumber()\r\n    {\r\n        var beatmap = createTestingBeatmap(Array.Empty<Lyric>());\r\n        var stageInfo = createTestingStageInfo(stage =>\r\n        {\r\n            var layoutElement = stage.LyricLayoutCategory.AvailableElements.First();\r\n            layoutElement.Line = MIN_LINE_SIZE - 1;\r\n        });\r\n        AssertNotOk<IssueTemplateLyricLayoutInvalidLineNumber>(getContext(beatmap, stageInfo));\r\n\r\n        var beatmap2 = createTestingBeatmap(Array.Empty<Lyric>());\r\n        var stageInfo2 = createTestingStageInfo(stage =>\r\n        {\r\n            var layoutElement = stage.LyricLayoutCategory.AvailableElements.First();\r\n            layoutElement.Line = MAX_LINE_SIZE + 1;\r\n        });\r\n        AssertNotOk<IssueTemplateLyricLayoutInvalidLineNumber>(getContext(beatmap2, stageInfo2));\r\n    }\r\n\r\n    #endregion\r\n\r\n    private static IBeatmap createTestingBeatmap(IEnumerable<Lyric>? lyrics)\r\n    {\r\n        var karaokeBeatmap = new KaraokeBeatmap\r\n        {\r\n            BeatmapInfo =\r\n            {\r\n                Ruleset = new KaraokeRuleset().RulesetInfo,\r\n            },\r\n            HitObjects = lyrics?.OfType<KaraokeHitObject>().ToList() ?? new List<KaraokeHitObject>(),\r\n        };\r\n        return new EditorBeatmap(karaokeBeatmap);\r\n    }\r\n\r\n    private static StageInfo createTestingStageInfo(Action<ClassicLyricTimingInfo>? editStageAction = null)\r\n    {\r\n        return createTestingStageInfo(info =>\r\n        {\r\n            // clear the timing info created in the base method.\r\n            info.LyricTimingInfo.Timings.Clear();\r\n            editStageAction?.Invoke(info.LyricTimingInfo);\r\n        });\r\n    }\r\n\r\n    private static StageInfo createTestingStageInfo(Action<ClassicStageInfo>? editStageAction = null)\r\n    {\r\n        var stageInfo = new ClassicStageInfo();\r\n\r\n        // add two elements to prevent no element error.\r\n        stageInfo.LyricLayoutCategory.AddElement(x => x.Line = MIN_LINE_SIZE);\r\n        stageInfo.LyricLayoutCategory.AddElement(x => x.Line = MIN_LINE_SIZE);\r\n        stageInfo.StageDefinition.LineHeight = MIN_ROW_HEIGHT;\r\n\r\n        // add default timing info to prevent the error.\r\n        stageInfo.LyricTimingInfo.AddTimingPoint(x => x.Time = 0);\r\n        stageInfo.LyricTimingInfo.AddTimingPoint(x => x.Time = MIN_TIMING_INTERVAL + 1);\r\n\r\n        editStageAction?.Invoke(stageInfo);\r\n\r\n        return stageInfo;\r\n    }\r\n\r\n    private static BeatmapVerifierContext getContext(IBeatmap beatmap, StageInfo stageInfo)\r\n        => new(beatmap, new TestWorkingBeatmap(beatmap));\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Checks/CheckLyricLanguageTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Globalization;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks.Issues;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing static osu.Game.Rulesets.Karaoke.Edit.Checks.CheckLyricLanguage;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Checks;\r\n\r\npublic class CheckLyricLanguageTest : HitObjectCheckTest<Lyric, CheckLyricLanguage>\r\n{\r\n    [TestCase(\"Ja-jp\")]\r\n    [TestCase(\"\")] // should not have issue if CultureInfo accept it.\r\n    public void TestCheck(string language)\r\n    {\r\n        var lyric = new Lyric\r\n        {\r\n            Language = new CultureInfo(language),\r\n        };\r\n\r\n        AssertOk(lyric);\r\n    }\r\n\r\n    [TestCase(null)]\r\n    public void TestCheckNotFill(string? language)\r\n    {\r\n        var lyric = new Lyric\r\n        {\r\n            Language = language != null ? new CultureInfo(language) : null,\r\n        };\r\n\r\n        AssertNotOk<LyricIssue, IssueTemplateNotFill>(lyric);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Checks/CheckLyricReferenceLyricTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks.Issues;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Properties;\r\nusing osu.Game.Rulesets.Objects;\r\nusing static osu.Game.Rulesets.Karaoke.Edit.Checks.CheckLyricReferenceLyric;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Checks;\r\n\r\npublic class CheckLyricReferenceLyricTest : HitObjectCheckTest<Lyric, CheckLyricReferenceLyric>\r\n{\r\n    [Test]\r\n    public void TestCheck()\r\n    {\r\n        var referencedLyric = new Lyric();\r\n        var lyric = new Lyric\r\n        {\r\n            ReferenceLyricId = referencedLyric.ID,\r\n            ReferenceLyric = referencedLyric,\r\n            ReferenceLyricConfig = new ReferenceLyricConfig(),\r\n        };\r\n\r\n        AssertOk(new HitObject[] { referencedLyric, lyric });\r\n    }\r\n\r\n    [Test]\r\n    public void TestCheckSelfReference()\r\n    {\r\n        var lyric = new Lyric\r\n        {\r\n            ReferenceLyricConfig = new ReferenceLyricConfig(),\r\n        };\r\n\r\n        lyric.ReferenceLyricId = lyric.ID;\r\n        lyric.ReferenceLyric = lyric;\r\n\r\n        AssertNotOk<LyricIssue, IssueTemplateSelfReference>(lyric);\r\n    }\r\n\r\n    [Test]\r\n    public void TestCheckInvalidReferenceLyric()\r\n    {\r\n        var referencedLyric = new Lyric();\r\n        var lyric = new Lyric\r\n        {\r\n            ReferenceLyricId = referencedLyric.ID,\r\n            ReferenceLyric = referencedLyric,\r\n            ReferenceLyricConfig = new ReferenceLyricConfig(),\r\n        };\r\n\r\n        AssertNotOk<LyricIssue, IssueTemplateInvalidReferenceLyric>(lyric);\r\n    }\r\n\r\n    [Test]\r\n    public void TestCheckNullReferenceLyricConfig()\r\n    {\r\n        var referencedLyric = new Lyric();\r\n        var lyric = new Lyric\r\n        {\r\n            ReferenceLyricId = referencedLyric.ID,\r\n            ReferenceLyric = referencedLyric,\r\n        };\r\n\r\n        AssertNotOk<LyricIssue, IssueTemplateNullReferenceLyricConfig>(new HitObject[] { referencedLyric, lyric });\r\n    }\r\n\r\n    [Test]\r\n    public void TestCheckHasReferenceLyricConfigWhenNoReferenceLyric()\r\n    {\r\n        var lyric = new Lyric\r\n        {\r\n            ReferenceLyric = null,\r\n            ReferenceLyricConfig = new ReferenceLyricConfig(),\r\n        };\r\n\r\n        AssertNotOk<LyricIssue, IssueTemplateHasReferenceLyricConfigWhenNoReferenceLyric>(lyric);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Checks/CheckLyricRubyTagTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks.Issues;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\nusing static osu.Game.Rulesets.Karaoke.Edit.Checks.CheckLyricRubyTag;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Checks;\r\n\r\npublic class CheckLyricRubyTagTest : HitObjectCheckTest<Lyric, CheckLyricRubyTag>\r\n{\r\n    [TestCase(\"カラオケ\", new[] { \"[0]:か\", \"[1]:ら\", \"[2]:お\", \"[3]:け\" })]\r\n    [TestCase(\"カラオケ\", new[] { \"[0,3]:からおけ\" })]\r\n    public void TestCheck(string text, string[] rubies)\r\n    {\r\n        var lyric = new Lyric\r\n        {\r\n            Text = text,\r\n            RubyTags = TestCaseTagHelper.ParseRubyTags(rubies),\r\n        };\r\n\r\n        AssertOk(lyric);\r\n    }\r\n\r\n    [TestCase(\"カラオケ\", new[] { \"[-1]:か\" })]\r\n    [TestCase(\"カラオケ\", new[] { \"[4]:け\" })]\r\n    public void TestCheckOutOfRange(string text, string[] rubies)\r\n    {\r\n        var lyric = new Lyric\r\n        {\r\n            Text = text,\r\n            RubyTags = TestCaseTagHelper.ParseRubyTags(rubies),\r\n        };\r\n\r\n        AssertNotOk<LyricRubyTagIssue, IssueTemplateOutOfRange>(lyric);\r\n    }\r\n\r\n    [TestCase(\"カラオケ\", new[] { \"[0]:か\", \"[0]:ら\" })]\r\n    [TestCase(\"カラオケ\", new[] { \"[0,3]:か\", \"[1,2]:ら\" })]\r\n    public void TestCheckOverlapping(string text, string[] rubies)\r\n    {\r\n        var lyric = new Lyric\r\n        {\r\n            Text = text,\r\n            RubyTags = TestCaseTagHelper.ParseRubyTags(rubies),\r\n        };\r\n\r\n        AssertNotOk<LyricRubyTagIssue, IssueTemplateOverlapping>(lyric);\r\n    }\r\n\r\n    [TestCase(\"カラオケ\", new[] { \"[0,3]:\" })]\r\n    [TestCase(\"カラオケ\", new[] { \"[0,3]: \" })]\r\n    [TestCase(\"カラオケ\", new[] { \"[0,3]:　\" })]\r\n    public void TestCheckEmptyText(string text, string[] rubies)\r\n    {\r\n        var lyric = new Lyric\r\n        {\r\n            Text = text,\r\n            RubyTags = TestCaseTagHelper.ParseRubyTags(rubies),\r\n        };\r\n\r\n        AssertNotOk<LyricRubyTagIssue, IssueTemplateEmptyText>(lyric);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Checks/CheckLyricSingerTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks.Issues;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\nusing static osu.Game.Rulesets.Karaoke.Edit.Checks.CheckLyricSinger;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Checks;\r\n\r\n[TestFixture]\r\npublic class CheckLyricSingerTest : HitObjectCheckTest<Lyric, CheckLyricSinger>\r\n{\r\n    [TestCase(new[] { 1, 2, 3 })]\r\n    [TestCase(new[] { 1 })]\r\n    [TestCase(new[] { 100 })] // although singer is not exist, but should not check in this test case.\r\n    public void TestCheck(int[] singers)\r\n    {\r\n        var lyric = new Lyric\r\n        {\r\n            SingerIds = TestCaseElementIdHelper.CreateElementIdsByNumbers(singers),\r\n        };\r\n\r\n        AssertOk(lyric);\r\n    }\r\n\r\n    [TestCase(new int[] { })]\r\n    public void TestCheckNoSinger(int[] singers)\r\n    {\r\n        var lyric = new Lyric\r\n        {\r\n            SingerIds = TestCaseElementIdHelper.CreateElementIdsByNumbers(singers),\r\n        };\r\n\r\n        AssertNotOk<LyricIssue, IssueTemplateNoSinger>(lyric);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Checks/CheckLyricTextTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks.Issues;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing static osu.Game.Rulesets.Karaoke.Edit.Checks.CheckLyricText;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Checks;\r\n\r\n[TestFixture]\r\npublic class CheckLyricTextTest : HitObjectCheckTest<Lyric, CheckLyricText>\r\n{\r\n    [TestCase(\"karaoke\")]\r\n    [TestCase(\"k\")] // not limit min size for now.\r\n    [TestCase(\"カラオケ\")] // not limit language.\r\n    public void TestCheck(string text)\r\n    {\r\n        var lyric = new Lyric\r\n        {\r\n            Text = text,\r\n        };\r\n\r\n        AssertOk(lyric);\r\n    }\r\n\r\n    [TestCase(\" \")] // but should not be empty or white space.\r\n    [TestCase(\"　\")] // but should not be empty or white space.\r\n    [TestCase(\"\")]\r\n    [TestCase(null)]\r\n    public void TestCheckEmptyText(string text)\r\n    {\r\n        var lyric = new Lyric\r\n        {\r\n            Text = text,\r\n        };\r\n\r\n        AssertNotOk<LyricIssue, IssueTemplateEmptyText>(lyric);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Checks/CheckLyricTimeTagTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks.Issues;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\nusing static osu.Game.Rulesets.Karaoke.Edit.Checks.CheckLyricTimeTag;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Checks;\r\n\r\npublic class CheckLyricTimeTagTest : HitObjectCheckTest<Lyric, CheckLyricTimeTag>\r\n{\r\n    [TestCase(\"カラオケ\", new[] { \"[0,start]:1000\", \"[1,start]:2000\", \"[2,start]:3000\", \"[3,start]:4000\", \"[3,end]:5000\" })]\r\n    [TestCase(\"カラオケ\", new[] { \"[0,start]:1000\", \"[3,end]:5000\" })]\r\n    [TestCase(\"カラオケ\", new[] { \"[0,start]:1000\", \"[3,end]:5000\" })]\r\n    public void TestCheck(string text, string[] timeTags)\r\n    {\r\n        var lyric = new Lyric\r\n        {\r\n            Text = text,\r\n            TimeTags = TestCaseTagHelper.ParseTimeTags(timeTags),\r\n        };\r\n\r\n        AssertOk(lyric);\r\n    }\r\n\r\n    [TestCase(\"カラオケ\", new string[] { })]\r\n    public void TestCheckEmpty(string text, string[] timeTags)\r\n    {\r\n        var lyric = new Lyric\r\n        {\r\n            Text = text,\r\n            TimeTags = TestCaseTagHelper.ParseTimeTags(timeTags),\r\n        };\r\n\r\n        AssertNotOk<LyricIssue, IssueTemplateEmpty>(lyric);\r\n    }\r\n\r\n    [TestCase(\"カラオケ\", new[] { \"[3,end]:5000\" })]\r\n    public void TestCheckMissingStart(string text, string[] timeTags)\r\n    {\r\n        var lyric = new Lyric\r\n        {\r\n            Text = text,\r\n            TimeTags = TestCaseTagHelper.ParseTimeTags(timeTags),\r\n        };\r\n\r\n        AssertNotOk<LyricIssue, IssueTemplateMissingStart>(lyric);\r\n    }\r\n\r\n    [TestCase(\"カラオケ\", new[] { \"[0,start]:5000\" })]\r\n    public void TestCheckMissingEnd(string text, string[] timeTags)\r\n    {\r\n        var lyric = new Lyric\r\n        {\r\n            Text = text,\r\n            TimeTags = TestCaseTagHelper.ParseTimeTags(timeTags),\r\n        };\r\n\r\n        AssertNotOk<LyricIssue, IssueTemplateMissingEnd>(lyric);\r\n    }\r\n\r\n    [TestCase(\"カラオケ\", new[] { \"[-1,start]:0\", \"[0,start]:1000\", \"[3,end]:1000\" })] // out-of range start time-tag time.\r\n    [TestCase(\"カラオケ\", new[] { \"[0,start]:1000\", \"[3,end]:1000\", \"[4,start]:2000\" })] // out-of range end time-tag time.\r\n    public void TestCheckOutOfRange(string text, string[] timeTags)\r\n    {\r\n        var lyric = new Lyric\r\n        {\r\n            Text = text,\r\n            TimeTags = TestCaseTagHelper.ParseTimeTags(timeTags),\r\n        };\r\n\r\n        AssertNotOk<LyricTimeTagIssue, IssueTemplateOutOfRange>(lyric);\r\n    }\r\n\r\n    [TestCase(\"カラオケ\", new[] { \"[0,start]:5000\", \"[3,end]:1000\" })]\r\n    public void TestCheckOverlapping(string text, string[] timeTags)\r\n    {\r\n        var lyric = new Lyric\r\n        {\r\n            Text = text,\r\n            TimeTags = TestCaseTagHelper.ParseTimeTags(timeTags),\r\n        };\r\n\r\n        AssertNotOk<LyricTimeTagIssue, IssueTemplateOverlapping>(lyric);\r\n    }\r\n\r\n    [TestCase(\"カラオケ\", new[] { \"[0,start]\", \"[3,end]:1000\" })] // empty start time-tag time.\r\n    [TestCase(\"カラオケ\", new[] { \"[0,start]:1000\", \"[3,end]\" })] // empty end time-tag time.\r\n    [TestCase(\"カラオケ\", new[] { \"[0,start]:1000\", \"[1,start]\", \"[3,end]:2000\" })] // empty center time-tag time.\r\n    public void TestCheckEmptyTime(string text, string[] timeTags)\r\n    {\r\n        var lyric = new Lyric\r\n        {\r\n            Text = text,\r\n            TimeTags = TestCaseTagHelper.ParseTimeTags(timeTags),\r\n        };\r\n\r\n        AssertNotOk<LyricTimeTagIssue, IssueTemplateEmptyTime>(lyric);\r\n    }\r\n\r\n    [TestCase(\"カラオケ\", \"\")] // should not be empty.\r\n    [TestCase(\"カラオケ\", \" \")] // should not be white-space only.\r\n    [TestCase(\"カラオケ\", \"卡拉OK\")] // should be within latin.\r\n    public void TestCheckInvalidRomanisedSyllable(string text, string romanisedSyllable)\r\n    {\r\n        var lyric = new Lyric\r\n        {\r\n            Text = text,\r\n            TimeTags = new[]\r\n            {\r\n                new TimeTag(new TextIndex())\r\n                {\r\n                    RomanisedSyllable = romanisedSyllable,\r\n                    Time = 1000,\r\n                },\r\n                new TimeTag(new TextIndex(3, TextIndex.IndexState.End))\r\n                {\r\n                    Time = 2000,\r\n                },\r\n            },\r\n        };\r\n\r\n        AssertNotOk<LyricTimeTagIssue, IssueTemplateInvalidRomanisedSyllable>(lyric);\r\n    }\r\n\r\n    [TestCase(\"カラオケ\", null)] // should not be white-space only.\r\n    public void TestCheckShouldFillRomanisedSyllable(string text, string romanisedSyllable)\r\n    {\r\n        var lyric = new Lyric\r\n        {\r\n            Text = text,\r\n            TimeTags = new[]\r\n            {\r\n                new TimeTag(new TextIndex())\r\n                {\r\n                    RomanisedSyllable = romanisedSyllable,\r\n                    FirstSyllable = true,\r\n                    Time = 1000,\r\n                },\r\n                new TimeTag(new TextIndex(3, TextIndex.IndexState.End))\r\n                {\r\n                    Time = 2000,\r\n                },\r\n            },\r\n        };\r\n\r\n        AssertNotOk<LyricTimeTagIssue, IssueTemplateShouldFillRomanisedSyllable>(lyric);\r\n    }\r\n\r\n    [TestCase(\"カラオケ\", \"\")] // should not have empty text if end.\r\n    [TestCase(\"カラオケ\", \" \")] // should not have empty text if end.\r\n    [TestCase(\"カラオケ\", \"123\")] // should not have empty text if end.\r\n    public void TestCheckShouldNotFillRomanisedSyllable(string text, string romanisedSyllable)\r\n    {\r\n        var lyric = new Lyric\r\n        {\r\n            Text = text,\r\n            TimeTags = new[]\r\n            {\r\n                new TimeTag(new TextIndex())\r\n                {\r\n                    Time = 1000,\r\n                },\r\n                new TimeTag(new TextIndex(3, TextIndex.IndexState.End))\r\n                {\r\n                    RomanisedSyllable = romanisedSyllable,\r\n                    Time = 2000,\r\n                },\r\n            },\r\n        };\r\n\r\n        AssertNotOk<LyricTimeTagIssue, IssueTemplateShouldNotFillRomanisedSyllable>(lyric);\r\n    }\r\n\r\n    [Test]\r\n    public void TestCheckShouldNotMarkFirstSyllable()\r\n    {\r\n        var lyric = new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            TimeTags = new[]\r\n            {\r\n                new TimeTag(new TextIndex())\r\n                {\r\n                    Time = 1000,\r\n                },\r\n                new TimeTag(new TextIndex(3, TextIndex.IndexState.End))\r\n                {\r\n                    FirstSyllable = true, // is invalid.\r\n                    Time = 2000,\r\n                },\r\n            },\r\n        };\r\n\r\n        AssertNotOk<LyricTimeTagIssue, IssueTemplateShouldNotMarkFirstSyllable>(lyric);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Checks/CheckLyricTranslationsTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Globalization;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks.Issues;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing static osu.Game.Rulesets.Karaoke.Edit.Checks.CheckLyricTranslations;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Checks;\r\n\r\npublic class CheckLyricTranslationsTest : HitObjectCheckTest<Lyric, CheckLyricTranslations>\r\n{\r\n    [TestCase(\"translation\")]\r\n    [TestCase(\"k\")] // not limit min size for now.\r\n    [TestCase(\"翻譯\")] // not limit language.\r\n    public void TestCheck(string text)\r\n    {\r\n        var lyric = new Lyric\r\n        {\r\n            Translations = new Dictionary<CultureInfo, string>\r\n            {\r\n                { new CultureInfo(\"Ja-jp\"), text },\r\n            },\r\n        };\r\n\r\n        AssertOk(lyric);\r\n    }\r\n\r\n    [TestCase(null)]\r\n    [TestCase(\"\")]\r\n    [TestCase(\" \")] // but should not be empty or white space.\r\n    [TestCase(\"　\")] // but should not be empty or white space.\r\n    public void TestCheckEmptyText(string text)\r\n    {\r\n        var lyric = new Lyric\r\n        {\r\n            Translations = new Dictionary<CultureInfo, string>\r\n            {\r\n                { new CultureInfo(\"Ja-jp\"), text },\r\n            },\r\n        };\r\n\r\n        AssertNotOk<LyricIssue, IssueTemplateEmptyText>(lyric);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Checks/CheckNoteReferenceLyricTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks.Issues;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\nusing osu.Game.Rulesets.Objects;\r\nusing static osu.Game.Rulesets.Karaoke.Edit.Checks.CheckNoteReferenceLyric;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Checks;\r\n\r\n[TestFixture]\r\npublic class CheckNoteReferenceLyricTest : HitObjectCheckTest<Note, CheckNoteReferenceLyric>\r\n{\r\n    [TestCase(0, new[] { \"[0,start]:1000\", \"[3,end]:5000\" })]\r\n    [TestCase(0, new[] { \"[0,start]:1000\", \"[1,start]:2000\", \"[2,start]:3000\", \"[3,start]:4000\", \"[3,end]:5000\" })]\r\n    [TestCase(1, new[] { \"[0,start]:1000\", \"[1,start]:2000\", \"[2,start]:3000\", \"[3,start]:4000\", \"[3,end]:5000\" })]\r\n    [TestCase(2, new[] { \"[0,start]:1000\", \"[1,start]:2000\", \"[2,start]:3000\", \"[3,start]:4000\", \"[3,end]:5000\" })]\r\n    [TestCase(3, new[] { \"[0,start]:1000\", \"[1,start]:2000\", \"[2,start]:3000\", \"[3,start]:4000\", \"[3,end]:5000\" })]\r\n    public void TestCheck(int referenceTimeTagIndex, string[] timeTags)\r\n    {\r\n        var referencedLyric = new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            TimeTags = TestCaseTagHelper.ParseTimeTags(timeTags),\r\n        };\r\n        var note = new Note\r\n        {\r\n            ReferenceLyricId = referencedLyric.ID,\r\n            ReferenceLyric = referencedLyric,\r\n            ReferenceTimeTagIndex = referenceTimeTagIndex,\r\n        };\r\n\r\n        AssertOk(new HitObject[] { referencedLyric, note });\r\n    }\r\n\r\n    [Test]\r\n    public void TestCheckNullReferenceLyric()\r\n    {\r\n        var note = new Note\r\n        {\r\n            ReferenceLyricId = null,\r\n            ReferenceLyric = null, // reference should not be null.\r\n        };\r\n\r\n        AssertNotOk<NoteIssue, IssueTemplateNullReferenceLyric>(note);\r\n    }\r\n\r\n    [Test]\r\n    public void TestCheckInvalidReferenceLyric()\r\n    {\r\n        var referencedLyric = new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            TimeTags = TestCaseTagHelper.ParseTimeTags(new[] { \"[0,start]:1000\", \"[3,end]:5000\" }),\r\n        };\r\n        var note = new Note\r\n        {\r\n            ReferenceLyricId = referencedLyric.ID,\r\n            ReferenceLyric = referencedLyric, // reference lyric should be in the beatmap.\r\n            ReferenceTimeTagIndex = 0,\r\n        };\r\n\r\n        AssertNotOk<NoteIssue, IssueTemplateInvalidReferenceLyric>(note);\r\n    }\r\n\r\n    [TestCase(2, new[] { \"[0,start]:1000\", \"[3,end]:5000\" })] // will find the time-tag at index 2 and 3.\r\n    [TestCase(-2, new[] { \"[0,start]:1000\", \"[3,end]:5000\" })] // will find the time-tag at index -2 and -1.\r\n    [TestCase(0, new string[] { })] // should have error because start and end time-tag not found.\r\n    [TestCase(1, new[] { \"[0,start]:1000\" })]\r\n    [TestCase(-2, new[] { \"[0,start]:1000\" })]\r\n    public void TestCheckMissingReferenceTimeTag(int referenceTimeTagIndex, string[] timeTags)\r\n    {\r\n        var referencedLyric = new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            TimeTags = TestCaseTagHelper.ParseTimeTags(timeTags),\r\n        };\r\n        var note = new Note\r\n        {\r\n            ReferenceLyricId = referencedLyric.ID,\r\n            ReferenceLyric = referencedLyric,\r\n            ReferenceTimeTagIndex = referenceTimeTagIndex,\r\n        };\r\n\r\n        AssertNotOk<NoteIssue, IssueTemplateMissingReferenceTimeTag>(new HitObject[] { referencedLyric, note });\r\n    }\r\n\r\n    [TestCase(-1, new[] { \"[0,start]:1000\", \"[3,end]:5000\" })]\r\n    [TestCase(-1, new[] { \"[0,start]:1000\", \"[1,start]:2000\", \"[2,start]:3000\", \"[3,start]:4000\", \"[3,end]:5000\" })]\r\n    public void TestCheckMissingStartReferenceTimeTag(int referenceTimeTagIndex, string[] timeTags)\r\n    {\r\n        var referencedLyric = new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            TimeTags = TestCaseTagHelper.ParseTimeTags(timeTags),\r\n        };\r\n        var note = new Note\r\n        {\r\n            ReferenceLyricId = referencedLyric.ID,\r\n            ReferenceLyric = referencedLyric,\r\n            ReferenceTimeTagIndex = referenceTimeTagIndex,\r\n        };\r\n\r\n        AssertNotOk<NoteIssue, IssueTemplateMissingStartReferenceTimeTag>(new HitObject[] { referencedLyric, note });\r\n    }\r\n\r\n    [TestCase(0, new[] { \"[0,start]\", \"[3,end]:5000\" })]\r\n    [TestCase(0, new[] { \"[0,start]\", \"[1,start]:2000\", \"[2,start]:3000\", \"[3,start]:4000\", \"[3,end]:5000\" })]\r\n    [TestCase(1, new[] { \"[0,start]:1000\", \"[1,start]\", \"[2,start]:3000\", \"[3,start]:4000\", \"[3,end]:5000\" })]\r\n    public void TestCheckStartReferenceTimeTagMissingTime(int referenceTimeTagIndex, string[] timeTags)\r\n    {\r\n        var referencedLyric = new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            TimeTags = TestCaseTagHelper.ParseTimeTags(timeTags),\r\n        };\r\n        var note = new Note\r\n        {\r\n            ReferenceLyricId = referencedLyric.ID,\r\n            ReferenceLyric = referencedLyric,\r\n            ReferenceTimeTagIndex = referenceTimeTagIndex,\r\n        };\r\n\r\n        AssertNotOk<NoteIssue, IssueTemplateStartReferenceTimeTagMissingTime>(new HitObject[] { referencedLyric, note });\r\n    }\r\n\r\n    [TestCase(1, new[] { \"[0,start]:1000\", \"[3,end]:5000\" })]\r\n    [TestCase(4, new[] { \"[0,start]:1000\", \"[1,start]:2000\", \"[2,start]:3000\", \"[3,start]:4000\", \"[3,end]:5000\" })]\r\n    public void TestCheckMissingEndReferenceTimeTag(int referenceTimeTagIndex, string[] timeTags)\r\n    {\r\n        var referencedLyric = new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            TimeTags = TestCaseTagHelper.ParseTimeTags(timeTags),\r\n        };\r\n        var note = new Note\r\n        {\r\n            ReferenceLyricId = referencedLyric.ID,\r\n            ReferenceLyric = referencedLyric,\r\n            ReferenceTimeTagIndex = referenceTimeTagIndex,\r\n        };\r\n\r\n        AssertNotOk<NoteIssue, IssueTemplateMissingEndReferenceTimeTag>(new HitObject[] { referencedLyric, note });\r\n    }\r\n\r\n    [TestCase(0, new[] { \"[0,start]:1000\", \"[3,end]\" })]\r\n    [TestCase(3, new[] { \"[0,start]:1000\", \"[1,start]:2000\", \"[2,start]:3000\", \"[3,start]:4000\", \"[3,end]\" })]\r\n    [TestCase(2, new[] { \"[0,start]:1000\", \"[1,start]:2000\", \"[2,start]:3000\", \"[3,start]\", \"[3,end]:5000\" })]\r\n    public void TestCheckEndReferenceTimeTagMissingTime(int referenceTimeTagIndex, string[] timeTags)\r\n    {\r\n        var referencedLyric = new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            TimeTags = TestCaseTagHelper.ParseTimeTags(timeTags),\r\n        };\r\n        var note = new Note\r\n        {\r\n            ReferenceLyricId = referencedLyric.ID,\r\n            ReferenceLyric = referencedLyric,\r\n            ReferenceTimeTagIndex = referenceTimeTagIndex,\r\n        };\r\n\r\n        AssertNotOk<NoteIssue, IssueTemplateEndReferenceTimeTagMissingTime>(new HitObject[] { referencedLyric, note });\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Checks/CheckNoteTextTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks.Issues;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Objects;\r\nusing static osu.Game.Rulesets.Karaoke.Edit.Checks.CheckNoteText;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Checks;\r\n\r\npublic class CheckNoteTextTest : HitObjectCheckTest<Note, CheckNoteText>\r\n{\r\n    [Test]\r\n    public void TestCheck()\r\n    {\r\n        var note = new Note\r\n        {\r\n            Text = \"karaoke\",\r\n            RubyText = null, // ruby text should be null or having the value.\r\n        };\r\n\r\n        AssertOk(new HitObject[] { note });\r\n    }\r\n\r\n    [TestCase(null)]\r\n    [TestCase(\"\")]\r\n    [TestCase(\" \")] // but should not be empty or white space.\r\n    [TestCase(\"　\")] // but should not be empty or white space.\r\n    public void TestCheckEmptyText(string text)\r\n    {\r\n        var note = new Note\r\n        {\r\n            Text = text,\r\n        };\r\n\r\n        AssertNotOk<NoteIssue, IssueTemplateEmptyText>(note);\r\n    }\r\n\r\n    [TestCase(\"\")]\r\n    [TestCase(\" \")] // but should not be empty or white space.\r\n    [TestCase(\"　\")] // but should not be empty or white space.\r\n    public void TestCheckEmptyRubyText(string? rubyText)\r\n    {\r\n        var note = new Note\r\n        {\r\n            Text = \"karaoke\",\r\n            RubyText = rubyText,\r\n        };\r\n\r\n        AssertNotOk<NoteIssue, IssueTemplateEmptyRubyText>(note);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Checks/CheckNoteTimeTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks.Issues;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\nusing osu.Game.Rulesets.Objects;\r\nusing static osu.Game.Rulesets.Karaoke.Edit.Checks.CheckNoteTime;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Checks;\r\n\r\npublic class CheckNoteTimeTest : HitObjectCheckTest<Note, CheckNoteTime>\r\n{\r\n    [TestCase(0, new[] { \"[0,start]:1000\", \"[1,start]:2000\", \"[2,start]:3000\", \"[3,start]:4000\", \"[3,end]:5000\" })]\r\n    [TestCase(3, new[] { \"[0,start]:1000\", \"[1,start]:2000\", \"[2,start]:3000\", \"[3,start]:4000\", \"[3,end]:5000\" })]\r\n    public void TestCheck(int referenceTimeTagIndex, string[] timeTags)\r\n    {\r\n        var referencedLyric = new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            TimeTags = TestCaseTagHelper.ParseTimeTags(timeTags),\r\n        };\r\n        var note = new Note\r\n        {\r\n            ReferenceLyricId = referencedLyric.ID,\r\n            ReferenceLyric = referencedLyric,\r\n            ReferenceTimeTagIndex = referenceTimeTagIndex,\r\n        };\r\n\r\n        AssertOk(new HitObject[] { referencedLyric, note });\r\n    }\r\n\r\n    [Test]\r\n    public void TestCheckWithNoReferenceLyric()\r\n    {\r\n        var note = new Note\r\n        {\r\n            Text = \"karaoke\",\r\n            ReferenceLyricId = null,\r\n            ReferenceLyric = null,\r\n        };\r\n\r\n        // should not have error because this check will be handled in other check.\r\n        AssertOk(new HitObject[] { note });\r\n    }\r\n\r\n    [TestCase(3, new[] { \"[0,start]:1000\", \"[1,start]:2000\", \"[2,start]:3000\", \"[3,start]:4000\", \"[3,end]\" })] // will missing start time-tag.\r\n    [TestCase(2, new[] { \"[0,start]:1000\", \"[1,start]:2000\", \"[2,start]:3000\", \"[3,start]\", \"[3,end]:5000\" })] // will missing end time-tag.\r\n    public void TestCheckMissingStartOrEndTimeTag(int referenceTimeTagIndex, string[] timeTags)\r\n    {\r\n        var referencedLyric = new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            TimeTags = TestCaseTagHelper.ParseTimeTags(timeTags),\r\n        };\r\n        var note = new Note\r\n        {\r\n            ReferenceLyricId = referencedLyric.ID,\r\n            ReferenceLyric = referencedLyric,\r\n            ReferenceTimeTagIndex = referenceTimeTagIndex,\r\n        };\r\n\r\n        // should not have error because this check will be handled in other check.\r\n        AssertOk(new HitObject[] { referencedLyric, note });\r\n    }\r\n\r\n    [TestCase(\"[0,start]:2000\", \"[1,start]:1000\")]\r\n    [TestCase(\"[0,end]:2000\", \"[1,end]:1000\")]\r\n    [TestCase(\"[0,start]:2000\", \"[1,end]:1000\")]\r\n    [TestCase(\"[0,end]:2000\", \"[1,start]:1000\")]\r\n    [TestCase(\"[1,start]:2000\", \"[0,start]:1000\")] // should have error even if time-tag index is not sorted. we did not care about the time-tag index in here.\r\n    public void TestCheckInvalidReferenceTimeTagTime(string startTimeTag, string endTimeTag)\r\n    {\r\n        var referencedLyric = new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            TimeTags = TestCaseTagHelper.ParseTimeTags(new[] { startTimeTag, endTimeTag }),\r\n        };\r\n        var note = new Note\r\n        {\r\n            ReferenceLyricId = referencedLyric.ID,\r\n            ReferenceLyric = referencedLyric,\r\n            ReferenceTimeTagIndex = 0,\r\n        };\r\n\r\n        AssertNotOk<NoteIssue, IssueTemplateInvalidReferenceTimeTagTime>(new HitObject[] { referencedLyric, note });\r\n    }\r\n\r\n    [TestCase(\"[0,start]\", \"[1,start]\")]\r\n    [TestCase(\"[0,end]\", \"[1,end]\")]\r\n    [TestCase(\"[0,start]\", \"[1,end]\")]\r\n    [TestCase(\"[0,end]\", \"[1,start]\")]\r\n    [TestCase(\"[1,start]\", \"[0,start]\")] // should have error even if time-tag index is not sorted. we did not care about the time-tag index in here.\r\n    public void TestCheckDurationTooShort(string startTimeTag, string endTimeTag)\r\n    {\r\n        var referencedLyric = new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            TimeTags = TestCaseTagHelper.ParseTimeTags(new[] { $\"{startTimeTag}:0\", $\"{endTimeTag}:{MIN_DURATION - 1}\" }),\r\n        };\r\n        var note = new Note\r\n        {\r\n            ReferenceLyricId = referencedLyric.ID,\r\n            ReferenceLyric = referencedLyric,\r\n            ReferenceTimeTagIndex = 0,\r\n        };\r\n\r\n        AssertNotOk<NoteIssue, IssueTemplateDurationTooShort>(new HitObject[] { referencedLyric, note });\r\n    }\r\n\r\n    [TestCase(\"[0,start]\", \"[1,start]\")]\r\n    [TestCase(\"[0,end]\", \"[1,end]\")]\r\n    [TestCase(\"[0,start]\", \"[1,end]\")]\r\n    [TestCase(\"[0,end]\", \"[1,start]\")]\r\n    [TestCase(\"[1,start]\", \"[0,start]\")] // should have error even if time-tag index is not sorted. we did not care about the time-tag index in here.\r\n    public void TestCheckDurationTooLong(string startTimeTag, string endTimeTag)\r\n    {\r\n        var referencedLyric = new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            TimeTags = TestCaseTagHelper.ParseTimeTags(new[] { $\"{startTimeTag}:0\", $\"{endTimeTag}:{MAX_DURATION + 1}\" }),\r\n        };\r\n        var note = new Note\r\n        {\r\n            ReferenceLyricId = referencedLyric.ID,\r\n            ReferenceLyric = referencedLyric,\r\n            ReferenceTimeTagIndex = 0,\r\n        };\r\n\r\n        AssertNotOk<NoteIssue, IssueTemplateDurationTooLong>(new HitObject[] { referencedLyric, note });\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Checks/CheckStageInfoTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Rulesets.Edit;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos.Classic;\r\nusing osu.Game.Screens.Edit;\r\nusing osu.Game.Tests.Beatmaps;\r\nusing static osu.Game.Rulesets.Karaoke.Edit.Checks.CheckStageInfo<osu.Game.Rulesets.Karaoke.Stages.Infos.Classic.ClassicStageInfo>;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Checks;\r\n\r\n[Ignore(\"Disable this test until able to get the stage info from the resource file.\")]\r\npublic class CheckStageInfoTest : BaseCheckTest<CheckStageInfoTest.CheckStageInfo>\r\n{\r\n    [Test]\r\n    public void TestCheckNoElement()\r\n    {\r\n        var beatmap = createTestingBeatmap(Array.Empty<Lyric>());\r\n        var stageInfo = createTestingStageInfo();\r\n        AssertNotOk<IssueTemplateNoElement>(getContext(beatmap, stageInfo));\r\n    }\r\n\r\n    [Test]\r\n    public void TestCheckMappingHitObjectNotExist()\r\n    {\r\n        var lyric = new Lyric();\r\n\r\n        // note that this lyric does not added in to the beatmap.\r\n        var beatmap = createTestingBeatmap(Array.Empty<Lyric>());\r\n        var stageInfo = createTestingStageInfo(category =>\r\n        {\r\n            // add two elements to prevent no element error.\r\n            category.AddElement();\r\n            category.AddElement();\r\n\r\n            var firstElement = category.AvailableElements.First();\r\n            category.AddToMapping(firstElement, lyric);\r\n        });\r\n        AssertNotOk<IssueTemplateMappingHitObjectNotExist>(getContext(beatmap, stageInfo));\r\n    }\r\n\r\n    [Test]\r\n    public void TestCheckMappingItemNotExist()\r\n    {\r\n        var lyric = new Lyric();\r\n\r\n        var beatmap = createTestingBeatmap(new[] { lyric });\r\n        var stageInfo = createTestingStageInfo(category =>\r\n        {\r\n            // add two elements to prevent no element error.\r\n            category.AddElement();\r\n            category.AddElement();\r\n\r\n            // write value to the mapping directly to reproduce the behavior like loading value from the beatmap.\r\n            category.Mappings.Add(lyric.ID, ElementId.NewElementId());\r\n        });\r\n        AssertNotOk<IssueTemplateMappingItemNotExist>(getContext(beatmap, stageInfo));\r\n    }\r\n\r\n    public class CheckStageInfo : CheckStageInfo<ClassicStageInfo>\r\n    {\r\n        protected override string Description => \"Checks for testing the shared logic\";\r\n\r\n        public CheckStageInfo()\r\n        {\r\n            // Note that we only test the lyric layout category.\r\n            RegisterCategory(x => x.StyleCategory, 0);\r\n            RegisterCategory(x => x.LyricLayoutCategory, 2);\r\n        }\r\n\r\n        public override IEnumerable<IssueTemplate> CustomTemplates => Array.Empty<IssueTemplate>();\r\n\r\n        public override IEnumerable<Issue> CheckStageInfoWithHitObjects(ClassicStageInfo stageInfo, IReadOnlyList<KaraokeHitObject> hitObjects)\r\n        {\r\n            yield break;\r\n        }\r\n\r\n        protected override IEnumerable<Issue> CheckElement<TStageElement>(TStageElement element)\r\n        {\r\n            yield break;\r\n        }\r\n    }\r\n\r\n    private static IBeatmap createTestingBeatmap(IEnumerable<Lyric>? lyrics)\r\n    {\r\n        var karaokeBeatmap = new KaraokeBeatmap\r\n        {\r\n            BeatmapInfo =\r\n            {\r\n                Ruleset = new KaraokeRuleset().RulesetInfo,\r\n            },\r\n            HitObjects = lyrics?.OfType<KaraokeHitObject>().ToList() ?? new List<KaraokeHitObject>(),\r\n        };\r\n        return new EditorBeatmap(karaokeBeatmap);\r\n    }\r\n\r\n    private static StageInfo createTestingStageInfo(Action<ClassicLyricLayoutCategory>? editStageAction = null)\r\n    {\r\n        var stageInfo = new ClassicStageInfo();\r\n        editStageAction?.Invoke(stageInfo.LyricLayoutCategory);\r\n\r\n        return stageInfo;\r\n    }\r\n\r\n    private static BeatmapVerifierContext getContext(IBeatmap beatmap, StageInfo stageInfo)\r\n        => new(beatmap, new TestWorkingBeatmap(beatmap));\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Checks/HitObjectCheckTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Rulesets.Edit;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osu.Game.Rulesets.Objects;\r\nusing osu.Game.Tests.Beatmaps;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Checks;\r\n\r\npublic abstract class HitObjectCheckTest<THitObject, TCheck> : BaseCheckTest<TCheck> where TCheck : class, ICheck, new()\r\n{\r\n    protected void AssertOk(HitObject hitObject)\r\n    {\r\n        AssertOk(new[] { hitObject });\r\n    }\r\n\r\n    protected void AssertOk(IEnumerable<HitObject> hitObjects)\r\n    {\r\n        AssertOk(getContext(hitObjects));\r\n    }\r\n\r\n    protected void AssertNotOk<TIssue, TIssueTemplate>(HitObject hitObject)\r\n        where TIssue : Issue\r\n        where TIssueTemplate : IssueTemplate\r\n    {\r\n        AssertNotOk<TIssue, TIssueTemplate>(new[] { hitObject });\r\n    }\r\n\r\n    protected void AssertNotOk<TIssue, TIssueTemplate>(IEnumerable<HitObject> hitObjects)\r\n        where TIssue : Issue\r\n        where TIssueTemplate : IssueTemplate\r\n    {\r\n        AssertNotOk<TIssue, TIssueTemplate>(getContext(hitObjects));\r\n    }\r\n\r\n    private BeatmapVerifierContext getContext(IEnumerable<HitObject> hitObjects)\r\n    {\r\n        var beatmap = new Beatmap<HitObject> { HitObjects = hitObjects.ToList() };\r\n\r\n        return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Generator/BaseGeneratorSelectorTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Generator;\r\n\r\npublic abstract class BaseGeneratorSelectorTest<TGenerator, TItem, TProperty>\r\n    : BasePropertyGeneratorTest<TGenerator, TItem, TProperty>\r\n    where TGenerator : PropertyGenerator<TItem, TProperty>\r\n{\r\n    protected TGenerator CreateSelector()\r\n    {\r\n        var configManager = new KaraokeRulesetEditGeneratorConfigManager();\r\n        return ActivatorUtils.CreateInstance<TGenerator>(configManager);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Generator/BasePropertyDetectorTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Generator;\r\n\r\npublic abstract class BasePropertyDetectorTest<TDetector, TItem, TProperty, TConfig>\r\n    : BasePropertyDetectorTest<TDetector, TItem, TProperty>\r\n    where TDetector : PropertyDetector<TItem, TProperty>\r\n    where TConfig : GeneratorConfig, new()\r\n{\r\n    protected static TConfig GeneratorEmptyConfig(Action<TConfig>? action = null)\r\n    {\r\n        var config = new TConfig();\r\n        GeneratorConfigHelper.ClearValue(config);\r\n\r\n        action?.Invoke(config);\r\n        return config;\r\n    }\r\n\r\n    protected static TConfig GeneratorDefaultConfig(Action<TConfig>? action = null)\r\n    {\r\n        var config = new TConfig();\r\n\r\n        action?.Invoke(config);\r\n        return config;\r\n    }\r\n\r\n    protected static void CheckCanDetect(TItem item, bool canDetect, TConfig config)\r\n    {\r\n        var detector = ActivatorUtils.CreateInstance<TDetector>(config);\r\n\r\n        CheckCanDetect(item, canDetect, detector);\r\n    }\r\n\r\n    protected void CheckDetectResult(TItem item, TProperty expected, TConfig config)\r\n    {\r\n        var detector = ActivatorUtils.CreateInstance<TDetector>(config);\r\n\r\n        CheckDetectResult(item, expected, detector);\r\n    }\r\n}\r\n\r\npublic abstract class BasePropertyDetectorTest<TDetector, TItem, TProperty>\r\n    where TDetector : PropertyDetector<TItem, TProperty>\r\n{\r\n    protected static void CheckCanDetect(TItem item, bool canDetect, TDetector detector)\r\n    {\r\n        bool actual = detector.CanDetect(item);\r\n        Assert.That(actual, Is.EqualTo(canDetect));\r\n    }\r\n\r\n    protected void CheckDetectResult(TItem item, TProperty expected, TDetector detector)\r\n    {\r\n        // create time tag and actually time tag.\r\n        var actual = detector.Detect(item);\r\n        AssertEqual(expected, actual);\r\n    }\r\n\r\n    protected abstract void AssertEqual(TProperty expected, TProperty actual);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Generator/BasePropertyGeneratorTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Generator;\r\n\r\npublic abstract class BasePropertyGeneratorTest<TGenerator, TItem, TProperty, TConfig>\r\n    : BasePropertyGeneratorTest<TGenerator, TItem, TProperty>\r\n    where TGenerator : PropertyGenerator<TItem, TProperty>\r\n    where TConfig : GeneratorConfig, new()\r\n{\r\n    protected static TConfig GeneratorEmptyConfig(Action<TConfig>? action = null)\r\n    {\r\n        var config = new TConfig();\r\n        GeneratorConfigHelper.ClearValue(config);\r\n\r\n        action?.Invoke(config);\r\n        return config;\r\n    }\r\n\r\n    protected static TConfig GeneratorDefaultConfig(Action<TConfig>? action = null)\r\n    {\r\n        var config = new TConfig();\r\n\r\n        action?.Invoke(config);\r\n        return config;\r\n    }\r\n\r\n    protected static void CheckCanGenerate(TItem item, bool canGenerate, TConfig config)\r\n    {\r\n        var generator = ActivatorUtils.CreateInstance<TGenerator>(config);\r\n\r\n        CheckCanGenerate(item, canGenerate, generator);\r\n    }\r\n\r\n    protected void CheckGenerateResult(TItem item, TProperty expected, TConfig config)\r\n    {\r\n        var generator = ActivatorUtils.CreateInstance<TGenerator>(config);\r\n\r\n        CheckGenerateResult(item, expected, generator);\r\n    }\r\n}\r\n\r\npublic abstract class BasePropertyGeneratorTest<TGenerator, TItem, TProperty>\r\n    where TGenerator : PropertyGenerator<TItem, TProperty>\r\n{\r\n    protected static void CheckCanGenerate(TItem item, bool canGenerate, TGenerator generator)\r\n    {\r\n        bool actual = generator.CanGenerate(item);\r\n        Assert.That(actual, Is.EqualTo(canGenerate));\r\n    }\r\n\r\n    protected void CheckGenerateResult(TItem item, TProperty expected, TGenerator generator)\r\n    {\r\n        // create time tag and actually time tag.\r\n        var actual = generator.Generate(item);\r\n        AssertEqual(expected, actual);\r\n    }\r\n\r\n    protected abstract void AssertEqual(TProperty expected, TProperty actual);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Generator/Beatmaps/BaseBeatmapDetectorTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Beatmaps;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Generator.Beatmaps;\r\n\r\npublic abstract class BaseBeatmapDetectorTest<TDetector, TObject, TConfig>\r\n    : BasePropertyDetectorTest<TDetector, KaraokeBeatmap, TObject, TConfig>\r\n    where TDetector : BeatmapPropertyDetector<TObject, TConfig>\r\n    where TConfig : GeneratorConfig, new();\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Generator/Beatmaps/BaseBeatmapGeneratorTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Beatmaps;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Generator.Beatmaps;\r\n\r\npublic abstract class BaseBeatmapGeneratorTest<TGenerator, TObject, TConfig>\r\n    : BasePropertyGeneratorTest<TGenerator, KaraokeBeatmap, TObject, TConfig>\r\n    where TGenerator : BeatmapPropertyGenerator<TObject, TConfig>\r\n    where TConfig : GeneratorConfig, new();\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Generator/Beatmaps/Pages/PageGeneratorTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Checks;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Beatmaps.Pages;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Generator.Beatmaps.Pages;\r\n\r\n[TestFixture]\r\npublic class PageGeneratorTest : BaseBeatmapGeneratorTest<PageGenerator, Page[], PageGeneratorConfig>\r\n{\r\n    private const double min_interval = CheckBeatmapPageInfo.MIN_INTERVAL;\r\n    private const double max_interval = CheckBeatmapPageInfo.MAX_INTERVAL;\r\n\r\n    [TestCase(new[] { \"[1000,3000]:karaoke\" }, true)]\r\n    [TestCase(new[] { \"[1000,3000]:karaoke\", \"[4000,6000]:karaoke\" }, true)]\r\n    [TestCase(new[] { \"[1000,3000]:karaoke\", \"[1000,3000]:karaoke\" }, true)] // should still runnable even if lyric is overlapping.\r\n    [TestCase(new[] { \"\" }, false)] // should not be able to generate if lyric is empty.\r\n    [TestCase(new string[] { }, false)]\r\n    public void TestCanGenerate(string[] lyrics, bool canGenerate)\r\n    {\r\n        var config = GeneratorDefaultConfig();\r\n        var beatmap = new KaraokeBeatmap\r\n        {\r\n            HitObjects = TestCaseTagHelper.ParseLyrics(lyrics).OfType<KaraokeHitObject>().ToList(),\r\n        };\r\n\r\n        CheckCanGenerate(beatmap, canGenerate, config);\r\n    }\r\n\r\n    [TestCase(\"[1000,3000]:karaoke\", new double[] { 1000, 3000 })]\r\n    [TestCase(\"[1000,23000]:karaoke\", new[] { 1000, 1000 + max_interval, 1000 + max_interval * 2, 23000 })]\r\n    public void TestGenerateWithSingleLyric(string lyric, double[] expectedTimes)\r\n    {\r\n        var config = GeneratorDefaultConfig();\r\n        var beatmap = new KaraokeBeatmap\r\n        {\r\n            HitObjects = new List<KaraokeHitObject>\r\n            {\r\n                TestCaseTagHelper.ParseLyric(lyric),\r\n            },\r\n        };\r\n\r\n        var expectedPages = expectedTimes.Select(x => new Page\r\n        {\r\n            Time = x,\r\n        }).ToArray();\r\n\r\n        CheckGenerateResult(beatmap, expectedPages, config);\r\n    }\r\n\r\n    [TestCase(\"[1000,4000]:karaoke\", \"[4000,7000]:karaoke\", new double[] { 1000, 4000, 7000 })]\r\n    [TestCase(\"[1000,4000]:karaoke\", \"[5000,8000]:karaoke\", new double[] { 1000, 4500, 8000 })]\r\n    [TestCase(\"[1000,3000]:karaoke\", \"[1000,3000]:karaoke\", new double[] { 1000, 3000 })] //should deal with overlapping lyric.\r\n    [TestCase(\"[1000,23000]:karaoke\", \"[1000,23000]:karaoke\", new[] { 1000, 1000 + max_interval, 1000 + max_interval * 2, 23000 })] //should deal with overlapping lyric with long time.\r\n    [TestCase(\"[1000,23000]:karaoke\", \"[3000,4000]:karaoke\", new[] { 1000, 1000 + max_interval, 1000 + max_interval * 2, 23000 })] // should ignore second lyric.\r\n    public void TestGenerateWithTwoLyrics(string firstLyric, string secondLyric, double[] expectedTimes)\r\n    {\r\n        var config = GeneratorDefaultConfig();\r\n        var beatmap = new KaraokeBeatmap\r\n        {\r\n            HitObjects = new List<KaraokeHitObject>\r\n            {\r\n                TestCaseTagHelper.ParseLyric(firstLyric),\r\n                TestCaseTagHelper.ParseLyric(secondLyric),\r\n            },\r\n        };\r\n\r\n        var expectedPages = expectedTimes.Select(x => new Page\r\n        {\r\n            Time = x,\r\n        }).ToArray();\r\n\r\n        CheckGenerateResult(beatmap, expectedPages, config);\r\n    }\r\n\r\n    [Ignore(\"Waiting for implementation.\")]\r\n    public void TestGenerateWithSingleLyricWithPage()\r\n    {\r\n    }\r\n\r\n    [Ignore(\"Waiting for implementation.\")]\r\n    public void TestGenerateWithTwoLyricsWithPage()\r\n    {\r\n    }\r\n\r\n    protected override void AssertEqual(Page[] expected, Page[] actual)\r\n    {\r\n        string expectedTimes = string.Join(\",\", expected.Select(x => x.Time));\r\n        string actualTimes = string.Join(\",\", actual.Select(x => x.Time));\r\n        Assert.That(actualTimes, Is.EqualTo(expectedTimes));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Generator/GeneratorConfigExtensionTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Globalization;\r\nusing System.Linq;\r\nusing System.Reflection;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Generator;\r\n\r\npublic class GeneratorConfigExtensionTest\r\n{\r\n    [Test]\r\n    public void TestGetOrderedConfigsSourceDictionary()\r\n    {\r\n        var config = new TestGeneratorConfig();\r\n        var defaultCategory = new ConfigCategoryAttribute(\"Category 0\");\r\n        var result = config.GetOrderedConfigsSourceDictionary(defaultCategory);\r\n\r\n        Assert.That(result[defaultCategory].Length, Is.EqualTo(1));\r\n        Assert.That(result[new ConfigCategoryAttribute(\"Category 1\")].Length, Is.EqualTo(2));\r\n    }\r\n\r\n    [Test]\r\n    public void TestGetOrderedConfigsSourceProperties()\r\n    {\r\n        var config = new TestGeneratorConfig();\r\n        var result = config.GetOrderedConfigsSourceProperties().ToArray();\r\n\r\n        checkSingleProperty(result[0], \"Culture infos\");\r\n        checkSingleProperty(result[1], \"Boolean\");\r\n        checkSingleProperty(result[2], \"Double\"); // todo: this shit should be the first one.\r\n\r\n        static void checkSingleProperty((ConfigSourceAttribute, ConfigCategoryAttribute?, PropertyInfo) property, string title) =>\r\n            Assert.That(property.Item1.Label.ToString(), Is.EqualTo(title));\r\n    }\r\n\r\n    [Test]\r\n    public void TestGetConfigSourceProperties()\r\n    {\r\n        var config = new TestGeneratorConfig();\r\n        var result = config.GetConfigSourceProperties().ToArray();\r\n\r\n        checkSingleProperty(result[0], \"Double\", \"Edit the double value\");\r\n        checkSingleProperty(result[1], \"Boolean\", \"Edit the boolean value\", \"Category 1\");\r\n        checkSingleProperty(result[2], \"Culture infos\", \"Edit the culture infos\", \"Category 1\");\r\n\r\n        static void checkSingleProperty((ConfigSourceAttribute, ConfigCategoryAttribute?, PropertyInfo) property, string title, string description, string? category = null)\r\n        {\r\n            Assert.That(property.Item1.Label.ToString(), Is.EqualTo(title));\r\n            Assert.That(property.Item1.Description.ToString(), Is.EqualTo(description));\r\n            Assert.That(property.Item2?.Category.ToString(), Is.EqualTo(category));\r\n        }\r\n    }\r\n\r\n    private class TestGeneratorConfig : GeneratorConfig\r\n    {\r\n        [ConfigSource(\"Double\", \"Edit the double value\")]\r\n        public Bindable<double> Double { get; } = new BindableDouble();\r\n\r\n        [ConfigCategory(\"Category 1\")]\r\n        [ConfigSource(\"Boolean\", \"Edit the boolean value\", 1)]\r\n        public Bindable<bool> Boolean { get; } = new BindableBool();\r\n\r\n        [ConfigCategory(\"Category 1\")]\r\n        [ConfigSource(\"Culture infos\", \"Edit the culture infos\", 0)]\r\n        public Bindable<CultureInfo[]> CultureInfos { get; } = new(Array.Empty<CultureInfo>());\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Generator/GeneratorConfigHelper.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing System.Reflection;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Extensions.TypeExtensions;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Generator;\r\n\r\npublic class GeneratorConfigHelper\r\n{\r\n    public static void ClearValue(GeneratorConfig config)\r\n    {\r\n        foreach (var property in getAllBindableProperty(config))\r\n        {\r\n            object propertyInstance = property.GetValue(config, null)!;\r\n\r\n            // set the default value to the value.\r\n            var propertyType = propertyInstance.GetType();\r\n            propertyType.GetProperty(nameof(Bindable<object>.Value))!.SetValue(propertyInstance, default);\r\n        }\r\n    }\r\n\r\n    private static IEnumerable<PropertyInfo> getAllBindableProperty(GeneratorConfig config)\r\n    {\r\n        var properties = config.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);\r\n\r\n        foreach (var property in properties)\r\n        {\r\n            object propertyInstance = property.GetValue(config, null)!;\r\n            var propertyType = propertyInstance.GetType();\r\n\r\n            if (propertyType.EnumerateBaseTypes().All(t => !t.IsGenericType || t.GetGenericTypeDefinition() != typeof(Bindable<>)))\r\n                continue;\r\n\r\n            yield return property;\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Generator/Lyrics/BaseLyricDetectorTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Generator.Lyrics;\r\n\r\npublic abstract class BaseLyricDetectorTest<TDetector, TObject, TConfig>\r\n    : BasePropertyDetectorTest<TDetector, Lyric, TObject, TConfig>\r\n    where TDetector : LyricPropertyDetector<TObject, TConfig>\r\n    where TConfig : GeneratorConfig, new();\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Generator/Lyrics/BaseLyricGeneratorSelectorTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Generator.Lyrics;\r\n\r\npublic abstract class BaseLyricGeneratorSelectorTest<TGenerator, TProperty>\r\n    : BaseGeneratorSelectorTest<TGenerator, Lyric, TProperty>\r\n    where TGenerator : PropertyGenerator<Lyric, TProperty>;\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Generator/Lyrics/BaseLyricGeneratorTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Generator.Lyrics;\r\n\r\npublic abstract class BaseLyricGeneratorTest<TGenerator, TObject, TConfig>\r\n    : BasePropertyGeneratorTest<TGenerator, Lyric, TObject, TConfig>\r\n    where TGenerator : LyricPropertyGenerator<TObject, TConfig>\r\n    where TConfig : GeneratorConfig, new();\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Generator/Lyrics/Language/LanguageDetectorTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Globalization;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.Language;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Generator.Lyrics.Language;\r\n\r\n[TestFixture]\r\npublic class LanguageDetectorTest : BaseLyricDetectorTest<LanguageDetector, CultureInfo?, LanguageDetectorConfig>\r\n{\r\n    [TestCase(\"花火大会\", true)]\r\n    [TestCase(\"\", false)] // will not able to detect the language if lyric is empty.\r\n    [TestCase(\"   \", false)]\r\n    [TestCase(null, false)]\r\n    public void TestCanDetect(string text, bool canDetect)\r\n    {\r\n        var lyric = new Lyric { Text = text };\r\n        var config = GeneratorEmptyConfig();\r\n        CheckCanDetect(lyric, canDetect, config);\r\n    }\r\n\r\n    [TestCase(\"花火大会\", \"zh-CN\")]\r\n    [TestCase(\"花火大會\", \"zh-TW\")]\r\n    [TestCase(\"Testing\", \"en\")]\r\n    [TestCase(\"ハナビ\", \"ja\")]\r\n    [TestCase(\"はなび\", \"ja\")]\r\n    public void TestDetect(string text, string language)\r\n    {\r\n        var lyric = new Lyric { Text = text };\r\n        var config = GeneratorEmptyConfig();\r\n        var expected = new CultureInfo(language);\r\n        CheckDetectResult(lyric, expected, config);\r\n    }\r\n\r\n    protected override void AssertEqual(CultureInfo? expected, CultureInfo? actual)\r\n    {\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Generator/Lyrics/Notes/NoteGeneratorTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.Notes;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Generator.Lyrics.Notes;\r\n\r\n[TestFixture]\r\npublic class NoteGeneratorTest : BaseLyricGeneratorTest<NoteGenerator, Note[], NoteGeneratorConfig>\r\n{\r\n    [TestCase(new[] { \"[0,start]:1000\", \"[1,start]:2000\", \"[2,start]:3000\", \"[3,start]:4000\", \"[3,end]:5000\" }, true)]\r\n    [TestCase(new[] { \"[0,start]:1000\", \"[1,start]:2000\" }, true)]\r\n    [TestCase(new[] { \"[0,start]:1000\", \"[1,start]:2000\", \"[2,start]\" }, false)] // all time-tag should with time.\r\n    [TestCase(new[] { \"[0,start]:1000\", \"[1,start]\" }, false)] // should have at least two time-tags with time.\r\n    [TestCase(new[] { \"[0,start]:1000\" }, false)] // should have at least two time-tags.\r\n    [TestCase(new[] { \"[0,start]\" }, false)] // no-time.\r\n    [TestCase(new string[] { }, false)]\r\n    public void TestCanGenerate(string[] timeTags, bool canGenerate)\r\n    {\r\n        var config = GeneratorEmptyConfig();\r\n        var lyric = new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            TimeTags = TestCaseTagHelper.ParseTimeTags(timeTags),\r\n        };\r\n\r\n        CheckCanGenerate(lyric, canGenerate, config);\r\n    }\r\n\r\n    [TestCase(new[] { \"[0,start]:1000\", \"[1,start]:2000\", \"[2,start]:3000\", \"[3,start]:4000\", \"[3,end]:5000\" }, new[] { \"カ\", \"ラ\", \"オ\", \"ケ\" })]\r\n    [TestCase(new[] { \"[3,end]:1000\", \"[3,start]:2000\", \"[2,start]:3000\", \"[1,start]:4000\", \"[0,start]:5000\" }, new string[] { })]\r\n    [TestCase(new[] { \"[0,start]:1000\", \"[1,start]:1000\", \"[2,start]:3000\", \"[3,start]:4000\", \"[3,end]:5000\" }, new[] { \"カラ\", \"オ\", \"ケ\" })] // will combine the note if time is duplicated.\r\n    public void TestGenerate(string[] timeTags, string[] expectedNoteTexts)\r\n    {\r\n        var config = GeneratorEmptyConfig();\r\n        var lyric = new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            TimeTags = TestCaseTagHelper.ParseTimeTags(timeTags),\r\n        };\r\n\r\n        var expectedNotes = expectedNoteTexts.Select(x => new Note { Text = x }).ToArray();\r\n        CheckGenerateResult(lyric, expectedNotes, config);\r\n    }\r\n\r\n    [TestCase(new string[] { }, new[] { \"カ\", \"ラ\", \"オ\", \"ケ\" })]\r\n    [TestCase(new[] { \"[0]:か\", \"[1]:ら\", \"[2]:お\", \"[3]:け\" }, new[] { \"カ,か\", \"ラ,ら\", \"オ,お\", \"ケ,け\" })]\r\n    public void TestGenerateWithRuby(string[] rubyTags, string[] expectedNoteTexts)\r\n    {\r\n        var config = GeneratorEmptyConfig();\r\n        var lyric = new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            TimeTags = TestCaseTagHelper.ParseTimeTags(new[] { \"[0,start]:1000\", \"[1,start]:2000\", \"[2,start]:3000\", \"[3,start]:4000\", \"[3,end]:5000\" }),\r\n            RubyTags = TestCaseTagHelper.ParseRubyTags(rubyTags),\r\n        };\r\n\r\n        var expectedNotes = expectedNoteTexts.Select(x =>\r\n        {\r\n            if (x.Contains(','))\r\n            {\r\n                return new Note\r\n                {\r\n                    Text = x.Split(',')[0],\r\n                    RubyText = x.Split(',')[1],\r\n                };\r\n            }\r\n\r\n            return new Note\r\n            {\r\n                Text = x,\r\n            };\r\n        }).ToArray();\r\n        CheckGenerateResult(lyric, expectedNotes, config);\r\n    }\r\n\r\n    protected override void AssertEqual(Note[] expected, Note[] actual)\r\n    {\r\n        Assert.That(actual.Select(x => x.Text), Is.EqualTo(expected.Select(x => x.Text)));\r\n        Assert.That(actual.Select(x => x.RubyText), Is.EqualTo(expected.Select(x => x.RubyText)));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Generator/Lyrics/ReferenceLyric/ReferenceLyricDetectorTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.ReferenceLyric;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Generator.Lyrics.ReferenceLyric;\r\n\r\n[TestFixture]\r\npublic class ReferenceLyricDetectorTest : BaseLyricDetectorTest<ReferenceLyricDetector, Lyric?, ReferenceLyricDetectorConfig>\r\n{\r\n    [TestCase(\"karaoke\", \"karaoke\", true)]\r\n    [TestCase(\"karaoke\", \"karaoke -\", false)] // should be able to detect only if two lyric text are the same.\r\n    [TestCase(\"- karaoke\", \"karaoke\", false)] // should be able to detect only if two lyric text are the same.\r\n    [TestCase(\"karaoke\", \"カラオケ\", false)] // should be able to detect only if two lyric text are the same.\r\n    public void TestCanDetect(string lyricText, string detectedLyricText, bool canDetect)\r\n    {\r\n        var detectedLyric = new Lyric\r\n        {\r\n            Text = detectedLyricText,\r\n        };\r\n\r\n        var lyrics = new[]\r\n        {\r\n            new Lyric\r\n            {\r\n                Text = lyricText,\r\n            },\r\n            detectedLyric,\r\n        };\r\n        var config = GeneratorEmptyConfig();\r\n        CheckCanDetect(lyrics, detectedLyric, canDetect, config);\r\n    }\r\n\r\n    [TestCase(\"karaoke\", \"karaoke\", true)]\r\n    [TestCase(\"karaoke\", \"カラオケ\", false)]\r\n    [TestCase(\"karaoke\", \"karaoke -\", true)]\r\n    [TestCase(\"karaoke\", \"- karaoke\", true)]\r\n    [TestCase(\"karaoke\", \"- karaoke -\", true)]\r\n    [TestCase(\"karaoke\", \"karaokeカラオケ\", false)]\r\n    [TestCase(\"karaoke -\", \"karaoke\", true)]\r\n    [TestCase(\"- karaoke\", \"karaoke\", true)]\r\n    [TestCase(\"- karaoke -\", \"karaoke\", true)]\r\n    [TestCase(\"カラオケkaraoke\", \"karaoke\", false)]\r\n    [TestCase(\"- karaoke\", \"karaoke -\", false)] // it's not supported now.\r\n    public void TestCanDetectWithIgnorePrefixAndPostfixSymbol(string lyricText, string detectedLyricText, bool canDetect)\r\n    {\r\n        var detectedLyric = new Lyric\r\n        {\r\n            Text = detectedLyricText,\r\n        };\r\n\r\n        var lyrics = new[]\r\n        {\r\n            new Lyric\r\n            {\r\n                Text = lyricText,\r\n            },\r\n            detectedLyric,\r\n        };\r\n        var config = GeneratorEmptyConfig(x => x.IgnorePrefixAndPostfixSymbol.Value = true);\r\n        CheckCanDetect(lyrics, detectedLyric, canDetect, config);\r\n    }\r\n\r\n    [TestCase(\"karaoke\", \"karaoke\", true)]\r\n    public void TestDetect(string firstLyricText, string secondLyricText, bool referenced)\r\n    {\r\n        var firstLyric = new Lyric\r\n        {\r\n            Text = firstLyricText,\r\n            Order = 1,\r\n        };\r\n        var secondLyric = new Lyric\r\n        {\r\n            Text = secondLyricText,\r\n            Order = 2,\r\n        };\r\n\r\n        var config = GeneratorEmptyConfig();\r\n\r\n        // first lyric cannot referenced by second lyric.\r\n        CheckDetectResult(new[] { firstLyric, secondLyric }, firstLyric, null, config);\r\n        CheckDetectResult(new[] { firstLyric, secondLyric }, secondLyric, referenced ? firstLyric : null, config);\r\n    }\r\n\r\n    protected override void AssertEqual(Lyric? expected, Lyric? actual)\r\n    {\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    #region Utility function\r\n\r\n    protected static void CheckCanDetect(IEnumerable<Lyric> lyrics, Lyric lyric, bool canDetect, ReferenceLyricDetectorConfig config)\r\n    {\r\n        var detector = new ReferenceLyricDetector(lyrics, config);\r\n\r\n        CheckCanDetect(lyric, canDetect, detector);\r\n    }\r\n\r\n    protected void CheckDetectResult(IEnumerable<Lyric> lyrics, Lyric lyric, Lyric? expected, ReferenceLyricDetectorConfig config)\r\n    {\r\n        var detector = new ReferenceLyricDetector(lyrics, config);\r\n\r\n        CheckDetectResult(lyric, expected, detector);\r\n    }\r\n\r\n    #endregion\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Generator/Lyrics/Romanisation/BaseRomanisationGeneratorTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.Romanisation;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Asserts;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Generator.Lyrics.Romanisation;\r\n\r\npublic abstract class BaseRomanisationGeneratorTest<TRomanisationGenerator, TConfig> : BaseLyricGeneratorTest<TRomanisationGenerator, IReadOnlyDictionary<TimeTag, RomanisationGenerateResult>, TConfig>\r\n    where TRomanisationGenerator : RomanisationGenerator<TConfig> where TConfig : RomanisationGeneratorConfig, new()\r\n{\r\n    protected void CheckGenerateResult(Lyric lyric, string[] expectedRubies, TConfig config)\r\n    {\r\n        var expected = RomanisationGenerateResultHelper.ParseRomanisationGenerateResults(lyric.TimeTags, expectedRubies);\r\n        CheckGenerateResult(lyric, expected, config);\r\n    }\r\n\r\n    protected override void AssertEqual(IReadOnlyDictionary<TimeTag, RomanisationGenerateResult> expected, IReadOnlyDictionary<TimeTag, RomanisationGenerateResult> actual)\r\n    {\r\n        TimeTagAssert.ArePropertyEqual(expected.Select(x => x.Key).ToArray(), actual.Select(x => x.Key).ToArray());\r\n        Assert.That(actual.Select(x => x.Value), Is.EqualTo(expected.Select(x => x.Value)));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Generator/Lyrics/Romanisation/Ja/JaRomanisationGeneratorTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.Romanisation.Ja;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Generator.Lyrics.Romanisation.Ja;\r\n\r\npublic class JaRomanisationGeneratorTest : BaseRomanisationGeneratorTest<JaRomanisationGenerator, JaRomanisationGeneratorConfig>\r\n{\r\n    [TestCase(\"花火大会\", new[] { \"[0,start]\", \"[3,end]\" }, true)]\r\n    [TestCase(\"花火大会\", new[] { \"[0,start]\" }, true)]\r\n    [TestCase(\"花火大会\", new[] { \"[3,end]\" }, false)] // not able to generate the has no start time-tag.\r\n    [TestCase(\"花火大会\", new string[] { }, false)] // not able to make the romanisation if has no time-tag.\r\n    [TestCase(\"\", new string[] { }, false)] // not able to make the romanisation if lyric is empty.\r\n    [TestCase(\"   \", new string[] { }, false)]\r\n    [TestCase(null, new string[] { }, false)]\r\n    public void TestCanGenerate(string text, string[] timeTagStrings, bool canGenerate)\r\n    {\r\n        var config = GeneratorEmptyConfig();\r\n\r\n        var timeTags = TestCaseTagHelper.ParseTimeTags(timeTagStrings);\r\n        var lyric = new Lyric\r\n        {\r\n            Text = text,\r\n            TimeTags = timeTags,\r\n        };\r\n\r\n        CheckCanGenerate(lyric, canGenerate, config);\r\n    }\r\n\r\n    // the generated result is not perfect, but it's OK for now.\r\n    [TestCase(\"はなび\", new[] { \"[0,start]\" }, new[] { \"^hana bi\" })]\r\n    [TestCase(\"花火大会\", new[] { \"[0,start]\", \"[3,end]\" }, new[] { \"^hanabi taikai\", \"\" })]\r\n    [TestCase(\"花火大会\", new[] { \"[0,start]\", \"[2,start]\", \"[3,end]\" }, new[] { \"^hanabi\", \"taikai\", \"\" })]\r\n    [TestCase(\"枯れた世界に輝く\",\r\n        new[] { \"[0,start]\", \"[1,start]\", \"[2,start]\", \"[3,start]\", \"[4,start]\", \"[5,start]\", \"[6,start]\", \"[6,start]\", \"[6,start]\", \"[7,start]\", \"[7,end]\" },\r\n        new[] { \"^kare\", \"\", \"ta\", \"sekai\", \"\", \"ni\", \"kagayaku\", \"\", \"\", \"\", \"\" })]\r\n    public void TestGenerate(string text, string[] timeTagStrings, string[] expectedRomanisedSyllables)\r\n    {\r\n        var config = GeneratorEmptyConfig();\r\n\r\n        var timeTags = TestCaseTagHelper.ParseTimeTags(timeTagStrings);\r\n        var lyric = new Lyric\r\n        {\r\n            Text = text,\r\n            TimeTags = timeTags,\r\n        };\r\n\r\n        CheckGenerateResult(lyric, expectedRomanisedSyllables, config);\r\n    }\r\n\r\n    [TestCase(\"はなび\", new[] { \"[0,start]\" }, new[] { \"^HANA BI\" })]\r\n    [TestCase(\"花火大会\", new[] { \"[0,start]\", \"[2,start]\", \"[3,end]\" }, new[] { \"^HANABI\", \"TAIKAI\", \"\" })]\r\n    public void TestGenerateWithUppercase(string text, string[] timeTagStrings, string[] expectedRomanisedSyllables)\r\n    {\r\n        var config = GeneratorEmptyConfig(x => x.Uppercase.Value = true);\r\n\r\n        var timeTags = TestCaseTagHelper.ParseTimeTags(timeTagStrings);\r\n        var lyric = new Lyric\r\n        {\r\n            Text = text,\r\n            TimeTags = timeTags,\r\n        };\r\n\r\n        CheckGenerateResult(lyric, expectedRomanisedSyllables, config);\r\n    }\r\n\r\n    [TestCase(\"花\", new[] { \"[0,start]\", \"[0,end]\" }, new[] { \"[0]:hana\" }, new[] { \"^hana\", \"\" })]\r\n    [TestCase(\"花火\", new[] { \"[0,start]\", \"[1,end]\" }, new[] { \"[0]:hana\", \"[1]:bi\" }, new[] { \"^hana bi\", \"\" })]\r\n    [TestCase(\"花火\", new[] { \"[0,start]\", \"[1,start]\", \"[1,end]\" }, new[] { \"[0]:hana\", \"[1]:bi\" }, new[] { \"^hana\", \"bi\", \"\" })]\r\n    [TestCase(\"花火\", new[] { \"[0,start]\", \"[0,start]\", \"[1,start]\", \"[1,end]\" }, new[] { \"[0]:hana\", \"[1]:bi\" }, new[] { \"^hana\", \"\", \"bi\", \"\" })]\r\n    [TestCase(\"はなび\", new[] { \"[0,start]\", \"[1,start]\", \"[2,start]\", \"[2,end]\" }, new[] { \"[0]:hana\", \"[2]:bi\" }, new[] { \"^hana\", \"\", \"bi\", \"\" })]\r\n    public void TestConvertToRomanisationGenerateResult(string text, string[] timeTagStrings, string[] romanisationParams, string[] expectedResults)\r\n    {\r\n        var timeTags = TestCaseTagHelper.ParseTimeTags(timeTagStrings);\r\n        var results = parseRomanisationGenerateResults(romanisationParams);\r\n\r\n        var expected = RomanisationGenerateResultHelper.ParseRomanisationGenerateResults(timeTags, expectedResults);\r\n        var actual = JaRomanisationGenerator.Convert(timeTags, results);\r\n\r\n        AssertEqual(expected, actual);\r\n        return;\r\n\r\n        static JaRomanisationGenerator.RomanisationGeneratorParameter[] parseRomanisationGenerateResults(IEnumerable<string> strings)\r\n            => strings.Select(parseRomanisationGenerateResult).ToArray();\r\n\r\n        static JaRomanisationGenerator.RomanisationGeneratorParameter parseRomanisationGenerateResult(string str)\r\n        {\r\n            // because format is same as the ruby-tag testing format, so just use the ruby helper.\r\n            var tag = TestCaseTagHelper.ParseRubyTag(str);\r\n            return new JaRomanisationGenerator.RomanisationGeneratorParameter\r\n            {\r\n                StartIndex = tag.StartIndex,\r\n                EndIndex = tag.EndIndex,\r\n                RomanisedSyllable = tag.Text,\r\n            };\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Generator/Lyrics/Romanisation/RomanisationGenerateResultHelper.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.Romanisation;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Generator.Lyrics.Romanisation;\r\n\r\npublic class RomanisationGenerateResultHelper\r\n{\r\n    /// <summary>\r\n    /// Convert the string format into the <see cref=\"RomanisationGenerateResult\"/>.\r\n    /// </summary>\r\n    /// <example>\r\n    /// karaoke\r\n    /// ^karaoke\r\n    /// </example>\r\n    /// <param name=\"timeTag\">Origin time-tag</param>\r\n    /// <param name=\"str\">Generate result string format</param>\r\n    /// <returns><see cref=\"RomanisationGenerateResult\"/>Romanisation generate result.</returns>\r\n    public static KeyValuePair<TimeTag, RomanisationGenerateResult> ParseRomanisationGenerateResult(TimeTag timeTag, string str)\r\n    {\r\n        var result = new RomanisationGenerateResult\r\n        {\r\n            FirstSyllable = str.StartsWith('^'),\r\n            RomanisedSyllable = str.Replace(\"^\", \"\"),\r\n        };\r\n\r\n        return new KeyValuePair<TimeTag, RomanisationGenerateResult>(timeTag, result);\r\n    }\r\n\r\n    public static IReadOnlyDictionary<TimeTag, RomanisationGenerateResult> ParseRomanisationGenerateResults(IList<TimeTag> timeTags, IList<string> strings)\r\n    {\r\n        if (timeTags.Count != strings.Count)\r\n            throw new InvalidOperationException();\r\n\r\n        return parseRomanisationGenerateResults(timeTags, strings).ToDictionary(k => k.Key, v => v.Value);\r\n\r\n        static IEnumerable<KeyValuePair<TimeTag, RomanisationGenerateResult>> parseRomanisationGenerateResults(IList<TimeTag> timeTags, IList<string> strings)\r\n        {\r\n            for (int i = 0; i < timeTags.Count; i++)\r\n            {\r\n                yield return ParseRomanisationGenerateResult(timeTags[i], strings[i]);\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Generator/Lyrics/Romanisation/RomanisationGeneratorSelectorTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Globalization;\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.Romanisation;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Asserts;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Generator.Lyrics.Romanisation;\r\n\r\npublic class RomanisationGeneratorSelectorTest : BaseLyricGeneratorSelectorTest<RomanisationGeneratorSelector, IReadOnlyDictionary<TimeTag, RomanisationGenerateResult>>\r\n{\r\n    [TestCase(17, \"花火大会\", true)]\r\n    [TestCase(17, \"我是中文\", true)] // only change the language code to decide should be able to generate or not.\r\n    [TestCase(17, \"\", false)] // will not able to make the romanisation if lyric is empty.\r\n    [TestCase(17, \"   \", false)]\r\n    [TestCase(17, null, false)]\r\n    [TestCase(1028, \"はなび\", false)] // Should not be able to generate if language is not supported.\r\n    public void TestCanGenerate(int lcid, string text, bool canGenerate)\r\n    {\r\n        var selector = CreateSelector();\r\n        var lyric = new Lyric\r\n        {\r\n            Language = new CultureInfo(lcid),\r\n            Text = text,\r\n            TimeTags = new[]\r\n            {\r\n                new TimeTag(new TextIndex()),\r\n            },\r\n        };\r\n\r\n        CheckCanGenerate(lyric, canGenerate, selector);\r\n    }\r\n\r\n    [TestCase(17, \"はなび\", new[] { \"[0,start]\" }, new[] { \"^hana bi\" })] // Japanese\r\n    [TestCase(1041, \"花火大会\", new[] { \"[0,start]\", \"[3,end]\" }, new[] { \"^hanabi taikai\", \"\" })] // Japanese\r\n    public void TestGenerate(int lcid, string text, string[] timeTagStrings, string[] expectedRomanisedSyllables)\r\n    {\r\n        var selector = CreateSelector();\r\n\r\n        var timeTags = TestCaseTagHelper.ParseTimeTags(timeTagStrings);\r\n        var lyric = new Lyric\r\n        {\r\n            Language = new CultureInfo(lcid),\r\n            Text = text,\r\n            TimeTags = timeTags,\r\n        };\r\n\r\n        var expected = RomanisationGenerateResultHelper.ParseRomanisationGenerateResults(timeTags, expectedRomanisedSyllables);\r\n        CheckGenerateResult(lyric, expected, selector);\r\n    }\r\n\r\n    protected override void AssertEqual(IReadOnlyDictionary<TimeTag, RomanisationGenerateResult> expected, IReadOnlyDictionary<TimeTag, RomanisationGenerateResult> actual)\r\n    {\r\n        TimeTagAssert.ArePropertyEqual(expected.Select(x => x.Key).ToArray(), actual.Select(x => x.Key).ToArray());\r\n        Assert.That(actual.Select(x => x.Value), Is.EqualTo(expected.Select(x => x.Value)));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Generator/Lyrics/RubyTags/BaseRubyTagGeneratorTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.RubyTags;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Asserts;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Generator.Lyrics.RubyTags;\r\n\r\npublic abstract class BaseRubyTagGeneratorTest<TRubyTagGenerator, TConfig> : BaseLyricGeneratorTest<TRubyTagGenerator, RubyTag[], TConfig>\r\n    where TRubyTagGenerator : RubyTagGenerator<TConfig> where TConfig : RubyTagGeneratorConfig, new()\r\n{\r\n    protected static void CheckCanGenerate(string text, bool canGenerate, TConfig config)\r\n    {\r\n        var lyric = new Lyric { Text = text };\r\n        CheckCanGenerate(lyric, canGenerate, config);\r\n    }\r\n\r\n    protected void CheckGenerateResult(string text, string[] expectedRubies, TConfig config)\r\n    {\r\n        var expected = TestCaseTagHelper.ParseRubyTags(expectedRubies);\r\n        var lyric = new Lyric { Text = text };\r\n        CheckGenerateResult(lyric, expected, config);\r\n    }\r\n\r\n    protected override void AssertEqual(RubyTag[] expected, RubyTag[] actual)\r\n    {\r\n        RubyTagAssert.ArePropertyEqual(expected, actual);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Generator/Lyrics/RubyTags/Ja/JaRubyTagGeneratorTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.RubyTags.Ja;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Generator.Lyrics.RubyTags.Ja;\r\n\r\n[TestFixture]\r\npublic class JaRubyTagGeneratorTest : BaseRubyTagGeneratorTest<JaRubyTagGenerator, JaRubyTagGeneratorConfig>\r\n{\r\n    [TestCase(\"花火大会\", true)]\r\n    [TestCase(\"\", false)] // will not able to generate the ruby if lyric is empty.\r\n    [TestCase(\"   \", false)]\r\n    [TestCase(null, false)]\r\n    public void TestCanGenerate(string text, bool canGenerate)\r\n    {\r\n        var config = GeneratorEmptyConfig();\r\n        CheckCanGenerate(text, canGenerate, config);\r\n    }\r\n\r\n    [TestCase(\"花火大会\", new[] { \"[0,1]:はなび\", \"[2,3]:たいかい\" })]\r\n    [TestCase(\"はなび\", new string[] { })]\r\n    public void TestGenerate(string text, string[] expectedRubies)\r\n    {\r\n        var config = GeneratorEmptyConfig();\r\n        CheckGenerateResult(text, expectedRubies, config);\r\n    }\r\n\r\n    [TestCase(\"花火大会\", new[] { \"[0,1]:ハナビ\", \"[2,3]:タイカイ\" })]\r\n    [TestCase(\"ハナビ\", new string[] { })]\r\n    public void TestGenerateWithRubyAsKatakana(string text, string[] expectedRubies)\r\n    {\r\n        var config = GeneratorEmptyConfig(x => x.RubyAsKatakana.Value = true);\r\n        CheckGenerateResult(text, expectedRubies, config);\r\n    }\r\n\r\n    [TestCase(\"はなび\", new[] { \"[0,1]:はな\", \"[2]:び\" })]\r\n    [TestCase(\"ハナビ\", new[] { \"[0,2]:はなび\" })]\r\n    public void TestGenerateWithEnableDuplicatedRuby(string text, string[] expectedRubies)\r\n    {\r\n        var config = GeneratorEmptyConfig(x => x.EnableDuplicatedRuby.Value = true);\r\n        CheckGenerateResult(text, expectedRubies, config);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Generator/Lyrics/RubyTags/RubyTagGeneratorSelectorTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Globalization;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.RubyTags;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Asserts;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Generator.Lyrics.RubyTags;\r\n\r\npublic class RubyTagGeneratorSelectorTest : BaseLyricGeneratorSelectorTest<RubyTagGeneratorSelector, RubyTag[]>\r\n{\r\n    [TestCase(17, \"花火大会\", true)]\r\n    [TestCase(17, \"我是中文\", true)] // only change the language code to decide should be able to generate or not.\r\n    [TestCase(17, \"\", false)] // will not able to generate the romanisation if lyric is empty.\r\n    [TestCase(17, \"   \", false)]\r\n    [TestCase(17, null, false)]\r\n    [TestCase(1028, \"はなび\", false)] // Should not be able to generate if language is not supported.\r\n    public void TestCanGenerate(int lcid, string text, bool canGenerate)\r\n    {\r\n        var selector = CreateSelector();\r\n        var lyric = new Lyric\r\n        {\r\n            Language = new CultureInfo(lcid),\r\n            Text = text,\r\n        };\r\n\r\n        CheckCanGenerate(lyric, canGenerate, selector);\r\n    }\r\n\r\n    [TestCase(17, \"花火大会\", new[] { \"[0,1]:はなび\", \"[2,3]:たいかい\" })] // Japanese\r\n    [TestCase(1041, \"はなび\", new string[] { })] // Japanese\r\n    public void TestGenerate(int lcid, string text, string[] expectedRubies)\r\n    {\r\n        var selector = CreateSelector();\r\n        var lyric = new Lyric\r\n        {\r\n            Language = new CultureInfo(lcid),\r\n            Text = text,\r\n        };\r\n\r\n        var expected = TestCaseTagHelper.ParseRubyTags(expectedRubies);\r\n        CheckGenerateResult(lyric, expected, selector);\r\n    }\r\n\r\n    protected override void AssertEqual(RubyTag[] expected, RubyTag[] actual)\r\n    {\r\n        RubyTagAssert.ArePropertyEqual(expected, actual);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Generator/Lyrics/TimeTags/BaseTimeTagGeneratorTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.TimeTags;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Asserts;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Generator.Lyrics.TimeTags;\r\n\r\npublic abstract class BaseTimeTagGeneratorTest<TTimeTagGenerator, TConfig> : BaseLyricGeneratorTest<TTimeTagGenerator, TimeTag[], TConfig>\r\n    where TTimeTagGenerator : TimeTagGenerator<TConfig> where TConfig : TimeTagGeneratorConfig, new()\r\n{\r\n    protected static void CheckCanGenerate(string text, bool canGenerate, TConfig config)\r\n    {\r\n        var lyric = new Lyric { Text = text };\r\n        CheckCanGenerate(lyric, canGenerate, config);\r\n    }\r\n\r\n    protected void CheckGenerateResult(string text, string[] expectedTimeTags, TConfig config)\r\n    {\r\n        var expected = TestCaseTagHelper.ParseTimeTags(expectedTimeTags);\r\n        var lyric = new Lyric { Text = text };\r\n        CheckGenerateResult(lyric, expected, config);\r\n    }\r\n\r\n    protected void CheckGenerateResult(Lyric lyric, string[] expectedTimeTags, TConfig config)\r\n    {\r\n        var expected = TestCaseTagHelper.ParseTimeTags(expectedTimeTags);\r\n        CheckGenerateResult(lyric, expected, config);\r\n    }\r\n\r\n    protected override void AssertEqual(TimeTag[] expected, TimeTag[] actual)\r\n    {\r\n        TimeTagAssert.ArePropertyEqual(expected, actual);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Generator/Lyrics/TimeTags/Ja/JaTimeTagGeneratorTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.TimeTags.Ja;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Generator.Lyrics.TimeTags.Ja;\r\n\r\n[TestFixture]\r\npublic class JaTimeTagGeneratorTest : BaseTimeTagGeneratorTest<JaTimeTagGenerator, JaTimeTagGeneratorConfig>\r\n{\r\n    [TestCase(\"花火大会\", true)]\r\n    [TestCase(\"！\", true)]\r\n    [TestCase(\"   \", true)]\r\n    [TestCase(\"\", false)] // will not able to generate the romanisation if lyric is empty.\r\n    [TestCase(null, false)]\r\n    public void TestCanGenerate(string text, bool canGenerate)\r\n    {\r\n        var config = GeneratorEmptyConfig();\r\n        CheckCanGenerate(text, canGenerate, config);\r\n    }\r\n\r\n    [TestCase(\"がんばって\", new[] { \"[0,start]\", \"[2,start]\", \"[4,start]\" }, false)]\r\n    [TestCase(\"がんばって\", new[] { \"[0,start]\", \"[1,start]\", \"[2,start]\", \"[4,start]\" }, true)]\r\n    public void TestGenerateWithCheckWhiteCheckん(string lyric, string[] expectedTimeTags, bool applyConfig)\r\n    {\r\n        var config = GeneratorEmptyConfig(x => x.Checkん.Value = applyConfig);\r\n        CheckGenerateResult(lyric, expectedTimeTags, config);\r\n    }\r\n\r\n    [TestCase(\"買って\", new[] { \"[0,start]\", \"[2,start]\" }, false)]\r\n    [TestCase(\"買って\", new[] { \"[0,start]\", \"[1,start]\", \"[2,start]\" }, true)]\r\n    public void TestGenerateWithCheckっ(string lyric, string[] expectedTimeTags, bool applyConfig)\r\n    {\r\n        var config = GeneratorEmptyConfig(x => x.Checkっ.Value = applyConfig);\r\n        CheckGenerateResult(lyric, expectedTimeTags, config);\r\n    }\r\n\r\n    [TestCase(\" \", new string[] { }, false)]\r\n    [TestCase(\" \", new[] { \"[0,start]\" }, true)]\r\n    public void TestGenerateWithCheckBlankLine(string lyric, string[] expectedTimeTags, bool applyConfig)\r\n    {\r\n        var config = GeneratorEmptyConfig(x => x.CheckBlankLine.Value = applyConfig);\r\n        CheckGenerateResult(lyric, expectedTimeTags, config);\r\n    }\r\n\r\n    [TestCase(\"か\", new[] { \"[0,start]\" }, false)]\r\n    [TestCase(\"か\", new[] { \"[0,start]\", \"[0,end]\" }, true)]\r\n    public void TestGenerateWithCheckLineEndKeyUp(string lyric, string[] expectedTimeTags, bool applyConfig)\r\n    {\r\n        var config = GeneratorEmptyConfig(x => x.CheckLineEndKeyUp.Value = applyConfig);\r\n        CheckGenerateResult(lyric, expectedTimeTags, config);\r\n    }\r\n\r\n    [TestCase(\"か     \", new[] { \"[0,start]\", \"[1,start]\", \"[2,start]\", \"[3,start]\", \"[4,start]\", \"[5,start]\" }, false)]\r\n    [TestCase(\"か     \", new[] { \"[0,start]\", \"[1,start]\" }, true)]\r\n    public void TestGenerateWithCheckWhiteSpace(string lyric, string[] expectedTimeTags, bool applyConfig)\r\n    {\r\n        var config = GeneratorEmptyConfig(x => x.CheckWhiteSpace.Value = applyConfig);\r\n        CheckGenerateResult(lyric, expectedTimeTags, config);\r\n    }\r\n\r\n    [TestCase(\"か \", new[] { \"[0,start]\", \"[1,start]\" }, false)]\r\n    [TestCase(\"か \", new[] { \"[0,start]\", \"[0,end]\" }, true)]\r\n    public void TestGenerateWithCheckWhiteSpaceKeyUp(string lyric, string[] expectedTimeTags, bool applyConfig)\r\n    {\r\n        var config = GeneratorEmptyConfig(x =>\r\n        {\r\n            x.CheckWhiteSpace.Value = true;\r\n            x.CheckWhiteSpaceKeyUp.Value = applyConfig;\r\n        });\r\n        CheckGenerateResult(lyric, expectedTimeTags, config);\r\n    }\r\n\r\n    [TestCase(\"a　b　c\", new[] { \"[0,start]\", \"[2,start]\", \"[4,start]\" }, false, false)]\r\n    [TestCase(\"a　b　c\", new[] { \"[0,start]\", \"[1,start]\", \"[2,start]\", \"[3,start]\", \"[4,start]\" }, true, false)]\r\n    [TestCase(\"a　b　c\", new[] { \"[0,start]\", \"[0,end]\", \"[2,start]\", \"[2,end]\", \"[4,start]\" }, true, true)]\r\n    [TestCase(\"Ａ　Ｂ　Ｃ\", new[] { \"[0,start]\", \"[2,start]\", \"[4,start]\" }, false, false)]\r\n    [TestCase(\"Ａ　Ｂ　Ｃ\", new[] { \"[0,start]\", \"[1,start]\", \"[2,start]\", \"[3,start]\", \"[4,start]\" }, true, false)]\r\n    [TestCase(\"Ａ　Ｂ　Ｃ\", new[] { \"[0,start]\", \"[0,end]\", \"[2,start]\", \"[2,end]\", \"[4,start]\" }, true, true)]\r\n    public void TestGenerateWithCheckWhiteSpaceAlphabet(string lyric, string[] expectedTimeTags, bool applyConfig, bool keyUp)\r\n    {\r\n        var config = GeneratorEmptyConfig(x =>\r\n        {\r\n            x.CheckWhiteSpace.Value = true;\r\n            x.CheckWhiteSpaceAlphabet.Value = applyConfig;\r\n            x.CheckWhiteSpaceKeyUp.Value = keyUp;\r\n        });\r\n        CheckGenerateResult(lyric, expectedTimeTags, config);\r\n    }\r\n\r\n    [TestCase(\"0　1　2\", new[] { \"[0,start]\", \"[2,start]\", \"[4,start]\" }, false, false)]\r\n    [TestCase(\"0　1　2\", new[] { \"[0,start]\", \"[1,start]\", \"[2,start]\", \"[3,start]\", \"[4,start]\" }, true, false)]\r\n    [TestCase(\"0　1　2\", new[] { \"[0,start]\", \"[0,end]\", \"[2,start]\", \"[2,end]\", \"[4,start]\" }, true, true)]\r\n    [TestCase(\"０　１　２\", new[] { \"[0,start]\", \"[2,start]\", \"[4,start]\" }, false, false)]\r\n    [TestCase(\"０　１　２\", new[] { \"[0,start]\", \"[1,start]\", \"[2,start]\", \"[3,start]\", \"[4,start]\" }, true, false)]\r\n    [TestCase(\"０　１　２\", new[] { \"[0,start]\", \"[0,end]\", \"[2,start]\", \"[2,end]\", \"[4,start]\" }, true, true)]\r\n    public void TestGenerateWithCheckWhiteSpaceDigit(string lyric, string[] expectedTimeTags, bool applyConfig, bool keyUp)\r\n    {\r\n        var config = GeneratorEmptyConfig(x =>\r\n        {\r\n            x.CheckWhiteSpace.Value = true;\r\n            x.CheckWhiteSpaceDigit.Value = applyConfig;\r\n            x.CheckWhiteSpaceKeyUp.Value = keyUp;\r\n        });\r\n        CheckGenerateResult(lyric, expectedTimeTags, config);\r\n    }\r\n\r\n    [TestCase(\"!　!　!\", new[] { \"[0,start]\", \"[2,start]\", \"[4,start]\" }, false, false)]\r\n    [TestCase(\"!　!　!\", new[] { \"[0,start]\", \"[1,start]\", \"[2,start]\", \"[3,start]\", \"[4,start]\" }, true, false)]\r\n    [TestCase(\"!　!　!\", new[] { \"[0,start]\", \"[0,end]\", \"[2,start]\", \"[2,end]\", \"[4,start]\" }, true, true)]\r\n    public void TestGenerateWitCheckWhiteSpaceAsciiSymbol(string lyric, string[] expectedTimeTags, bool applyConfig, bool keyUp)\r\n    {\r\n        var config = GeneratorEmptyConfig(x =>\r\n        {\r\n            x.CheckWhiteSpace.Value = true;\r\n            x.CheckWhiteSpaceAsciiSymbol.Value = applyConfig;\r\n            x.CheckWhiteSpaceKeyUp.Value = keyUp;\r\n        });\r\n        CheckGenerateResult(lyric, expectedTimeTags, config);\r\n    }\r\n\r\n    [Test]\r\n    public void TestGenerateWithRubyLyric()\r\n    {\r\n        var config = GeneratorEmptyConfig();\r\n        var lyric = new Lyric\r\n        {\r\n            Text = \"明日いっしょに遊びましょう！\",\r\n            RubyTags = new[]\r\n            {\r\n                new RubyTag\r\n                {\r\n                    StartIndex = 0,\r\n                    EndIndex = 1,\r\n                    Text = \"あした\",\r\n                },\r\n                new RubyTag\r\n                {\r\n                    StartIndex = 7,\r\n                    EndIndex = 7,\r\n                    Text = \"あそ\",\r\n                },\r\n            },\r\n        };\r\n\r\n        string[] expectedTimeTags =\r\n        {\r\n            \"[0,start]\",\r\n            \"[0,start]\",\r\n            \"[0,start]\",\r\n            \"[2,start]\",\r\n            \"[4,start]\",\r\n            \"[6,start]\",\r\n            \"[7,start]\",\r\n            \"[7,start]\",\r\n            \"[8,start]\",\r\n            \"[9,start]\",\r\n            \"[10,start]\",\r\n            \"[12,start]\",\r\n            \"[13,start]\",\r\n        };\r\n        CheckGenerateResult(lyric, expectedTimeTags, config);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Generator/Lyrics/TimeTags/TimeTagGeneratorSelectorTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Globalization;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.TimeTags;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Asserts;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Generator.Lyrics.TimeTags;\r\n\r\npublic class TimeTagGeneratorSelectorTest : BaseLyricGeneratorSelectorTest<TimeTagGeneratorSelector, TimeTag[]>\r\n{\r\n    [TestCase(17, \"花火大会\", true)]\r\n    [TestCase(1028, \"喵\", true)] // Support the chinese.\r\n    [TestCase(3081, \"hello\", false)] // English is not supported.\r\n    [TestCase(17, \"\", false)] // will not able to generate the romanisation if lyric is empty.\r\n    [TestCase(17, \"   \", false)]\r\n    [TestCase(17, null, false)]\r\n    public void TestCanGenerate(int lcid, string text, bool canGenerate)\r\n    {\r\n        var selector = CreateSelector();\r\n        var lyric = new Lyric\r\n        {\r\n            Language = new CultureInfo(lcid),\r\n            Text = text,\r\n        };\r\n\r\n        CheckCanGenerate(lyric, canGenerate, selector);\r\n    }\r\n\r\n    [TestCase(17, \"か\", new[] { \"[0,start]\", \"[0,end]\" })] // Japanese\r\n    [TestCase(1041, \"か\", new[] { \"[0,start]\", \"[0,end]\" })] // Japanese\r\n    [TestCase(1028, \"喵\", new[] { \"[0,start]\" })] // Chinese\r\n    public void TestGenerate(int lcid, string text, string[] expectedTimeTags)\r\n    {\r\n        var selector = CreateSelector();\r\n        var lyric = new Lyric\r\n        {\r\n            Language = new CultureInfo(lcid),\r\n            Text = text,\r\n        };\r\n\r\n        var expected = TestCaseTagHelper.ParseTimeTags(expectedTimeTags);\r\n        CheckGenerateResult(lyric, expected, selector);\r\n    }\r\n\r\n    protected override void AssertEqual(TimeTag[] expected, TimeTag[] actual)\r\n    {\r\n        TimeTagAssert.ArePropertyEqual(expected, actual);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Generator/Lyrics/TimeTags/Zh/ZhTimeTagGeneratorTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Lyrics.TimeTags.Zh;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Generator.Lyrics.TimeTags.Zh;\r\n\r\n[TestFixture]\r\npublic class ZhTimeTagGeneratorTest : BaseTimeTagGeneratorTest<ZhTimeTagGenerator, ZhTimeTagGeneratorConfig>\r\n{\r\n    [TestCase(\"拉拉拉~~~\", true)]\r\n    [TestCase(\"~~~\", true)]\r\n    [TestCase(\"   \", true)]\r\n    [TestCase(\"\", false)] // will not able to generate the romanisation if lyric is empty.\r\n    [TestCase(null, false)]\r\n    public void TestCanGenerate(string text, bool canGenerate)\r\n    {\r\n        var config = GeneratorEmptyConfig();\r\n        CheckCanGenerate(text, canGenerate, config);\r\n    }\r\n\r\n    [TestCase(\"測試一些歌詞\", new[] { \"[0,start]\", \"[1,start]\", \"[2,start]\", \"[3,start]\", \"[4,start]\", \"[5,start]\", \"[5,end]\" })]\r\n    [TestCase(\"拉拉拉~~~\", new[] { \"[0,start]\", \"[1,start]\", \"[2,start]\", \"[5,end]\" })]\r\n    [TestCase(\"拉~拉~拉~\", new[] { \"[0,start]\", \"[2,start]\", \"[4,start]\", \"[5,end]\" })]\r\n    public void TestGenerateWithCheckLineEndKeyUp(string lyric, string[] expectedTimeTags)\r\n    {\r\n        var config = GeneratorEmptyConfig(x => x.CheckLineEndKeyUp.Value = true);\r\n        CheckGenerateResult(lyric, expectedTimeTags, config);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Generator/Stages/BaseStageElementCategoryGeneratorTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Stages;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Generator.Stages;\r\n\r\npublic abstract class BaseLyricStageElementCategoryGeneratorTest<TGenerator, TObject, TStageElement, TConfig>\r\n    : BaseStageElementCategoryGeneratorTest<TGenerator, TObject, TStageElement, Lyric, TConfig>\r\n    where TGenerator : StageInfoPropertyGenerator<TObject, TConfig>\r\n    where TObject : StageElementCategory<TStageElement, Lyric>\r\n    where TStageElement : StageElement, new()\r\n    where TConfig : GeneratorConfig, new();\r\n\r\npublic abstract class BaseStageElementCategoryGeneratorTest<TGenerator, TObject, TStageElement, THitObject, TConfig>\r\n    : BaseStageInfoPropertyGeneratorTest<TGenerator, TObject, TConfig>\r\n    where TGenerator : StageInfoPropertyGenerator<TObject, TConfig>\r\n    where TObject : StageElementCategory<TStageElement, THitObject>\r\n    where TStageElement : StageElement, new()\r\n    where THitObject : KaraokeHitObject, IHasPrimaryKey\r\n    where TConfig : GeneratorConfig, new()\r\n{\r\n    protected sealed override void AssertEqual(TObject expected, TObject actual)\r\n    {\r\n        for (int i = 0; i < expected.AvailableElements.Count; i++)\r\n        {\r\n            var expectedElement = expected.AvailableElements[i];\r\n            var actualElement = actual.AvailableElements[i];\r\n\r\n            AssertEqual(expectedElement, actualElement);\r\n\r\n            var expectedHitObjectIds = expected.GetHitObjectIdsByElement(expectedElement);\r\n            var actualHitObjectIds = actual.GetHitObjectIdsByElement(actualElement);\r\n            Assert.That(actualHitObjectIds, Is.EqualTo(expectedHitObjectIds));\r\n        }\r\n    }\r\n\r\n    protected abstract void AssertEqual(TStageElement expected, TStageElement actual);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Generator/Stages/BaseStageInfoGeneratorTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Stages;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Generator.Stages;\r\n\r\npublic abstract class BaseStageInfoGeneratorTest<TGenerator, TStageInfo, TConfig>\r\n    : BasePropertyGeneratorTest<TGenerator, KaraokeBeatmap, StageInfo, TConfig>\r\n    where TStageInfo : StageInfo\r\n    where TGenerator : StageInfoGenerator<TConfig>\r\n    where TConfig : StageInfoGeneratorConfig, new()\r\n{\r\n    protected sealed override void AssertEqual(StageInfo expected, StageInfo actual)\r\n    {\r\n        if (expected is not TStageInfo expectedStageInfo)\r\n            throw new ArgumentException($\"Expected type is not {typeof(TStageInfo).Name}\", nameof(expected));\r\n\r\n        if (actual is not TStageInfo actualStageInfo)\r\n            throw new ArgumentException($\"Actual type is not {typeof(TStageInfo).Name}\", nameof(actual));\r\n\r\n        AssertEqual(expectedStageInfo, actualStageInfo);\r\n    }\r\n\r\n    protected abstract void AssertEqual(TStageInfo expected, TStageInfo actual);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Generator/Stages/BaseStageInfoPropertyGeneratorTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Stages;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Generator.Stages;\r\n\r\npublic abstract class BaseStageInfoPropertyGeneratorTest<TGenerator, TObject, TConfig>\r\n    : BasePropertyGeneratorTest<TGenerator, KaraokeBeatmap, TObject, TConfig>\r\n    where TGenerator : StageInfoPropertyGenerator<TObject, TConfig>\r\n    where TConfig : GeneratorConfig, new();\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Generator/Stages/Classic/ClassicLyricLayoutCategoryGeneratorTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Stages.Classic;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos.Classic;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Generator.Stages.Classic;\r\n\r\npublic class ClassicLyricLayoutCategoryGeneratorTest\r\n    : BaseLyricStageElementCategoryGeneratorTest<ClassicLyricLayoutCategoryGenerator, ClassicLyricLayoutCategory, ClassicLyricLayout, ClassicLyricLayoutCategoryGeneratorConfig>\r\n{\r\n    [Test]\r\n    public void TestCanGenerate()\r\n    {\r\n        var config = GeneratorDefaultConfig();\r\n        var beatmap = new KaraokeBeatmap\r\n        {\r\n            HitObjects = new List<KaraokeHitObject>\r\n            {\r\n                TestCaseTagHelper.ParseLyric(\"[1000,3000]:lyric1\"),\r\n                TestCaseTagHelper.ParseLyric(\"[4000,6000]:lyric2\"),\r\n                TestCaseTagHelper.ParseLyric(\"[7000,9000]:lyric3\"),\r\n                TestCaseTagHelper.ParseLyric(\"[10000,12000]:lyric4\"),\r\n            },\r\n        };\r\n\r\n        CheckCanGenerate(beatmap, true, config);\r\n    }\r\n\r\n    [Test]\r\n    public void TestCanGenerateWithNonLyricBeatmap()\r\n    {\r\n        var config = GeneratorDefaultConfig();\r\n        var beatmap = new KaraokeBeatmap();\r\n        CheckCanGenerate(beatmap, false, config);\r\n    }\r\n\r\n    [Test]\r\n    public void TestGenerate()\r\n    {\r\n        var config = GeneratorDefaultConfig();\r\n\r\n        var lyric1 = TestCaseTagHelper.ParseLyric(\"[1000,3000]:lyric1\", 1);\r\n        var lyric2 = TestCaseTagHelper.ParseLyric(\"[4000,6000]:lyric2\", 2);\r\n        var lyric3 = TestCaseTagHelper.ParseLyric(\"[7000,9000]:lyric3\", 3);\r\n        var lyric4 = TestCaseTagHelper.ParseLyric(\"[10000,12000]:lyric4\", 4);\r\n        var beatmap = new KaraokeBeatmap\r\n        {\r\n            HitObjects = new List<KaraokeHitObject>\r\n            {\r\n                lyric1,\r\n                lyric2,\r\n                lyric3,\r\n                lyric4,\r\n            },\r\n        };\r\n\r\n        var expected = new ClassicLyricLayoutCategory();\r\n        var layout1 = expected.AddElement(x => x.Alignment = ClassicLyricLayoutAlignment.Left);\r\n        var layout2 = expected.AddElement(x => x.Alignment = ClassicLyricLayoutAlignment.Right);\r\n        expected.AddToMapping(layout1, lyric1);\r\n        expected.AddToMapping(layout2, lyric2);\r\n        expected.AddToMapping(layout1, lyric3);\r\n        expected.AddToMapping(layout2, lyric4);\r\n\r\n        CheckGenerateResult(beatmap, expected, config);\r\n    }\r\n\r\n    [Test]\r\n    public void TestGenerateWithThreeLyrics()\r\n    {\r\n        var config = GeneratorDefaultConfig(x => x.LyricRowAmount.Value = 3);\r\n\r\n        var lyric1 = TestCaseTagHelper.ParseLyric(\"[1000,3000]:lyric1\", 1);\r\n        var lyric2 = TestCaseTagHelper.ParseLyric(\"[4000,6000]:lyric2\", 2);\r\n        var lyric3 = TestCaseTagHelper.ParseLyric(\"[7000,9000]:lyric3\", 3);\r\n        var lyric4 = TestCaseTagHelper.ParseLyric(\"[10000,12000]:lyric4\", 4);\r\n        var lyric5 = TestCaseTagHelper.ParseLyric(\"[10000,12000]:lyric5\", 5);\r\n        var beatmap = new KaraokeBeatmap\r\n        {\r\n            HitObjects = new List<KaraokeHitObject>\r\n            {\r\n                lyric1,\r\n                lyric2,\r\n                lyric3,\r\n                lyric4,\r\n                lyric5,\r\n            },\r\n        };\r\n\r\n        var expected = new ClassicLyricLayoutCategory();\r\n        var layout1 = expected.AddElement(x => x.Alignment = ClassicLyricLayoutAlignment.Left);\r\n        var layout2 = expected.AddElement(x => x.Alignment = ClassicLyricLayoutAlignment.Center);\r\n        var layout3 = expected.AddElement(x => x.Alignment = ClassicLyricLayoutAlignment.Right);\r\n        expected.AddToMapping(layout1, lyric1);\r\n        expected.AddToMapping(layout2, lyric2);\r\n        expected.AddToMapping(layout3, lyric3);\r\n        expected.AddToMapping(layout1, lyric4);\r\n        expected.AddToMapping(layout2, lyric5);\r\n\r\n        CheckGenerateResult(beatmap, expected, config);\r\n    }\r\n\r\n    [Test]\r\n    public void TestGenerateWithNotMapping()\r\n    {\r\n        var config = GeneratorDefaultConfig(x => x.ApplyMappingToTheLyric.Value = false);\r\n\r\n        var lyric1 = TestCaseTagHelper.ParseLyric(\"[1000,3000]:lyric1\", 1);\r\n        var lyric2 = TestCaseTagHelper.ParseLyric(\"[4000,6000]:lyric2\", 2);\r\n        var lyric3 = TestCaseTagHelper.ParseLyric(\"[7000,9000]:lyric3\", 3);\r\n        var lyric4 = TestCaseTagHelper.ParseLyric(\"[10000,12000]:lyric4\", 4);\r\n        var beatmap = new KaraokeBeatmap\r\n        {\r\n            HitObjects = new List<KaraokeHitObject>\r\n            {\r\n                lyric1,\r\n                lyric2,\r\n                lyric3,\r\n                lyric4,\r\n            },\r\n        };\r\n\r\n        var expected = new ClassicLyricLayoutCategory();\r\n        expected.AddElement(x => x.Alignment = ClassicLyricLayoutAlignment.Left);\r\n        expected.AddElement(x => x.Alignment = ClassicLyricLayoutAlignment.Right);\r\n\r\n        CheckGenerateResult(beatmap, expected, config);\r\n    }\r\n\r\n    protected override void AssertEqual(ClassicLyricLayout expected, ClassicLyricLayout actual)\r\n    {\r\n        Assert.That(actual.Alignment, Is.EqualTo(expected.Alignment));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Generator/Stages/Classic/ClassicLyricTimingInfoGeneratorTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Stages.Classic;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos.Classic;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Generator.Stages.Classic;\r\n\r\npublic class ClassicLyricTimingInfoGeneratorTest\r\n    : BaseStageInfoPropertyGeneratorTest<ClassicLyricTimingInfoGenerator, ClassicLyricTimingInfo, ClassicLyricTimingInfoGeneratorConfig>\r\n{\r\n    [Test]\r\n    public void TestCanGenerate()\r\n    {\r\n        var config = GeneratorDefaultConfig();\r\n        var beatmap = new KaraokeBeatmap\r\n        {\r\n            HitObjects = new List<KaraokeHitObject>\r\n            {\r\n                TestCaseTagHelper.ParseLyric(\"[1000,3000]:lyric1\"),\r\n                TestCaseTagHelper.ParseLyric(\"[4000,6000]:lyric2\"),\r\n                TestCaseTagHelper.ParseLyric(\"[7000,9000]:lyric3\"),\r\n                TestCaseTagHelper.ParseLyric(\"[10000,12000]:lyric4\"),\r\n            },\r\n        };\r\n\r\n        CheckCanGenerate(beatmap, true, config);\r\n    }\r\n\r\n    [Test]\r\n    public void TestCanGenerateWithNonLyricBeatmap()\r\n    {\r\n        var config = GeneratorDefaultConfig();\r\n        var beatmap = new KaraokeBeatmap();\r\n        CheckCanGenerate(beatmap, false, config);\r\n    }\r\n\r\n    [Test]\r\n    public void TestGenerate()\r\n    {\r\n        var config = GeneratorDefaultConfig();\r\n\r\n        var lyric1 = TestCaseTagHelper.ParseLyric(\"[1000,3000]:lyric1\", 1);\r\n        var lyric2 = TestCaseTagHelper.ParseLyric(\"[4000,6000]:lyric2\", 2);\r\n        var lyric3 = TestCaseTagHelper.ParseLyric(\"[7000,9000]:lyric3\", 3);\r\n        var lyric4 = TestCaseTagHelper.ParseLyric(\"[10000,12000]:lyric4\", 4);\r\n        var beatmap = new KaraokeBeatmap\r\n        {\r\n            HitObjects = new List<KaraokeHitObject>\r\n            {\r\n                lyric1,\r\n                lyric2,\r\n                lyric3,\r\n                lyric4,\r\n            },\r\n        };\r\n\r\n        var expected = new ClassicLyricTimingInfo();\r\n        var timing1 = expected.AddTimingPoint(x => x.Time = 0); // should show the lyric at screen when loaded.\r\n        expected.AddToMapping(timing1, lyric1); // show\r\n        expected.AddToMapping(timing1, lyric2); // show\r\n\r\n        var timing2 = expected.AddTimingPoint(x => x.Time = 3000); // it's time to hide lyric1 and show lyric3.\r\n        expected.AddToMapping(timing2, lyric1); // hide\r\n        expected.AddToMapping(timing2, lyric3); // show\r\n\r\n        var timing3 = expected.AddTimingPoint(x => x.Time = 6000); // it's time to hide lyric2 and show lyric4.\r\n        expected.AddToMapping(timing3, lyric2); // hide\r\n        expected.AddToMapping(timing3, lyric4); // show\r\n\r\n        var timing4 = expected.AddTimingPoint(x => x.Time = 12000); // it's time to hide lyric3 and lyric4.\r\n        expected.AddToMapping(timing4, lyric3); // hide\r\n        expected.AddToMapping(timing4, lyric4); // hide\r\n\r\n        CheckGenerateResult(beatmap, expected, config);\r\n    }\r\n\r\n    [Test]\r\n    public void TestGenerateWithThreeLyrics()\r\n    {\r\n        var config = GeneratorDefaultConfig(x => x.LyricRowAmount.Value = 3);\r\n\r\n        var lyric1 = TestCaseTagHelper.ParseLyric(\"[1000,3000]:lyric1\", 1);\r\n        var lyric2 = TestCaseTagHelper.ParseLyric(\"[4000,6000]:lyric2\", 2);\r\n        var lyric3 = TestCaseTagHelper.ParseLyric(\"[7000,9000]:lyric3\", 3);\r\n        var lyric4 = TestCaseTagHelper.ParseLyric(\"[10000,12000]:lyric4\", 4);\r\n        var lyric5 = TestCaseTagHelper.ParseLyric(\"[10000,12000]:lyric5\", 5);\r\n        var beatmap = new KaraokeBeatmap\r\n        {\r\n            HitObjects = new List<KaraokeHitObject>\r\n            {\r\n                lyric1,\r\n                lyric2,\r\n                lyric3,\r\n                lyric4,\r\n                lyric5,\r\n            },\r\n        };\r\n\r\n        var expected = new ClassicLyricTimingInfo();\r\n        var timing1 = expected.AddTimingPoint(x => x.Time = 0);\r\n        expected.AddToMapping(timing1, lyric1); // show\r\n        expected.AddToMapping(timing1, lyric2); // show\r\n        expected.AddToMapping(timing1, lyric3); // show\r\n\r\n        var timing2 = expected.AddTimingPoint(x => x.Time = 3000);\r\n        expected.AddToMapping(timing2, lyric1); // hide\r\n        expected.AddToMapping(timing2, lyric4); // show\r\n\r\n        var timing3 = expected.AddTimingPoint(x => x.Time = 6000);\r\n        expected.AddToMapping(timing3, lyric2); // hide\r\n        expected.AddToMapping(timing3, lyric5); // show\r\n\r\n        var timing4 = expected.AddTimingPoint(x => x.Time = 12000);\r\n        expected.AddToMapping(timing4, lyric3); // hide\r\n        expected.AddToMapping(timing4, lyric4); // hide\r\n        expected.AddToMapping(timing4, lyric5); // hide\r\n\r\n        CheckGenerateResult(beatmap, expected, config);\r\n    }\r\n\r\n    protected override void AssertEqual(ClassicLyricTimingInfo expected, ClassicLyricTimingInfo actual)\r\n    {\r\n        Assert.That(actual.Timings.Select(x => x.Time), Is.EqualTo(expected.Timings.Select(x => x.Time)));\r\n\r\n        // because we cannot check the id in the mapping value, so just check the key.\r\n        Assert.That(actual.Mappings.Keys, Is.EqualTo(expected.Mappings.Keys));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Generator/Stages/Classic/ClassicStageInfoGeneratorTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Stages.Classic;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos.Classic;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Generator.Stages.Classic;\r\n\r\npublic class ClassicStageInfoGeneratorTest : BaseStageInfoGeneratorTest<ClassicStageInfoGenerator, ClassicStageInfo, ClassicStageInfoGeneratorConfig>\r\n{\r\n    [Test]\r\n    public void TestCanGenerate()\r\n    {\r\n        var config = GeneratorDefaultConfig();\r\n        var beatmap = new KaraokeBeatmap\r\n        {\r\n            HitObjects = new List<KaraokeHitObject>\r\n            {\r\n                TestCaseTagHelper.ParseLyric(\"[1000,3000]:lyric1\"),\r\n                TestCaseTagHelper.ParseLyric(\"[4000,6000]:lyric2\"),\r\n                TestCaseTagHelper.ParseLyric(\"[7000,9000]:lyric3\"),\r\n                TestCaseTagHelper.ParseLyric(\"[10000,12000]:lyric4\"),\r\n            },\r\n        };\r\n\r\n        CheckCanGenerate(beatmap, true, config);\r\n    }\r\n\r\n    [Test]\r\n    public void TestCanGenerateWithNonLyricBeatmap()\r\n    {\r\n        var config = GeneratorDefaultConfig();\r\n        var beatmap = new KaraokeBeatmap();\r\n        CheckCanGenerate(beatmap, false, config);\r\n    }\r\n\r\n    [Test]\r\n    public void TestGenerate()\r\n    {\r\n        var config = GeneratorDefaultConfig();\r\n\r\n        var lyric1 = TestCaseTagHelper.ParseLyric(\"[1000,3000]:lyric1\", 1);\r\n        var lyric2 = TestCaseTagHelper.ParseLyric(\"[4000,6000]:lyric2\", 2);\r\n        var lyric3 = TestCaseTagHelper.ParseLyric(\"[7000,9000]:lyric3\", 3);\r\n        var lyric4 = TestCaseTagHelper.ParseLyric(\"[10000,12000]:lyric4\", 4);\r\n        var beatmap = new KaraokeBeatmap\r\n        {\r\n            HitObjects = new List<KaraokeHitObject>\r\n            {\r\n                lyric1,\r\n                lyric2,\r\n                lyric3,\r\n                lyric4,\r\n            },\r\n        };\r\n\r\n        // Note: we did not care about the generator result here.\r\n        var expected = new ClassicStageInfo();\r\n        CheckGenerateResult(beatmap, expected, config);\r\n    }\r\n\r\n    protected override void AssertEqual(ClassicStageInfo expected, ClassicStageInfo actual)\r\n    {\r\n        // as we already test the property in the other generator, there's no need to compare it again?\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Generator/Stages/Preview/PreviewStageInfoGeneratorTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Stages.Preview;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos.Preview;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Generator.Stages.Preview;\r\n\r\npublic class PreviewStageInfoGeneratorTest : BaseStageInfoGeneratorTest<PreviewStageInfoGenerator, PreviewStageInfo, PreviewStageInfoGeneratorConfig>\r\n{\r\n    [Test]\r\n    public void TestCanGenerate()\r\n    {\r\n        var config = GeneratorDefaultConfig();\r\n        var beatmap = new KaraokeBeatmap\r\n        {\r\n            HitObjects = new List<KaraokeHitObject>\r\n            {\r\n                TestCaseTagHelper.ParseLyric(\"[1000,3000]:lyric1\"),\r\n                TestCaseTagHelper.ParseLyric(\"[4000,6000]:lyric2\"),\r\n                TestCaseTagHelper.ParseLyric(\"[7000,9000]:lyric3\"),\r\n                TestCaseTagHelper.ParseLyric(\"[10000,12000]:lyric4\"),\r\n            },\r\n        };\r\n\r\n        CheckCanGenerate(beatmap, true, config);\r\n    }\r\n\r\n    [Test]\r\n    public void TestCanGenerateWithNonLyricBeatmap()\r\n    {\r\n        var config = GeneratorDefaultConfig();\r\n        var beatmap = new KaraokeBeatmap();\r\n\r\n        // we did not care about is there any lyric in beatmap.\r\n        CheckCanGenerate(beatmap, true, config);\r\n    }\r\n\r\n    [Test]\r\n    public void TestGenerate()\r\n    {\r\n        var config = GeneratorDefaultConfig();\r\n\r\n        var lyric1 = TestCaseTagHelper.ParseLyric(\"[1000,3000]:lyric1\", 1);\r\n        var lyric2 = TestCaseTagHelper.ParseLyric(\"[4000,6000]:lyric2\", 2);\r\n        var lyric3 = TestCaseTagHelper.ParseLyric(\"[7000,9000]:lyric3\", 3);\r\n        var lyric4 = TestCaseTagHelper.ParseLyric(\"[10000,12000]:lyric4\", 4);\r\n        var beatmap = new KaraokeBeatmap\r\n        {\r\n            HitObjects = new List<KaraokeHitObject>\r\n            {\r\n                lyric1,\r\n                lyric2,\r\n                lyric3,\r\n                lyric4,\r\n            },\r\n        };\r\n\r\n        // Note: we did not care about the generator result here.\r\n        var expected = new PreviewStageInfo();\r\n        CheckGenerateResult(beatmap, expected, config);\r\n    }\r\n\r\n    protected override void AssertEqual(PreviewStageInfo expected, PreviewStageInfo actual)\r\n    {\r\n        // as we already test the property in the other generator, there's no need to compare it again?\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Generator/Stages/StageInfoGeneratorSelectorTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Generator.Stages;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos.Classic;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos.Preview;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Generator.Stages;\r\n\r\n[TestFixture(typeof(ClassicStageInfo))]\r\n[TestFixture(typeof(PreviewStageInfo))]\r\npublic class StageInfoGeneratorSelectorTest<TStageInfo> : BaseGeneratorSelectorTest<StageInfoGeneratorSelector<TStageInfo>, KaraokeBeatmap, StageInfo>\r\n    where TStageInfo : StageInfo, new()\r\n{\r\n    [Test]\r\n    public void TestCanGenerate()\r\n    {\r\n        var selector = CreateSelector();\r\n        var beatmap = createBeatmap();\r\n\r\n        CheckCanGenerate(beatmap, true, selector);\r\n    }\r\n\r\n    [Test]\r\n    public void TestGenerate()\r\n    {\r\n        var selector = CreateSelector();\r\n        var beatmap = createBeatmap();\r\n\r\n        var expected = new TStageInfo();\r\n        CheckGenerateResult(beatmap, expected, selector);\r\n    }\r\n\r\n    protected override void AssertEqual(StageInfo expected, StageInfo actual)\r\n    {\r\n        // There's no need to check the content in the stage info.\r\n        // Just make sure that the type in the test case is supported.\r\n        Assert.That(actual.GetType(), Is.EqualTo(expected.GetType()));\r\n    }\r\n\r\n    private KaraokeBeatmap createBeatmap()\r\n    {\r\n        return new KaraokeBeatmap\r\n        {\r\n            HitObjects = new List<KaraokeHitObject>\r\n            {\r\n                TestCaseTagHelper.ParseLyric(\"[1000,3000]:lyric1\"),\r\n                TestCaseTagHelper.ParseLyric(\"[4000,6000]:lyric2\"),\r\n                TestCaseTagHelper.ParseLyric(\"[7000,9000]:lyric3\"),\r\n                TestCaseTagHelper.ParseLyric(\"[10000,12000]:lyric4\"),\r\n            },\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/TestSceneEditor.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Beatmaps;\r\nusing osu.Game.Tests.Visual;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor;\r\n\r\n[TestFixture]\r\npublic partial class TestSceneEditor : EditorTestScene\r\n{\r\n    protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestKaraokeBeatmap(ruleset);\r\n\r\n    protected override Ruleset CreateEditorRuleset() => new KaraokeRuleset();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/TestSceneSetupScreen.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Edit;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Screens.Edit;\r\nusing osu.Game.Screens.Edit.Setup;\r\nusing osu.Game.Tests.Visual;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor;\r\n\r\n[TestFixture]\r\npublic partial class TestSceneSetupScreen : EditorClockTestScene\r\n{\r\n    [Cached(typeof(EditorBeatmap))]\r\n    [Cached(typeof(IBeatSnapProvider))]\r\n    private readonly EditorBeatmap editorBeatmap = new(new KaraokeBeatmap\r\n    {\r\n        BeatmapInfo =\r\n        {\r\n            Ruleset = new KaraokeRuleset().RulesetInfo,\r\n        },\r\n    });\r\n\r\n    [Cached]\r\n    private readonly OverlayColourProvider colourProvider = new(OverlayColourScheme.Blue);\r\n\r\n    [Test]\r\n    public void TestKaraoke() => runForRuleset(new KaraokeRuleset().RulesetInfo);\r\n\r\n    private void runForRuleset(RulesetInfo rulesetInfo)\r\n    {\r\n        AddStep(\"create screen\", () =>\r\n        {\r\n            editorBeatmap.BeatmapInfo.Ruleset = rulesetInfo;\r\n\r\n            Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap);\r\n\r\n            Child = new SetupScreen\r\n            {\r\n                State = { Value = Visibility.Visible },\r\n            };\r\n        });\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/TestSceneTimeTagTooltip.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Components.Cursor;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Tests.Visual;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor;\r\n\r\n[TestFixture]\r\npublic partial class TestSceneTimeTagTooltip : OsuTestScene\r\n{\r\n    private TimeTagTooltip toolTip = null!;\r\n\r\n    [SetUp]\r\n    public void SetUp() => Schedule(() =>\r\n    {\r\n        Child = toolTip = new TimeTagTooltip\r\n        {\r\n            Anchor = Anchor.Centre,\r\n            Origin = Anchor.Centre,\r\n        };\r\n        toolTip.Show();\r\n    });\r\n\r\n    [Test]\r\n    public void TestDisplayToolTip()\r\n    {\r\n        setTooltip(\"Start time tag.\", new TimeTag(new TextIndex(0), 1280));\r\n        setTooltip(\"End time tag.\", new TimeTag(new TextIndex(0, TextIndex.IndexState.End), 1280));\r\n        setTooltip(\"Null time\", new TimeTag(new TextIndex(0)));\r\n    }\r\n\r\n    private void setTooltip(string testName, TimeTag timeTag)\r\n    {\r\n        AddStep(testName, () =>\r\n        {\r\n            toolTip.SetContent(timeTag);\r\n        });\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Utils/HitObjectWritableUtilsTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing Newtonsoft.Json;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Properties;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Types;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Utils;\r\n\r\npublic class HitObjectWritableUtilsTest\r\n{\r\n    #region Remove lyrics.\r\n\r\n    [Test]\r\n    public void TestIsRemoveLyricLocked()\r\n    {\r\n        // standard.\r\n        test(new Lyric());\r\n\r\n        // test lock state.\r\n        foreach (var lockState in Enum.GetValues<LockState>())\r\n        {\r\n            test(new Lyric\r\n            {\r\n                Lock = lockState,\r\n            });\r\n        }\r\n\r\n        // reference lyric.\r\n        test(new Lyric\r\n        {\r\n            ReferenceLyricConfig = new ReferenceLyricConfig(),\r\n        });\r\n\r\n        test(new Lyric\r\n        {\r\n            ReferenceLyricConfig = new SyncLyricConfig(),\r\n        });\r\n\r\n        static void test(Lyric lyric)\r\n        {\r\n            HitObjectWritableUtils.IsRemoveLyricLocked(lyric);\r\n            HitObjectWritableUtils.GetRemoveLyricLockedBy(lyric);\r\n        }\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Lyric property\r\n\r\n    [Test]\r\n    public void TestIsWriteLyricPropertyLocked()\r\n    {\r\n        // standard.\r\n        test(new Lyric());\r\n\r\n        // test lock state.\r\n        foreach (var lockState in Enum.GetValues<LockState>())\r\n        {\r\n            test(new Lyric\r\n            {\r\n                Lock = lockState,\r\n            });\r\n        }\r\n\r\n        // reference lyric.\r\n        test(new Lyric\r\n        {\r\n            ReferenceLyricConfig = new ReferenceLyricConfig(),\r\n        });\r\n\r\n        test(new Lyric\r\n        {\r\n            ReferenceLyricConfig = new SyncLyricConfig(),\r\n        });\r\n\r\n        static void test(Lyric lyric)\r\n        {\r\n            testEveryWritablePropertiesInObjectAtTheSameTime(lyric, (l, propertyName) => HitObjectWritableUtils.IsWriteLyricPropertyLocked(l, propertyName));\r\n            testEveryWritablePropertiesInObject(lyric, (l, propertyName) => HitObjectWritableUtils.IsWriteLyricPropertyLocked(l, propertyName));\r\n\r\n            testEveryWritablePropertiesInObjectAtTheSameTime(lyric, (l, propertyName) => HitObjectWritableUtils.GetLyricPropertyLockedBy(l, propertyName));\r\n            testEveryWritablePropertiesInObject(lyric, (l, propertyName) => HitObjectWritableUtils.GetLyricPropertyLockedBy(l, propertyName));\r\n        }\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Create or remove notes.\r\n\r\n    [Test]\r\n    public void TestIsCreateOrRemoveNoteLocked()\r\n    {\r\n        // standard.\r\n        test(new Lyric());\r\n\r\n        // test lock state.\r\n        foreach (var lockState in Enum.GetValues<LockState>())\r\n        {\r\n            test(new Lyric\r\n            {\r\n                Lock = lockState,\r\n            });\r\n        }\r\n\r\n        // reference lyric.\r\n        test(new Lyric\r\n        {\r\n            ReferenceLyricConfig = new ReferenceLyricConfig(),\r\n        });\r\n\r\n        test(new Lyric\r\n        {\r\n            ReferenceLyricConfig = new SyncLyricConfig(),\r\n        });\r\n\r\n        static void test(Lyric lyric)\r\n        {\r\n            HitObjectWritableUtils.IsCreateOrRemoveNoteLocked(lyric);\r\n            HitObjectWritableUtils.GetCreateOrRemoveNoteLockedBy(lyric);\r\n        }\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Note property\r\n\r\n    [Test]\r\n    public void TestIsWriteNotePropertyLocked()\r\n    {\r\n        // standard.\r\n        test(new Note());\r\n\r\n        // test with reference lyric.\r\n        var referencedLyric = new Lyric();\r\n        test(new Note\r\n        {\r\n            ReferenceLyricId = referencedLyric.ID,\r\n            ReferenceLyric = referencedLyric,\r\n        });\r\n\r\n        static void test(Note note)\r\n        {\r\n            testEveryWritablePropertiesInObjectAtTheSameTime(note, (l, propertyName) => HitObjectWritableUtils.IsWriteNotePropertyLocked(l, propertyName));\r\n            testEveryWritablePropertiesInObject(note, (l, propertyName) => HitObjectWritableUtils.IsWriteNotePropertyLocked(l, propertyName));\r\n\r\n            testEveryWritablePropertiesInObjectAtTheSameTime(note, (l, propertyName) => HitObjectWritableUtils.GetNotePropertyLockedBy(l, propertyName));\r\n            testEveryWritablePropertiesInObject(note, (l, propertyName) => HitObjectWritableUtils.GetNotePropertyLockedBy(l, propertyName));\r\n        }\r\n    }\r\n\r\n    #endregion\r\n\r\n    private static void testEveryWritablePropertiesInObjectAtTheSameTime<THitObject>(THitObject hitObject, Action<THitObject, string[]> action)\r\n    {\r\n        // the purpose of this test case if focus on checking every property in the hit-object should be able to know the writable or not.\r\n        // return value is not in the test scope.\r\n        string[] allWriteableProperties = typeof(THitObject).GetProperties()\r\n                                                            .Where(x => x.CanRead && x.CanWrite)\r\n                                                            .Where(x => x.CustomAttributes.All(customAttributeData => customAttributeData.AttributeType != typeof(JsonIgnoreAttribute)))\r\n                                                            .Select(x => x.Name)\r\n                                                            .ToArray();\r\n        action(hitObject, allWriteableProperties);\r\n    }\r\n\r\n    private static void testEveryWritablePropertiesInObject<THitObject>(THitObject hitObject, Action<THitObject, string> action)\r\n    {\r\n        testEveryWritablePropertiesInObjectAtTheSameTime(hitObject, (l, propertyNames) =>\r\n        {\r\n            foreach (string propertyName in propertyNames)\r\n            {\r\n                action(hitObject, propertyName);\r\n            }\r\n        });\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Utils/LockStateUtilsTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Types;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Utils;\r\n\r\npublic class LockStateUtilsTest\r\n{\r\n    [TestCase(new[] { LockState.Full, LockState.Partial, LockState.None }, 1)]\r\n    [TestCase(new LockState[] { }, 0)]\r\n    public void TestFindUnlockObjects(LockState[] lockStates, int? expected)\r\n    {\r\n        var lyrics = lockStates.Select(x => new Lyric\r\n        {\r\n            Text = \"karaoke\",\r\n            Lock = x,\r\n        });\r\n\r\n        int actual = LockStateUtils.FindUnlockObjects(lyrics).Length;\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Editor/Utils/ValueChangedEventUtilsTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Editor.Utils;\r\n\r\npublic class ValueChangedEventUtilsTest\r\n{\r\n    [Test]\r\n    public void TestLyricChangedWithSameLyric()\r\n    {\r\n        var lyric1 = new Lyric\r\n        {\r\n            Text = \"lyric 1\",\r\n        };\r\n\r\n        var oldCaret = new ClickingCaretPosition(lyric1);\r\n        var newCaret = new ClickingCaretPosition(lyric1);\r\n\r\n        Assert.That(ValueChangedEventUtils.LyricChanged(new ValueChangedEvent<ICaretPosition?>(oldCaret, newCaret)), Is.False);\r\n    }\r\n\r\n    [Test]\r\n    public void TestLyricChangedWithDifferentLyric()\r\n    {\r\n        var lyric1 = new Lyric\r\n        {\r\n            Text = \"lyric 1\",\r\n        };\r\n\r\n        var lyric2 = new Lyric\r\n        {\r\n            Text = \"lyric 2\",\r\n        };\r\n\r\n        var oldCaret = new ClickingCaretPosition(lyric1);\r\n        var newCaret = new ClickingCaretPosition(lyric2);\r\n\r\n        Assert.That(ValueChangedEventUtils.LyricChanged(new ValueChangedEvent<ICaretPosition?>(oldCaret, newCaret)));\r\n    }\r\n\r\n    [Test]\r\n    public void TestLyricChangedWithSameLyricButDifferentCaretPosition()\r\n    {\r\n        var lyric1 = new Lyric\r\n        {\r\n            Text = \"lyric 1\",\r\n        };\r\n\r\n        var oldCaret = new ClickingCaretPosition(lyric1);\r\n        var newCaret = new RecordingTimeTagCaretPosition(lyric1, new TimeTag(new TextIndex(1)));\r\n\r\n        Assert.That(ValueChangedEventUtils.LyricChanged(new ValueChangedEvent<ICaretPosition?>(oldCaret, newCaret)), Is.False);\r\n    }\r\n\r\n    [Test]\r\n    public void TestEditModeChangedWithDefaultValue()\r\n    {\r\n        var oldMode = default(EditorModeWithEditStep);\r\n        var newMode = new EditorModeWithEditStep\r\n        {\r\n            Mode = LyricEditorMode.View,\r\n        };\r\n\r\n        Assert.That(ValueChangedEventUtils.EditModeChanged(new ValueChangedEvent<EditorModeWithEditStep>(oldMode, newMode)));\r\n    }\r\n\r\n    [Test]\r\n    public void TestEditModeChanged()\r\n    {\r\n        var oldMode = new EditorModeWithEditStep\r\n        {\r\n            Mode = LyricEditorMode.View,\r\n        };\r\n        var newMode = new EditorModeWithEditStep\r\n        {\r\n            Mode = LyricEditorMode.View,\r\n        };\r\n\r\n        Assert.That(ValueChangedEventUtils.EditModeChanged(new ValueChangedEvent<EditorModeWithEditStep>(oldMode, newMode)), Is.False);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Extensions/EnumerableExtensionsTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Extensions;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Extensions;\r\n\r\npublic class EnumerableExtensionsTest\r\n{\r\n    [TestCase(new[] { 1, 2, 3, 4, 5, 6 }, 1, 3, 3)]\r\n    [TestCase(new[] { 1, 3, 2, 4, 6, 5 }, 1, 6, 6)]\r\n    [TestCase(new[] { 1, 2, 3, 4, 5, 6 }, 3, 1, 0)]\r\n    [TestCase(new[] { 1, 2, 3, 4, 5, 6 }, 1, 7, 0)]\r\n    public void TestGetNextMatch(int[] values, int startFrom, int matchCondition, int expected)\r\n    {\r\n        int actual = values.GetNextMatch(startFrom, x => x == matchCondition);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(new[] { 1, 2, 3, 4, 5, 6 }, 6, 3, 3)]\r\n    [TestCase(new[] { 1, 3, 2, 4, 6, 5 }, 5, 3, 3)]\r\n    [TestCase(new[] { 1, 2, 3, 4, 5, 6 }, 3, 6, 0)]\r\n    [TestCase(new[] { 2, 3, 4, 5, 6 }, 6, 1, 0)]\r\n    public void TestGetPreviousMatch(int[] values, int startFrom, int matchCondition, int expected)\r\n    {\r\n        int actual = values.GetPreviousMatch(startFrom, x => x == matchCondition);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Extensions/PlayerTestSceneExtensions.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.UI;\r\nusing osu.Game.Rulesets.UI;\r\nusing osu.Game.Tests.Visual;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Extensions;\r\n\r\npublic static class PlayerTestSceneExtensions\r\n{\r\n    public static DrawableKaraokeRuleset? GetDrawableRuleset(this TestPlayer testPlayer)\r\n    {\r\n        return testPlayer.DrawableRuleset as DrawableKaraokeRuleset;\r\n    }\r\n\r\n    public static KaraokePlayfield? GetPlayfield(this TestPlayer testPlayer)\r\n    {\r\n        return testPlayer.GetDrawableRuleset()?.Playfield;\r\n    }\r\n\r\n    public static Playfield? GetNotePlayfield(this TestPlayer testPlayer)\r\n    {\r\n        return testPlayer.GetPlayfield()?.NotePlayfield;\r\n    }\r\n\r\n    public static Playfield? GetLyricPlayfield(this TestPlayer testPlayer)\r\n    {\r\n        return testPlayer.GetPlayfield()?.LyricPlayfield;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Extensions/PrimaryKeyObjectExtension.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Extensions;\r\n\r\npublic static class PrimaryKeyObjectExtension\r\n{\r\n    public static TObject ChangeId<TObject>(this TObject obj, int id)\r\n        where TObject : IHasPrimaryKey\r\n    {\r\n        var elementId = TestCaseElementIdHelper.CreateElementIdByNumber(id);\r\n        return obj.ChangeId(elementId);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Flags/FlagStateTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Flags;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Flags;\r\n\r\npublic class FlagStateTest\r\n{\r\n    [TestCase(default, new[] { TestEnum.Enum0, TestEnum.Enum1, TestEnum.Enum2 })]\r\n    [TestCase(TestEnum.Enum0, new[] { TestEnum.Enum1, TestEnum.Enum2 })]\r\n    [TestCase(TestEnum.Enum0 | TestEnum.Enum1, new[] { TestEnum.Enum2 })]\r\n    [TestCase(TestEnum.Enum0 | TestEnum.Enum1 | TestEnum.Enum2, new TestEnum[] { })]\r\n    [TestCase(TestEnum.Enum0 | TestEnum.Enum2, new[] { TestEnum.Enum1 })] // Should work without continuous values.\r\n    [TestCase(TestEnum.Enum0 | TestEnum.Enum0, new[] { TestEnum.Enum1, TestEnum.Enum2 })] // Test edge case.\r\n    public void TestInvalidate(TestEnum invalidFlags, TestEnum[] expectedValue)\r\n    {\r\n        var validator = new FlagState<TestEnum>();\r\n        validator.ValidateAll();\r\n\r\n        // check the value.\r\n        validator.Invalidate(invalidFlags);\r\n        Assert.That(validator.GetAllValidFlags(), Is.EqualTo(expectedValue));\r\n\r\n        // value should not changed if did the same action.\r\n        validator.Invalidate(invalidFlags);\r\n        Assert.That(validator.GetAllValidFlags(), Is.EqualTo(expectedValue));\r\n\r\n        // should not be negative if remove the value from the validator.\r\n        validator.InvalidateAll();\r\n        validator.Invalidate(invalidFlags);\r\n        Assert.That(validator.GetAllValidFlags(), Is.EqualTo(Array.Empty<TestEnum>()));\r\n        Assert.That(validator.GetAllValidFlags().Sum(x => Convert.ToInt32(x)), Is.EqualTo(0));\r\n    }\r\n\r\n    [TestCase(TestEnum.Enum0)]\r\n    [TestCase(TestEnum.Enum0 | TestEnum.Enum1)]\r\n    [TestCase(TestEnum.Enum0 | TestEnum.Enum1 | TestEnum.Enum2)]\r\n    [TestCase(TestEnum.Enum0 | TestEnum.Enum2)] // Should work without continuous values.\r\n    [TestCase(TestEnum.Enum0 | TestEnum.Enum0)] // Test edge case.\r\n    public void TestInvalidateAll(TestEnum defaultFlags)\r\n    {\r\n        var validator = new FlagState<TestEnum>();\r\n        validator.Validate(defaultFlags);\r\n\r\n        // check the value.\r\n        validator.InvalidateAll();\r\n        Assert.That(validator.GetAllValidFlags(), Is.EqualTo(Array.Empty<TestEnum>()));\r\n        Assert.That(validator.GetAllValidFlags().Sum(x => Convert.ToInt32(x)), Is.EqualTo(0));\r\n    }\r\n\r\n    [TestCase(default, new TestEnum[] { })]\r\n    [TestCase(TestEnum.Enum0, new[] { TestEnum.Enum0 })]\r\n    [TestCase(TestEnum.Enum0 | TestEnum.Enum1, new[] { TestEnum.Enum0, TestEnum.Enum1 })]\r\n    [TestCase(TestEnum.Enum0 | TestEnum.Enum1 | TestEnum.Enum2, new[] { TestEnum.Enum0, TestEnum.Enum1, TestEnum.Enum2 })]\r\n    [TestCase(TestEnum.Enum0 | TestEnum.Enum2, new[] { TestEnum.Enum0, TestEnum.Enum2 })] // Should work without continuous values.\r\n    [TestCase(TestEnum.Enum0 | TestEnum.Enum0, new[] { TestEnum.Enum0 })] // Test edge case.\r\n    [TestCase(TestEnum.Enum2 | TestEnum.Enum2, new[] { TestEnum.Enum2 })] // Test edge case.\r\n    public void TestValidate(TestEnum validateFlags, TestEnum[] expectedValue)\r\n    {\r\n        var validator = new FlagState<TestEnum>();\r\n\r\n        // check the value.\r\n        validator.Validate(validateFlags);\r\n        Assert.That(validator.GetAllValidFlags(), Is.EqualTo(expectedValue));\r\n\r\n        // value should not changed if did the same action.\r\n        validator.Validate(validateFlags);\r\n        Assert.That(validator.GetAllValidFlags(), Is.EqualTo(expectedValue));\r\n\r\n        // should not exceed sum values if remove the value from the validator.\r\n        validator.ValidateAll();\r\n        validator.Validate(validateFlags);\r\n        Assert.That(validator.GetAllValidFlags(), Is.EqualTo(Enum.GetValues<TestEnum>()));\r\n        Assert.That(validator.GetAllValidFlags().Sum(x => Convert.ToInt32(x)), Is.EqualTo(Enum.GetValues<TestEnum>().Sum(x => Convert.ToInt32(x))));\r\n    }\r\n\r\n    [TestCase(TestEnum.Enum0)]\r\n    [TestCase(TestEnum.Enum0 | TestEnum.Enum1)]\r\n    [TestCase(TestEnum.Enum0 | TestEnum.Enum1 | TestEnum.Enum2)]\r\n    [TestCase(TestEnum.Enum0 | TestEnum.Enum2)] // Should work without continuous values.\r\n    [TestCase(TestEnum.Enum0 | TestEnum.Enum0)] // Test edge case.\r\n    public void TestValidateAll(TestEnum validateFlags)\r\n    {\r\n        var validator = new FlagState<TestEnum>();\r\n\r\n        // check the value.\r\n        validator.ValidateAll();\r\n        Assert.That(validator.GetAllValidFlags(), Is.EqualTo(Enum.GetValues<TestEnum>()));\r\n        Assert.That(validator.GetAllValidFlags().Sum(x => Convert.ToInt32(x)), Is.EqualTo(Enum.GetValues<TestEnum>().Sum(x => Convert.ToInt32(x))));\r\n    }\r\n\r\n    [TestCase(TestEnum.Enum0, TestEnum.Enum0, true)]\r\n    [TestCase(TestEnum.Enum0, TestEnum.Enum1, false)]\r\n    [TestCase(TestEnum.Enum0 | TestEnum.Enum2, TestEnum.Enum0, true)]\r\n    [TestCase(TestEnum.Enum0 | TestEnum.Enum2, TestEnum.Enum1, false)]\r\n    [TestCase(TestEnum.Enum0 | TestEnum.Enum2, TestEnum.Enum2, true)]\r\n    [TestCase(TestEnum.Enum0 | TestEnum.Enum0, TestEnum.Enum0, true)] // Test edge case.\r\n    [TestCase(TestEnum.Enum0 | TestEnum.Enum0, TestEnum.Enum2, false)] // Test edge case.\r\n    [TestCase(TestEnum.Enum2 | TestEnum.Enum2, TestEnum.Enum0, false)] // Test edge case.\r\n    [TestCase(TestEnum.Enum2 | TestEnum.Enum2, TestEnum.Enum2, true)] // Test edge case.\r\n    public void TestIsValid(TestEnum validateFlags, TestEnum validFlag, bool expectedValue)\r\n    {\r\n        var validator = new FlagState<TestEnum>();\r\n\r\n        // check the value.\r\n        validator.Validate(validateFlags);\r\n        Assert.That(validator.IsValid(validFlag), Is.EqualTo(expectedValue));\r\n    }\r\n\r\n    [Test]\r\n    public void TestGetAllValidFlags()\r\n    {\r\n        var validator = new FlagState<TestEnum>();\r\n\r\n        // Should be possible to get all tags.\r\n        validator.ValidateAll();\r\n        Assert.That(validator.GetAllValidFlags(), Is.EqualTo(Enum.GetValues<TestEnum>()));\r\n\r\n        // Should not be possible to get any tags.\r\n        validator.InvalidateAll();\r\n        Assert.That(validator.GetAllValidFlags(), Is.EqualTo(Array.Empty<TestEnum>()));\r\n    }\r\n\r\n    [Test]\r\n    public void TestGetAllInvalidFlags()\r\n    {\r\n        var validator = new FlagState<TestEnum>();\r\n\r\n        // Should be possible to get all tags.\r\n        validator.ValidateAll();\r\n        Assert.That(validator.GetAllInvalidFlags(), Is.EqualTo(Array.Empty<TestEnum>()));\r\n\r\n        // Should not be possible to get any tags.\r\n        validator.InvalidateAll();\r\n        Assert.That(validator.GetAllInvalidFlags(), Is.EqualTo(Enum.GetValues<TestEnum>()));\r\n    }\r\n\r\n    [Test]\r\n    public void TestGetAllValidFlagsWithAndCondition()\r\n    {\r\n        var validator = new FlagState<TestAndEnum>();\r\n\r\n        validator.Validate(TestAndEnum.Enum0);\r\n        Assert.That(validator.GetAllValidFlags(), Is.EqualTo(new[] { TestAndEnum.Enum0 }));\r\n\r\n        validator.Validate(TestAndEnum.Enum0 | TestAndEnum.Enum1);\r\n        Assert.That(validator.GetAllValidFlags(), Is.EqualTo(new[] { TestAndEnum.Enum0, TestAndEnum.Enum1, TestAndEnum.Enum0And1 }));\r\n        Assert.That(validator.GetAllValidFlags(), Is.EqualTo(Enum.GetValues<TestAndEnum>()));\r\n\r\n        validator.ValidateAll();\r\n        Assert.That(validator.GetAllValidFlags(), Is.EqualTo(new[] { TestAndEnum.Enum0, TestAndEnum.Enum1, TestAndEnum.Enum0And1 }));\r\n        Assert.That(validator.GetAllValidFlags(), Is.EqualTo(Enum.GetValues<TestAndEnum>()));\r\n    }\r\n\r\n    [Test]\r\n    public void TestGetAllInvalidFlagsWithAndCondition()\r\n    {\r\n        var validator = new FlagState<TestAndEnum>();\r\n        validator.ValidateAll();\r\n\r\n        validator.Invalidate(TestAndEnum.Enum0);\r\n        Assert.That(validator.GetAllValidFlags(), Is.EqualTo(new[] { TestAndEnum.Enum1 }));\r\n\r\n        validator.Invalidate(TestAndEnum.Enum1);\r\n        Assert.That(validator.GetAllValidFlags(), Is.EqualTo(Array.Empty<TestEnum>()));\r\n\r\n        validator.InvalidateAll();\r\n        Assert.That(validator.GetAllValidFlags(), Is.EqualTo(Array.Empty<TestEnum>()));\r\n    }\r\n\r\n    [Flags]\r\n    public enum TestEnum\r\n    {\r\n        Enum0 = 1,\r\n\r\n        Enum1 = 1 << 1,\r\n\r\n        Enum2 = 1 << 2,\r\n    }\r\n\r\n    [Flags]\r\n    public enum TestAndEnum\r\n    {\r\n        Enum0 = 1,\r\n\r\n        Enum1 = 1 << 1,\r\n\r\n        Enum0And1 = Enum0 | Enum1,\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Graphics/Sprites/DisplayLyricProcessorTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Graphics.Sprites;\r\n\r\npublic class DisplayLyricProcessorTest\r\n{\r\n    // check changed count\r\n    private int topTextChangeCount;\r\n    private int centerTextChangeCount;\r\n    private int bottomTextChangeCount;\r\n    private int timeTagsChangeCount;\r\n\r\n    private Lyric? lyric;\r\n    private DisplayLyricProcessor? testProcessor;\r\n\r\n    [SetUp]\r\n    public void Setup()\r\n    {\r\n        // create processor\r\n        lyric = new Lyric();\r\n        testProcessor = new DisplayLyricProcessor(lyric);\r\n\r\n        // bind event\r\n        testProcessor.TopTextChanged += _ =>\r\n        {\r\n            topTextChangeCount++;\r\n        };\r\n        testProcessor.CenterTextChanged += _ =>\r\n        {\r\n            centerTextChangeCount++;\r\n        };\r\n        testProcessor.BottomTextChanged += _ =>\r\n        {\r\n            bottomTextChangeCount++;\r\n        };\r\n        testProcessor.TimeTagsChanged += _ =>\r\n        {\r\n            timeTagsChangeCount++;\r\n        };\r\n\r\n        // should trigger update all first after bind event.\r\n        testProcessor.UpdateAll();\r\n\r\n        // check the changed count. should be updated.\r\n        Assert.That(topTextChangeCount, Is.EqualTo(1));\r\n        Assert.That(centerTextChangeCount, Is.EqualTo(1));\r\n        Assert.That(bottomTextChangeCount, Is.EqualTo(1));\r\n        Assert.That(timeTagsChangeCount, Is.EqualTo(1));\r\n\r\n        // reset the count to 0 for the remaining test.\r\n        topTextChangeCount = 0;\r\n        centerTextChangeCount = 0;\r\n        bottomTextChangeCount = 0;\r\n        timeTagsChangeCount = 0;\r\n    }\r\n\r\n    [Test]\r\n    public void TestTextChanged()\r\n    {\r\n        // change the property.\r\n        lyric!.Text = \"karaoke\";\r\n\r\n        // check the changed count\r\n        Assert.That(topTextChangeCount, Is.EqualTo(1));\r\n        Assert.That(centerTextChangeCount, Is.EqualTo(1));\r\n        Assert.That(bottomTextChangeCount, Is.EqualTo(1));\r\n        Assert.That(timeTagsChangeCount, Is.EqualTo(1));\r\n    }\r\n\r\n    [Test]\r\n    public void TestSwitchDisplayType()\r\n    {\r\n        // change the display type.\r\n        testProcessor!.DisplayType = LyricDisplayType.RomanisedSyllable;\r\n\r\n        // check the changed count\r\n        Assert.That(topTextChangeCount, Is.EqualTo(1));\r\n        Assert.That(centerTextChangeCount, Is.EqualTo(1));\r\n        Assert.That(bottomTextChangeCount, Is.EqualTo(1));\r\n        Assert.That(timeTagsChangeCount, Is.EqualTo(1));\r\n    }\r\n\r\n    [Test]\r\n    public void TestSwitchDisplayProperty()\r\n    {\r\n        // change the display property.\r\n        testProcessor!.DisplayProperty = LyricDisplayProperty.TopText;\r\n\r\n        // check the changed count\r\n        Assert.That(topTextChangeCount, Is.EqualTo(1));\r\n        Assert.That(centerTextChangeCount, Is.EqualTo(1));\r\n        Assert.That(bottomTextChangeCount, Is.EqualTo(1));\r\n        Assert.That(timeTagsChangeCount, Is.EqualTo(1));\r\n    }\r\n\r\n    [TearDown]\r\n    public void TearDown()\r\n    {\r\n        // reset the count to 0 for the next test.\r\n        topTextChangeCount = 0;\r\n        centerTextChangeCount = 0;\r\n        bottomTextChangeCount = 0;\r\n        timeTagsChangeCount = 0;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Graphics/Sprites/Processor/DisplayProcessorTestScene.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Timing;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.Sprites.Processor;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\nusing osu.Game.Tests.Visual;\r\nusing osuTK;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Graphics.Sprites.Processor;\r\n\r\n/// <summary>\r\n/// Test should be focus on:\r\n/// 1. Test one or more property changed, all related property should be changed.\r\n/// 2. Test if the property is being triggered.\r\n/// </summary>\r\npublic abstract partial class DisplayProcessorTestScene : OsuGridTestScene\r\n{\r\n    // check value.\r\n    private IReadOnlyList<PositionText>? topText;\r\n    private string? centerText;\r\n    private IReadOnlyList<PositionText>? bottomText;\r\n    private IReadOnlyDictionary<double, TextIndex>? timeTags;\r\n\r\n    // check changed count\r\n    private int topTextChangeCount;\r\n    private int centerTextChangeCount;\r\n    private int bottomTextChangeCount;\r\n    private int timeTagsChangeCount;\r\n\r\n    private Lyric? lyric;\r\n    private BaseDisplayProcessor? testProcessor;\r\n\r\n    private readonly ManualClock manualClock = new();\r\n\r\n    protected DisplayProcessorTestScene()\r\n        : base(2, 1)\r\n    {\r\n        AddSliderStep(\"Adjust clock time\", 0, 5000, 2500, time =>\r\n        {\r\n            manualClock.CurrentTime = time;\r\n        });\r\n    }\r\n\r\n    #region Override properties.\r\n\r\n    protected abstract LyricDisplayType DisplayType { get; }\r\n\r\n    #endregion\r\n\r\n    #region Tests\r\n\r\n    [Test]\r\n    public void TestDisplayProperty([Values] LyricDisplayProperty property)\r\n    {\r\n        Initialize(property);\r\n\r\n        // should not have any change.\r\n        AssertTopTextNotChanged();\r\n        AssertCenterTextNotChanged();\r\n        AssertBottomTextNotChanged();\r\n        AssertTimeTagsNotChanged();\r\n\r\n        TriggerChange(lyric =>\r\n        {\r\n            switch (testProcessor)\r\n            {\r\n                // will trigger all properties changed if the center text is changed.\r\n                case LyricFirstDisplayProcessor:\r\n                    lyric.Text = \"カラオケ\";\r\n                    break;\r\n\r\n                case RomanisedSyllableFirstDisplayProcessor:\r\n                    lyric.TimeTags = TestCaseTagHelper.ParseTimeTags(new[] { \"[0,start]:1000#^ka\", \"[1,start]:2000#ra\", \"[2,start]:3000#o\", \"[3,start]:4000#ke\", \"[3,end]:5000\" });\r\n                    break;\r\n\r\n                default:\r\n                    throw new InvalidOperationException();\r\n            }\r\n        });\r\n\r\n        if (property.HasFlag(LyricDisplayProperty.TopText))\r\n        {\r\n            AssertTopTextChanged();\r\n        }\r\n        else\r\n        {\r\n            AssertTopTextNotChanged();\r\n        }\r\n\r\n        AssertCenterTextChanged();\r\n\r\n        if (property.HasFlag(LyricDisplayProperty.BottomText))\r\n        {\r\n            AssertBottomTextChanged();\r\n        }\r\n        else\r\n        {\r\n            AssertBottomTextNotChanged();\r\n        }\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Tools\r\n\r\n    protected void Initialize(LyricDisplayProperty displayProperty = LyricDisplayProperty.Both)\r\n    {\r\n        Initialize(() => new Lyric(), displayProperty);\r\n    }\r\n\r\n    protected void Initialize(Func<Lyric> createLyricFunc, LyricDisplayProperty displayProperty = LyricDisplayProperty.Both)\r\n    {\r\n        AddStep(\"Initialize\", () =>\r\n        {\r\n            // reset the value\r\n            topText = default;\r\n            centerText = default;\r\n            bottomText = default;\r\n            timeTags = default;\r\n\r\n            // reset the count\r\n            topTextChangeCount = 0;\r\n            centerTextChangeCount = 0;\r\n            bottomTextChangeCount = 0;\r\n            timeTagsChangeCount = 0;\r\n\r\n            // create processor\r\n            lyric = createLyricFunc();\r\n            testProcessor = DisplayLyricProcessor.GetLyricDisplayProcessor(lyric, DisplayType, displayProperty);\r\n\r\n            // bind event\r\n            testProcessor.TopTextChanged += texts =>\r\n            {\r\n                topText = texts;\r\n                topTextChangeCount++;\r\n            };\r\n            testProcessor.CenterTextChanged += text =>\r\n            {\r\n                centerText = text;\r\n                centerTextChangeCount++;\r\n            };\r\n            testProcessor.BottomTextChanged += texts =>\r\n            {\r\n                bottomText = texts;\r\n                bottomTextChangeCount++;\r\n            };\r\n            testProcessor.TimeTagsChanged += tags =>\r\n            {\r\n                timeTags = tags;\r\n                timeTagsChangeCount++;\r\n            };\r\n\r\n            // create the drawable for preview.\r\n            createSampleSpriteText(lyric, DisplayType, displayProperty);\r\n        });\r\n    }\r\n\r\n    private void createSampleSpriteText(Lyric lyric, LyricDisplayType displayType, LyricDisplayProperty displayProperty)\r\n    {\r\n        Cell(0).Child = createProvider(\"karaoke sprite text\", new DrawableKaraokeSpriteText(lyric)\r\n        {\r\n            LeftTextColour = Color4.Green,\r\n            RightTextColour = Color4.Red,\r\n            Clock = new FramedClock(manualClock),\r\n            DisplayType = displayType,\r\n            DisplayProperty = displayProperty,\r\n        });\r\n        Cell(1).Child = createProvider(\"lyric sprite text\", new DrawableLyricSpriteText(lyric)\r\n        {\r\n            DisplayType = displayType,\r\n            DisplayProperty = displayProperty,\r\n        });\r\n        return;\r\n\r\n        static Drawable createProvider(string name, Drawable sample) =>\r\n            new Container\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n                BorderColour = Color4.White,\r\n                BorderThickness = 3,\r\n                Masking = true,\r\n\r\n                Children = new[]\r\n                {\r\n                    new Box\r\n                    {\r\n                        AlwaysPresent = true,\r\n                        Alpha = 0,\r\n                        RelativeSizeAxes = Axes.Both,\r\n                    },\r\n                    new OsuSpriteText\r\n                    {\r\n                        Text = name,\r\n                        Scale = new Vector2(1.5f),\r\n                        Padding = new MarginPadding(5),\r\n                    },\r\n                    sample.With(x =>\r\n                    {\r\n                        x.Anchor = Anchor.Centre;\r\n                        x.Origin = Anchor.Centre;\r\n                        x.Scale = new Vector2(2);\r\n                    }),\r\n                },\r\n            };\r\n    }\r\n\r\n    protected void TriggerChange(Action<Lyric> targetLyric)\r\n    {\r\n        AddStep(\"Change property\", () =>\r\n        {\r\n            if (lyric == null)\r\n                throw new InvalidOperationException(\"Test lyric should not be null.\");\r\n\r\n            if (testProcessor == null)\r\n                throw new InvalidOperationException(\"Process should not be null.\");\r\n\r\n            targetLyric(lyric);\r\n        });\r\n    }\r\n\r\n    protected void AssertTopTextChanged(IEnumerable<PositionText> expectedValue)\r\n    {\r\n        AssertTopTextChanged();\r\n        AddAssert(\"Check top text value\", () => topText?.SequenceEqual(expectedValue) ?? false);\r\n    }\r\n\r\n    protected void AssertTopTextChanged()\r\n    {\r\n        AddAssert(\"Top text should trigger only once\", () => topTextChangeCount == 1);\r\n    }\r\n\r\n    protected void AssertTopTextNotChanged()\r\n    {\r\n        AddAssert(\"Top text should not triggered\", () => topTextChangeCount == 0);\r\n    }\r\n\r\n    protected void AssertCenterTextChanged(string expectedValue)\r\n    {\r\n        AssertCenterTextChanged();\r\n        AddAssert(\"Check center text value\", () => centerText == expectedValue);\r\n    }\r\n\r\n    protected void AssertCenterTextChanged()\r\n    {\r\n        AddAssert(\"Center text should trigger only once\", () => centerTextChangeCount == 1);\r\n    }\r\n\r\n    protected void AssertCenterTextNotChanged()\r\n    {\r\n        AddAssert(\"Center text should not triggered\", () => centerTextChangeCount == 0);\r\n    }\r\n\r\n    protected void AssertBottomTextChanged(IEnumerable<PositionText> expectedValue)\r\n    {\r\n        AssertBottomTextChanged();\r\n        AddAssert(\"Check bottom text value\", () => bottomText?.SequenceEqual(expectedValue) ?? false);\r\n    }\r\n\r\n    protected void AssertBottomTextChanged()\r\n    {\r\n        AddAssert(\"Bottom text should trigger only once\", () => bottomTextChangeCount == 1);\r\n    }\r\n\r\n    protected void AssertBottomTextNotChanged()\r\n    {\r\n        AddAssert(\"Bottom text should not triggered\", () => bottomTextChangeCount == 0);\r\n    }\r\n\r\n    protected void AssertTimeTagsChanged(Dictionary<double, TextIndex> expectedValue)\r\n    {\r\n        AssertTimeTagsChanged();\r\n        AddAssert(\"Check time tags value\", () => timeTags?.SequenceEqual(expectedValue) ?? false);\r\n    }\r\n\r\n    protected void AssertTimeTagsChanged()\r\n    {\r\n        AddAssert(\"Time-tag should trigger only once\", () => timeTagsChangeCount == 1);\r\n    }\r\n\r\n    protected void AssertTimeTagsNotChanged()\r\n    {\r\n        AddAssert(\"Time-tag should not triggered\", () => timeTagsChangeCount == 0);\r\n    }\r\n\r\n    #endregion\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Graphics/Sprites/Processor/TestSceneLyricFirstDisplayProcessor.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Graphics.Sprites.Processor;\r\n\r\npublic partial class TestSceneLyricFirstDisplayProcessor : DisplayProcessorTestScene\r\n{\r\n    protected override LyricDisplayType DisplayType => LyricDisplayType.Lyric;\r\n\r\n    #region Happy path\r\n\r\n    [Test]\r\n    public void TestTextChanged()\r\n    {\r\n        Initialize();\r\n        TriggerChange(lyric =>\r\n        {\r\n            lyric.Text = \"カラオケ\";\r\n        });\r\n\r\n        AssertTopTextChanged(Array.Empty<PositionText>());\r\n        AssertCenterTextChanged(\"カラオケ\");\r\n        AssertBottomTextChanged(Array.Empty<PositionText>());\r\n        AssertTimeTagsChanged(new Dictionary<double, TextIndex>());\r\n    }\r\n\r\n    [Test]\r\n    public void TestRubiesChanged()\r\n    {\r\n        Initialize(() => new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n        });\r\n\r\n        TriggerChange(lyric =>\r\n        {\r\n            lyric.RubyTags = TestCaseTagHelper.ParseRubyTags(new[] { \"[0]:か\", \"[1]:ら\", \"[2]:お\", \"[3]:け\" });\r\n        });\r\n\r\n        AssertTopTextChanged(new PositionText[]\r\n        {\r\n            new(\"か\", 0, 0),\r\n            new(\"ら\", 1, 1),\r\n            new(\"お\", 2, 2),\r\n            new(\"け\", 3, 3),\r\n        });\r\n        AssertCenterTextNotChanged();\r\n        AssertBottomTextNotChanged();\r\n        AssertTimeTagsNotChanged();\r\n    }\r\n\r\n    [Test]\r\n    public void TestTimeTagsChanged()\r\n    {\r\n        Initialize(() => new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n        });\r\n\r\n        TriggerChange(lyric =>\r\n        {\r\n            lyric.TimeTags = TestCaseTagHelper.ParseTimeTags(new[] { \"[0,start]:1000#^ka\", \"[1,start]:2000#ra\", \"[2,start]:3000#^o\", \"[3,start]:4000#ke\", \"[3,end]:5000\" });\r\n        });\r\n\r\n        AssertTopTextNotChanged();\r\n        AssertCenterTextNotChanged();\r\n        AssertBottomTextChanged(new PositionText[]\r\n        {\r\n            new(\"kara\", 0, 1),\r\n            new(\"oke\", 2, 3),\r\n        });\r\n        AssertTimeTagsChanged(new Dictionary<double, TextIndex>\r\n        {\r\n            { 1000, new TextIndex(0) },\r\n            { 2000, new TextIndex(1) },\r\n            { 3000, new TextIndex(2) },\r\n            { 4000, new TextIndex(3) },\r\n            { 5000, new TextIndex(3, TextIndex.IndexState.End) },\r\n        });\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region With empty lyric\r\n\r\n    [Test]\r\n    public void TestRubiesChangedWithEmptyText()\r\n    {\r\n        Initialize();\r\n\r\n        TriggerChange(lyric =>\r\n        {\r\n            lyric.RubyTags = TestCaseTagHelper.ParseRubyTags(new[] { \"[0]:か\", \"[1]:ら\", \"[2]:お\", \"[3]:け\" });\r\n        });\r\n\r\n        // it's OK not to filter the ruby that out of range. karaoke/sprite text will not display that.\r\n        AssertTopTextChanged(new PositionText[]\r\n        {\r\n            new(\"か\", 0, 0),\r\n            new(\"ら\", 1, 1),\r\n            new(\"お\", 2, 2),\r\n            new(\"け\", 3, 3),\r\n        });\r\n        AssertCenterTextNotChanged();\r\n        AssertBottomTextNotChanged();\r\n        AssertTimeTagsNotChanged();\r\n    }\r\n\r\n    [Test]\r\n    public void TestTimeTagsChangedWithEmptyText()\r\n    {\r\n        Initialize();\r\n\r\n        TriggerChange(lyric =>\r\n        {\r\n            lyric.TimeTags = TestCaseTagHelper.ParseTimeTags(new[] { \"[0,start]:1000#^ka\", \"[1,start]:2000#ra\", \"[2,start]:3000#o\", \"[3,start]:4000#ke\", \"[3,end]:5000\" });\r\n        });\r\n\r\n        AssertTopTextNotChanged();\r\n        AssertCenterTextNotChanged();\r\n        // it's OK not to filter the romanisation that out of range. karaoke/sprite text will not display that.\r\n        AssertBottomTextChanged(new PositionText[]\r\n        {\r\n            new(\"karaoke\", 0, 3),\r\n        });\r\n        // it's OK not to filter the time-tag that out of range. karaoke/sprite text will not display that.\r\n        AssertTimeTagsChanged(new Dictionary<double, TextIndex>\r\n        {\r\n            { 1000, new TextIndex(0) },\r\n            { 2000, new TextIndex(1) },\r\n            { 3000, new TextIndex(2) },\r\n            { 4000, new TextIndex(3) },\r\n            { 5000, new TextIndex(3, TextIndex.IndexState.End) },\r\n        });\r\n    }\r\n\r\n    #endregion\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Graphics/Sprites/Processor/TestSceneRomanisedSyllableFirstDisplayProcessor.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Graphics.Sprites.Processor;\r\n\r\npublic partial class TestSceneRomanisedSyllableFirstDisplayProcessor : DisplayProcessorTestScene\r\n{\r\n    protected override LyricDisplayType DisplayType => LyricDisplayType.RomanisedSyllable;\r\n\r\n    #region Happy path\r\n\r\n    [Test]\r\n    public void TestTextChanged()\r\n    {\r\n        Initialize(() => new Lyric\r\n        {\r\n            TimeTags = TestCaseTagHelper.ParseTimeTags(new[] { \"[0,start]:1000#^ka\", \"[1,start]:2000#ra\", \"[2,start]:3000#o\", \"[3,start]:4000#ke\", \"[3,end]:5000\" }),\r\n        });\r\n        TriggerChange(lyric =>\r\n        {\r\n            lyric.Text = \"カラオケ\";\r\n        });\r\n\r\n        AssertTopTextNotChanged();\r\n        AssertCenterTextNotChanged();\r\n        // todo: implementation needed.\r\n        AssertBottomTextChanged(Array.Empty<PositionText>());\r\n        AssertTimeTagsNotChanged();\r\n    }\r\n\r\n    [Test]\r\n    public void TestRubiesChanged()\r\n    {\r\n        Initialize(() => new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            TimeTags = TestCaseTagHelper.ParseTimeTags(new[] { \"[0,start]:1000#^ka\", \"[1,start]:2000#ra\", \"[2,start]:3000#o\", \"[3,start]:4000#ke\", \"[3,end]:5000\" }),\r\n        });\r\n\r\n        TriggerChange(lyric =>\r\n        {\r\n            lyric.RubyTags = TestCaseTagHelper.ParseRubyTags(new[] { \"[0]:か\", \"[1]:ら\", \"[2]:お\", \"[3]:け\" });\r\n        });\r\n\r\n        // todo: implementation needed.\r\n        AssertTopTextChanged(Array.Empty<PositionText>());\r\n        AssertCenterTextNotChanged();\r\n        AssertBottomTextNotChanged();\r\n        AssertTimeTagsNotChanged();\r\n    }\r\n\r\n    [Test]\r\n    public void TestTimeTagsChanged()\r\n    {\r\n        Initialize();\r\n\r\n        TriggerChange(lyric =>\r\n        {\r\n            lyric.TimeTags = TestCaseTagHelper.ParseTimeTags(new[] { \"[0,start]:1000#^ka\", \"[1,start]:2000#ra\", \"[2,start]:3000#o\", \"[3,start]:4000#ke\", \"[3,end]:5000\" });\r\n        });\r\n\r\n        AssertTopTextChanged(Array.Empty<PositionText>());\r\n        AssertCenterTextChanged(\"karaoke\");\r\n        // todo: implementation needed.\r\n        AssertBottomTextChanged(Array.Empty<PositionText>());\r\n        // todo: implementation needed.\r\n        AssertTimeTagsChanged(new Dictionary<double, TextIndex>());\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region With empty time-tags\r\n\r\n    [Test]\r\n    public void TestTextChangedWithNoTimeTags()\r\n    {\r\n        Initialize();\r\n        TriggerChange(lyric =>\r\n        {\r\n            lyric.Text = \"カラオケ\";\r\n        });\r\n\r\n        AssertTopTextNotChanged();\r\n        AssertCenterTextNotChanged();\r\n        // todo: implementation needed.\r\n        AssertBottomTextChanged(Array.Empty<PositionText>());\r\n        AssertTimeTagsNotChanged();\r\n    }\r\n\r\n    [Test]\r\n    public void TestRubiesChangedWithNoTimeTags()\r\n    {\r\n        Initialize(() => new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n        });\r\n\r\n        TriggerChange(lyric =>\r\n        {\r\n            lyric.RubyTags = TestCaseTagHelper.ParseRubyTags(new[] { \"[0]:か\", \"[1]:ら\", \"[2]:お\", \"[3]:け\" });\r\n        });\r\n\r\n        AssertTopTextChanged(Array.Empty<PositionText>());\r\n        AssertCenterTextNotChanged();\r\n        AssertBottomTextNotChanged();\r\n        AssertTimeTagsNotChanged();\r\n    }\r\n\r\n    #endregion\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Graphics/TestSceneFontSelector.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Rulesets.Karaoke.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.UserInterfaceV2;\r\nusing osu.Game.Rulesets.Karaoke.Skinning.Fonts;\r\nusing osu.Game.Tests.Visual;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Graphics;\r\n\r\npublic partial class TestSceneFontSelector : OsuManualInputManagerTestScene\r\n{\r\n    protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };\r\n\r\n    private FontManager fontManager = null!;\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        base.Content.AddRange(new Drawable[]\r\n        {\r\n            Content,\r\n            fontManager = new FontManager(),\r\n        });\r\n\r\n        Dependencies.Cache(fontManager);\r\n    }\r\n\r\n    [Test]\r\n    public void TestAllFiles()\r\n    {\r\n        AddStep(\"create\", () =>\r\n        {\r\n            var language = new BindableFontUsage\r\n            {\r\n                MinFontSize = 32,\r\n                MaxFontSize = 72,\r\n            };\r\n            Child = new FontSelector\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n                Size = new Vector2(0.6f, 0.8f),\r\n                Anchor = Anchor.Centre,\r\n                Origin = Anchor.Centre,\r\n                Current = language,\r\n            };\r\n        });\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Graphics/TestSceneLanguageSelector.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Globalization;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.UserInterfaceV2;\r\nusing osu.Game.Tests.Visual;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Graphics;\r\n\r\npublic partial class TestSceneLanguageSelector : OsuManualInputManagerTestScene\r\n{\r\n    [Test]\r\n    public void TestAllLanguages()\r\n    {\r\n        AddStep(\"show the selector\", () =>\r\n        {\r\n            var language = new Bindable<CultureInfo?>(new CultureInfo(\"ja\"));\r\n            Child = new LanguageSelector\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n                Size = new Vector2(0.5f, 0.8f),\r\n                Anchor = Anchor.Centre,\r\n                Origin = Anchor.Centre,\r\n                Current = language,\r\n            };\r\n        });\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Graphics/TestSceneLyricTooltip.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.Cursor;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Beatmaps;\r\nusing osu.Game.Tests.Visual;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Graphics;\r\n\r\n[TestFixture]\r\npublic partial class TestSceneLyricTooltip : OsuTestScene\r\n{\r\n    private LyricTooltip toolTip = null!;\r\n\r\n    [SetUp]\r\n    public void SetUp() => Schedule(() =>\r\n    {\r\n        Child = toolTip = new LyricTooltip\r\n        {\r\n            Anchor = Anchor.Centre,\r\n            Origin = Anchor.Centre,\r\n        };\r\n        toolTip.Show();\r\n    });\r\n\r\n    [Test]\r\n    public void TestDisplayToolTip()\r\n    {\r\n        var beatmap = new TestKaraokeBeatmap(Ruleset.Value);\r\n        var lyrics = beatmap.HitObjects.OfType<Lyric>().ToList();\r\n\r\n        foreach (var lyric in lyrics)\r\n        {\r\n            AddStep($\"Test lyric: {lyric.Text}\", () => { toolTip.SetContent(lyric); });\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Graphics/TestSceneRightTriangle.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.Shapes;\r\nusing osu.Game.Tests.Visual;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Graphics;\r\n\r\npublic partial class TestSceneRightTriangle : OsuTestScene\r\n{\r\n    private RightTriangle rightTriangle = null!;\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OsuColour colours)\r\n    {\r\n        Child = new Container\r\n        {\r\n            Anchor = Anchor.Centre,\r\n            Origin = Anchor.Centre,\r\n            AutoSizeAxes = Axes.Both,\r\n            Children = new Drawable[]\r\n            {\r\n                new Box\r\n                {\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    Colour = colours.Gray4,\r\n                },\r\n                rightTriangle = new RightTriangle\r\n                {\r\n                    Size = new Vector2(100),\r\n                    Colour = colours.Yellow,\r\n                },\r\n            },\r\n        };\r\n    }\r\n\r\n    [Test]\r\n    public void TestRightAngleDirections()\r\n    {\r\n        foreach (var direction in Enum.GetValues<TriangleRightAngleDirection>())\r\n        {\r\n            AddStep($\"Test direction {direction}\", () =>\r\n            {\r\n                rightTriangle.RightAngleDirection = direction;\r\n            });\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Graphics/TestSceneSingerToolTip.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\nusing osu.Game.Rulesets.Karaoke.Graphics.Cursor;\r\nusing osu.Game.Tests.Visual;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Graphics;\r\n\r\n[TestFixture]\r\npublic partial class TestSceneSingerToolTip : OsuTestScene\r\n{\r\n    private SingerToolTip toolTip = null!;\r\n\r\n    [SetUp]\r\n    public void SetUp() => Schedule(() =>\r\n    {\r\n        Child = toolTip = new SingerToolTip\r\n        {\r\n            Anchor = Anchor.Centre,\r\n            Origin = Anchor.Centre,\r\n        };\r\n        toolTip.Show();\r\n    });\r\n\r\n    [Test]\r\n    public void TestDisplayToolTip()\r\n    {\r\n        setTooltip(\"Test normal singer\", singer => { singer.Name = \"Normal singer\"; });\r\n\r\n        setTooltip(\"Test singer with description\", singer =>\r\n        {\r\n            singer.Name = \"Singer with description\";\r\n            singer.Description = \"International superstar vocaloid Hatsune Miku.\";\r\n        });\r\n\r\n        setTooltip(\"Test singer with large description\", singer =>\r\n        {\r\n            singer.Name = \"Singer with large description\";\r\n            singer.Description =\r\n                \"International superstar vocaloid Hatsune Miku on Sept 9 assumed her new position as Coronavirus Countermeasure Supporter in the Office for Novel Coronavirus Disease Control of the Japanese government’s Cabinet Secretariat.\";\r\n        });\r\n\r\n        setTooltip(\"Test singer with english name\", singer =>\r\n        {\r\n            singer.Name = \"Singer with English name\";\r\n            singer.EnglishName = \"Hatsune Miku\";\r\n        });\r\n\r\n        setTooltip(\"Test singer with romanisation\", singer =>\r\n        {\r\n            singer.Name = \"Singer with romanisation\";\r\n            singer.Romanisation = \"Hatsune Miku\";\r\n        });\r\n\r\n        setTooltip(\"Test singer with large context\", singer =>\r\n        {\r\n            singer.Name = \"Singer with romanisation large large large large large large large large large\";\r\n            singer.Romanisation = \"Hatsune Miku large large large large large large large large large\";\r\n            singer.EnglishName = \"Hatsune Miku large large large large large large large large large\";\r\n            singer.Description =\r\n                \"International superstar vocaloid Hatsune Miku on Sept 9 assumed her new position as Coronavirus Countermeasure Supporter in the Office for Novel Coronavirus Disease Control of the Japanese government’s Cabinet Secretariat.\";\r\n        });\r\n    }\r\n\r\n    private void setTooltip(string testName, Action<Singer> callBack)\r\n    {\r\n        AddStep(testName, () =>\r\n        {\r\n            var singer = new Singer();\r\n            callBack(singer);\r\n            toolTip.SetContent(singer);\r\n        });\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Helper/TestCaseCheckHelper.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Globalization;\r\nusing System.Linq;\r\nusing System.Reflection;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\nusing osu.Game.Rulesets.Karaoke.Edit;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos.Classic;\r\nusing osu.Game.Rulesets.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Helper;\r\n\r\npublic class TestCaseCheckHelper\r\n{\r\n    public static IEnumerable<ICheck> GetAllAvailableChecks()\r\n    {\r\n        var beatmapVerifier = new KaraokeBeatmapVerifier();\r\n        var field = typeof(KaraokeBeatmapVerifier).GetField(\"checks\", BindingFlags.Instance | BindingFlags.NonPublic);\r\n        if (field == null)\r\n            throw new ArgumentNullException(nameof(field));\r\n\r\n        return (List<ICheck>)field.GetValue(beatmapVerifier)!;\r\n    }\r\n\r\n    public static IReadOnlyDictionary<ICheck, IEnumerable<IssueTemplate>> GetAllAvailableIssueTemplates()\r\n    {\r\n        return GetAllAvailableChecks().ToDictionary(k => k, v => v.PossibleTemplates);\r\n    }\r\n\r\n    public static IReadOnlyDictionary<ICheck, Issue[]> CreateAllAvailableIssues()\r\n    {\r\n        return GetAllAvailableIssueTemplates().ToDictionary(k => k.Key,\r\n            v => v.Value.Select(createIssueByIssueTemplate).ToArray());\r\n\r\n        Issue createIssueByIssueTemplate(IssueTemplate issueTemplate)\r\n        {\r\n            var method = issueTemplate.GetType().GetMethod(\"Create\");\r\n            if (method == null)\r\n                throw new MissingMethodException(\"Test method is not exist.\");\r\n\r\n            object[] paramsInfo = method.GetParameters().Select(generateDefaultTypeByParameterInfo).ToArray();\r\n            if (method.Invoke(issueTemplate, paramsInfo) is not Issue issue)\r\n                throw new InvalidOperationException(\"Issue should not be null.\");\r\n\r\n            return issue;\r\n        }\r\n\r\n        object generateDefaultTypeByParameterInfo(ParameterInfo parameterInfos) =>\r\n            parameterInfos.ParameterType switch\r\n            {\r\n                // Metadata in the beatmap.\r\n                Type t when ReferenceEquals(t, typeof(Page)) => new Page(),\r\n                Type t when ReferenceEquals(t, typeof(ClassicLyricTimingPoint)) => new ClassicLyricTimingPoint(),\r\n                Type t when ReferenceEquals(t, typeof(IEnumerable<HitObject>)) => new List<HitObject>(),\r\n                // Hit-object.\r\n                Type t when ReferenceEquals(t, typeof(Lyric)) => new Lyric(),\r\n                Type t when ReferenceEquals(t, typeof(Note)) => new Note(),\r\n                Type t when ReferenceEquals(t, typeof(RubyTag)) => new RubyTag(),\r\n                Type t when ReferenceEquals(t, typeof(TimeTag)) => new TimeTag(new TextIndex(), 0),\r\n                // Other type.\r\n                Type t when ReferenceEquals(t, typeof(int)) => 0,\r\n                Type t when ReferenceEquals(t, typeof(CultureInfo)) => new CultureInfo(\"Ja-jp\"),\r\n                _ => throw new InvalidOperationException(),\r\n            };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Helper/TestCaseElementIdHelper.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Helper;\r\n\r\npublic static class TestCaseElementIdHelper\r\n{\r\n    /// <summary>\r\n    /// Because it's hard to create characters as id in the test case.\r\n    /// So create a tool to convert the number to ElementId.\r\n    /// </summary>\r\n    /// <example>\r\n    /// 1       -> 0000001<br/>\r\n    /// 2       -> 0000002<br/>\r\n    /// -1      -> fffffff<br/>\r\n    /// -2      -> ffffffe<br/>\r\n    /// </example>\r\n    /// <param name=\"number\"></param>\r\n    /// <returns></returns>\r\n    public static ElementId CreateElementIdByNumber(int number)\r\n    {\r\n        const int length = 7;\r\n        string id = string.Concat(number.ToString(\"x\").PadLeft(length, '0').TakeLast(length));\r\n        return new ElementId(id);\r\n    }\r\n\r\n    public static ElementId[] CreateElementIdsByNumbers(IEnumerable<int> ids)\r\n        => ids.Select(CreateElementIdByNumber).ToArray();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Helper/TestCaseNoteHelper.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Extensions;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Helper;\r\n\r\npublic static class TestCaseNoteHelper\r\n{\r\n    public static Lyric CreateLyricForNote(int id, string text, double startTime, double duration)\r\n    {\r\n        return new Lyric\r\n        {\r\n            Text = text,\r\n            TimeTags = new List<TimeTag>\r\n            {\r\n                new(new TextIndex(0), startTime),\r\n                new(new TextIndex(text.Length - 1, TextIndex.IndexState.End), startTime + duration),\r\n            },\r\n        }.ChangeId(id);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Helper/TestCaseTagHelper.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing System.Text.RegularExpressions;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\nusing osu.Game.Rulesets.Karaoke.Extensions;\r\nusing osu.Game.Rulesets.Karaoke.Integration.Formats;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Extensions;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Helper;\r\n\r\npublic static class TestCaseTagHelper\r\n{\r\n    private const string char_index_range_str = @\"\\[(?<start>[-0-9]+)(?:,(?<end>[-0-9]+))?\\]\";\r\n    private const string time_index_regex = @\"\\[(?<index>[-0-9]+),(?<state>start|end)]\";\r\n    private const string time_range_str = @\"\\[(?<startTime>[-0-9]+)(?:,(?<endTime>[-0-9]+))?\\]\";\r\n    private const string id_str = \"(?<id>[-0-9]+)]\";\r\n\r\n    private static string getStringPropertyRegex(char prefix, string propertyName)\r\n        => @$\"(?:{prefix}(?<{propertyName}>[^\\s]+))?\";\r\n\r\n    private static string getNumberPropertyRegex(char prefix, string propertyName)\r\n        => $\"(?:{prefix}(?<{propertyName}>[-0-9]+|s*|))?\";\r\n\r\n    private static string generateRegex(string regexPrefix, IEnumerable<string> regexProperties)\r\n        => regexPrefix + string.Join(\"\", regexProperties);\r\n\r\n    private static TObject getMatchByStatement<TObject>(string? str, string regexStr, Func<Match?, TObject> returnValue)\r\n    {\r\n        if (string.IsNullOrEmpty(str))\r\n            return returnValue(null);\r\n\r\n        var regex = new Regex(regexStr);\r\n        var result = regex.Match(str);\r\n        if (!result.Success)\r\n            throw new RegexMatchTimeoutException(nameof(str));\r\n\r\n        return returnValue(result);\r\n    }\r\n\r\n    /// <summary>\r\n    /// Process test case ruby string format into <see cref=\"RubyTag\"/>\r\n    /// </summary>\r\n    /// <example>\r\n    /// [0]:ruby        -> has range index with text.<br/>\r\n    /// [0,3]:ruby      -> has range index with text.<br/>\r\n    /// [0]:            -> has range index with empty text.<br/>\r\n    /// [0,3]:          -> has range index with empty text.<br/>\r\n    /// [0]             -> has range index with empty text.<br/>\r\n    /// [0,3]           -> has range index with empty text.<br/>\r\n    /// </example>\r\n    /// <param name=\"str\">Ruby tag string format</param>\r\n    /// <returns><see cref=\"RubyTag\"/>Ruby tag object</returns>\r\n    public static RubyTag ParseRubyTag(string? str)\r\n    {\r\n        string regex = generateRegex(char_index_range_str, new[]\r\n        {\r\n            getStringPropertyRegex(':', \"ruby\"),\r\n        });\r\n\r\n        return getMatchByStatement(str, regex, result =>\r\n        {\r\n            if (result == null)\r\n                return new RubyTag();\r\n\r\n            int startIndex = result.GetGroupValue<int>(\"start\");\r\n            int? endIndex = result.GetGroupValue<int?>(\"end\");\r\n            string text = result.GetGroupValue<string>(\"ruby\");\r\n\r\n            return new RubyTag\r\n            {\r\n                StartIndex = startIndex,\r\n                EndIndex = endIndex ?? startIndex,\r\n                Text = text,\r\n            };\r\n        });\r\n    }\r\n\r\n    /// <summary>\r\n    /// Process test case time tag string format into <see cref=\"TimeTag\"/>\r\n    /// </summary>\r\n    /// <example>\r\n    /// [0,start]:1000              -> has time-tag index with time.<br/>\r\n    /// [0,start]                   -> has time-tag index with no time.<br/>\r\n    /// [0,start]:                  -> has time-tag index with no time.<br/>\r\n    /// [0,start]#karaoke           -> has time-tag index with romanised syllable.<br/>\r\n    /// [0,start]#^karaoke          -> has time-tag index with romanised syllable, and it's the first one.<br/>\r\n    /// [0,start]:1000#karaoke      -> has time-tag index with time and romanised syllable.<br/>\r\n    /// </example>\r\n    /// <param name=\"str\">Time tag string format</param>\r\n    /// <returns><see cref=\"TimeTag\"/>Time tag object</returns>\r\n    public static TimeTag ParseTimeTag(string? str)\r\n    {\r\n        string regex = generateRegex(time_index_regex, new[]\r\n        {\r\n            getNumberPropertyRegex(':', \"time\"),\r\n            getStringPropertyRegex('#', \"text\"),\r\n        });\r\n\r\n        return getMatchByStatement(str, regex, result =>\r\n        {\r\n            if (result == null)\r\n                return new TimeTag(new TextIndex());\r\n\r\n            int index = result.GetGroupValue<int>(\"index\");\r\n            var state = result.GetGroupValue<string>(\"state\") == \"start\" ? TextIndex.IndexState.Start : TextIndex.IndexState.End;\r\n            int? time = result.GetGroupValue<int?>(\"time\");\r\n            string? text = result.GetGroupValue<string?>(\"text\");\r\n            bool? firstSyllable = text?.StartsWith('^');\r\n\r\n            return new TimeTag(new TextIndex(index, state), time)\r\n            {\r\n                FirstSyllable = firstSyllable ?? default,\r\n                RomanisedSyllable = text?.Replace(\"^\", \"\"),\r\n            };\r\n        });\r\n    }\r\n\r\n    /// <summary>\r\n    /// Process test case text index string format into <see cref=\"TextIndex\"/>\r\n    /// </summary>\r\n    /// <example>\r\n    /// [0,start]   -> has time-tag index with time.<br/>\r\n    /// [0,end]     -> has time-tag index with time.<br/>\r\n    /// </example>\r\n    /// <param name=\"str\">Text tag string format</param>\r\n    /// <returns><see cref=\"TimeTag\"/>Text tag object</returns>\r\n    public static TextIndex ParseTextIndex(string? str)\r\n    {\r\n        string regex = generateRegex(time_index_regex, Array.Empty<string>());\r\n\r\n        return getMatchByStatement(str, regex, result =>\r\n        {\r\n            if (result == null)\r\n                return new TextIndex();\r\n\r\n            int index = result.GetGroupValue<int>(\"index\");\r\n            var state = result.GetGroupValue<string>(\"state\") == \"start\" ? TextIndex.IndexState.Start : TextIndex.IndexState.End;\r\n\r\n            return new TextIndex(index, state);\r\n        });\r\n    }\r\n\r\n    /// <summary>\r\n    /// Process test case lyric string format into <see cref=\"Lyric\"/>\r\n    /// </summary>\r\n    /// <example>\r\n    /// [1000,3000]:karaoke     -> has time-range and lyric.<br/>\r\n    /// [1000,3000]:            -> has time-range.<br/>\r\n    /// [1000,3000]             -> has time-range.<br/>\r\n    /// </example>\r\n    /// <param name=\"str\">Lyric string format</param>\r\n    /// <param name=\"id\">Id if needed</param>\r\n    /// <returns><see cref=\"Lyric\"/>Lyric object</returns>\r\n    public static Lyric ParseLyric(string str, int? id = null)\r\n    {\r\n        string regex = generateRegex(time_range_str, new[]\r\n        {\r\n            getStringPropertyRegex(':', \"lyric\"),\r\n        });\r\n\r\n        return getMatchByStatement(str, regex, result =>\r\n        {\r\n            if (result == null)\r\n                return new Lyric();\r\n\r\n            double startTime = result.GetGroupValue<double>(\"startTime\");\r\n            double endTime = result.GetGroupValue<double>(\"endTime\");\r\n            string text = result.GetGroupValue<string>(\"lyric\");\r\n\r\n            return new Lyric\r\n            {\r\n                Text = text,\r\n                TimeTags = new[]\r\n                {\r\n                    new TimeTag(new TextIndex(0), startTime),\r\n                    new TimeTag(new TextIndex(text.Length - 1, TextIndex.IndexState.End), endTime),\r\n                },\r\n            }.ChangeId(id != null ? TestCaseElementIdHelper.CreateElementIdByNumber(id.Value) : ElementId.Empty);\r\n        });\r\n    }\r\n\r\n    /// <summary>\r\n    /// Process test case lyric string format into <see cref=\"Lyric\"/>\r\n    /// </summary>\r\n    /// <example>\r\n    /// \"[00:01.00]か[00:02.00]ら[00:03.00]お[00:04.00]け[00:05.00]\"\r\n    /// </example>\r\n    /// <param name=\"str\">Lyric string format</param>\r\n    /// <returns><see cref=\"Lyric\"/>Lyric object</returns>\r\n    public static Lyric ParseLyricWithTimeTag(string? str)\r\n    {\r\n        if (string.IsNullOrEmpty(str))\r\n            return new Lyric();\r\n\r\n        // Create karaoke note decoder\r\n        var decoder = new KarDecoder();\r\n        return decoder.Decode(str).First();\r\n    }\r\n\r\n    /// <summary>\r\n    /// Process test case singer string format into <see cref=\"Singer\"/>\r\n    /// </summary>\r\n    /// <example>\r\n    /// [0]name:singer001       -> singer with id and name.<br/>\r\n    /// [0]romanisation:singer001     -> singer with id and romanisation.<br/>\r\n    /// [0]eg:singer001         -> singer with id and english name.<br/>\r\n    /// </example>\r\n    /// <param name=\"str\">Singer string format</param>\r\n    /// <returns><see cref=\"Singer\"/>sSinger object</returns>\r\n    public static Singer ParseSinger(string? str)\r\n    {\r\n        string regex = generateRegex(id_str, Array.Empty<string>());\r\n\r\n        return getMatchByStatement(str, regex, result =>\r\n        {\r\n            if (result == null)\r\n                return new Singer().ChangeId(ElementId.Empty);\r\n\r\n            // todo : implementation\r\n            int id = result.GetGroupValue<int>(\"id\");\r\n\r\n            return new Singer().ChangeId(id);\r\n        });\r\n    }\r\n\r\n    public static RubyTag[] ParseRubyTags(IEnumerable<string?> strings)\r\n        => strings.Select(ParseRubyTag).ToArray();\r\n\r\n    public static TimeTag[] ParseTimeTags(IEnumerable<string?> strings)\r\n        => strings.Select(ParseTimeTag).ToArray();\r\n\r\n    public static Lyric[] ParseLyrics(IEnumerable<string> strings)\r\n        => strings.Select((str, index) => ParseLyric(str, index)).ToArray();\r\n\r\n    public static Singer[] ParseSingers(IEnumerable<string?> strings)\r\n        => strings.Select(ParseSinger).ToArray();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Helper/TestCaseToneHelper.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Helper;\r\n\r\npublic static class TestCaseToneHelper\r\n{\r\n    public static Tone NumberToTone(double tone)\r\n    {\r\n        bool half = Math.Abs(tone) % 1 == 0.5;\r\n        int scale = tone < 0 ? (int)tone - (half ? 1 : 0) : (int)tone;\r\n        return new Tone(scale, half);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/IO/Serialization/Converters/BaseSingleConverterTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing Newtonsoft.Json;\r\nusing osu.Game.IO.Serialization;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.IO.Serialization.Converters;\r\n\r\npublic abstract class BaseSingleConverterTest<TConverter> where TConverter : JsonConverter, new()\r\n{\r\n    protected JsonSerializerSettings CreateSettings()\r\n    {\r\n        var globalSetting = JsonSerializableExtensions.CreateGlobalSettings();\r\n        globalSetting.Formatting = Formatting.None; // do not change new line in testing.\r\n        globalSetting.Converters.Add(new TConverter());\r\n\r\n        var converters = CreateExtraConverts();\r\n\r\n        foreach (var converter in converters)\r\n        {\r\n            globalSetting.Converters.Add(converter);\r\n        }\r\n\r\n        return globalSetting;\r\n    }\r\n\r\n    protected virtual IEnumerable<JsonConverter> CreateExtraConverts() => Array.Empty<JsonConverter>();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/IO/Serialization/Converters/ColourConverterTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing Newtonsoft.Json;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Extensions.Color4Extensions;\r\nusing osu.Game.Rulesets.Karaoke.IO.Serialization.Converters;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.IO.Serialization.Converters;\r\n\r\npublic class ColourConverterTest : BaseSingleConverterTest<ColourConverter>\r\n{\r\n    [TestCase(\"#aaaaaa\", \"#AAAAAA\")]\r\n    [TestCase(\"#aaaaaaaa\", \"#AAAAAAAA\")]\r\n    public void TestSerialize(string hex, string json)\r\n    {\r\n        var colour = Color4Extensions.FromHex(hex);\r\n\r\n        string expected = $\"\\\"{json}\\\"\";\r\n        string actual = JsonConvert.SerializeObject(colour, CreateSettings());\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(\"#aaaaaa\", \"#AAAAAA\")]\r\n    [TestCase(\"#AAAAAA\", \"#AAAAAA\")]\r\n    [TestCase(\"#AAAAAAAA\", \"#AAAAAAAA\")]\r\n    [TestCase(\"\", null)] // should throw exception\r\n    [TestCase(null, null)] // should throw exception\r\n    public void TestDeserialize(string? json, string? hex)\r\n    {\r\n        if (hex != null)\r\n        {\r\n            var expected = Color4Extensions.FromHex(hex);\r\n            var actual = JsonConvert.DeserializeObject<Color4>($\"\\\"{json}\\\"\", CreateSettings());\r\n            Assert.That(expected, Is.EqualTo(actual));\r\n        }\r\n        else\r\n        {\r\n            Assert.Catch(() => JsonConvert.DeserializeObject<Color4>($\"\\\"{json}\\\"\", CreateSettings()));\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/IO/Serialization/Converters/CultureInfoConverterTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Globalization;\r\nusing Newtonsoft.Json;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.IO.Serialization.Converters;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.IO.Serialization.Converters;\r\n\r\n[TestFixture]\r\npublic class CultureInfoConverterTest : BaseSingleConverterTest<CultureInfoConverter>\r\n{\r\n    [TestCase(1, \"1\")]\r\n    [TestCase(null, \"null\")]\r\n    public void TestSerialize(int? lcid, string json)\r\n    {\r\n        var language = lcid != null ? new CultureInfo(lcid.Value) : default;\r\n        string actual = JsonConvert.SerializeObject(language, CreateSettings());\r\n        Assert.That(actual, Is.EqualTo(json));\r\n    }\r\n\r\n    [TestCase(\"1\", 1)]\r\n    [TestCase(\"null\", null)]\r\n    public void TestDeserialize(string json, int? lcid)\r\n    {\r\n        var result = JsonConvert.DeserializeObject<CultureInfo>(json, CreateSettings());\r\n        Assert.That(result?.LCID, Is.EqualTo(lcid));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/IO/Serialization/Converters/ElementIdConverterTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing Newtonsoft.Json;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.IO.Serialization.Converters;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.IO.Serialization.Converters;\r\n\r\npublic class ElementIdConverterTest : BaseSingleConverterTest<ElementIdConverter>\r\n{\r\n    [TestCase(\"1234567\", \"\\\"1234567\\\"\")]\r\n    [TestCase(\"\", \"\\\"\\\"\")]\r\n    [TestCase(null, \"null\")] // support the case with nullable property.\r\n    public void TestSerialize(string? id, string json)\r\n    {\r\n        var elementId = createElementId(id);\r\n        string actual = JsonConvert.SerializeObject(elementId, CreateSettings());\r\n        Assert.That(actual, Is.EqualTo(json));\r\n    }\r\n\r\n    [TestCase(\"\\\"1234567\\\"\", \"1234567\")]\r\n    [TestCase(\"\\\"\\\"\", \"\")]\r\n    [TestCase(\"null\", null)] // support the case with nullable property.\r\n    public void TestDeserialize(string json, string? id)\r\n    {\r\n        var expected = createElementId(id);\r\n        var result = JsonConvert.DeserializeObject<ElementId?>(json, CreateSettings());\r\n        Assert.That(result, Is.EqualTo(expected));\r\n    }\r\n\r\n    private static ElementId? createElementId(string? str) =>\r\n        str switch\r\n        {\r\n            null => null,\r\n            \"\" => ElementId.Empty,\r\n            _ => new ElementId(str),\r\n        };\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/IO/Serialization/Converters/FontUsageConverterTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing Newtonsoft.Json;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.IO.Serialization.Converters;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.IO.Serialization.Converters;\r\n\r\npublic class FontUsageConverterTest : BaseSingleConverterTest<FontUsageConverter>\r\n{\r\n    [TestCase(\"\", 20, \"\", false, false, \"{}\")]\r\n    [TestCase(\"OpenSans\", 20, \"\", false, false, \"{\\\"family\\\":\\\"OpenSans\\\"}\")]\r\n    [TestCase(\"OpenSans\", 30, \"\", false, false, \"{\\\"family\\\":\\\"OpenSans\\\",\\\"size\\\":30.0}\")]\r\n    [TestCase(\"OpenSans\", 30, \"RegularItalic\", false, false, \"{\\\"family\\\":\\\"OpenSans\\\",\\\"weight\\\":\\\"RegularItalic\\\",\\\"size\\\":30.0}\")]\r\n    [TestCase(\"OpenSans\", 30, \"RegularItalic\", true, false, \"{\\\"family\\\":\\\"OpenSans\\\",\\\"weight\\\":\\\"RegularItalic\\\",\\\"size\\\":30.0,\\\"italics\\\":true}\")]\r\n    [TestCase(\"OpenSans\", 30, \"RegularItalic\", true, true, \"{\\\"family\\\":\\\"OpenSans\\\",\\\"weight\\\":\\\"RegularItalic\\\",\\\"size\\\":30.0,\\\"italics\\\":true,\\\"fixedWidth\\\":true}\")]\r\n    public void TestSerialize(string family, float size, string weight, bool italics, bool fixedWidth, string expected)\r\n    {\r\n        var font = new FontUsage(family, size, weight, italics, fixedWidth);\r\n\r\n        string actual = JsonConvert.SerializeObject(font, CreateSettings());\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(\"{}\", null, 20, null, false, false)]\r\n    [TestCase(\"{\\\"family\\\": \\\"OpenSans\\\"}\", \"OpenSans\", 20, null, false, false)]\r\n    [TestCase(\"{\\\"family\\\": \\\"OpenSans\\\",\\\"size\\\": 30.0}\", \"OpenSans\", 30, null, false, false)]\r\n    [TestCase(\"{\\\"family\\\": \\\"OpenSans\\\",\\\"weight\\\": \\\"RegularItalic\\\",\\\"size\\\": 30.0}\", \"OpenSans\", 30, \"RegularItalic\", false, false)]\r\n    [TestCase(\"{\\\"family\\\": \\\"OpenSans\\\",\\\"weight\\\": \\\"RegularItalic\\\",\\\"size\\\": 30.0,\\\"italics\\\": true}\", \"OpenSans\", 30, \"RegularItalic\", true, false)]\r\n    [TestCase(\"{\\\"family\\\": \\\"OpenSans\\\",\\\"weight\\\": \\\"RegularItalic\\\",\\\"size\\\": 30.0,\\\"italics\\\": true,\\\"fixedWidth\\\": true}\", \"OpenSans\", 30, \"RegularItalic\", true, true)]\r\n    public void TestDeserialize(string json, string? family, float size, string? weight, bool italics, bool fixedWidth)\r\n    {\r\n        var expected = new FontUsage(family, size, weight, italics, fixedWidth);\r\n        var actual = JsonConvert.DeserializeObject<FontUsage>(json, CreateSettings());\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/IO/Serialization/Converters/KaraokeSkinElementConverterTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing Newtonsoft.Json;\r\nusing NUnit.Framework;\r\nusing osu.Framework.IO.Serialization;\r\nusing osu.Game.Rulesets.Karaoke.IO.Serialization.Converters;\r\nusing osu.Game.Rulesets.Karaoke.Skinning.Elements;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Asserts;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.IO.Serialization.Converters;\r\n\r\npublic class KaraokeSkinElementConverterTest : BaseSingleConverterTest<KaraokeSkinElementConverter>\r\n{\r\n    protected override IEnumerable<JsonConverter> CreateExtraConverts()\r\n    {\r\n        yield return new ColourConverter();\r\n        yield return new Vector2Converter();\r\n        yield return new ShaderConverter();\r\n        yield return new FontUsageConverter();\r\n    }\r\n\r\n    [Test]\r\n    public void TestLyricConfigSerializer()\r\n    {\r\n        var lyricConfig = LyricFontInfo.CreateDefault();\r\n\r\n        const string expected =\r\n            \"{\\\"$type\\\":0,\\\"name\\\":\\\"Default\\\",\\\"smart_horizon\\\":2,\\\"lyrics_interval\\\":4,\\\"ruby_interval\\\":2,\\\"romanisation_interval\\\":2,\\\"ruby_alignment\\\":2,\\\"romanisation_alignment\\\":2,\\\"ruby_margin\\\":4,\\\"romanisation_margin\\\":4,\\\"main_text_font\\\":{\\\"family\\\":\\\"Torus\\\",\\\"weight\\\":\\\"Bold\\\",\\\"size\\\":48.0},\\\"ruby_text_font\\\":{\\\"family\\\":\\\"Torus\\\",\\\"weight\\\":\\\"Bold\\\"},\\\"romanisation_text_font\\\":{\\\"family\\\":\\\"Torus\\\",\\\"weight\\\":\\\"Bold\\\"}}\";\r\n        string actual = JsonConvert.SerializeObject(lyricConfig, CreateSettings());\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [Test]\r\n    public void TestLyricConfigDeserialize()\r\n    {\r\n        const string json =\r\n            \"{\\\"$type\\\":0,\\\"name\\\":\\\"Default\\\",\\\"smart_horizon\\\":2,\\\"lyrics_interval\\\":4,\\\"ruby_interval\\\":2,\\\"romanisation_interval\\\":2,\\\"ruby_alignment\\\":2,\\\"romanisation_alignment\\\":2,\\\"ruby_margin\\\":4,\\\"romanisation_margin\\\":4,\\\"main_text_font\\\":{\\\"family\\\":\\\"Torus\\\",\\\"weight\\\":\\\"Bold\\\",\\\"size\\\":48.0},\\\"ruby_text_font\\\":{\\\"family\\\":\\\"Torus\\\",\\\"weight\\\":\\\"Bold\\\"},\\\"romanisation_text_font\\\":{\\\"family\\\":\\\"Torus\\\",\\\"weight\\\":\\\"Bold\\\"}}\";\r\n\r\n        var expected = LyricFontInfo.CreateDefault();\r\n        var actual = (LyricFontInfo)JsonConvert.DeserializeObject<IKaraokeSkinElement>(json, CreateSettings())!;\r\n        ObjectAssert.ArePropertyEqual(expected, actual);\r\n    }\r\n\r\n    [Test]\r\n    public void TestNoteStyleSerializer()\r\n    {\r\n        var lyricConfig = NoteStyle.CreateDefault();\r\n\r\n        const string expected = \"{\\\"$type\\\":1,\\\"name\\\":\\\"Default\\\",\\\"note_color\\\":\\\"#44AADD\\\",\\\"blink_color\\\":\\\"#FF66AA\\\",\\\"text_color\\\":\\\"#FFFFFF\\\",\\\"bold_text\\\":true}\";\r\n        string actual = JsonConvert.SerializeObject(lyricConfig, CreateSettings());\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [Test]\r\n    public void TestNoteStyleDeserializer()\r\n    {\r\n        const string json = \"{\\\"$type\\\":1,\\\"name\\\":\\\"Default\\\",\\\"note_color\\\":\\\"#44AADD\\\",\\\"blink_color\\\":\\\"#FF66AA\\\",\\\"text_color\\\":\\\"#FFFFFF\\\",\\\"bold_text\\\":true}\";\r\n\r\n        var expected = NoteStyle.CreateDefault();\r\n        var actual = (NoteStyle)JsonConvert.DeserializeObject<IKaraokeSkinElement>(json, CreateSettings())!;\r\n        ObjectAssert.ArePropertyEqual(expected, actual);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/IO/Serialization/Converters/LyricConverterTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing Newtonsoft.Json;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.IO.Serialization.Converters;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Properties;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Asserts;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.IO.Serialization.Converters;\r\n\r\npublic class LyricConverterTest : BaseSingleConverterTest<LyricConverter>\r\n{\r\n    protected override IEnumerable<JsonConverter> CreateExtraConverts()\r\n    {\r\n        yield return new ReferenceLyricPropertyConfigConverter();\r\n        yield return new ElementIdConverter();\r\n    }\r\n\r\n    [Test]\r\n    public void TestLyricConverterWithNoConfig()\r\n    {\r\n        var lyric = new Lyric();\r\n\r\n        string expected =\r\n            $\"{{\\\"time_preempt\\\":600.0,\\\"time_fade_in\\\":400.0,\\\"start_time_bindable\\\":0.0,\\\"samples_bindable\\\":[],\\\"id\\\":\\\"{lyric.ID}\\\",\\\"text\\\":\\\"\\\",\\\"time_tags\\\":[],\\\"ruby_tags\\\":[],\\\"singer_ids\\\":[],\\\"translations\\\":{{}},\\\"samples\\\":[],\\\"auxiliary_samples\\\":[]}}\";\r\n        string actual = JsonConvert.SerializeObject(lyric, CreateSettings());\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [Test]\r\n    public void TestDeserializeWithNoConfig()\r\n    {\r\n        var expected = new Lyric();\r\n\r\n        string json =\r\n            $\"{{\\\"time_preempt\\\":600.0,\\\"time_fade_in\\\":400.0,\\\"start_time_bindable\\\":0.0,\\\"samples_bindable\\\":[],\\\"id\\\":\\\"{expected.ID}\\\",\\\"text\\\":\\\"\\\",\\\"time_tags\\\":[],\\\"ruby_tags\\\":[],\\\"singer_ids\\\":[],\\\"translations\\\":{{}},\\\"samples\\\":[],\\\"auxiliary_samples\\\":[]}}\";\r\n        var actual = JsonConvert.DeserializeObject<Lyric>(json, CreateSettings())!;\r\n\r\n        Assert.That(actual.ID, Is.EqualTo(expected.ID));\r\n        Assert.That(actual.Text, Is.EqualTo(expected.Text));\r\n        TimeTagAssert.ArePropertyEqual(expected.TimeTags, actual.TimeTags);\r\n        RubyTagAssert.ArePropertyEqual(expected.RubyTags, actual.RubyTags);\r\n        Assert.That(actual.StartTime, Is.EqualTo(expected.StartTime));\r\n        Assert.That(actual.Duration, Is.EqualTo(expected.Duration));\r\n        Assert.That(actual.EndTime, Is.EqualTo(expected.EndTime));\r\n        Assert.That(actual.SingerIds, Is.EqualTo(expected.SingerIds));\r\n        Assert.That(actual.Translations, Is.EqualTo(expected.Translations));\r\n        Assert.That(actual.Language, Is.EqualTo(expected.Language));\r\n        Assert.That(actual.Lock, Is.EqualTo(expected.Lock));\r\n        Assert.That(actual.ReferenceLyric, Is.EqualTo(expected.ReferenceLyric));\r\n        Assert.That(actual.ReferenceLyricId, Is.EqualTo(expected.ReferenceLyricId));\r\n        Assert.That(actual.ReferenceLyricConfig, Is.EqualTo(expected.ReferenceLyricConfig));\r\n    }\r\n\r\n    [Test]\r\n    public void TestLyricConverterWithSyncConfig()\r\n    {\r\n        var referencedLyric = new Lyric();\r\n        var lyric = new Lyric\r\n        {\r\n            ReferenceLyricId = referencedLyric.ID,\r\n            ReferenceLyric = referencedLyric,\r\n            ReferenceLyricConfig = new SyncLyricConfig(),\r\n        };\r\n\r\n        string expected =\r\n            $\"{{\\\"time_preempt\\\":600.0,\\\"time_fade_in\\\":400.0,\\\"start_time_bindable\\\":0.0,\\\"samples_bindable\\\":[],\\\"id\\\":\\\"{lyric.ID}\\\",\\\"reference_lyric_id\\\":\\\"{lyric.ReferenceLyricId}\\\",\\\"reference_lyric_config\\\":{{\\\"$type\\\":\\\"SyncLyricConfig\\\"}},\\\"samples\\\":[],\\\"auxiliary_samples\\\":[]}}\";\r\n        string actual = JsonConvert.SerializeObject(lyric, CreateSettings());\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [Test]\r\n    public void TestLyricConverterWithReferenceConfig()\r\n    {\r\n        var referencedLyric = new Lyric();\r\n        var lyric = new Lyric\r\n        {\r\n            ReferenceLyricId = referencedLyric.ID,\r\n            ReferenceLyric = referencedLyric,\r\n            ReferenceLyricConfig = new ReferenceLyricConfig(),\r\n        };\r\n\r\n        string expected =\r\n            $\"{{\\\"time_preempt\\\":600.0,\\\"time_fade_in\\\":400.0,\\\"start_time_bindable\\\":0.0,\\\"samples_bindable\\\":[],\\\"id\\\":\\\"{lyric.ID}\\\",\\\"text\\\":\\\"\\\",\\\"time_tags\\\":[],\\\"ruby_tags\\\":[],\\\"singer_ids\\\":[],\\\"translations\\\":{{}},\\\"reference_lyric_id\\\":\\\"{lyric.ReferenceLyricId}\\\",\\\"reference_lyric_config\\\":{{\\\"$type\\\":\\\"ReferenceLyricConfig\\\"}},\\\"samples\\\":[],\\\"auxiliary_samples\\\":[]}}\";\r\n        string actual = JsonConvert.SerializeObject(lyric, CreateSettings());\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/IO/Serialization/Converters/ReferenceLyricPropertyConfigConverterTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing Newtonsoft.Json;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.IO.Serialization.Converters;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Properties;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.IO.Serialization.Converters;\r\n\r\npublic class ReferenceLyricPropertyConfigConverterTest : BaseSingleConverterTest<ReferenceLyricPropertyConfigConverter>\r\n{\r\n    [Test]\r\n    public void TestReferenceLyricConfigSerializer()\r\n    {\r\n        var config = new ReferenceLyricConfig\r\n        {\r\n            OffsetTime = 100,\r\n        };\r\n\r\n        const string expected = \"{\\\"$type\\\":\\\"ReferenceLyricConfig\\\",\\\"offset_time\\\":100.0}\";\r\n        string actual = JsonConvert.SerializeObject(config, CreateSettings());\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [Test]\r\n    public void TestReferenceLyricConfigDeserializer()\r\n    {\r\n        const string json = \"{\\\"$type\\\":\\\"ReferenceLyricConfig\\\",\\\"offset_time\\\":100.0}\";\r\n\r\n        var expected = new ReferenceLyricConfig\r\n        {\r\n            OffsetTime = 100,\r\n        };\r\n        var actual = (ReferenceLyricConfig)JsonConvert.DeserializeObject<IReferenceLyricPropertyConfig>(json, CreateSettings())!;\r\n        Assert.That(actual.OffsetTime, Is.EqualTo(expected.OffsetTime));\r\n    }\r\n\r\n    [Test]\r\n    public void TestSyncLyricConfigSerializer()\r\n    {\r\n        var config = new SyncLyricConfig\r\n        {\r\n            OffsetTime = 100,\r\n            SyncSingerProperty = true,\r\n            SyncTimeTagProperty = false,\r\n        };\r\n\r\n        const string expected = \"{\\\"$type\\\":\\\"SyncLyricConfig\\\",\\\"offset_time\\\":100.0,\\\"sync_time_tag_property\\\":false}\";\r\n        string actual = JsonConvert.SerializeObject(config, CreateSettings());\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [Test]\r\n    public void TestSyncLyricConfigDeserializer()\r\n    {\r\n        const string json = \"{\\\"$type\\\":\\\"SyncLyricConfig\\\",\\\"offset_time\\\":100.0,\\\"sync_time_tag_property\\\":false}\";\r\n\r\n        var expected = new SyncLyricConfig\r\n        {\r\n            OffsetTime = 100,\r\n            SyncSingerProperty = true,\r\n            SyncTimeTagProperty = false,\r\n        };\r\n        var actual = (SyncLyricConfig)JsonConvert.DeserializeObject<IReferenceLyricPropertyConfig>(json, CreateSettings())!;\r\n        Assert.That(actual.OffsetTime, Is.EqualTo(expected.OffsetTime));\r\n        Assert.That(actual.SyncSingerProperty, Is.EqualTo(expected.SyncSingerProperty));\r\n        Assert.That(actual.SyncTimeTagProperty, Is.EqualTo(expected.SyncTimeTagProperty));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/IO/Serialization/Converters/RubyTagConverterTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing Newtonsoft.Json;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.IO.Serialization.Converters;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Asserts;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.IO.Serialization.Converters;\r\n\r\npublic class RubyTagConverterTest : BaseSingleConverterTest<RubyTagConverter>\r\n{\r\n    [TestCase(0, 1, \"ルビ\", \"[0,1]:ルビ\")]\r\n    [TestCase(0, 0, \"ルビ\", \"[0]:ルビ\")]\r\n    [TestCase(-1, -2, \"ルビ\", \"[-1,-2]:ルビ\")] // Should not check ruby is out of range in here.\r\n    [TestCase(0, 1, \"::[][]\", \"[0,1]:::[][]\")]\r\n    [TestCase(0, 1, \"\", \"[0,1]:\")]\r\n    public void TestSerialize(int startIndex, int endIndex, string text, string json)\r\n    {\r\n        var rubyTag = new RubyTag\r\n        {\r\n            StartIndex = startIndex,\r\n            EndIndex = endIndex,\r\n            Text = text,\r\n        };\r\n\r\n        string expected = $\"\\\"{json}\\\"\";\r\n        string actual = JsonConvert.SerializeObject(rubyTag, CreateSettings());\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(\"[0,1]:ルビ\", 0, 1, \"ルビ\")]\r\n    [TestCase(\"[0]:ルビ\", 0, 0, \"ルビ\")]\r\n    [TestCase(\"[-1,-2]:ルビ\", -1, -2, \"ルビ\")] // Should not check ruby is out of range in here.\r\n    [TestCase(\"[0,1]:::[][]\", 0, 1, \"::[][]\")]\r\n    [TestCase(\"[0,1]:\", 0, 1, null)] // todo: expected value should be string.empty.\r\n    [TestCase(\"[0,1]:null\", 0, 1, \"null\")]\r\n    [TestCase(\"\", 0, 0, \"\")] // Test deal with format is not right below.\r\n    [TestCase(\"[0,1]\", 0, 0, \"\")]\r\n    [TestCase(\"[1,]\", 0, 0, \"\")]\r\n    [TestCase(\"[,1]\", 0, 0, \"\")]\r\n    [TestCase(\"[]\", 0, 0, \"\")]\r\n    public void TestDeserialize(string json, int startIndex, int endIndex, string text)\r\n    {\r\n        var expected = new RubyTag\r\n        {\r\n            StartIndex = startIndex,\r\n            EndIndex = endIndex,\r\n            Text = text,\r\n        };\r\n        var actual = JsonConvert.DeserializeObject<RubyTag>($\"\\\"{json}\\\"\", CreateSettings()) ?? throw new InvalidCastException();\r\n        RubyTagAssert.ArePropertyEqual(expected, actual);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/IO/Serialization/Converters/RubyTagsConverterTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing Newtonsoft.Json;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.IO.Serialization.Converters;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Asserts;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.IO.Serialization.Converters;\r\n\r\npublic class RubyTagsConverterTest : BaseSingleConverterTest<RubyTagsConverter>\r\n{\r\n    protected override IEnumerable<JsonConverter> CreateExtraConverts()\r\n    {\r\n        yield return new RubyTagConverter();\r\n    }\r\n\r\n    [Test]\r\n    public void TestSerialize()\r\n    {\r\n        var timeTags = new[]\r\n        {\r\n            new RubyTag\r\n            {\r\n                StartIndex = 1,\r\n                EndIndex = 1,\r\n                Text = \"ビ\",\r\n            },\r\n            new RubyTag\r\n            {\r\n                StartIndex = 0,\r\n                EndIndex = 0,\r\n                Text = \"ル\",\r\n            },\r\n        };\r\n\r\n        const string expected = \"[\\\"[0]:ル\\\",\\\"[1]:ビ\\\"]\";\r\n        string actual = JsonConvert.SerializeObject(timeTags, CreateSettings());\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [Test]\r\n    public void TestDeserialize()\r\n    {\r\n        const string json = \"[\\\"[1]:ビ\\\",\\\"[0]:ル\\\"]\";\r\n\r\n        var expected = new[]\r\n        {\r\n            new RubyTag\r\n            {\r\n                StartIndex = 0,\r\n                EndIndex = 0,\r\n                Text = \"ル\",\r\n            },\r\n            new RubyTag\r\n            {\r\n                StartIndex = 1,\r\n                EndIndex = 1,\r\n                Text = \"ビ\",\r\n            },\r\n        };\r\n        var actual = JsonConvert.DeserializeObject<RubyTag[]>(json, CreateSettings()) ?? throw new InvalidCastException();\r\n        RubyTagAssert.ArePropertyEqual(expected, actual);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/IO/Serialization/Converters/ShaderConverterTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing Newtonsoft.Json;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Extensions.Color4Extensions;\r\nusing osu.Framework.Graphics.Shaders;\r\nusing osu.Game.Rulesets.Karaoke.IO.Serialization.Converters;\r\nusing osuTK;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.IO.Serialization.Converters;\r\n\r\npublic class ShaderConverterTest : BaseSingleConverterTest<ShaderConverter>\r\n{\r\n    protected override IEnumerable<JsonConverter> CreateExtraConverts()\r\n    {\r\n        yield return new ColourConverter();\r\n    }\r\n\r\n    [Test]\r\n    public void TestSerializer()\r\n    {\r\n        var shader = new ShadowShader\r\n        {\r\n            ShadowOffset = new Vector2(3),\r\n            ShadowColour = new Color4(0.5f, 0.5f, 0.5f, 0.5f),\r\n        };\r\n\r\n        const string expected = \"{\\\"$type\\\":\\\"ShadowShader\\\",\\\"shadow_colour\\\":\\\"#7F7F7F7F\\\",\\\"shadow_offset\\\":{\\\"x\\\":3.0,\\\"y\\\":3.0}}\";\r\n        string result = JsonConvert.SerializeObject(shader, CreateSettings());\r\n        Assert.That(result, Is.EqualTo(expected));\r\n    }\r\n\r\n    [Test]\r\n    public void TestDeserialize()\r\n    {\r\n        const string json = \"{\\\"$type\\\":\\\"ShadowShader\\\",\\\"shadow_colour\\\":\\\"#7F7F7F7F\\\",\\\"shadow_offset\\\":{\\\"x\\\":3.0,\\\"y\\\":3.0}}\";\r\n\r\n        var expected = new ShadowShader\r\n        {\r\n            ShadowOffset = new Vector2(3),\r\n            ShadowColour = new Color4(0.5f, 0.5f, 0.5f, 0.5f),\r\n        };\r\n        var actual = (ShadowShader)JsonConvert.DeserializeObject<ICustomizedShader>(json, CreateSettings())!;\r\n        Assert.That(actual.ShadowOffset, Is.EqualTo(expected.ShadowOffset));\r\n        Assert.That(actual.ShadowColour.ToHex(), Is.EqualTo(expected.ShadowColour.ToHex()));\r\n    }\r\n\r\n    [Test]\r\n    public void TestSerializerListItems()\r\n    {\r\n        var shader = new StepShader\r\n        {\r\n            Name = \"HelloShader\",\r\n            StepShaders = new[]\r\n            {\r\n                new ShadowShader\r\n                {\r\n                    ShadowOffset = new Vector2(3),\r\n                    ShadowColour = new Color4(0.5f, 0.5f, 0.5f, 0.5f),\r\n                },\r\n            },\r\n        };\r\n\r\n        const string expected =\r\n            \"{\\\"$type\\\":\\\"StepShader\\\",\\\"name\\\":\\\"HelloShader\\\",\\\"draw\\\":true,\\\"step_shaders\\\":[{\\\"$type\\\":\\\"ShadowShader\\\",\\\"shadow_colour\\\":\\\"#7F7F7F7F\\\",\\\"shadow_offset\\\":{\\\"x\\\":3.0,\\\"y\\\":3.0}}]}\";\r\n        string actual = JsonConvert.SerializeObject(shader, CreateSettings());\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [Test]\r\n    public void TestDeserializeListItems()\r\n    {\r\n        const string json =\r\n            \"{\\\"$type\\\":\\\"StepShader\\\",\\\"name\\\":\\\"HelloShader\\\",\\\"draw\\\":true,\\\"step_shaders\\\":[{\\\"$type\\\":\\\"ShadowShader\\\",\\\"shadow_colour\\\":\\\"#7F7F7F7F\\\",\\\"shadow_offset\\\":{\\\"x\\\":3.0,\\\"y\\\":3.0}}]}\";\r\n\r\n        var expected = new StepShader\r\n        {\r\n            Name = \"HelloShader\",\r\n            StepShaders = new[]\r\n            {\r\n                new ShadowShader\r\n                {\r\n                    ShadowOffset = new Vector2(3),\r\n                    ShadowColour = new Color4(0.5f, 0.5f, 0.5f, 0.5f),\r\n                },\r\n            },\r\n        };\r\n        var actual = (StepShader)JsonConvert.DeserializeObject<ICustomizedShader>(json, CreateSettings())!;\r\n\r\n        // test step shader.\r\n        Assert.That(actual.StepShaders.Count, Is.EqualTo(expected.StepShaders.Count));\r\n\r\n        // test shadow shader inside.\r\n        var expectedShadowShader = (ShadowShader)expected.StepShaders.First();\r\n        var actualShadowShader = (ShadowShader)actual.StepShaders.First();\r\n        Assert.That(actualShadowShader.ShadowOffset, Is.EqualTo(expectedShadowShader.ShadowOffset));\r\n        Assert.That(actualShadowShader.ShadowColour.ToHex(), Is.EqualTo(expectedShadowShader.ShadowColour.ToHex()));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/IO/Serialization/Converters/StageInfoConverterTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing Newtonsoft.Json;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.IO.Serialization.Converters;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos.Classic;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos.Preview;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Asserts;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.IO.Serialization.Converters;\r\n\r\npublic class StageInfoConverterTest : BaseSingleConverterTest<StageInfoConverter>\r\n{\r\n    [Test]\r\n    public void TestClassicStageInfoSerializer()\r\n    {\r\n        var stageInfo = new ClassicStageInfo();\r\n\r\n        const string expected =\r\n            \"{\\\"$type\\\":\\\"classic\\\",\\\"stage_definition\\\":{\\\"border_width\\\":25.0,\\\"border_height\\\":25.0,\\\"fade_in_time\\\":150.0,\\\"fade_out_time\\\":150.0,\\\"fade_in_easing\\\":22,\\\"fade_out_easing\\\":22,\\\"lyric_scale\\\":2.0,\\\"line_height\\\":72.0,\\\"first_lyric_start_time_offset\\\":1000.0,\\\"lyric_end_time_offset\\\":300.0,\\\"last_lyric_end_time_offset\\\":10000.0},\\\"style_category\\\":{},\\\"lyric_layout_category\\\":{},\\\"lyric_timing_info\\\":{\\\"timings\\\":[],\\\"mappings\\\":{}}}\";\r\n        string actual = JsonConvert.SerializeObject(stageInfo, CreateSettings());\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [Test]\r\n    [Ignore(\"todo: need to implement the serializer for the ClassicLyricTimingInfo\")]\r\n    public void TestClassicStageInfoDeserializer()\r\n    {\r\n        const string json =\r\n            \"{\\\"$type\\\":\\\"classic\\\",\\\"stage_definition\\\":{\\\"border_width\\\":25.0,\\\"border_height\\\":25.0,\\\"fade_in_time\\\":150.0,\\\"fade_out_time\\\":150.0,\\\"fade_in_easing\\\":22,\\\"fade_out_easing\\\":22,\\\"lyric_scale\\\":2.0,\\\"line_height\\\":72.0,\\\"first_lyric_start_time_offset\\\":1000.0,\\\"lyric_end_time_offset\\\":300.0,\\\"last_lyric_end_time_offset\\\":10000.0},\\\"style_category\\\":{},\\\"lyric_layout_category\\\":{},\\\"lyric_timing_info\\\":{\\\"timings\\\":[],\\\"mappings\\\":{}}}\";\r\n\r\n        var expected = new ClassicStageInfo();\r\n        var actual = (ClassicStageInfo)JsonConvert.DeserializeObject<StageInfo>(json, CreateSettings())!;\r\n        ObjectAssert.ArePropertyEqual(expected, actual);\r\n    }\r\n\r\n    [Test]\r\n    public void TestPreviewStageInfoSerializer()\r\n    {\r\n        var stageInfo = new PreviewStageInfo();\r\n\r\n        const string expected =\r\n            \"{\\\"$type\\\":\\\"preview\\\",\\\"stage_definition\\\":{\\\"blue_level\\\":0.5,\\\"dim_level\\\":0.5,\\\"fading_time\\\":300.0,\\\"fading_offset_position\\\":64.0,\\\"fade_in_easing\\\":22,\\\"fade_out_easing\\\":22,\\\"moving_in_easing\\\":22,\\\"move_out_easing\\\":22,\\\"inactive_alpha\\\":0.3,\\\"active_time\\\":350.0,\\\"active_easing\\\":22,\\\"number_of_lyrics\\\":5,\\\"lyric_height\\\":64.0,\\\"line_moving_time\\\":500.0,\\\"line_moving_easing\\\":22,\\\"line_moving_offset_time\\\":50.0}}\";\r\n        string actual = JsonConvert.SerializeObject(stageInfo, CreateSettings());\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [Test]\r\n    public void TestPreviewStageInfoDeserializer()\r\n    {\r\n        const string json =\r\n            \"{\\\"$type\\\":\\\"preview\\\",\\\"stage_definition\\\":{\\\"blue_level\\\":0.5,\\\"dim_level\\\":0.5,\\\"fading_time\\\":300.0,\\\"fading_offset_position\\\":64.0,\\\"fade_in_easing\\\":22,\\\"fade_out_easing\\\":22,\\\"moving_in_easing\\\":22,\\\"move_out_easing\\\":22,\\\"inactive_alpha\\\":0.3,\\\"active_time\\\":350.0,\\\"active_easing\\\":22,\\\"number_of_lyrics\\\":5,\\\"lyric_height\\\":64.0,\\\"line_moving_time\\\":500.0,\\\"line_moving_easing\\\":22,\\\"line_moving_offset_time\\\":50.0}}\";\r\n\r\n        var expected = new PreviewStageInfo();\r\n        var actual = (PreviewStageInfo)JsonConvert.DeserializeObject<StageInfo>(json, CreateSettings())!;\r\n        ObjectAssert.ArePropertyEqual(expected, actual);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/IO/Serialization/Converters/TimeTagConverterTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing Newtonsoft.Json;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.IO.Serialization.Converters;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.IO.Serialization.Converters;\r\n\r\n[TestFixture]\r\npublic class TimeTagConverterTest : BaseSingleConverterTest<TimeTagConverter>\r\n{\r\n    [TestCase(1, TextIndex.IndexState.Start, 1000, \"[1,start]:1000\")]\r\n    [TestCase(1, TextIndex.IndexState.End, 1000, \"[1,end]:1000\")]\r\n    [TestCase(-1, TextIndex.IndexState.Start, 1000, \"[-1,start]:1000\")] // Should not check index is out of range in here.\r\n    [TestCase(1, TextIndex.IndexState.Start, -1000, \"[1,start]:-1000\")] // Should not check if time is negative.\r\n    [TestCase(1, TextIndex.IndexState.Start, null, \"[1,start]:\")]\r\n    public void TestSerialize(int index, TextIndex.IndexState state, int? time, string json)\r\n    {\r\n        var timeTag = new TimeTag(new TextIndex(index, state), time);\r\n\r\n        string expected = $\"\\\"{json}\\\"\";\r\n        string actual = JsonConvert.SerializeObject(timeTag, CreateSettings());\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(\"[1,start]:1000\", 1, TextIndex.IndexState.Start, 1000)]\r\n    [TestCase(\"[1,end]:1000\", 1, TextIndex.IndexState.End, 1000)]\r\n    [TestCase(\"[-1,start]:1000\", -1, TextIndex.IndexState.Start, 1000)] // Should not check index is out of range in here.\r\n    [TestCase(\"[1,start]:-1000\", 1, TextIndex.IndexState.Start, -1000)] // Should not check if time is negative.\r\n    [TestCase(\"[1,start]:\", 1, TextIndex.IndexState.Start, null)]\r\n    [TestCase(\"\", 0, TextIndex.IndexState.Start, null)] // Test deal with format is not right below.\r\n    [TestCase(\"[1,???]:\", 0, TextIndex.IndexState.Start, null)]\r\n    [TestCase(\"[1,]\", 0, TextIndex.IndexState.Start, null)]\r\n    [TestCase(\"[,start]\", 0, TextIndex.IndexState.Start, null)]\r\n    [TestCase(\"[]\", 0, TextIndex.IndexState.Start, null)]\r\n    public void TestDeserialize(string json, int index, TextIndex.IndexState state, int? time)\r\n    {\r\n        var expected = new TimeTag(new TextIndex(index, state), time);\r\n        var actual = JsonConvert.DeserializeObject<TimeTag>($\"\\\"{json}\\\"\", CreateSettings())!;\r\n        Assert.That(actual.Index, Is.EqualTo(expected.Index));\r\n        Assert.That(actual.Time, Is.EqualTo(expected.Time));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/IO/Serialization/Converters/TimeTagsConverterTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing Newtonsoft.Json;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.IO.Serialization.Converters;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Asserts;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.IO.Serialization.Converters;\r\n\r\n[TestFixture]\r\npublic class TimeTagsConverterTest : BaseSingleConverterTest<TimeTagsConverter>\r\n{\r\n    protected override IEnumerable<JsonConverter> CreateExtraConverts()\r\n    {\r\n        yield return new TimeTagConverter();\r\n    }\r\n\r\n    [Test]\r\n    public void TestSerialize()\r\n    {\r\n        var timeTags = new[]\r\n        {\r\n            new TimeTag(new TextIndex(0, TextIndex.IndexState.End), 1000),\r\n            new TimeTag(new TextIndex(0), 0),\r\n        };\r\n\r\n        const string expected = \"[\\\"[0,start]:0\\\",\\\"[0,end]:1000\\\"]\";\r\n        string actual = JsonConvert.SerializeObject(timeTags, CreateSettings());\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [Test]\r\n    public void TestDeserialize()\r\n    {\r\n        const string json = \"[\\\"[0,end]:1000\\\",\\\"[0,start]:0\\\"]\";\r\n\r\n        var expected = new[]\r\n        {\r\n            new TimeTag(new TextIndex(0), 0),\r\n            new TimeTag(new TextIndex(0, TextIndex.IndexState.End), 1000),\r\n        };\r\n        var actual = JsonConvert.DeserializeObject<TimeTag[]>(json, CreateSettings()) ?? throw new InvalidCastException();\r\n        TimeTagAssert.ArePropertyEqual(expected, actual);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/IO/Serialization/Converters/ToneConverterTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing Newtonsoft.Json;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.IO.Serialization.Converters;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.IO.Serialization.Converters;\r\n\r\npublic class ToneConverterTest : BaseSingleConverterTest<ToneConverter>\r\n{\r\n    [TestCase(1, true, \"1.5\")]\r\n    [TestCase(1, false, \"1.0\")]\r\n    [TestCase(0, true, \"0.5\")]\r\n    [TestCase(0, false, \"0.0\")]\r\n    [TestCase(-1, true, \"-0.5\")]\r\n    [TestCase(-1, false, \"-1.0\")]\r\n    public void TestSerialize(int scale, bool half, string json)\r\n    {\r\n        var tone = new Tone\r\n        {\r\n            Scale = scale,\r\n            Half = half,\r\n        };\r\n\r\n        string expected = $\"{json}\";\r\n        string actual = JsonConvert.SerializeObject(tone, CreateSettings());\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(\"1.5\", 1, true)]\r\n    [TestCase(\"1.0\", 1, false)]\r\n    [TestCase(\"0.5\", 0, true)]\r\n    [TestCase(\"0.0\", 0, false)]\r\n    [TestCase(\"-0.5\", -1, true)]\r\n    [TestCase(\"-1.0\", -1, false)]\r\n    public void TestDeserialize(string json, int scale, bool half)\r\n    {\r\n        var expected = new Tone\r\n        {\r\n            Scale = scale,\r\n            Half = half,\r\n        };\r\n        var actual = JsonConvert.DeserializeObject<Tone>($\"{json}\", CreateSettings());\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/IO/Serialization/Converters/TranslationConverterTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Globalization;\r\nusing Newtonsoft.Json;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.IO.Serialization.Converters;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.IO.Serialization.Converters;\r\n\r\npublic class TranslationConverterTest : BaseSingleConverterTest<TranslationConverter>\r\n{\r\n    protected override IEnumerable<JsonConverter> CreateExtraConverts()\r\n    {\r\n        yield return new CultureInfoConverter();\r\n    }\r\n\r\n    [Test]\r\n    public void TestSerialize()\r\n    {\r\n        var translations = new Dictionary<CultureInfo, string>\r\n        {\r\n            { new CultureInfo(\"en-US\"), \"karaoke\" },\r\n            { new CultureInfo(\"Ja-jp\"), \"カラオケ\" },\r\n        };\r\n\r\n        const string expected = \"[{\\\"key\\\":1033,\\\"value\\\":\\\"karaoke\\\"},{\\\"key\\\":1041,\\\"value\\\":\\\"カラオケ\\\"}]\";\r\n        string actual = JsonConvert.SerializeObject(translations, CreateSettings());\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [Test]\r\n    public void TestDeserialize()\r\n    {\r\n        const string json = \"[{\\\"key\\\":1033,\\\"value\\\":\\\"karaoke\\\"},{\\\"key\\\":1041,\\\"value\\\":\\\"カラオケ\\\"}]\";\r\n\r\n        var expected = new Dictionary<CultureInfo, string>\r\n        {\r\n            { new CultureInfo(\"en-US\"), \"karaoke\" },\r\n            { new CultureInfo(\"Ja-jp\"), \"カラオケ\" },\r\n        };\r\n\r\n        var actual = JsonConvert.DeserializeObject<Dictionary<CultureInfo, string>>(json, CreateSettings()) ?? throw new InvalidCastException();\r\n        Assert.That(actual, Is.EquivalentTo(expected));\r\n\r\n        var actualWithInterface = JsonConvert.DeserializeObject<IDictionary<CultureInfo, string>>(json, CreateSettings()) ?? throw new InvalidCastException();\r\n        Assert.That(actualWithInterface, Is.EquivalentTo(expected));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/IO/Serialization/KaraokeJsonSerializableExtensionsTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing Newtonsoft.Json;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.IO.Serialization;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Asserts;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.IO.Serialization;\r\n\r\npublic class KaraokeJsonSerializableExtensionsTest\r\n{\r\n    [Test]\r\n    public void TestSerializeLyric()\r\n    {\r\n        var lyric = new Lyric();\r\n\r\n        string expected =\r\n            @$\"{{\"\"time_preempt\"\":600.0,\"\"time_fade_in\"\":400.0,\"\"start_time_bindable\"\":0.0,\"\"samples_bindable\"\":[],\"\"id\"\":\"\"{lyric.ID}\"\",\"\"text\"\":\"\"\"\",\"\"time_tags\"\":[],\"\"ruby_tags\"\":[],\"\"singer_ids\"\":[],\"\"translations\"\":[],\"\"samples\"\":[],\"\"auxiliary_samples\"\":[]}}\";\r\n\r\n        string actual = JsonConvert.SerializeObject(lyric, createSettings());\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [Test]\r\n    public void TestDeserializeLyric()\r\n    {\r\n        var expected = new Lyric();\r\n        string json =\r\n            @$\"{{\"\"time_preempt\"\":600.0,\"\"time_fade_in\"\":400.0,\"\"start_time_bindable\"\":0.0,\"\"samples_bindable\"\":[],\"\"id\"\":\"\"{expected.ID}\"\",\"\"text\"\":\"\"\"\",\"\"time_tags\"\":[],\"\"ruby_tags\"\":[],\"\"singer_ids\"\":[],\"\"translations\"\":[],\"\"samples\"\":[],\"\"auxiliary_samples\"\":[]}}\";\r\n\r\n        var actual = JsonConvert.DeserializeObject<Lyric>(json, createSettings())!;\r\n        ObjectAssert.ArePropertyEqual(expected, actual);\r\n    }\r\n\r\n    [Test]\r\n    public void TestSerializeNote()\r\n    {\r\n        var note = new Note();\r\n\r\n        const string expected =\r\n            @\"{\"\"time_preempt\"\":600.0,\"\"time_fade_in\"\":400.0,\"\"start_time_bindable\"\":0.0,\"\"samples_bindable\"\":[],\"\"samples\"\":[],\"\"auxiliary_samples\"\":[]}\";\r\n\r\n        string actual = JsonConvert.SerializeObject(note, createSettings());\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [Test]\r\n    public void TestDeserializeNote()\r\n    {\r\n        const string json =\r\n            @\"{\"\"time_preempt\"\":600.0,\"\"time_fade_in\"\":400.0,\"\"start_time_bindable\"\":0.0,\"\"samples_bindable\"\":[],\"\"samples\"\":[],\"\"auxiliary_samples\"\":[]}\";\r\n\r\n        var expected = new Note();\r\n        var actual = JsonConvert.DeserializeObject<Note>(json, createSettings())!;\r\n        ObjectAssert.ArePropertyEqual(expected, actual);\r\n    }\r\n\r\n    private JsonSerializerSettings createSettings()\r\n    {\r\n        var settings = KaraokeJsonSerializableExtensions.CreateGlobalSettings();\r\n        settings.Formatting = Formatting.None;\r\n        return settings;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/IO/Serialization/SkinJsonSerializableExtensionsTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Tests.IO.Serialization.Converters;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.IO.Serialization;\r\n\r\n[Ignore($\"Test case already in the {nameof(KaraokeSkinElementConverterTest)}\")]\r\npublic class SkinJsonSerializableExtensionsTest;\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/IO/Stores/BaseGlyphStoreTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Extensions;\r\nusing osu.Framework.Graphics.Textures;\r\nusing osu.Framework.IO.Stores;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Resources;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.IO.Stores;\r\n\r\n/// <summary>\r\n/// Use <see cref=\"GlyphStore\"/> as test case actual result for comparing.\r\n/// </summary>\r\n/// <typeparam name=\"TGlyphStore\"></typeparam>\r\npublic abstract class BaseGlyphStoreTest<TGlyphStore> where TGlyphStore : class, IGlyphStore\r\n{\r\n    protected TGlyphStore CustomizeGlyphStore { get; private set; } = null!;\r\n\r\n    protected GlyphStore GlyphStore { get; private set; } = null!;\r\n\r\n    [OneTimeSetUp]\r\n    protected void OneTimeSetUp()\r\n    {\r\n        // create and load glyph store.\r\n        var fontResourceStore = new NamespacedResourceStore<byte[]>(TestResources.GetStore(), \"Resources.Testing.Fonts.Fnt.OpenSans\");\r\n        GlyphStore = new GlyphStore(fontResourceStore, FontName);\r\n        GlyphStore.LoadFontAsync().WaitSafely();\r\n\r\n        // create load load customize glyph store.\r\n        var customizeFontResourceStore = new NamespacedResourceStore<byte[]>(TestResources.GetStore(), $\"Resources.Testing.Fonts.{FontType}\");\r\n        CustomizeGlyphStore = CreateFontStore(customizeFontResourceStore, FontName);\r\n        CustomizeGlyphStore.LoadFontAsync().WaitSafely();\r\n    }\r\n\r\n    protected abstract string FontType { get; }\r\n\r\n    protected abstract string FontName { get; }\r\n\r\n    protected abstract TGlyphStore CreateFontStore(ResourceStore<byte[]> store, string assetName);\r\n\r\n    [Test]\r\n    public void TestCompareFontNameWithOrigin()\r\n    {\r\n        string expected = GlyphStore.FontName;\r\n        string actual = CustomizeGlyphStore.FontName;\r\n        Assert.That(actual, Is.EqualTo(expected));\r\n    }\r\n\r\n    [TestCase('a')]\r\n    [TestCase(' ')]\r\n    [TestCase('ㄅ')] // should not have those texts in store.\r\n    [TestCase('あ')] // should not have those texts in store.\r\n    public void TestCompareHasGlyphWithOrigin(char c)\r\n    {\r\n        bool expected = GlyphStore.HasGlyph(c);\r\n        bool actual = CustomizeGlyphStore.HasGlyph(c);\r\n        Assert.That(actual, Is.EqualTo(expected));\r\n    }\r\n\r\n    [Test]\r\n    public void TestCompareGetBaseHeightWithOrigin()\r\n    {\r\n        float? expected = GlyphStore.Baseline;\r\n        float? actual = CustomizeGlyphStore.Baseline;\r\n        Assert.That(actual, Is.EqualTo(expected));\r\n    }\r\n\r\n    [TestCase('a')]\r\n    [TestCase('A')]\r\n    [TestCase('1')]\r\n    [TestCase(' ')]\r\n    [TestCase('@')]\r\n    [TestCase('#')]\r\n    public void TestCompareGetCharacterGlyphWithOrigin(char c)\r\n    {\r\n        var expected = GlyphStore.Get(c)!;\r\n        var actual = CustomizeGlyphStore.Get(c)!;\r\n\r\n        // because get character glyph should make sure that this glyph store contains char, so will not be null.\r\n        Assert.That(expected, Is.Not.Null);\r\n        Assert.That(actual, Is.Not.Null);\r\n\r\n        // test all property should be matched.\r\n        Assert.That(actual.Character, Is.EqualTo(expected.Character));\r\n        Assert.That(actual.XOffset, Is.EqualTo(expected.XOffset));\r\n        Assert.That(actual.YOffset, Is.EqualTo(expected.YOffset));\r\n        Assert.That(actual.XAdvance, Is.EqualTo(expected.XAdvance));\r\n    }\r\n\r\n    [TestCase('a', 'a')]\r\n    [TestCase('a', 'b')]\r\n    [TestCase('i', 'v')] // todo: should got a result that is not zero.\r\n    [TestCase('t', 'i')]\r\n    [TestCase('a', 'あ')]\r\n    public void TestCompareGetKerningWithOrigin(char left, char right)\r\n    {\r\n        int expected = GlyphStore.GetKerning(left, right);\r\n        int actual = CustomizeGlyphStore.GetKerning(left, right);\r\n        Assert.That(actual, Is.EqualTo(expected));\r\n    }\r\n\r\n    [TestCase('a')]\r\n    [TestCase('A')]\r\n    [TestCase('1')]\r\n    [TestCase(' ')]\r\n    [TestCase('@')]\r\n    [TestCase('#')]\r\n    public void TestCompareGetTextureUploadWithOrigin(char c)\r\n    {\r\n        var expected = GlyphStore.Get(new string(new[] { c }));\r\n        var actual = (CustomizeGlyphStore as IResourceStore<TextureUpload>)?.Get(new string(new[] { c }));\r\n\r\n        if (expected == null || actual == null)\r\n            throw new ArgumentNullException();\r\n\r\n        // todo : should test with pixel perfect, but it's ok to pass if size is almost the same.\r\n        Assert.That(actual.Width, Is.EqualTo(expected.Width));\r\n        Assert.That(actual.Height, Is.EqualTo(expected.Height));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/IO/Stores/TtfGlyphStoreTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Framework.IO.Stores;\r\nusing osu.Game.Rulesets.Karaoke.IO.Stores;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.IO.Stores;\r\n\r\n[Ignore(\"This shit is not implemented.\")]\r\npublic class TtfGlyphStoreTest : BaseGlyphStoreTest<TtfGlyphStore>\r\n{\r\n    protected override string FontType => \"Ttf\";\r\n\r\n    protected override string FontName => \"OpenSans-Regular\";\r\n\r\n    protected override TtfGlyphStore CreateFontStore(ResourceStore<byte[]> store, string assetName)\r\n        => new(store, assetName);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Integration/Formats/KarDecoderTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Integration.Formats;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Asserts;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Integration.Formats;\r\n\r\npublic class KarDecoderTest\r\n{\r\n    [TestCase(\"[00:01.00]か[00:02.00]ら[00:03.00]お[00:04.00]け[00:05.00]\", \"からおけ\", 1000, 5000)]\r\n    public void TestLyricTextAndTime(string lyricText, string expectedText, double expectedStartTime, double expectedEndTime)\r\n    {\r\n        // Get first lyric from beatmap\r\n        var lyrics = new KarDecoder().Decode(lyricText);\r\n        var actual = lyrics.FirstOrDefault()!;\r\n\r\n        Assert.That(actual, Is.Not.Null);\r\n        Assert.That(actual.Text, Is.EqualTo(expectedText));\r\n        Assert.That(actual.StartTime, Is.EqualTo(expectedStartTime));\r\n        Assert.That(actual.EndTime, Is.EqualTo(expectedEndTime));\r\n    }\r\n\r\n    [TestCase(\"[00:01.00]か[00:02.00]ら[00:03.00]お[00:04.00]け[00:05.00]\", new[] { \"[0,start]:1000\", \"[1,start]:2000\", \"[2,start]:3000\", \"[3,start]:4000\", \"[3,end]:5000\" })]\r\n    public void TestLyricTimeTag(string text, string[] timeTags)\r\n    {\r\n        // Get first lyric from beatmap\r\n        var lyrics = new KarDecoder().Decode(text);\r\n        var lyric = lyrics.First();\r\n\r\n        // Check time tag\r\n        var expected = TestCaseTagHelper.ParseTimeTags(timeTags);\r\n        var actual = lyric.TimeTags;\r\n        TimeTagAssert.ArePropertyEqual(expected, actual);\r\n    }\r\n\r\n    [Ignore(\"Time-tags with same time might be allowed.\")]\r\n    [TestCase(\"[00:04.00]か[00:04.00]ら[00:05.00]お[00:06.00]け[00:07.00]\")]\r\n    public void TestDecodeLyricWithDuplicatedTimeTag(string text)\r\n    {\r\n        Assert.Throws<FormatException>(() => new KarDecoder().Decode(text));\r\n    }\r\n\r\n    [Ignore(\"Waiting for lyric parser update.\")]\r\n    [TestCase(\"[00:04.00]か[00:03.00]ら[00:02.00]お[00:01.00]け[00:00.00]\")]\r\n    public void TestDecodeLyricWithTimeTagNotOrder(string text)\r\n    {\r\n        Assert.Throws<FormatException>(() => new KarDecoder().Decode(text));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Integration/Formats/KarEncoderTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing NUnit.Framework;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Integration.Formats;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\nusing osu.Game.Rulesets.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Integration.Formats;\r\n\r\npublic class KarEncoderTest\r\n{\r\n    [TestCase(\"からおけ\", new string[] { }, \"からおけ\")]\r\n    [TestCase(\"からおけ\", new[] { \"[0,start]:1000\", \"[1,start]:2000\", \"[2,start]:3000\", \"[3,start]:4000\", \"[3,end]:5000\" }, \"[00:01.00]か[00:02.00]ら[00:03.00]お[00:04.00]け[00:05.00]\")]\r\n    public void TestLyricWithTimeTag(string lyricText, string[] timeTags, string expected)\r\n    {\r\n        var lyric = new Lyric\r\n        {\r\n            Text = lyricText,\r\n            TimeTags = TestCaseTagHelper.ParseTimeTags(timeTags),\r\n        };\r\n\r\n        string actual = new KarEncoder().Encode(new Beatmap\r\n        {\r\n            HitObjects = new List<HitObject>\r\n            {\r\n                lyric,\r\n            },\r\n        });\r\n\r\n        Assert.That(actual, Is.EqualTo(expected));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Integration/Formats/KarFileTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.IO;\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.IO;\r\nusing osu.Game.IO.Serialization;\r\nusing osu.Game.Rulesets.Karaoke.Integration.Formats;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Resources;\r\nusing osu.Game.Rulesets.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Integration.Formats;\r\n\r\npublic class KarFileTest\r\n{\r\n    private static IEnumerable<string> allKarFileNames => TestResources.GetStore().GetAvailableResources()\r\n                                                                       .Where(res => res.EndsWith(\".kar\", StringComparison.Ordinal)).Select(x => Path.GetFileNameWithoutExtension(x!));\r\n\r\n    [TestCaseSource(nameof(allKarFileNames))]\r\n    public void TestDecodeEncodedBeatmap(string fileName)\r\n    {\r\n        var decoded = decode(fileName, out var encoded);\r\n\r\n        // Note : this test case does not cover ruby property\r\n        Assert.That(decoded.HitObjects.Count, Is.EqualTo(encoded.HitObjects.Count));\r\n        Assert.That(encoded.Serialize(), Is.EqualTo(decoded.Serialize()));\r\n    }\r\n\r\n    private static Beatmap decode(string filename, out Beatmap encoded)\r\n    {\r\n        using var stream = TestResources.OpenKarResource(filename);\r\n        using var sr = new LineBufferedReader(stream);\r\n\r\n        // Read file and decode to file\r\n        var legacyDecoded = new Beatmap\r\n        {\r\n            HitObjects = new KarDecoder().Decode(sr.ReadToEnd()).OfType<HitObject>().ToList(),\r\n        };\r\n\r\n        using var ms = new MemoryStream();\r\n        using var sw = new StreamWriter(ms);\r\n        using var sr2 = new LineBufferedReader(ms);\r\n\r\n        // Then encode file to stream\r\n        string encodeResult = new KarEncoder().Encode(legacyDecoded);\r\n        sw.WriteLine(encodeResult);\r\n        sw.Flush();\r\n\r\n        ms.Position = 0;\r\n\r\n        encoded = new Beatmap\r\n        {\r\n            HitObjects = new KarDecoder().Decode(sr2.ReadToEnd()).OfType<HitObject>().ToList(),\r\n        };\r\n        return legacyDecoded;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Integration/Formats/LrcDecoderTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing NUnit.Framework;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Integration.Formats;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\nusing osu.Game.Rulesets.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Integration.Formats;\r\n\r\npublic class LrcDecoderTest\r\n{\r\n    [TestCase(\"からおけ\", new string[] { }, \"[00:00.00] からおけ\")] // todo: handle the start time.\r\n    [TestCase(\"からおけ\", new[] { \"[0,start]:1000\", \"[1,start]:2000\", \"[2,start]:3000\", \"[3,start]:4000\", \"[3,end]:5000\" }, \"[00:00.00] <00:01.00>か<00:02.00>ら<00:03.00>お<00:04.00>け<00:05.00>\")]\r\n    public void TestLyricWithTimeTag(string lyricText, string[] timeTags, string expected)\r\n    {\r\n        var lyric = new Lyric\r\n        {\r\n            Text = lyricText,\r\n            TimeTags = TestCaseTagHelper.ParseTimeTags(timeTags),\r\n        };\r\n\r\n        string actual = new LrcEncoder().Encode(new Beatmap\r\n        {\r\n            HitObjects = new List<HitObject>\r\n            {\r\n                lyric,\r\n            },\r\n        });\r\n\r\n        Assert.That(actual, Is.EqualTo(expected));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Integration/Formats/LrcEncoderTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Integration.Formats;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Asserts;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Integration.Formats;\r\n\r\npublic class LrcEncoderTest\r\n{\r\n    [TestCase(\"[00:01.00]<00:01.00>か<00:02.00>ら<00:03.00>お<00:04.00>け<00:05.00>\", \"からおけ\", 1000, 5000)]\r\n    public void TestLyricTextAndTime(string lyricText, string expectedText, double expectedStartTime, double expectedEndTime)\r\n    {\r\n        // Get first lyric from beatmap\r\n        var lyrics = new LrcDecoder().Decode(lyricText);\r\n        var actual = lyrics.FirstOrDefault()!;\r\n\r\n        Assert.That(actual, Is.Not.Null);\r\n        Assert.That(actual.Text, Is.EqualTo(expectedText));\r\n        Assert.That(actual.StartTime, Is.EqualTo(expectedStartTime));\r\n        Assert.That(actual.EndTime, Is.EqualTo(expectedEndTime));\r\n    }\r\n\r\n    [TestCase(\"[00:01.00]<00:01.00>か<00:02.00>ら<00:03.00>お<00:04.00>け<00:05.00>\", new[] { \"[0,start]:1000\", \"[1,start]:2000\", \"[2,start]:3000\", \"[3,start]:4000\", \"[3,end]:5000\" })]\r\n    public void TestLyricTimeTag(string text, string[] timeTags)\r\n    {\r\n        // Get first lyric from beatmap\r\n        var lyrics = new LrcDecoder().Decode(text);\r\n        var lyric = lyrics.First();\r\n\r\n        // Check time tag\r\n        var expected = TestCaseTagHelper.ParseTimeTags(timeTags);\r\n        var actual = lyric.TimeTags;\r\n        TimeTagAssert.ArePropertyEqual(expected, actual);\r\n    }\r\n\r\n    [Ignore(\"Time-tags with same time might be allowed.\")]\r\n    [TestCase(\"[00:04.00]<00:04.00>か<00:04.00>ら<00:05.00>お<00:06.00>け<00:07.00>\")]\r\n    public void TestDecodeLyricWithDuplicatedTimeTag(string text)\r\n    {\r\n        Assert.Throws<FormatException>(() => new LrcDecoder().Decode(text));\r\n    }\r\n\r\n    [Ignore(\"Waiting for lyric parser update.\")]\r\n    [TestCase(\"[00:04.00]<00:04.00>か<00:03.00>ら<00:02.00>お<00:01.00>け<00:00.00>\")]\r\n    public void TestDecodeLyricWithTimeTagNotOrder(string text)\r\n    {\r\n        Assert.Throws<FormatException>(() => new LrcDecoder().Decode(text));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Integration/Formats/LrcParserUtilsTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Integration.Formats;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Integration.Formats;\r\n\r\npublic class LrcParserUtilsTest\r\n{\r\n    [TestCase(new[] { \"[0,start]:1100\", \"[0,end]:2000\", \"[1,start]:2100\", \"[1,end]:3000\" }, new double[] { 1100, 2000, 2100, 3000 })]\r\n    [TestCase(new[] { \"[1,end]:3000\", \"[1,start]:2100\", \"[0,end]:2000\", \"[0,start]:1100\" }, new double[] { 1100, 2000, 2100, 3000 })]\r\n    [TestCase(new[] { \"[0,start]\", \"[0,start]\", \"[0,end]:2000\", \"[0,start]:1100\" }, new double[] { 1100, 2000 })]\r\n    [TestCase(new[] { \"[0,start]:1000\", \"[0,start]:1100\", \"[0,end]:2000\", \"[0,start]:1100\" }, new double[] { 1000, 2000 })]\r\n    [TestCase(new[] { \"[0,start]\", \"[0,end]\", \"[0,start]\", \"[1,start]\", \"[1,end]\" }, new double[] { })]\r\n    [TestCase(new[] { \"[0,start]:2000\", \"[0,end]:1000\" }, new double[] { 2000, 2000 })]\r\n    [TestCase(new[] { \"[0,start]:1100\", \"[0,end]:2100\", \"[1,start]:2000\", \"[1,end]:3000\" }, new double[] { 1100, 2100, 2100, 3000 })]\r\n    [TestCase(new[] { \"[0,start]:1000\", \"[0,end]:5000\", \"[1,start]:2000\", \"[1,end]:3000\" }, new double[] { 1000, 5000, 5000, 5000 })]\r\n    [TestCase(new[] { \"[0,start]:1000\", \"[0,end]:2000\", \"[1,start]:0\", \"[1,end]:3000\" }, new double[] { 1000, 2000, 2000, 3000 })]\r\n    //[TestCase(new[] { \"[0,start]:4000\", \"[0,end]:3000\", \"[1,start]:2000\", \"[1,end]:1000\" }, new double[] { 4000, 4000, 4000, 4000 })]\r\n    public void TestToDictionary(string[] timeTagTexts, double[] expected)\r\n    {\r\n        var timeTags = TestCaseTagHelper.ParseTimeTags(timeTagTexts);\r\n\r\n        double[] actual = LrcParserUtils.ToDictionary(timeTags).Values.ToArray();\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Integration/Formats/LyricTextDecoderTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Integration.Formats;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Integration.Formats;\r\n\r\npublic class LyricTextDecoderTest\r\n{\r\n    [TestCase(\"karaoke\", new[] { \"[0,0]:karaoke\" })] // only one lyric.\r\n    [TestCase(\"か\\nら\\nお\\nけ\", new[] { \"[0,0]:か\", \"[0,0]:ら\", \"[0,0]:お\", \"[0,0]:け\" })] // multi lyric.\r\n    public void TestDecodeBeatmapToPureText(string expected, string[] lyrics)\r\n    {\r\n        var decoder = new LyricTextDecoder();\r\n        var actual = decoder.Decode(expected);\r\n\r\n        var expectedLyrics = TestCaseTagHelper.ParseLyrics(lyrics);\r\n        Assert.That(actual.Length, Is.EqualTo(expectedLyrics.Length));\r\n\r\n        for (int i = 0; i < expectedLyrics.Length; i++)\r\n        {\r\n            Assert.That(actual[i].Text, Is.EqualTo(expectedLyrics[i].Text));\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Integration/Formats/LyricTextEncoderTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Integration.Formats;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Integration.Formats;\r\n\r\n[TestFixture]\r\npublic class LyricTextEncoderTest\r\n{\r\n    [TestCase(new[] { \"[0,0]:karaoke\" }, \"karaoke\")] // only one lyric.\r\n    [TestCase(new[] { \"[0,0]:か\", \"[0,0]:ら\", \"[0,0]:お\", \"[0,0]:け\" }, \"か\\nら\\nお\\nけ\")] // multi lyric.\r\n    public void TestEncodeBeatmapToPureText(string[] lyrics, string expected)\r\n    {\r\n        var encoder = new LyricTextEncoder();\r\n        var beatmap = new KaraokeBeatmap\r\n        {\r\n            HitObjects = TestCaseTagHelper.ParseLyrics(lyrics).OfType<KaraokeHitObject>().ToList(),\r\n        };\r\n\r\n        string actual = encoder.Encode(beatmap);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/KaraokeTestBrowser.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Game.Tests;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests;\r\n\r\npublic partial class KaraokeTestBrowser : OsuTestBrowser\r\n{\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        // note: not add resource store here unless there's no other better choice.\r\n        // because it will let error related to missing resource harder to be tracked.\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Mods/KaraokeModStageTestScene.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Mods;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Beatmaps;\r\nusing osu.Game.Tests.Visual;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Mods;\r\n\r\npublic abstract partial class KaraokeModStageTestScene<TModStage, TStageInfo> : ModTestScene\r\n    where TModStage : ModStage<TStageInfo>, new()\r\n    where TStageInfo : StageInfo\r\n{\r\n    protected override Ruleset CreatePlayerRuleset() => new KaraokeRuleset();\r\n\r\n    [Test]\r\n    public void TestCreateModWithStage()\r\n    {\r\n        CreateModTest(new ModTestData\r\n        {\r\n            Mod = new TModStage(),\r\n            CreateBeatmap = () => new TestKaraokeBeatmap(Ruleset.Value),\r\n            PassCondition = () => true,\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestCreateModWithoutStage()\r\n    {\r\n        CreateModTest(new ModTestData\r\n        {\r\n            Mod = new TModStage(),\r\n            // todo: add the stage info to beatmap.\r\n            CreateBeatmap = () => new TestKaraokeBeatmap(Ruleset.Value),\r\n            PassCondition = () => true,\r\n        });\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Mods/KaraokeModTestScene.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Tests.Visual;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Mods;\r\n\r\npublic abstract partial class KaraokeModTestScene : ModTestScene\r\n{\r\n    protected override Ruleset CreatePlayerRuleset() => new KaraokeRuleset();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Mods/ModsTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing System.Reflection;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Mods;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Mods;\r\n\r\npublic class ModsTest\r\n{\r\n    [Test]\r\n    public void TestCheckDuplicatedProperty()\r\n    {\r\n        var mods = getAllModsFromTheRuleset().ToArray();\r\n\r\n        // Name should not duplicated.\r\n        bool hasDuplicatedName = mods.GroupBy(x => x.Name).Any(g => g.Count() > 1);\r\n        Assert.That(hasDuplicatedName, Is.False);\r\n\r\n        // Acronym should not duplicated.\r\n        bool hasDuplicatedAcronym = mods.GroupBy(x => x.Acronym).Any(g => g.Count() > 1);\r\n        Assert.That(hasDuplicatedAcronym, Is.False);\r\n    }\r\n\r\n    /// <summary>\r\n    /// get the mods inherit the <see cref=\"Mod\"/> class in the karaoke ruleset by reflection.\r\n    /// </summary>\r\n    /// <returns></returns>\r\n    private IEnumerable<Mod> getAllModsFromTheRuleset() =>\r\n        Assembly.GetAssembly(typeof(KaraokeRuleset))!\r\n                .GetTypes()\r\n                .Where(t => t.IsSubclassOf(typeof(Mod)) && !t.IsAbstract)\r\n                .Select(t => (Mod)Activator.CreateInstance(t)!);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Mods/TestSceneKaraokeModAutoplay.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Mods;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Beatmaps;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Mods;\r\n\r\npublic partial class TestSceneKaraokeModAutoplay : KaraokeModTestScene\r\n{\r\n    [Ignore(\"mod auto-play will cause crash\")]\r\n    public void TestMod() => CreateModTest(new ModTestData\r\n    {\r\n        Mod = new KaraokeModAutoplay(),\r\n        Autoplay = true,\r\n        CreateBeatmap = () => new TestKaraokeBeatmap(Ruleset.Value),\r\n        PassCondition = () => true,\r\n    });\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Mods/TestSceneKaraokeModAutoplayBySinger.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Mods;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Beatmaps;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Mods;\r\n\r\npublic partial class TestSceneKaraokeModAutoplayBySinger : KaraokeModTestScene\r\n{\r\n    [Ignore(\"mod auto-play will cause crash\")]\r\n    public void TestMod() => CreateModTest(new ModTestData\r\n    {\r\n        Mod = new KaraokeModAutoplayBySinger(),\r\n        Autoplay = true,\r\n        CreateBeatmap = () => new TestKaraokeBeatmap(Ruleset.Value),\r\n        PassCondition = () => true,\r\n    });\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Mods/TestSceneKaraokeModClassicStage.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Mods;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos.Classic;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Mods;\r\n\r\npublic partial class TestSceneKaraokeModClassicStage : KaraokeModStageTestScene<KaraokeModClassicStage, ClassicStageInfo>;\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Mods/TestSceneKaraokeModDisableNote.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Mods;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Extensions;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Mods;\r\n\r\npublic partial class TestSceneKaraokeModDisableNote : KaraokeModTestScene\r\n{\r\n    [Test]\r\n    public void TestCheckNoteExistInPlayfield() => CreateModTest(new ModTestData\r\n    {\r\n        Mod = new KaraokeModDisableNote(),\r\n        Autoplay = true,\r\n        CreateBeatmap = () => new TestKaraokeBeatmap(Ruleset.Value),\r\n        PassCondition = () =>\r\n        {\r\n            var lyricPlayfield = Player.GetLyricPlayfield();\r\n            var notePlayfield = Player.GetNotePlayfield();\r\n            if (lyricPlayfield == null || notePlayfield == null)\r\n                return false;\r\n\r\n            // check has no note in playfield\r\n            return lyricPlayfield.AllHitObjects.Any() && notePlayfield.Alpha == 0f;\r\n        },\r\n    });\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Mods/TestSceneKaraokeModFlashlight.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Testing;\r\nusing osu.Game.Rulesets.Karaoke.Mods;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Extensions;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Mods;\r\n\r\npublic partial class TestSceneKaraokeModFlashlight : KaraokeModTestScene\r\n{\r\n    [Test]\r\n    public void TestFlashlightExist() => CreateModTest(new ModTestData\r\n    {\r\n        Mod = new KaraokeModFlashlight(),\r\n        Autoplay = true,\r\n        CreateBeatmap = () => new TestKaraokeBeatmap(Ruleset.Value),\r\n        PassCondition = () =>\r\n        {\r\n            var drawableRuleset = Player.GetDrawableRuleset();\r\n            if (drawableRuleset == null)\r\n                return false;\r\n\r\n            // Should has at least one flashlight\r\n            return drawableRuleset.KeyBindingInputManager.ChildrenOfType<KaraokeModFlashlight.KaraokeFlashlight>().Any();\r\n        },\r\n    });\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Mods/TestSceneKaraokeModFun.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Mods;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Beatmaps;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Mods;\r\n\r\npublic partial class TestSceneKaraokeModFun : KaraokeModTestScene\r\n{\r\n    [Test]\r\n    public void TestSnowMod() => CreateModTest(new ModTestData\r\n    {\r\n        Mod = new KaraokeModSnow(),\r\n        Autoplay = false,\r\n        CreateBeatmap = () => new TestKaraokeBeatmap(Ruleset.Value),\r\n        PassCondition = () => true,\r\n    });\r\n\r\n    [Test]\r\n    public void TestWindowsUpdateMod() => CreateModTest(new ModTestData\r\n    {\r\n        Mod = new KaraokeModWindowsUpdate(),\r\n        Autoplay = false,\r\n        CreateBeatmap = () => new TestKaraokeBeatmap(Ruleset.Value),\r\n        PassCondition = () => true,\r\n    });\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Mods/TestSceneKaraokeModLyricConfiguration.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Mods;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Beatmaps;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Mods;\r\n\r\npublic partial class TestSceneKaraokeModLyricConfiguration : KaraokeModTestScene\r\n{\r\n    [Test]\r\n    public void TestAllPanelExist() => CreateModTest(new ModTestData\r\n    {\r\n        Mod = new KaraokeModLyricConfiguration(),\r\n        Autoplay = false,\r\n        CreateBeatmap = () => new TestKaraokeBeatmap(new RulesetInfo()),\r\n        PassCondition = () => true,\r\n    });\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Mods/TestSceneKaraokeModPerfect.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Mods;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Tests.Visual;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Mods;\r\n\r\npublic partial class TestSceneKaraokeModPerfect : ModFailConditionTestScene\r\n{\r\n    protected override Ruleset CreatePlayerRuleset() => new KaraokeRuleset();\r\n\r\n    public TestSceneKaraokeModPerfect()\r\n        : base(new KaraokeModPerfect())\r\n    {\r\n    }\r\n\r\n    // TODO : test case = false will be added after scoring system is implemented.\r\n    [Ignore(\"Scoring should judgement by note, not lyric.\")]\r\n    public void TestLyric(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new Lyric\r\n    {\r\n        Text = \"カラオケ!\",\r\n        TimeTags = new[]\r\n        {\r\n            new TimeTag(new TextIndex(), 1000),\r\n            new TimeTag(new TextIndex(3, TextIndex.IndexState.End), 2000),\r\n        },\r\n    }), shouldMiss);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Mods/TestSceneKaraokeModPractice.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Mods;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.UI.HUD;\r\nusing osu.Game.Skinning;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Mods;\r\n\r\npublic partial class TestSceneKaraokeModPractice : KaraokeModTestScene\r\n{\r\n    [Test]\r\n    public void TestAllPanelExist() => CreateModTest(new ModTestData\r\n    {\r\n        Mod = new KaraokeModPractice(),\r\n        Autoplay = false,\r\n        CreateBeatmap = () => new TestKaraokeBeatmap(new RulesetInfo()),\r\n        PassCondition = () =>\r\n        {\r\n            // just need to check has setting button display area.\r\n            var skinnableTargetContainers = Player.HUDOverlay.OfType<ISerialisableDrawableContainer>().FirstOrDefault();\r\n\r\n            // todo: because setting buttons display created from skin transform , so might not able to get from here.\r\n            var hud = skinnableTargetContainers?.Components.OfType<SettingButtonsDisplay>().FirstOrDefault();\r\n            return true;\r\n            //return hud != null;\r\n        },\r\n    });\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Mods/TestSceneKaraokeModPreviewStage.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Mods;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos.Preview;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Mods;\r\n\r\npublic partial class TestSceneKaraokeModPreviewStage : KaraokeModStageTestScene<KaraokeModPreviewStage, PreviewStageInfo>;\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Mods/TestSceneKaraokeModSuddenDeath.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing NUnit.Framework;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Mods;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Replays;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\nusing osu.Game.Rulesets.Objects;\r\nusing osu.Game.Rulesets.Replays;\r\nusing osu.Game.Tests.Visual;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Mods;\r\n\r\n[Ignore(\"Scoring is not implemented.\")]\r\npublic partial class TestSceneKaraokeModSuddenDeath : ModFailConditionTestScene\r\n{\r\n    protected override Ruleset CreatePlayerRuleset() => new KaraokeRuleset();\r\n\r\n    private readonly Lyric referencedLyric = TestCaseNoteHelper.CreateLyricForNote(1, \"カラオケ\", 1000, 1000);\r\n\r\n    public TestSceneKaraokeModSuddenDeath()\r\n        : base(new KaraokeModSuddenDeath())\r\n    {\r\n    }\r\n\r\n    [Test]\r\n    public void TestGreatHit() => CreateModTest(new ModTestData\r\n    {\r\n        Mod = new KaraokeModSuddenDeath(),\r\n        PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(false),\r\n        Autoplay = false,\r\n        CreateBeatmap = () => new Beatmap\r\n        {\r\n            HitObjects = new List<HitObject>\r\n            {\r\n                referencedLyric,\r\n                new Note\r\n                {\r\n                    ReferenceLyricId = referencedLyric.ID,\r\n                    Tone = new Tone(0),\r\n                },\r\n            },\r\n        },\r\n        ReplayFrames = new List<ReplayFrame>\r\n        {\r\n            new KaraokeReplayFrame(1000, 0),\r\n            new KaraokeReplayFrame(2000, 0),\r\n        },\r\n    });\r\n\r\n    [Test]\r\n    public void TestBreakOnHoldNote() => CreateModTest(new ModTestData\r\n    {\r\n        Mod = new KaraokeModSuddenDeath(),\r\n        PassCondition = () => ((ModFailConditionTestPlayer)Player).CheckFailed(true) && Player.Results.Count == 2,\r\n        Autoplay = false,\r\n        CreateBeatmap = () => new Beatmap\r\n        {\r\n            HitObjects = new List<HitObject>\r\n            {\r\n                referencedLyric,\r\n                new Note\r\n                {\r\n                    ReferenceLyricId = referencedLyric.ID,\r\n                    Tone = new Tone(0),\r\n                },\r\n            },\r\n        },\r\n        ReplayFrames = new List<ReplayFrame>\r\n        {\r\n            new KaraokeReplayFrame(0, -1),\r\n        },\r\n    });\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Mods/TestSceneKaraokeModTranslation.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Mods;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Beatmaps;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Mods;\r\n\r\npublic partial class TestSceneKaraokeModTranslation : KaraokeModTestScene\r\n{\r\n    [Test]\r\n    public void TestAllPanelExist() => CreateModTest(new ModTestData\r\n    {\r\n        Mod = new KaraokeModTranslation(),\r\n        Autoplay = false,\r\n        CreateBeatmap = () => new TestKaraokeBeatmap(new RulesetInfo()),\r\n        PassCondition = () => true,\r\n    });\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Objects/LyricTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Globalization;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Properties;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Types;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Asserts;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Objects;\r\n\r\npublic class LyricTest\r\n{\r\n    #region Clone\r\n\r\n    [Test]\r\n    public void TestClone()\r\n    {\r\n        var referencedLyric = new Lyric();\r\n\r\n        var lyric = new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            TimeTags = TestCaseTagHelper.ParseTimeTags(new[] { \"[0,start]:1000^ka\", \"[1,start]:2000^ra\", \"[2,start]:3000^o\", \"[3,start]:4000^ke\", \"[3,end]:5000\" }),\r\n            RubyTags = TestCaseTagHelper.ParseRubyTags(new[] { \"[0]:か\", \"[1]:ら\", \"[2]:お\", \"[3]:け\" }),\r\n            SingerIds = TestCaseElementIdHelper.CreateElementIdsByNumbers(new[] { 1, 2 }),\r\n            Translations = new Dictionary<CultureInfo, string>\r\n            {\r\n                { new CultureInfo(\"en-US\"), \"karaoke\" },\r\n            },\r\n            Language = new CultureInfo(\"ja-JP\"),\r\n            Order = 1,\r\n            Lock = LockState.None,\r\n            ReferenceLyricId = referencedLyric.ID,\r\n            ReferenceLyric = referencedLyric,\r\n            ReferenceLyricConfig = new ReferenceLyricConfig\r\n            {\r\n                OffsetTime = 100,\r\n            },\r\n        };\r\n\r\n        var clonedLyric = lyric.DeepClone();\r\n\r\n        Assert.That(clonedLyric.ID, Is.Not.SameAs(lyric.ID));\r\n\r\n        Assert.That(clonedLyric.TextBindable, Is.Not.SameAs(lyric.TextBindable));\r\n        Assert.That(clonedLyric.Text, Is.EqualTo(lyric.Text));\r\n\r\n        Assert.That(clonedLyric.TimeTagsTimingVersion, Is.Not.SameAs(lyric.TimeTagsTimingVersion));\r\n        Assert.That(clonedLyric.TimeTagsBindable, Is.Not.SameAs(lyric.TimeTagsBindable));\r\n        TimeTagAssert.ArePropertyEqual(clonedLyric.TimeTags, lyric.TimeTags);\r\n\r\n        Assert.That(clonedLyric.RubyTagsVersion, Is.Not.SameAs(lyric.RubyTagsVersion));\r\n        Assert.That(clonedLyric.RubyTagsBindable, Is.Not.SameAs(lyric.RubyTagsBindable));\r\n        RubyTagAssert.ArePropertyEqual(clonedLyric.RubyTags, lyric.RubyTags);\r\n\r\n        Assert.That(clonedLyric.StartTimeBindable, Is.Not.SameAs(lyric.StartTimeBindable));\r\n        Assert.That(clonedLyric.StartTime, Is.EqualTo(lyric.StartTime));\r\n\r\n        Assert.That(clonedLyric.Duration, Is.EqualTo(lyric.Duration));\r\n\r\n        Assert.That(clonedLyric.EndTime, Is.EqualTo(lyric.EndTime));\r\n\r\n        Assert.That(clonedLyric.SingerIdsBindable, Is.Not.SameAs(lyric.SingerIdsBindable));\r\n        Assert.That(clonedLyric.SingerIds, Is.EquivalentTo(lyric.SingerIds));\r\n\r\n        Assert.That(clonedLyric.TranslationsBindable, Is.Not.SameAs(lyric.TranslationsBindable));\r\n        Assert.That(clonedLyric.Translations, Is.EquivalentTo(lyric.Translations));\r\n\r\n        Assert.That(clonedLyric.LanguageBindable, Is.Not.SameAs(lyric.LanguageBindable));\r\n        Assert.That(clonedLyric.Language, Is.EqualTo(lyric.Language));\r\n\r\n        Assert.That(clonedLyric.OrderBindable, Is.Not.SameAs(lyric.OrderBindable));\r\n        Assert.That(clonedLyric.Order, Is.EqualTo(lyric.Order));\r\n\r\n        Assert.That(clonedLyric.LockBindable, Is.Not.SameAs(lyric.LockBindable));\r\n        Assert.That(clonedLyric.Lock, Is.EqualTo(lyric.Lock));\r\n\r\n        Assert.That(clonedLyric.ReferenceLyricBindable, Is.Not.SameAs(lyric.ReferenceLyricBindable));\r\n        Assert.That(clonedLyric.ReferenceLyric, Is.SameAs(lyric.ReferenceLyric));\r\n        Assert.That(clonedLyric.ReferenceLyricId, Is.EqualTo(lyric.ReferenceLyricId));\r\n\r\n        Assert.That(clonedLyric.ReferenceLyricConfigBindable, Is.Not.SameAs(lyric.ReferenceLyricConfigBindable));\r\n        Assert.That(clonedLyric.ReferenceLyricConfig, Is.Not.SameAs(lyric.ReferenceLyricConfig));\r\n        Assert.That(clonedLyric.ReferenceLyricConfig?.OffsetTime, Is.EqualTo(lyric.ReferenceLyricConfig?.OffsetTime));\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Reference lyric\r\n\r\n    [Test]\r\n    public void TestSyncFromReferenceLyric()\r\n    {\r\n        var referencedLyric = new Lyric\r\n        {\r\n            Text = \"karaoke\",\r\n            TimeTags = TestCaseTagHelper.ParseTimeTags(new[] { \"[0,start]:1100#^ka\" }),\r\n            RubyTags = TestCaseTagHelper.ParseRubyTags(new[] { \"[0]:か\" }),\r\n            SingerIds = TestCaseElementIdHelper.CreateElementIdsByNumbers(new[] { 1 }),\r\n            Translations = new Dictionary<CultureInfo, string>\r\n            {\r\n                { new CultureInfo(17), \"からおけ\" },\r\n            },\r\n            Language = new CultureInfo(17),\r\n        };\r\n\r\n        var lyric = new Lyric\r\n        {\r\n            ReferenceLyricId = referencedLyric.ID,\r\n            ReferenceLyric = referencedLyric,\r\n            ReferenceLyricConfig = new SyncLyricConfig(),\r\n        };\r\n\r\n        Assert.That(referencedLyric.Text, Is.EqualTo(lyric.Text));\r\n        TimeTagAssert.ArePropertyEqual(referencedLyric.TimeTags, lyric.TimeTags);\r\n        RubyTagAssert.ArePropertyEqual(referencedLyric.RubyTags, lyric.RubyTags);\r\n        Assert.That(lyric.SingerIds, Is.EqualTo(referencedLyric.SingerIds));\r\n        Assert.That(lyric.Translations, Is.EqualTo(referencedLyric.Translations));\r\n        Assert.That(lyric.Language, Is.EqualTo(referencedLyric.Language));\r\n    }\r\n\r\n    [Test]\r\n    public void TestReferenceLyricPropertyChanged()\r\n    {\r\n        var referencedLyric = new Lyric();\r\n\r\n        var lyric = new Lyric\r\n        {\r\n            ReferenceLyricId = referencedLyric.ID,\r\n            ReferenceLyric = referencedLyric,\r\n            ReferenceLyricConfig = new SyncLyricConfig(),\r\n        };\r\n\r\n        referencedLyric.Text = \"karaoke\";\r\n        referencedLyric.TimeTags = TestCaseTagHelper.ParseTimeTags(new[] { \"[0,start]:1100^ka\" });\r\n        referencedLyric.RubyTags = TestCaseTagHelper.ParseRubyTags(new[] { \"[0]:か\" });\r\n        referencedLyric.SingerIds = TestCaseElementIdHelper.CreateElementIdsByNumbers(new[] { 1 });\r\n        referencedLyric.Translations = new Dictionary<CultureInfo, string>\r\n        {\r\n            { new CultureInfo(17), \"からおけ\" },\r\n        };\r\n        referencedLyric.Language = new CultureInfo(17);\r\n\r\n        Assert.That(lyric.Text, Is.EqualTo(referencedLyric.Text));\r\n        TimeTagAssert.ArePropertyEqual(referencedLyric.TimeTags, lyric.TimeTags);\r\n        RubyTagAssert.ArePropertyEqual(referencedLyric.RubyTags, lyric.RubyTags);\r\n        Assert.That(lyric.SingerIds, Is.EqualTo(referencedLyric.SingerIds));\r\n        Assert.That(lyric.Translations, Is.EqualTo(referencedLyric.Translations));\r\n        Assert.That(lyric.Language, Is.EqualTo(referencedLyric.Language));\r\n    }\r\n\r\n    [Test]\r\n    public void TestReferenceLyricListPropertyChanged()\r\n    {\r\n        // test modify property inside the list.\r\n        // e.g. ruby, time-tag and romanisation.\r\n        var timeTag = TestCaseTagHelper.ParseTimeTag(\"[0,start]:1100#^ka\");\r\n        var rubyTag = TestCaseTagHelper.ParseRubyTag(\"[0]:か\");\r\n\r\n        var referencedLyric = new Lyric\r\n        {\r\n            Text = \"karaoke\",\r\n            TimeTags = new[] { timeTag },\r\n            RubyTags = new[] { rubyTag },\r\n            SingerIds = TestCaseElementIdHelper.CreateElementIdsByNumbers(new[] { 1 }),\r\n            Translations = new Dictionary<CultureInfo, string>\r\n            {\r\n                { new CultureInfo(17), \"からおけ\" },\r\n            },\r\n            Language = new CultureInfo(17),\r\n        };\r\n\r\n        var lyric = new Lyric\r\n        {\r\n            ReferenceLyricId = referencedLyric.ID,\r\n            ReferenceLyric = referencedLyric,\r\n            ReferenceLyricConfig = new SyncLyricConfig(),\r\n        };\r\n\r\n        // property should be the same\r\n        TimeTagAssert.ArePropertyEqual(referencedLyric.TimeTags, lyric.TimeTags);\r\n        RubyTagAssert.ArePropertyEqual(referencedLyric.RubyTags, lyric.RubyTags);\r\n\r\n        // and because there's no change inside the tag, so there's version change.\r\n        Assert.That(lyric.TimeTagsTimingVersion.Value, Is.EqualTo(0));\r\n        Assert.That(lyric.TimeTagsRomanisationVersion.Value, Is.EqualTo(0));\r\n        Assert.That(lyric.RubyTagsVersion.Value, Is.EqualTo(0));\r\n\r\n        // it's time to change the property in the list.\r\n        timeTag.Time = 2000;\r\n        timeTag.RomanisedSyllable = \"ka--\";\r\n        rubyTag.Text = \"ruby\";\r\n\r\n        // property should be equal.\r\n        TimeTagAssert.ArePropertyEqual(referencedLyric.TimeTags, lyric.TimeTags);\r\n        RubyTagAssert.ArePropertyEqual(referencedLyric.RubyTags, lyric.RubyTags);\r\n\r\n        // and note that because only one property is different, so version should change once.\r\n        Assert.That(lyric.TimeTagsTimingVersion.Value, Is.EqualTo(1));\r\n        Assert.That(lyric.TimeTagsRomanisationVersion.Value, Is.EqualTo(1));\r\n        Assert.That(lyric.RubyTagsVersion.Value, Is.EqualTo(1));\r\n    }\r\n\r\n    [Test]\r\n    public void TestConfigChange()\r\n    {\r\n        // change the config from reference lyric into sync lyric config.\r\n        // than, should auto sync the value.\r\n        var config = new SyncLyricConfig\r\n        {\r\n            SyncSingerProperty = false,\r\n            SyncTimeTagProperty = false,\r\n        };\r\n\r\n        var referencedLyric = new Lyric\r\n        {\r\n            Text = \"karaoke\",\r\n            TimeTags = TestCaseTagHelper.ParseTimeTags(new[] { \"[0,start]:1100#ka\" }),\r\n            RubyTags = TestCaseTagHelper.ParseRubyTags(new[] { \"[0]:か\" }),\r\n            SingerIds = TestCaseElementIdHelper.CreateElementIdsByNumbers(new[] { 1 }),\r\n            Translations = new Dictionary<CultureInfo, string>\r\n            {\r\n                { new CultureInfo(17), \"からおけ\" },\r\n            },\r\n            Language = new CultureInfo(17),\r\n        };\r\n\r\n        var lyric = new Lyric\r\n        {\r\n            ReferenceLyricId = referencedLyric.ID,\r\n            ReferenceLyric = referencedLyric,\r\n            ReferenceLyricConfig = config,\r\n        };\r\n\r\n        // the property should not same as the reference reference because those properties are not sync.\r\n        Assert.That(lyric.TimeTags, Is.Empty);\r\n        Assert.That(lyric.SingerIds, Is.Not.EqualTo(referencedLyric.SingerIds));\r\n\r\n        // it's time to open the config.\r\n        config.SyncSingerProperty = true;\r\n        config.SyncTimeTagProperty = true;\r\n\r\n        // after open the config, the property should sync from the reference lyric now.\r\n        TimeTagAssert.ArePropertyEqual(referencedLyric.TimeTags, lyric.TimeTags);\r\n        Assert.That(lyric.SingerIds, Is.EqualTo(referencedLyric.SingerIds));\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region MyRegion\r\n\r\n    [Test]\r\n    public void TestLyricPropertyWritableVersion()\r\n    {\r\n        var lyric = new Lyric();\r\n        Assert.That(lyric.LyricPropertyWritableVersion.Value, Is.EqualTo(0));\r\n\r\n        lyric.Lock = LockState.Partial;\r\n        Assert.That(lyric.LyricPropertyWritableVersion.Value, Is.EqualTo(1));\r\n\r\n        var referencedLyric = new Lyric();\r\n        lyric.ReferenceLyricId = referencedLyric.ID;\r\n        lyric.ReferenceLyric = referencedLyric;\r\n        Assert.That(lyric.LyricPropertyWritableVersion.Value, Is.EqualTo(2));\r\n\r\n        lyric.ReferenceLyricConfig = new SyncLyricConfig();\r\n        Assert.That(lyric.LyricPropertyWritableVersion.Value, Is.EqualTo(3));\r\n\r\n        (lyric.ReferenceLyricConfig as SyncLyricConfig)!.OffsetTime = 200;\r\n        Assert.That(lyric.LyricPropertyWritableVersion.Value, Is.EqualTo(4));\r\n\r\n        // version number will not increase if change not related property or assign the same value.\r\n        lyric.Lock = LockState.Partial;\r\n        lyric.Text = \"karaoke\";\r\n        Assert.That(lyric.LyricPropertyWritableVersion.Value, Is.EqualTo(4));\r\n    }\r\n\r\n    #endregion\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Objects/NoteTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Framework.Extensions.IEnumerableExtensions;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Objects;\r\n\r\npublic class NoteTest\r\n{\r\n    [Test]\r\n    public void TestClone()\r\n    {\r\n        var referencedLyric = TestCaseNoteHelper.CreateLyricForNote(2, \"ノート\", 1000, 1000);\r\n        var note = new Note\r\n        {\r\n            Text = \"ノート\",\r\n            RubyText = \"Note\",\r\n            Display = true,\r\n            StartTimeOffset = 100,\r\n            EndTimeOffset = -100,\r\n            ReferenceLyricId = referencedLyric.ID,\r\n            ReferenceLyric = referencedLyric,\r\n            ReferenceTimeTagIndex = 0,\r\n        };\r\n\r\n        var clonedNote = note.DeepClone();\r\n\r\n        Assert.That(clonedNote.TextBindable, Is.Not.SameAs(note.TextBindable));\r\n        Assert.That(clonedNote.Text, Is.EqualTo(note.Text));\r\n\r\n        Assert.That(clonedNote.RubyTextBindable, Is.Not.SameAs(note.RubyTextBindable));\r\n        Assert.That(clonedNote.RubyText, Is.EqualTo(note.RubyText));\r\n\r\n        Assert.That(clonedNote.DisplayBindable, Is.Not.SameAs(note.DisplayBindable));\r\n        Assert.That(clonedNote.Display, Is.EqualTo(note.Display));\r\n\r\n        Assert.That(clonedNote.ToneBindable, Is.Not.SameAs(note.ToneBindable));\r\n        Assert.That(clonedNote.Tone, Is.EqualTo(note.Tone));\r\n\r\n        // note time will not being copied because the time is based on the time-tag in the lyric.\r\n        Assert.That(clonedNote.StartTimeBindable, Is.Not.SameAs(note.StartTimeBindable));\r\n        Assert.That(clonedNote.StartTime, Is.EqualTo(clonedNote.StartTime));\r\n\r\n        // note time will not being copied because the time is based on the time-tag in the lyric.\r\n        Assert.That(clonedNote.Duration, Is.EqualTo(clonedNote.Duration));\r\n\r\n        // note time will not being copied because the time is based on the time-tag in the lyric.\r\n        Assert.That(clonedNote.EndTime, Is.EqualTo(clonedNote.EndTime));\r\n\r\n        Assert.That(clonedNote.StartTimeOffset, Is.EqualTo(note.StartTimeOffset));\r\n\r\n        Assert.That(clonedNote.EndTimeOffset, Is.EqualTo(note.EndTimeOffset));\r\n\r\n        Assert.That(clonedNote.ReferenceLyric, Is.SameAs(note.ReferenceLyric));\r\n\r\n        Assert.That(clonedNote.ReferenceTimeTagIndexBindable, Is.Not.SameAs(note.ReferenceTimeTagIndexBindable));\r\n        Assert.That(clonedNote.ReferenceTimeTagIndex, Is.EqualTo(note.ReferenceTimeTagIndex));\r\n    }\r\n\r\n    [Test]\r\n    public void TestReferenceTime()\r\n    {\r\n        var note = new Note();\r\n\r\n        // Should not have the time.\r\n        Assert.That(note.StartTime, Is.EqualTo(0));\r\n        Assert.That(note.Duration, Is.EqualTo(0));\r\n        Assert.That(note.EndTime, Is.EqualTo(0));\r\n\r\n        const double first_time_tag_time = 1000;\r\n        const double second_time_tag_time = 3000;\r\n        const double duration = second_time_tag_time - first_time_tag_time;\r\n        var referencedLyric = TestCaseNoteHelper.CreateLyricForNote(2, \"Lyric\", first_time_tag_time, duration);\r\n        note.ReferenceLyricId = referencedLyric.ID;\r\n        note.ReferenceLyric = referencedLyric;\r\n\r\n        // Should have calculated time.\r\n        Assert.That(note.StartTime, Is.EqualTo(first_time_tag_time));\r\n        Assert.That(note.Duration, Is.EqualTo(duration));\r\n\r\n        const double time_tag_offset_time = 500;\r\n        referencedLyric.TimeTags.ForEach(x => x.Time += time_tag_offset_time);\r\n\r\n        // Should change the time if time-tag time has been changed.\r\n        Assert.That(note.StartTime, Is.EqualTo(first_time_tag_time + time_tag_offset_time));\r\n        Assert.That(note.Duration, Is.EqualTo(duration));\r\n\r\n        note.ReferenceTimeTagIndex = 1;\r\n\r\n        // Duration will be zero if there's no next time-tag.\r\n        Assert.That(note.StartTime, Is.EqualTo(second_time_tag_time + time_tag_offset_time));\r\n        Assert.That(note.Duration, Is.EqualTo(0));\r\n\r\n        note.ReferenceTimeTagIndex = 2;\r\n\r\n        // Time will be zero if there's no matched time-tag.\r\n        Assert.That(note.StartTime, Is.EqualTo(0));\r\n        Assert.That(note.Duration, Is.EqualTo(0));\r\n\r\n        const double note_start_offset_time = 500;\r\n        const double note_end_offset_time = 500;\r\n        note.ReferenceTimeTagIndex = 0;\r\n        note.StartTimeOffset = note_start_offset_time;\r\n        note.EndTimeOffset = note_end_offset_time;\r\n\r\n        // start time and end time will apply the offset time.\r\n        Assert.That(note.StartTime, Is.EqualTo(first_time_tag_time + time_tag_offset_time + note_start_offset_time));\r\n        Assert.That(note.Duration, Is.EqualTo(duration + time_tag_offset_time - note_end_offset_time));\r\n\r\n        note.EndTimeOffset = -100000;\r\n\r\n        // duration should not be empty.\r\n        Assert.That(note.StartTime, Is.EqualTo(first_time_tag_time + time_tag_offset_time + note_start_offset_time));\r\n        Assert.That(note.Duration, Is.EqualTo(0));\r\n\r\n        note.ReferenceLyricId = null;\r\n        note.ReferenceLyric = null;\r\n\r\n        // time will be zero if lyric has been removed.\r\n        Assert.That(note.StartTime, Is.EqualTo(0));\r\n        Assert.That(note.Duration, Is.EqualTo(0));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Objects/RubyTagTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Objects;\r\n\r\npublic class RubyTagTest\r\n{\r\n    [Test]\r\n    public void TestClone()\r\n    {\r\n        var rubyTag = new RubyTag\r\n        {\r\n            Text = \"ruby\",\r\n            StartIndex = 0,\r\n            EndIndex = 1,\r\n        };\r\n\r\n        var clonedRubyTag = rubyTag.DeepClone();\r\n\r\n        Assert.That(clonedRubyTag.TextBindable, Is.Not.SameAs(rubyTag.TextBindable));\r\n        Assert.That(rubyTag.Text, Is.EqualTo(clonedRubyTag.Text));\r\n\r\n        Assert.That(clonedRubyTag.StartIndexBindable, Is.Not.SameAs(rubyTag.StartIndexBindable));\r\n        Assert.That(rubyTag.StartIndex, Is.EqualTo(clonedRubyTag.StartIndex));\r\n\r\n        Assert.That(clonedRubyTag.EndIndexBindable, Is.Not.SameAs(rubyTag.EndIndexBindable));\r\n        Assert.That(rubyTag.EndIndex, Is.EqualTo(clonedRubyTag.EndIndex));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Objects/TimeTagTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Objects;\r\n\r\npublic class TimeTagTest\r\n{\r\n    [Test]\r\n    public void TestClone()\r\n    {\r\n        var timeTag = new TimeTag(new TextIndex(1, TextIndex.IndexState.End), 1000)\r\n        {\r\n            FirstSyllable = true,\r\n            RomanisedSyllable = \"karaoke\",\r\n        };\r\n\r\n        var clonedTimeTag = timeTag.DeepClone();\r\n\r\n        Assert.That(timeTag.Index, Is.EqualTo(clonedTimeTag.Index));\r\n\r\n        Assert.That(clonedTimeTag.TimeBindable, Is.Not.SameAs(timeTag.TimeBindable));\r\n        Assert.That(timeTag.Time, Is.EqualTo(clonedTimeTag.Time));\r\n        Assert.That(clonedTimeTag.FirstSyllable, Is.Not.SameAs(timeTag.FirstSyllable));\r\n        Assert.That(timeTag.RomanisedSyllable, Is.EqualTo(clonedTimeTag.RomanisedSyllable));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Objects/ToneCalculationTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Objects;\r\n\r\n[TestFixture]\r\npublic class ToneCalculationTest\r\n{\r\n    [TestCase(0, false, 0)]\r\n    [TestCase(0, true, 1)] // left tone should be larger than right tone.\r\n    [TestCase(1, false, 1)]\r\n    [TestCase(-1, false, -1)] // left tone should be smaller than right tone.\r\n    [TestCase(-1, true, -1)]\r\n    public void TestCompareTo(int scale, bool half, int expected)\r\n    {\r\n        var leftTone = new Tone(scale, half);\r\n        var rightTone = new Tone();\r\n\r\n        int actual = leftTone.CompareTo(rightTone);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(1, 1, 2)]\r\n    [TestCase(-1, 1, 0)]\r\n    [TestCase(1, -1, 0)]\r\n    [TestCase(1.5, 2.5, 4)]\r\n    [TestCase(-1.5, 2.5, 1)]\r\n    [TestCase(1.5, -2.5, -1)]\r\n    public void TestOperatorPlus(double tone1, double tone2, double tone)\r\n    {\r\n        var expected = TestCaseToneHelper.NumberToTone(tone);\r\n        var actual = TestCaseToneHelper.NumberToTone(tone1) + TestCaseToneHelper.NumberToTone(tone2);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(1, 1, 2)]\r\n    [TestCase(-1, 1, 0)]\r\n    [TestCase(1, -1, 0)]\r\n    public void TestOperatorPlusWithInt(double tone1, int scale1, double tone)\r\n    {\r\n        var expected = TestCaseToneHelper.NumberToTone(tone);\r\n        var actual = TestCaseToneHelper.NumberToTone(tone1) + scale1;\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(1, 1, 0)]\r\n    [TestCase(-1, 1, -2)]\r\n    [TestCase(1, -1, 2)]\r\n    [TestCase(1.5, 2.5, -1)]\r\n    [TestCase(-1.5, 2.5, -4)]\r\n    [TestCase(1.5, -2.5, 4)]\r\n    public void TestOperatorMinus(double tone1, double tone2, double tone)\r\n    {\r\n        var expected = TestCaseToneHelper.NumberToTone(tone);\r\n        var actual = TestCaseToneHelper.NumberToTone(tone1) - TestCaseToneHelper.NumberToTone(tone2);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(1, 1, 0)]\r\n    [TestCase(-1, 1, -2)]\r\n    [TestCase(1, -1, 2)]\r\n    public void TestOperatorMinusWithInt(double tone1, int scale1, double tone)\r\n    {\r\n        var expected = TestCaseToneHelper.NumberToTone(tone);\r\n        var actual = TestCaseToneHelper.NumberToTone(tone1) - scale1;\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(1, 1)]\r\n    [TestCase(1.5, 1.5)]\r\n    [TestCase(-1.5, -1.5)]\r\n    public void TestOperatorEqual(double tone1, double tone2)\r\n    {\r\n        var expected = TestCaseToneHelper.NumberToTone(tone1);\r\n        var actual = TestCaseToneHelper.NumberToTone(tone2);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(1, 1)]\r\n    [TestCase(-1, -1)]\r\n    public void TestOperatorEqualWithInt(double tone1, int scale1)\r\n    {\r\n        Assert.That(TestCaseToneHelper.NumberToTone(tone1) == scale1);\r\n    }\r\n\r\n    [TestCase(-1, 1)]\r\n    [TestCase(1, -1)]\r\n    [TestCase(1.5, -1.5)]\r\n    [TestCase(1.5, 1)]\r\n    [TestCase(-1.5, -1)]\r\n    [TestCase(-1.5, -2)]\r\n    public void TestOperatorNotEqual(double tone1, double tone2)\r\n    {\r\n        var expected = TestCaseToneHelper.NumberToTone(tone1);\r\n        var actual = TestCaseToneHelper.NumberToTone(tone2);\r\n        Assert.That(actual, Is.Not.EqualTo(expected));\r\n    }\r\n\r\n    [TestCase(-1, 1)]\r\n    [TestCase(1, -1)]\r\n    [TestCase(-1.5, -1)]\r\n    [TestCase(-1.5, -2)]\r\n    public void TestOperatorNotEqualWithInt(double tone1, int scale1)\r\n    {\r\n        Assert.That(TestCaseToneHelper.NumberToTone(tone1) != scale1);\r\n    }\r\n\r\n    [TestCase(1, 0, true)]\r\n    [TestCase(1, 0.5, true)]\r\n    [TestCase(1, 1, false)]\r\n    [TestCase(1, 1.5, false)]\r\n    public void TestOperatorGreater(double tone1, double tone2, bool expected)\r\n    {\r\n        bool actual = TestCaseToneHelper.NumberToTone(tone1) > TestCaseToneHelper.NumberToTone(tone2);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(1, 0, true)]\r\n    [TestCase(1, 1, false)]\r\n    public void TestOperatorGreaterWithInt(double tone1, int scale1, bool expected)\r\n    {\r\n        bool actual = TestCaseToneHelper.NumberToTone(tone1) > scale1;\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(1, 0, true)]\r\n    [TestCase(1, 0.5, true)]\r\n    [TestCase(1, 1, true)]\r\n    [TestCase(1, 1.5, false)]\r\n    public void TestOperatorGreaterOrEqual(double tone1, double tone2, bool expected)\r\n    {\r\n        bool actual = TestCaseToneHelper.NumberToTone(tone1) >= TestCaseToneHelper.NumberToTone(tone2);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(1, 0, true)]\r\n    [TestCase(1, 1, true)]\r\n    public void TestOperatorGreaterOrEqualWithInt(double tone1, int scale1, bool expected)\r\n    {\r\n        bool actual = TestCaseToneHelper.NumberToTone(tone1) >= scale1;\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(-1, 0, true)]\r\n    [TestCase(-1, -0.5, true)]\r\n    [TestCase(-1, -1, false)]\r\n    [TestCase(-1, -1.5, false)]\r\n    public void TestOperatorLess(double tone1, double tone2, bool expected)\r\n    {\r\n        bool actual = TestCaseToneHelper.NumberToTone(tone1) < TestCaseToneHelper.NumberToTone(tone2);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(-1, 0, true)]\r\n    [TestCase(-1, -1, false)]\r\n    public void TestOperatorLessWithInt(double tone1, int scale1, bool expected)\r\n    {\r\n        bool actual = TestCaseToneHelper.NumberToTone(tone1) < scale1;\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(-1, 0, true)]\r\n    [TestCase(-1, -0.5, true)]\r\n    [TestCase(-1, -1, true)]\r\n    [TestCase(-1, -1.5, false)]\r\n    public void TestOperatorLessOrEqual(double tone1, double tone2, bool expected)\r\n    {\r\n        bool actual = TestCaseToneHelper.NumberToTone(tone1) <= TestCaseToneHelper.NumberToTone(tone2);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(-1, 0, true)]\r\n    [TestCase(-1, -1, true)]\r\n    public void TestOperatorLessOrEqualWithInt(double tone1, int scale1, bool expected)\r\n    {\r\n        bool actual = TestCaseToneHelper.NumberToTone(tone1) <= scale1;\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Objects/Utils/LyricUtilsTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Asserts;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Objects.Utils;\r\n\r\npublic class LyricUtilsTest\r\n{\r\n    #region progessing\r\n\r\n    [TestCase(\"karaoke\", 2, 2, \"kaoke\")]\r\n    [TestCase(\"カラオケ\", 2, 2, \"カラ\")]\r\n    [TestCase(\"カラオケ\", -1, 2, null)] // test start char gap not in the range\r\n    [TestCase(\"カラオケ\", 4, 2, \"カラオケ\")] // test start char gap not in the range, but it's valid\r\n    [TestCase(\"カラオケ\", 0, -1, null)] // test end char gap not in the range\r\n    [TestCase(\"カラオケ\", 0, 100, \"\")] // test end char gap not in the range\r\n    [TestCase(\"\", 0, 0, \"\")]\r\n    public void TestRemoveText(string text, int charGap, int count, string? expected)\r\n    {\r\n        var lyric = new Lyric { Text = text };\r\n\r\n        if (expected != null)\r\n        {\r\n            LyricUtils.RemoveText(lyric, charGap, count);\r\n            Assert.That(lyric.Text, Is.EqualTo(expected));\r\n        }\r\n        else\r\n        {\r\n            Assert.Catch(() => LyricUtils.RemoveText(lyric, charGap, count));\r\n        }\r\n    }\r\n\r\n    [TestCase(new[] { \"[0]:か\", \"[1]:ら\", \"[2]:お\", \"[3]:け\" }, 0, 2, new[] { \"[0]:お\", \"[1]:け\" })]\r\n    [TestCase(new[] { \"[0]:か\", \"[1]:ら\", \"[2]:お\", \"[3]:け\" }, 1, 1, new[] { \"[0]:か\", \"[1]:お\", \"[2]:け\" })]\r\n    [TestCase(new[] { \"[0,1]:から\", \"[2,3]:おけ\" }, 1, 2, new[] { \"[0]:から\", \"[1]:おけ\" })]\r\n    [TestCase(new[] { \"[0,3]:からおけ\" }, 0, 1, new[] { \"[0,2]:からおけ\" })]\r\n    [TestCase(new[] { \"[0,3]:からおけ\" }, 1, 2, new[] { \"[0,1]:からおけ\" })]\r\n    public void TestRemoveTextRuby(string[] rubies, int charGap, int count, string[] targetRubies)\r\n    {\r\n        var lyric = new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            RubyTags = TestCaseTagHelper.ParseRubyTags(rubies),\r\n        };\r\n        LyricUtils.RemoveText(lyric, charGap, count);\r\n\r\n        var expected = TestCaseTagHelper.ParseRubyTags(targetRubies);\r\n        var actual = lyric.RubyTags;\r\n        RubyTagAssert.ArePropertyEqual(expected, actual);\r\n    }\r\n\r\n    [TestCase(new[] { \"[0,start]:1000\", \"[1,start]:2000\", \"[2,start]:3000\", \"[3,start]:4000\" }, 0, 2, new[] { \"[0,start]:3000\", \"[1,start]:4000\" })]\r\n    [TestCase(new[] { \"[0,start]\", \"[1,start]\", \"[2,start]\", \"[3,start]\" }, 0, 2, new[] { \"[0,start]\", \"[1,start]\" })]\r\n    [TestCase(new[] { \"[0,start]:1000\", \"[2,start]:3000\" }, 1, 2, new[] { \"[0,start]:1000\" })]\r\n    public void TestRemoveTextTimeTag(string[] timeTags, int charGap, int count, string[] actualTimeTags)\r\n    {\r\n        var lyric = new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            TimeTags = TestCaseTagHelper.ParseTimeTags(timeTags),\r\n        };\r\n        LyricUtils.RemoveText(lyric, charGap, count);\r\n\r\n        var expected = TestCaseTagHelper.ParseTimeTags(actualTimeTags);\r\n        var actual = lyric.TimeTags;\r\n        TimeTagAssert.ArePropertyEqual(expected, actual);\r\n    }\r\n\r\n    [TestCase(\"kake\", 2, \"rao\", \"karaoke\")]\r\n    [TestCase(\"karaoke\", 7, \"-\", \"karaoke-\")]\r\n    [TestCase(\"オケ\", 0, \"カラ\", \"カラオケ\")]\r\n    [TestCase(\"オケ\", -1, \"カラ\", \"カラオケ\")] // test start char gap not in the range, but it's valid.\r\n    [TestCase(\"カラ\", 4, \"オケ\", \"カラオケ\")] // test start char gap not in the range, but it's valid.\r\n    [TestCase(\"\", 0, \"カラオケ\", \"カラオケ\")]\r\n    public void TestAddTextText(string text, int charGap, string addedText, string expected)\r\n    {\r\n        var lyric = new Lyric { Text = text };\r\n        LyricUtils.AddText(lyric, charGap, addedText);\r\n\r\n        string actual = lyric.Text;\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(new[] { \"[0]:か\", \"[1]:ら\", \"[2]:お\", \"[3]:け\" }, 0, \"karaoke\", new[] { \"[7]:か\", \"[8]:ら\", \"[9]:お\", \"[10]:け\" })]\r\n    [TestCase(new[] { \"[0]:か\", \"[1]:ら\", \"[2]:お\", \"[3]:け\" }, 2, \"karaoke\", new[] { \"[0]:か\", \"[1]:ら\", \"[9]:お\", \"[10]:け\" })]\r\n    [TestCase(new[] { \"[0]:か\", \"[1]:ら\", \"[2]:お\", \"[3]:け\" }, 4, \"karaoke\", new[] { \"[0]:か\", \"[1]:ら\", \"[2]:お\", \"[3]:け\" })]\r\n    public void TestAddTextRuby(string[] rubies, int charGap, string addedText, string[] targetRubies)\r\n    {\r\n        var lyric = new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            RubyTags = TestCaseTagHelper.ParseRubyTags(rubies),\r\n        };\r\n        LyricUtils.AddText(lyric, charGap, addedText);\r\n\r\n        var expected = TestCaseTagHelper.ParseRubyTags(targetRubies);\r\n        var actual = lyric.RubyTags;\r\n        RubyTagAssert.ArePropertyEqual(expected, actual);\r\n    }\r\n\r\n    [TestCase(new[] { \"[0,start]:1000\", \"[1,start]:2000\", \"[2,start]:3000\", \"[3,start]:4000\" }, 0, \"karaoke\", new[] { \"[7,start]:1000\", \"[8,start]:2000\", \"[9,start]:3000\", \"[10,start]:4000\" })]\r\n    [TestCase(new[] { \"[0,start]:1000\", \"[1,start]:2000\", \"[2,start]:3000\", \"[3,start]:4000\" }, 2, \"karaoke\", new[] { \"[0,start]:1000\", \"[1,start]:2000\", \"[9,start]:3000\", \"[10,start]:4000\" })]\r\n    [TestCase(new[] { \"[0,start]:1000\", \"[1,start]:2000\", \"[2,start]:3000\", \"[3,start]:4000\" }, 4, \"karaoke\", new[] { \"[0,start]:1000\", \"[1,start]:2000\", \"[2,start]:3000\", \"[3,start]:4000\" })]\r\n    [TestCase(new[] { \"[0,start]\", \"[1,start]\", \"[2,start]\", \"[3,start]\" }, 0, \"karaoke\", new[] { \"[7,start]\", \"[8,start]\", \"[9,start]\", \"[10,start]\" })]\r\n    [TestCase(new[] { \"[0,start]\", \"[1,start]\", \"[2,start]\", \"[3,start]\" }, 2, \"karaoke\", new[] { \"[0,start]\", \"[1,start]\", \"[9,start]\", \"[10,start]\" })]\r\n    [TestCase(new[] { \"[0,start]\", \"[1,start]\", \"[2,start]\", \"[3,start]\" }, 4, \"karaoke\", new[] { \"[0,start]\", \"[1,start]\", \"[2,start]\", \"[3,start]\" })]\r\n    public void TestAddTextTimeTag(string[] timeTags, int charGap, string addedText, string[] actualTimeTags)\r\n    {\r\n        var lyric = new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            TimeTags = TestCaseTagHelper.ParseTimeTags(timeTags),\r\n        };\r\n        LyricUtils.AddText(lyric, charGap, addedText);\r\n\r\n        var expected = TestCaseTagHelper.ParseTimeTags(actualTimeTags);\r\n        var actual = lyric.TimeTags;\r\n        TimeTagAssert.ArePropertyEqual(expected, actual);\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Time tag\r\n\r\n    [TestCase(new[] { \"[0,start]:1000\", \"[1,start]:2000\", \"[2,start]:3000\", \"[3,start]:4000\" }, true)]\r\n    [TestCase(new string[] { }, false)]\r\n    public void TestHasTimedTimeTags(string[] timeTags, bool expected)\r\n    {\r\n        var lyric = new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            TimeTags = TestCaseTagHelper.ParseTimeTags(timeTags),\r\n        };\r\n\r\n        bool actual = LyricUtils.HasTimedTimeTags(lyric);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(\"[00:01.00]か[00:02.00]ら[00:03.00]お[00:04.00]け[00:05.00]\", \"[0,start]\", \"か-\")]\r\n    [TestCase(\"[00:01.00]か[00:02.00]ら[00:03.00]お[00:04.00]け[00:05.00]\", \"[0,end]\", \"-か\")]\r\n    [TestCase(\"[00:01.00]か[00:02.00]ら[00:03.00]お[00:04.00]け[00:05.00]\", \"[3,start]\", \"け-\")]\r\n    [TestCase(\"[00:01.00]か[00:02.00]ら[00:03.00]お[00:04.00]け[00:05.00]\", \"[3,end]\", \"-け\")]\r\n    [TestCase(\"[00:01.00]からおけ[00:05.00]\", \"[0,start]\", \"からおけ-\")]\r\n    [TestCase(\"[00:01.00]からおけ[00:05.00]\", \"[0,end]\", \"-か\")]\r\n    [TestCase(\"[00:01.00]からおけ[00:05.00]\", \"[3,start]\", \"け-\")]\r\n    [TestCase(\"[00:01.00]からおけ[00:05.00]\", \"[3,end]\", \"-からおけ\")]\r\n    [TestCase(\"からおけ\", \"[0,start]\", \"からおけ-\")]\r\n    [TestCase(\"からおけ\", \"[0,end]\", \"-か\")]\r\n    [TestCase(\"からおけ\", \"[3,start]\", \"け-\")]\r\n    [TestCase(\"からおけ\", \"[3,end]\", \"-からおけ\")]\r\n    [TestCase(\"からおけ\", \"[4,start]\", \"-\")] // not showing text if index out of range.\r\n    [TestCase(\"からおけ\", \"[4,end]\", \"-\")]\r\n    [TestCase(\"からおけ\", \"[-1,start]\", \"-\")]\r\n    [TestCase(\"からおけ\", \"[-1,end]\", \"-\")]\r\n    public void TestGetTimeTagIndexDisplayText(string text, string textIndexStr, string expected)\r\n    {\r\n        var lyric = TestCaseTagHelper.ParseLyricWithTimeTag(text);\r\n        var textIndex = TestCaseTagHelper.ParseTextIndex(textIndexStr);\r\n\r\n        string actual = LyricUtils.GetTimeTagIndexDisplayText(lyric, textIndex);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(\"[00:01.00]か[00:02.00]ら[00:03.00]お[00:04.00]け[00:05.00]\", \"[0,start]\", \"か-\")]\r\n    [TestCase(\"[00:01.00]か[00:02.00]ら[00:03.00]お[00:04.00]け[00:05.00]\", \"[3,start]\", \"け-\")]\r\n    [TestCase(\"[00:01.00]か[00:02.00]ら[00:03.00]お[00:04.00]け[00:05.00]\", \"[3,end]\", \"-け\")]\r\n    [TestCase(\"[00:01.00]からおけ[00:05.00]\", \"[0,start]\", \"からおけ-\")]\r\n    [TestCase(\"[00:01.00]からおけ[00:05.00]\", \"[3,end]\", \"-からおけ\")]\r\n    public void TestGetTimeTagDisplayText(string text, string textIndexStr, string expected)\r\n    {\r\n        var lyric = TestCaseTagHelper.ParseLyricWithTimeTag(text);\r\n        var textIndex = TestCaseTagHelper.ParseTextIndex(textIndexStr);\r\n        var timeTag = lyric.TimeTags.First(x => x.Index == textIndex);\r\n\r\n        string actual = LyricUtils.GetTimeTagDisplayText(lyric, timeTag);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(0, \"(か)-\")]\r\n    [TestCase(1, \"(か)-\")]\r\n    [TestCase(2, \"-(か)\")]\r\n    [TestCase(3, \"ラ-\")]\r\n    [TestCase(4, \"ラ-\")]\r\n    [TestCase(5, \"-ラ\")]\r\n    [TestCase(6, \"(お)-\")]\r\n    [TestCase(7, \"(け)-\")]\r\n    [TestCase(8, \"(け)-\")]\r\n    [TestCase(9, \"-(け)\")]\r\n    public void TestGetTimeTagDisplayRubyText(int indexOfTimeTag, string expected)\r\n    {\r\n        var lyric = new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            TimeTags = TestCaseTagHelper.ParseTimeTags(new[]\r\n            {\r\n                \"[0,start]:1000\",\r\n                \"[0,start]:1000\",\r\n                \"[0,end]:1000\",\r\n                \"[1,start]:2000\",\r\n                \"[1,start]:2000\",\r\n                \"[1,end]:2000\",\r\n                \"[2,start]:3000\",\r\n                \"[2,start]:3000\",\r\n                \"[3,start]:4000\",\r\n                \"[3,end]:5000\",\r\n            }),\r\n            RubyTags = TestCaseTagHelper.ParseRubyTags(new[]\r\n            {\r\n                \"[0]:か\",\r\n                \"[2,3]:おけ\",\r\n            }),\r\n        };\r\n        var timeTag = lyric.TimeTags[indexOfTimeTag];\r\n\r\n        string actual = LyricUtils.GetTimeTagDisplayRubyText(lyric, timeTag);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Ruby tag\r\n\r\n    [TestCase(\"からおけ\", 0, true)]\r\n    [TestCase(\"からおけ\", 4, true)]\r\n    [TestCase(\"からおけ\", -1, false)]\r\n    [TestCase(\"からおけ\", 5, false)]\r\n    [TestCase(\"\", 0, true)]\r\n    public void TestAbleToInsertRubyTagAtIndex(string text, int index, bool expected)\r\n    {\r\n        var lyric = TestCaseTagHelper.ParseLyricWithTimeTag(text);\r\n\r\n        bool actual = LyricUtils.AbleToInsertRubyTagAtIndex(lyric, index);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Time display\r\n\r\n    [TestCase(0, 0, \"00:00:000 - 00:00:000\")]\r\n    [TestCase(0, 1000, \"00:00:000 - 00:01:000\")]\r\n    [TestCase(1000, 0, \"00:00:000 - 00:01:000\")] // should check the order of time.\r\n    [TestCase(-1000, 0, \"-00:01:000 - 00:00:000\")]\r\n    [TestCase(0, -1000, \"-00:01:000 - 00:00:000\")] // should check the order of time.\r\n    public void TestLyricTimeFormattedString(double startTime, double endTime, string expected)\r\n    {\r\n        var lyric = new Lyric\r\n        {\r\n            TimeTags = new List<TimeTag>\r\n            {\r\n                new (new TextIndex(), startTime),\r\n                new (new TextIndex(), endTime),\r\n            },\r\n        };\r\n\r\n        string actual = LyricUtils.LyricTimeFormattedString(lyric);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(new[] { \"[0,start]:1000\", \"[1,start]:2000\", \"[2,start]:3000\", \"[3,start]:4000\" }, \"00:01:000 - 00:04:000\")]\r\n    [TestCase(new[] { \"[0,start]:4000\", \"[1,start]:3000\", \"[2,start]:2000\", \"[3,start]:1000\" }, \"00:01:000 - 00:04:000\")] // should display right-time even it's not being ordered.\r\n    [TestCase(new[] { \"[3,start]:4000\", \"[2,start]:3000\", \"[1,start]:2000\", \"[0,start]:1000\" }, \"00:01:000 - 00:04:000\")] // should display right-time even it's not being ordered.\r\n    [TestCase(new[] { \"[0,start]:1000\", \"[1,start]:2000\", \"[1,start]\" }, \"00:01:000 - 00:02:000\")] // with null case.\r\n    [TestCase(new[] { \"[1,start]\", \"[0,start]:1000\", \"[1,start]:2000\" }, \"00:01:000 - 00:02:000\")] // with null case.\r\n    [TestCase(new[] { \"[0,start]:1000\", \"[1,start]\" }, \"00:01:000 - 00:01:000\")] // with null case.\r\n    [TestCase(new[] { \"[0,start]:1000\" }, \"00:01:000 - 00:01:000\")]\r\n    [TestCase(new[] { \"[0,start]\" }, \"--:--:--- - --:--:---\")] // with null case.\r\n    [TestCase(new string[] { }, \"--:--:--- - --:--:---\")]\r\n    public void TestTimeTagTimeFormattedString(string[] timeTags, string expected)\r\n    {\r\n        var lyric = new Lyric\r\n        {\r\n            TimeTags = TestCaseTagHelper.ParseTimeTags(timeTags),\r\n        };\r\n\r\n        string actual = LyricUtils.TimeTagTimeFormattedString(lyric);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Singer\r\n\r\n    [TestCase(new[] { \"[1]name:Singer1\" }, \"[1]name:Singer1\", true)]\r\n    [TestCase(new[] { \"[1]name:Singer1\" }, \"[2]name:Singer2\", false)]\r\n    [TestCase(new string[] { }, \"[1]name:Singer1\", false)]\r\n    public void TestContainsSinger(string[] existSingers, string compareSinger, bool expected)\r\n    {\r\n        var singer = TestCaseTagHelper.ParseSinger(compareSinger);\r\n        var lyric = new Lyric\r\n        {\r\n            SingerIds = TestCaseTagHelper.ParseSingers(existSingers).Select(x => x.ID).ToArray(),\r\n        };\r\n\r\n        bool actual = LyricUtils.ContainsSinger(lyric, singer);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(new[] { \"[1]name:Singer1\" }, new[] { \"[1]name:Singer1\", \"[1]name:Singer1\" }, true)]\r\n    [TestCase(new[] { \"[1]name:Singer1\" }, new[] { \"[1]name:Singer1\" }, true)]\r\n    [TestCase(new[] { \"[1]name:Singer1\" }, new[] { \"[2]name:Singer2\" }, false)]\r\n    [TestCase(new string[] { }, new[] { \"[1]name:Singer1\" }, true)]\r\n    public void TestOnlyContainsSingers(string[] existSingers, string[] compareSingers, bool expected)\r\n    {\r\n        var singers = TestCaseTagHelper.ParseSingers(compareSingers).ToList();\r\n        var lyric = new Lyric\r\n        {\r\n            SingerIds = TestCaseTagHelper.ParseSingers(existSingers).Select(x => x.ID).ToArray(),\r\n        };\r\n\r\n        bool actual = LyricUtils.OnlyContainsSingers(lyric, singers);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    #endregion\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Objects/Utils/LyricsUtilsTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Globalization;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Asserts;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Objects.Utils;\r\n\r\npublic class LyricsUtilsTest\r\n{\r\n    #region separate\r\n\r\n    [TestCase(\"karaoke\", 4, \"kara\", \"oke\")]\r\n    [TestCase(\"カラオケ\", 2, \"カラ\", \"オケ\")]\r\n    [TestCase(\"\", 0, null, null)] // Test error\r\n    [TestCase(\"karaoke\", 100, null, null)]\r\n    [TestCase(\"\", 100, null, null)]\r\n    public void TestSeparateLyricText(string text, int splitIndex, string? expectedFirstText, string? expectedSecondText)\r\n    {\r\n        var lyric = new Lyric { Text = text };\r\n\r\n        if (expectedFirstText != null && expectedSecondText != null)\r\n        {\r\n            var (firstLyric, secondLyric) = LyricsUtils.SplitLyric(lyric, splitIndex);\r\n            Assert.That(firstLyric.Text, Is.EqualTo(expectedFirstText));\r\n            Assert.That(secondLyric.Text, Is.EqualTo(expectedSecondText));\r\n        }\r\n        else\r\n        {\r\n            Assert.Catch(() => LyricsUtils.SplitLyric(lyric, splitIndex));\r\n        }\r\n    }\r\n\r\n    [TestCase(\"カラオケ\", new[] { \"[0,start]:1000\", \"[1,start]:2000\", \"[2,start]:3000\", \"[3,start]:4000\", \"[3,end]:5000\" }, 2,\r\n        new[] { \"[0,start]:1000\", \"[1,start]:2000\", \"[1,end]:3000\" },\r\n        new[] { \"[0,start]:3000\", \"[1,start]:4000\", \"[1,end]:5000\" })]\r\n    public void TestSeparateLyricTimeTag(string text, string[] timeTags, int splitIndex, string[] firstTimeTags, string[] secondTimeTags)\r\n    {\r\n        var lyric = new Lyric\r\n        {\r\n            Text = text,\r\n            TimeTags = TestCaseTagHelper.ParseTimeTags(timeTags),\r\n        };\r\n\r\n        var (firstLyric, secondLyric) = LyricsUtils.SplitLyric(lyric, splitIndex);\r\n\r\n        TimeTagAssert.ArePropertyEqual(TestCaseTagHelper.ParseTimeTags(firstTimeTags), firstLyric.TimeTags);\r\n        TimeTagAssert.ArePropertyEqual(TestCaseTagHelper.ParseTimeTags(secondTimeTags), secondLyric.TimeTags);\r\n    }\r\n\r\n    [TestCase(\"カラオケ\", new[] { \"[0]:か\", \"[1]:ら\", \"[2]:お\", \"[3]:け\" }, 2,\r\n        new[] { \"[0]:か\", \"[1]:ら\" }, new[] { \"[0]:お\", \"[1]:け\" })]\r\n    [TestCase(\"カラオケ\", new[] { \"[0,2]:からおけ\" }, 2, new string[] { }, new string[] { })] // tag won't be assign to lyric if not fully in the range of the text.\r\n    [TestCase(\"カラオケ\", new[] { \"[1,3]:からおけ\" }, 2, new string[] { }, new string[] { })] // tag won't be assign to lyric if not fully in the range of the text.\r\n    [TestCase(\"カラオケ\", new[] { \"[1,2]:からおけ\" }, 2, new string[] { }, new string[] { })] // tag won't be assign to lyric if not fully in the range of the text.\r\n    [TestCase(\"カラオケ\", new[] { \"[0,3]:からおけ\" }, 2, new string[] { }, new string[] { })] // tag won't be assign to lyric if not fully in the range of the text.\r\n    [TestCase(\"カラオケ\", new string[] { }, 2, new string[] { }, new string[] { })]\r\n    public void TestSeparateLyricRubyTag(string text, string[] rubyTags, int splitIndex, string[] firstRubyTags, string[] secondRubyTags)\r\n    {\r\n        var lyric = new Lyric\r\n        {\r\n            Text = text,\r\n            RubyTags = TestCaseTagHelper.ParseRubyTags(rubyTags),\r\n        };\r\n\r\n        var (firstLyric, secondLyric) = LyricsUtils.SplitLyric(lyric, splitIndex);\r\n\r\n        RubyTagAssert.ArePropertyEqual(TestCaseTagHelper.ParseRubyTags(firstRubyTags), firstLyric.RubyTags);\r\n        RubyTagAssert.ArePropertyEqual(TestCaseTagHelper.ParseRubyTags(secondRubyTags), secondLyric.RubyTags);\r\n    }\r\n\r\n    [Ignore(\"Not really sure second lyric is based on lyric time or time-tag time.\")]\r\n    public void TestSeparateLyricStartTime()\r\n    {\r\n        // todo : implement\r\n    }\r\n\r\n    [Ignore(\"Not really sure second lyric is based on lyric time or time-tag time.\")]\r\n    public void TestSeparateLyricDuration()\r\n    {\r\n        // todo : implement\r\n    }\r\n\r\n    [TestCase(new[] { 1, 2 }, new[] { 1, 2 }, new[] { 1, 2 })]\r\n    [TestCase(new[] { 1 }, new[] { 1 }, new[] { 1 })]\r\n    [TestCase(new[] { -1 }, new[] { -1 }, new[] { -1 })] // copy singer index even it's invalid.\r\n    [TestCase(new int[] { }, new int[] { }, new int[] { })]\r\n    public void TestSeparateLyricSinger(int[] singerIndexes, int[] expectedFirstSingerIndexes, int[] expectedSecondSingerIndexes)\r\n    {\r\n        const int split_index = 2;\r\n        var lyric = new Lyric\r\n        {\r\n            Text = \"karaoke!\",\r\n            SingerIds = TestCaseElementIdHelper.CreateElementIdsByNumbers(singerIndexes),\r\n        };\r\n\r\n        var (firstLyric, secondLyric) = LyricsUtils.SplitLyric(lyric, split_index);\r\n        var expectedFirstSingerIds = TestCaseElementIdHelper.CreateElementIdsByNumbers(expectedFirstSingerIndexes);\r\n        var expectedSecondSingerIds = TestCaseElementIdHelper.CreateElementIdsByNumbers(expectedSecondSingerIndexes);\r\n\r\n        Assert.That(firstLyric.SingerIds, Is.EqualTo(expectedFirstSingerIds));\r\n        Assert.That(secondLyric.SingerIds, Is.EqualTo(expectedSecondSingerIds));\r\n\r\n        // also should check is not same object as origin lyric for safety purpose.\r\n        Assert.That(lyric.SingerIds, Is.Not.SameAs(firstLyric.SingerIds));\r\n        Assert.That(lyric.SingerIds, Is.Not.SameAs(secondLyric.SingerIds));\r\n    }\r\n\r\n    [TestCase(1, 1, 1)]\r\n    [TestCase(54, 54, 54)]\r\n    [TestCase(null, null, null)]\r\n    public void TestSeparateLyricLanguage(int? lcid, int? firstLcid, int? secondLcid)\r\n    {\r\n        var cultureInfo = lcid != null ? new CultureInfo(lcid.Value) : null;\r\n        var expectedFirstCultureInfo = firstLcid != null ? new CultureInfo(firstLcid.Value) : null;\r\n        var expectedSecondCultureInfo = secondLcid != null ? new CultureInfo(secondLcid.Value) : null;\r\n\r\n        const int split_index = 2;\r\n        var lyric = new Lyric\r\n        {\r\n            Text = \"karaoke!\",\r\n            Language = cultureInfo,\r\n        };\r\n\r\n        var (firstLyric, secondLyric) = LyricsUtils.SplitLyric(lyric, split_index);\r\n\r\n        Assert.That(firstLyric.Language, Is.EqualTo(expectedFirstCultureInfo));\r\n        Assert.That(secondLyric.Language, Is.EqualTo(expectedSecondCultureInfo));\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region combine\r\n\r\n    [TestCase(\"Kara\", \"oke\", \"Karaoke\")]\r\n    [TestCase(\"\", \"oke\", \"oke\")]\r\n    [TestCase(\"Kara\", \"\", \"Kara\")]\r\n    public void TestCombineLyricText(string firstText, string secondText, string expected)\r\n    {\r\n        var lyric1 = new Lyric { Text = firstText };\r\n        var lyric2 = new Lyric { Text = secondText };\r\n\r\n        var combineLyric = LyricsUtils.CombineLyric(lyric1, lyric2);\r\n        Assert.That(combineLyric.Text, Is.EqualTo(expected));\r\n    }\r\n\r\n    [TestCase(new[] { \"[0,start]\" }, new[] { \"[0,start]\" }, new[] { \"[0,start]\", \"[7,start]\" })]\r\n    [TestCase(new[] { \"[0,end]\" }, new[] { \"[0,end]\" }, new[] { \"[0,end]\", \"[7,end]\" })]\r\n    [TestCase(new[] { \"[0,start]:1000\" }, new[] { \"[0,start]:1000\" }, new[] { \"[0,start]:1000\", \"[7,start]:1000\" })] // deal with the case with time.\r\n    [TestCase(new[] { \"[0,start]:1000\" }, new[] { \"[0,start]:-1000\" }, new[] { \"[0,start]:1000\", \"[7,start]:-1000\" })] // deal with the case with not invalid time tag time.\r\n    [TestCase(new[] { \"[-1,start]\" }, new[] { \"[-1,start]\" }, new[] { \"[-1,start]\", \"[6,start]\" })] // deal with the case with not invalid time tag position.\r\n    public void TestCombineLyricTimeTag(string[] firstTimeTags, string[] secondTimeTags, string[] expectTimeTags)\r\n    {\r\n        var lyric1 = new Lyric\r\n        {\r\n            Text = \"karaoke\",\r\n            TimeTags = TestCaseTagHelper.ParseTimeTags(firstTimeTags),\r\n        };\r\n        var lyric2 = new Lyric\r\n        {\r\n            Text = \"karaoke\",\r\n            TimeTags = TestCaseTagHelper.ParseTimeTags(secondTimeTags),\r\n        };\r\n\r\n        var combineLyric = LyricsUtils.CombineLyric(lyric1, lyric2);\r\n        var timeTags = combineLyric.TimeTags;\r\n\r\n        for (int i = 0; i < timeTags.Count; i++)\r\n        {\r\n            var expected = TestCaseTagHelper.ParseTimeTag(expectTimeTags[i]);\r\n            Assert.That(timeTags[i].Index, Is.EqualTo(expected.Index));\r\n            Assert.That(timeTags[i].Time, Is.EqualTo(expected.Time));\r\n        }\r\n    }\r\n\r\n    [TestCase(new[] { \"[0]:ruby\" }, new[] { \"[0]:ルビ\" }, new[] { \"[0]:ruby\", \"[7]:ルビ\" })]\r\n    [TestCase(new[] { \"[0]:\" }, new[] { \"[0]:\" }, new[] { \"[0]:\", \"[7]:\" })]\r\n    [TestCase(new[] { \"[0,2]:\" }, new[] { \"[0,2]:\" }, new[] { \"[0,2]:\", \"[7,9]:\" })]\r\n    [TestCase(new[] { \"[0,9]:\" }, new[] { \"[0,9]:\" }, new[] { \"[0,9]:\", \"[7,13]:\" })] // will auto-fix ruby index.\r\n    [TestCase(new[] { \"[-10,-1]:\" }, new[] { \"[-10,-1]:\" }, new[] { \"[-10,-1]:\", \"[0,6]:\" })] // will auto-fix ruby index.\r\n    public void TestCombineLyricRubyTag(string[] firstRubyTags, string[] secondRubyTags, string[] expectedRubyTags)\r\n    {\r\n        var lyric1 = new Lyric\r\n        {\r\n            Text = \"karaoke\",\r\n            RubyTags = TestCaseTagHelper.ParseRubyTags(firstRubyTags),\r\n        };\r\n        var lyric2 = new Lyric\r\n        {\r\n            Text = \"karaoke\",\r\n            RubyTags = TestCaseTagHelper.ParseRubyTags(secondRubyTags),\r\n        };\r\n\r\n        var combineLyric = LyricsUtils.CombineLyric(lyric1, lyric2);\r\n\r\n        var expected = TestCaseTagHelper.ParseRubyTags(expectedRubyTags);\r\n        var actual = combineLyric.RubyTags;\r\n        RubyTagAssert.ArePropertyEqual(expected, actual);\r\n    }\r\n\r\n    [TestCase(new[] { 1 }, new[] { 2 }, new[] { 1, 2 })]\r\n    [TestCase(new[] { 1 }, new[] { 1 }, new[] { 1 })] // deal with duplicated case.\r\n    [TestCase(new[] { 1 }, new[] { -2 }, new[] { 1, -2 })] // deal with id not right case.\r\n    public void TestCombineLyricSinger(int[] firstSingerIndexes, int[] secondSingerIndexes, int[] combinedSingerIds)\r\n    {\r\n        var lyric1 = new Lyric { SingerIds = TestCaseElementIdHelper.CreateElementIdsByNumbers(firstSingerIndexes) };\r\n        var lyric2 = new Lyric { SingerIds = TestCaseElementIdHelper.CreateElementIdsByNumbers(secondSingerIndexes) };\r\n\r\n        var combineLyric = LyricsUtils.CombineLyric(lyric1, lyric2);\r\n\r\n        var expected = TestCaseElementIdHelper.CreateElementIdsByNumbers(combinedSingerIds);\r\n        var actual = combineLyric.SingerIds;\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(1, 1, 1)]\r\n    [TestCase(54, 54, 54)]\r\n    [TestCase(null, 1, null)]\r\n    [TestCase(1, null, null)]\r\n    [TestCase(null, null, null)]\r\n    public void TestCombineLayoutLanguage(int? firstLcid, int? secondLcid, int? expectedLcid)\r\n    {\r\n        var cultureInfo1 = firstLcid != null ? new CultureInfo(firstLcid.Value) : null;\r\n        var cultureInfo2 = secondLcid != null ? new CultureInfo(secondLcid.Value) : null;\r\n\r\n        var lyric1 = new Lyric { Language = cultureInfo1 };\r\n        var lyric2 = new Lyric { Language = cultureInfo2 };\r\n\r\n        var combineLyric = LyricsUtils.CombineLyric(lyric1, lyric2);\r\n\r\n        var expected = expectedLcid != null ? new CultureInfo(expectedLcid.Value) : null;\r\n        var actual = combineLyric.Language;\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    #endregion\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Objects/Utils/NoteUtilsTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Objects.Utils;\r\n\r\npublic class NoteUtilsTest\r\n{\r\n    [TestCase(\"karaoke\", \"\", false, \"karaoke\")]\r\n    [TestCase(\"karaoke\", \"ka- ra- o- ke-\", false, \"karaoke\")]\r\n    [TestCase(\"\", \"ka- ra- o- ke-\", false, \"\")]\r\n    [TestCase(\"karaoke\", \"\", true, \"karaoke\")]\r\n    [TestCase(\"karaoke\", \"ka- ra- o- ke-\", true, \"ka- ra- o- ke-\")]\r\n    [TestCase(\"\", \"ka- ra- o- ke-\", true, \"ka- ra- o- ke-\")]\r\n    public void TestDisplayText(string text, string rubyText, bool useRubyTextIfHave, string expected)\r\n    {\r\n        var note = new Note\r\n        {\r\n            Text = text,\r\n            RubyText = rubyText,\r\n        };\r\n\r\n        string actual = NoteUtils.DisplayText(note, useRubyTextIfHave);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Objects/Utils/NotesUtilsTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Objects.Utils;\r\n\r\npublic class NotesUtilsTest\r\n{\r\n    [TestCase(new double[] { 1000, 5000 }, 0.2, new double[] { 1000, 1000 }, new double[] { 2000, 4000 })]\r\n    [TestCase(new double[] { 1000, 5000 }, 0.5, new double[] { 1000, 2500 }, new double[] { 3500, 2500 })]\r\n    [TestCase(new double[] { 1000, 0 }, 0.2, new double[] { 1000, 0 }, new double[] { 1000, 0 })] // it's ok to split if duration is 0.\r\n    [TestCase(new double[] { 1000, 0 }, 0.7, new double[] { 1000, 0 }, new double[] { 1000, 0 })]\r\n    [TestCase(new double[] { 1000, 5000 }, -1, null, null)] // should be in the range.\r\n    [TestCase(new double[] { 1000, 5000 }, 3, null, null)]\r\n    [TestCase(new double[] { 1000, 5000 }, 0, null, null)] // should not be 0 or 1.\r\n    [TestCase(new double[] { 1000, 5000 }, 1, null, null)]\r\n    public void TestSplitNoteTime(double[] time, double percentage, double[]? firstTime, double[]? secondTime)\r\n    {\r\n        var referencedLyric = TestCaseNoteHelper.CreateLyricForNote(2, \"Lyric\", time[0], time[1]);\r\n        var note = new Note\r\n        {\r\n            ReferenceLyricId = referencedLyric.ID,\r\n            ReferenceLyric = referencedLyric,\r\n        };\r\n\r\n        if (firstTime != null && secondTime != null)\r\n        {\r\n            var (firstNote, secondNote) = NotesUtils.SplitNote(note, percentage);\r\n            Assert.That(firstNote.StartTime, Is.EqualTo(firstTime[0]));\r\n            Assert.That(firstNote.Duration, Is.EqualTo(firstTime[1]));\r\n\r\n            Assert.That(secondNote.StartTime, Is.EqualTo(secondTime[0]));\r\n            Assert.That(secondNote.Duration, Is.EqualTo(secondTime[1]));\r\n        }\r\n        else\r\n        {\r\n            Assert.Catch(() => NotesUtils.SplitNote(note, percentage));\r\n        }\r\n    }\r\n\r\n    [Test]\r\n    public void TestSplitNoteOtherProperty()\r\n    {\r\n        const double percentage = 0.3;\r\n\r\n        var referencedLyric = TestCaseNoteHelper.CreateLyricForNote(2, \"Lyric\", 1000, 2000);\r\n        referencedLyric.SingerIds = TestCaseElementIdHelper.CreateElementIdsByNumbers(new[] { 0 });\r\n\r\n        var note = new Note\r\n        {\r\n            Text = \"ka\",\r\n            Display = false,\r\n            Tone = new Tone(-1, true),\r\n            ReferenceLyricId = referencedLyric.ID,\r\n            ReferenceLyric = referencedLyric,\r\n            ReferenceTimeTagIndex = 0,\r\n        };\r\n\r\n        // create other property and make sure other class is applied value.\r\n        var (firstNote, secondNote) = NotesUtils.SplitNote(note, percentage);\r\n\r\n        Assert.That(firstNote.StartTime, Is.EqualTo(1000));\r\n        Assert.That(secondNote.StartTime, Is.EqualTo(1600));\r\n\r\n        Assert.That(firstNote.Duration, Is.EqualTo(600));\r\n        Assert.That(secondNote.Duration, Is.EqualTo(1400));\r\n\r\n        testRemainProperty(note, firstNote);\r\n        testRemainProperty(note, firstNote);\r\n\r\n        static void testRemainProperty(Note expect, Note actual)\r\n        {\r\n            Assert.That(actual.Text, Is.EqualTo(expect.Text));\r\n            Assert.That(actual.Display, Is.EqualTo(expect.Display));\r\n            Assert.That(actual.Tone, Is.EqualTo(expect.Tone));\r\n\r\n            Assert.That(actual.ReferenceLyric, Is.EqualTo(expect.ReferenceLyric));\r\n            Assert.That(actual.ReferenceTimeTagIndex, Is.EqualTo(expect.ReferenceTimeTagIndex));\r\n\r\n            Assert.That(actual.ReferenceLyric?.SingerIds, Is.EqualTo(expect.ReferenceLyric?.SingerIds));\r\n        }\r\n    }\r\n\r\n    [TestCase(new double[] { 1000, -1000 }, new double[] { 2000, -4000 }, new double[] { 1000, -1000 })]\r\n    [TestCase(new double[] { 1000, 0 }, new double[] { 1000, 0 }, new double[] { 1000, 0 })] // it's ok to combine if duration is 0.\r\n    public void TestCombineNoteTime(double[] firstOffset, double[] secondOffset, double[] expectedOffset)\r\n    {\r\n        var referencedLyric = TestCaseNoteHelper.CreateLyricForNote(2, \"Lyric\", 1000, 5000);\r\n\r\n        var firstNote = new Note\r\n        {\r\n            ReferenceLyricId = referencedLyric.ID,\r\n            ReferenceLyric = referencedLyric,\r\n            StartTimeOffset = firstOffset[0],\r\n            EndTimeOffset = firstOffset[1],\r\n            ReferenceTimeTagIndex = 0,\r\n        };\r\n\r\n        var secondNote = new Note\r\n        {\r\n            ReferenceLyricId = referencedLyric.ID,\r\n            ReferenceLyric = referencedLyric,\r\n            StartTimeOffset = secondOffset[0],\r\n            EndTimeOffset = secondOffset[1],\r\n            ReferenceTimeTagIndex = 0,\r\n        };\r\n\r\n        var combineNote = NotesUtils.CombineNote(firstNote, secondNote);\r\n        Assert.That(combineNote.StartTimeOffset, Is.EqualTo(expectedOffset[0]));\r\n        Assert.That(combineNote.EndTimeOffset, Is.EqualTo(expectedOffset[1]));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Objects/Utils/OrderUtilsTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Types;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Objects.Utils;\r\n\r\npublic class OrderUtilsTest\r\n{\r\n    [TestCase(new[] { 1, 2, 3, 4 }, false)]\r\n    [TestCase(new[] { 1, 2, 3, 5 }, false)]\r\n    [TestCase(new[] { 1, 2, 3, 3 }, true)]\r\n    [TestCase(new[] { 1, 1, 1, 1 }, true)]\r\n    [TestCase(new[] { -1, -2, -3, -4 }, false)] // should not include those ids but not check for now.\r\n    [TestCase(new int[] { }, false)]\r\n    public void TestContainDuplicatedId(int[] orders, bool expected)\r\n    {\r\n        var objects = orders.Select(x => new TestOrderObject { Order = x }).ToArray();\r\n\r\n        bool actual = OrderUtils.ContainDuplicatedId(objects);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(new[] { 1, 2, 3, 4 }, 1)]\r\n    [TestCase(new[] { 4, 3, 2, 1 }, 1)]\r\n    [TestCase(new int[] { }, 0)]\r\n    public void TestGetMinOrderNumber(int[] orders, int expected)\r\n    {\r\n        var objects = orders.Select(x => new TestOrderObject { Order = x }).ToArray();\r\n\r\n        int actual = OrderUtils.GetMinOrderNumber(objects);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(new[] { 1, 2, 3, 4 }, 4)]\r\n    [TestCase(new[] { 4, 3, 2, 1 }, 4)]\r\n    [TestCase(new int[] { }, 0)]\r\n    public void TestGetMaxOrderNumber(int[] orders, int expected)\r\n    {\r\n        var objects = orders.Select(x => new TestOrderObject { Order = x }).ToArray();\r\n\r\n        int actual = OrderUtils.GetMaxOrderNumber(objects);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(new[] { 1, 2, 3, 4 }, new[] { 1, 2, 3, 4 })]\r\n    [TestCase(new[] { 4, 3, 2, 1 }, new[] { 1, 2, 3, 4 })]\r\n    [TestCase(new[] { 4, 4, 2, 2 }, new[] { 2, 2, 4, 4 })] // should not happen but still make a order.\r\n    [TestCase(new int[] { }, new int[] { })]\r\n    public void TestSorted(int[] orders, int[] expected)\r\n    {\r\n        var objects = orders.Select(x => new TestOrderObject { Order = x });\r\n        var orderedArray = OrderUtils.Sorted(objects);\r\n\r\n        int[] actual = orderedArray.Select(x => x.Order).ToArray();\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(new[] { 1, 2, 3, 4 }, 1, new[] { 2, 3, 4, 5 })]\r\n    [TestCase(new[] { 1, 2, 3, 4 }, -1, new[] { 0, 1, 2, 3 })]\r\n    [TestCase(new[] { 1, 1, 1, 1 }, 1, new[] { 2, 2, 2, 2 })]\r\n    [TestCase(new[] { 4, 3, 2, 1 }, 1, new[] { 5, 4, 3, 2 })] // Not care order in objects and just doing shifting job.\r\n    public void TestShiftingOrder(int[] orders, int offset, int[] expected)\r\n    {\r\n        var objects = orders.Select(x => new TestOrderObject { Order = x }).ToArray();\r\n        OrderUtils.ShiftingOrder(objects, offset);\r\n\r\n        // convert order result.\r\n        int[] actual = objects.Select(x => x.Order).ToArray();\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(new[] { 1, 2, 3, 4 }, 1, new int[] { }, new[] { 1, 2, 3, 4 })]\r\n    [TestCase(new[] { 1, 2, 3, 4 }, -1, new[] { 1, 2, 3, 4 }, new[] { -1, 0, 1, 2 })]\r\n    [TestCase(new[] { 1, 2, 3, 4 }, 3, new[] { 4, 3, 2, 1 }, new[] { 3, 4, 5, 6 })] // change id should consider will affect exist mapping.\r\n    [TestCase(new[] { 4, 3, 2, 1 }, 3, new[] { 4, 3, 2, 1 }, new[] { 6, 5, 4, 3 })] // change id should consider will affect exist mapping.\r\n    [TestCase(new[] { 1, 3, 5, 7 }, 1, new[] { 3, 5, 7 }, new[] { 1, 2, 3, 4 })]\r\n    [TestCase(new[] { 1, 1, 1, 1 }, 1, new[] { 1, 1, 1 }, new[] { 1, 2, 3, 4 })] // invalid input might cause some of id mapping will be lost.\r\n    public void TestResortOrder(int[] orders, int startFrom, int[] expectedMovingOrders, int[] expectedNewOrder)\r\n    {\r\n        var objects = orders.Select(x => new TestOrderObject { Order = x }).ToArray();\r\n\r\n        var movingStepResult = new List<int>();\r\n        OrderUtils.ResortOrder(objects, startFrom, (_, o, _) =>\r\n        {\r\n            movingStepResult.Add(o);\r\n        });\r\n\r\n        // convert order result.\r\n        int[] result = objects.Select(x => x.Order).ToArray();\r\n        Assert.That(result, Is.EqualTo(expectedNewOrder));\r\n\r\n        // should check moving order step also。\r\n        Assert.That(movingStepResult.ToArray(), Is.EqualTo(expectedMovingOrders));\r\n    }\r\n\r\n    [TestCase(new[] { 1, 2, 3, 4 }, 1, 2, new[] { 1, 2, -1 }, new[] { 2, 1, 3, 4 })]\r\n    [TestCase(new[] { 1, 2, 3, 4 }, 2, 1, new[] { 2, 1, -1 }, new[] { 2, 1, 3, 4 })]\r\n    [TestCase(new[] { 1, 2, 3, 4 }, 1, 4, new[] { 1, 2, 3, 4, -1 }, new[] { 4, 1, 2, 3 })]\r\n    [TestCase(new[] { 1, 2, 3, 4 }, 4, 1, new[] { 4, 3, 2, 1, -1 }, new[] { 2, 3, 4, 1 })]\r\n    public void TestChangeOrder(int[] orders, int oldOrder, int nowOrder, int[] expectedMovingOrders, int[] expectedNewOrder)\r\n    {\r\n        var objects = orders.Select(x => new TestOrderObject { Order = x }).ToArray();\r\n\r\n        // record order index change step.\r\n        var movingStepResult = new List<int>();\r\n\r\n        // This utils only change order property.\r\n        OrderUtils.ChangeOrder(objects, oldOrder, nowOrder, (_, o, _) =>\r\n        {\r\n            movingStepResult.Add(o);\r\n        });\r\n\r\n        // change order result.\r\n        int[] result = objects.Select(x => x.Order).ToArray();\r\n        Assert.That(result, Is.EqualTo(expectedNewOrder));\r\n\r\n        // should check moving order step also.\r\n        Assert.That(movingStepResult.ToArray(), Is.EqualTo(expectedMovingOrders));\r\n    }\r\n\r\n    internal class TestOrderObject : IHasOrder\r\n    {\r\n        public int Order { get; set; }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Objects/Utils/RubyTagUtilsTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Asserts;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Objects.Utils;\r\n\r\n[TestFixture]\r\npublic class RubyTagUtilsTest\r\n{\r\n    [TestCase(\"[0,1]:ka\", \"karaoke\", \"[0,1]:ka\")]\r\n    [TestCase(\"[0,1]:\", \"karaoke\", \"[0,1]:\")]\r\n    [TestCase(\"[0,0]:ka\", \"karaoke\", \"[0,0]:ka\")] // ignore at same index\r\n    [TestCase(\"[-1,1]:ka\", \"karaoke\", \"[0,1]:ka\")]\r\n    [TestCase(\"[3,1]:ka\", \"karaoke\", \"[1,3]:ka\")]\r\n    [TestCase(\"[3,-1]:ka\", \"karaoke\", \"[0,3]:ka\")]\r\n    public void TestGetFixedIndex(string rubyTagStr, string lyric, string actualTag)\r\n    {\r\n        // test ruby tag.\r\n        var rubyTag = TestCaseTagHelper.ParseRubyTag(rubyTagStr);\r\n\r\n        var expectedRubyTag = TestCaseTagHelper.ParseRubyTag(actualTag);\r\n        var actualRubyTag = generateFixedTag(rubyTag, lyric);\r\n        RubyTagAssert.ArePropertyEqual(expectedRubyTag, actualRubyTag);\r\n\r\n        static RubyTag generateFixedTag(RubyTag rubyTag, string lyric)\r\n        {\r\n            (int startIndex, int endIndex) = RubyTagUtils.GetFixedIndex(rubyTag, lyric);\r\n            return new RubyTag\r\n            {\r\n                Text = rubyTag.Text,\r\n                StartIndex = startIndex,\r\n                EndIndex = endIndex,\r\n            };\r\n        }\r\n    }\r\n\r\n    [TestCase(\"[0]:ka\", \"karaoke\", 1, \"[1]:ka\")]\r\n    [TestCase(\"[0]:\", \"karaoke\", 1, \"[1]:\")]\r\n    [TestCase(\"[0]:ka\", \"karaoke\", -1, \"[0]:ka\")]\r\n    [TestCase(\"[0]:ka\", \"\", -1, null)] // should not be able to adjust the time-tag if lyric is empty.\r\n    [TestCase(\"[0]:ka\", \"\", 1, null)] // should not be able to adjust the time-tag if lyric is empty.\r\n    [TestCase(\"[1,0]:ka\", \"karaoke\", 0, \"[0,1]:ka\")] // will auto fix the position\r\n    [TestCase(\"[1,0]:ka\", \"karaoke\", 1, \"[1,2]:ka\")]\r\n    public void TestGetShiftingIndex(string rubyTagStr, string lyric, int offset, string? actualTag)\r\n    {\r\n        var rubyTag = TestCaseTagHelper.ParseRubyTag(rubyTagStr);\r\n\r\n        if (actualTag == null)\r\n        {\r\n            Assert.That(() => generateShiftingTag(rubyTag, lyric, offset), Throws.TypeOf<InvalidOperationException>());\r\n            Assert.That(() => generateShiftingTag(rubyTag, lyric, offset), Throws.TypeOf<InvalidOperationException>());\r\n            return;\r\n        }\r\n\r\n        // test ruby tag.\r\n        var expectedRubyTag = TestCaseTagHelper.ParseRubyTag(actualTag);\r\n        var actualRubyTag = generateShiftingTag(rubyTag, lyric, offset);\r\n        RubyTagAssert.ArePropertyEqual(expectedRubyTag, actualRubyTag);\r\n\r\n        static RubyTag generateShiftingTag(RubyTag rubyTag, string lyric, int offset)\r\n        {\r\n            (int startIndex, int endIndex) = RubyTagUtils.GetShiftingIndex(rubyTag, lyric, offset);\r\n            return new RubyTag\r\n            {\r\n                Text = rubyTag.Text,\r\n                StartIndex = startIndex,\r\n                EndIndex = endIndex,\r\n            };\r\n        }\r\n    }\r\n\r\n    [TestCase(\"[0,1]:ka\", \"karaoke\", false)]\r\n    [TestCase(\"[0,1]:ka\", \"\", true)]\r\n    [TestCase(\"[0,-1]:ka\", \"karaoke\", true)]\r\n    [TestCase(\"[1,0]:ka\", \"karaoke\", false)] // should not be counted as out of range if index is not ordered.\r\n    [TestCase(\"[0,0]:ka\", \"\", true)] // should be counted as out of range if lyric is empty\r\n    public void TestOutOfRange(string rubyTagStr, string lyric, bool expected)\r\n    {\r\n        var rubyTag = TestCaseTagHelper.ParseRubyTag(rubyTagStr);\r\n\r\n        bool actual = RubyTagUtils.OutOfRange(rubyTag, lyric);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(\"[0,1]:ka\", 0, true)]\r\n    [TestCase(\"[0,1]:ka\", 1, true)]\r\n    [TestCase(\"[0,1]:ka\", -1, true)] // should be ok with negative value because we only check if valid with current text-tag index.\r\n    [TestCase(\"[2,1]:ka\", 0, true)]\r\n    [TestCase(\"[2,1]:ka\", 1, true)]\r\n    [TestCase(\"[2,1]:ka\", 2, false)]\r\n    public void TestValidNewStartIndex(string rubyTagStr, int newStartIndex, bool expected)\r\n    {\r\n        var rubyTag = TestCaseTagHelper.ParseRubyTag(rubyTagStr);\r\n\r\n        bool actual = RubyTagUtils.ValidNewStartIndex(rubyTag, newStartIndex);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(\"[0,1]:ka\", 1, true)]\r\n    [TestCase(\"[0,1]:ka\", 0, true)]\r\n    [TestCase(\"[0,1]:ka\", 1000, true)] // should be ok with large value because we only check if valid with current text-tag index.\r\n    [TestCase(\"[2,1]:ka\", 0, false)]\r\n    [TestCase(\"[2,1]:ka\", 1, false)]\r\n    [TestCase(\"[2,1]:ka\", 2, true)]\r\n    [TestCase(\"[2,1]:ka\", 3, true)]\r\n    public void TestValidNewEndIndex(string rubyTagStr, int newEndIndex, bool expected)\r\n    {\r\n        var rubyTag = TestCaseTagHelper.ParseRubyTag(rubyTagStr);\r\n\r\n        bool actual = RubyTagUtils.ValidNewEndIndex(rubyTag, newEndIndex);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(\"karaoke\", 0, false)]\r\n    [TestCase(\"karaoke\", 6, false)]\r\n    [TestCase(\"karaoke\", -1, true)]\r\n    [TestCase(\"karaoke\", 7, true)]\r\n    [TestCase(\"\", -1, true)]\r\n    [TestCase(\"\", 0, true)]\r\n    [TestCase(\"\", 1, true)]\r\n    public void TestOutOfRange(string lyric, int index, bool expected)\r\n    {\r\n        bool actual = RubyTagUtils.OutOfRange(lyric, index);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(\"[0,1]:ka\", false)]\r\n    [TestCase(\"[0,1]:\", true)]\r\n    public void TestEmptyText(string rubyTagStr, bool expected)\r\n    {\r\n        var rubyTag = TestCaseTagHelper.ParseRubyTag(rubyTagStr);\r\n\r\n        bool actual = RubyTagUtils.EmptyText(rubyTag);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(\"[0,1]:ka\", \"ka(0 ~ 1)\")]\r\n    [TestCase(\"[0,1]:\", \"empty(0 ~ 1)\")]\r\n    [TestCase(\"[-1,1]:ka\", \"ka(-1 ~ 1)\")]\r\n    [TestCase(\"[-1,-1]:ka\", \"ka(-1 ~ -1)\")]\r\n    [TestCase(\"[-1,-2]:ka\", \"ka(-1 ~ -2)\")] // will not fix the order in display.\r\n    [TestCase(\"[2,1]:ka\", \"ka(2 ~ 1)\")] // will not fix the order in display.\r\n    public void TestPositionFormattedString(string rubyTagStr, string expected)\r\n    {\r\n        var rubyTag = TestCaseTagHelper.ParseRubyTag(rubyTagStr);\r\n\r\n        string actual = RubyTagUtils.PositionFormattedString(rubyTag);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(\"[0]:ka\", \"カラオケ\", \"カ\")]\r\n    [TestCase(\"[0,3]:karaoke\", \"カラオケ\", \"カラオケ\")]\r\n    [TestCase(\"[-1,0]:\", \"カラオケ\", \"カ\")] // will get the first char if out of the range.\r\n    [TestCase(\"[4]:\", \"カラオケ\", \"ケ\")] // will get the last char if out of the range.\r\n    [TestCase(\"[3,0]:karaoke\", \"カラオケ\", \"カラオケ\")] // should not have those state but still give it a value.\r\n    [TestCase(\"[0,3]:karaoke\", \"\", null)]\r\n    public void TestGetTextFromLyric(string rubyTagStr, string lyric, string? expected)\r\n    {\r\n        var rubyTag = TestCaseTagHelper.ParseRubyTag(rubyTagStr);\r\n\r\n        if (expected == null)\r\n        {\r\n            Assert.That(() => RubyTagUtils.GetTextFromLyric(rubyTag, lyric), Throws.TypeOf<InvalidOperationException>());\r\n        }\r\n        else\r\n        {\r\n            string actual = RubyTagUtils.GetTextFromLyric(rubyTag, lyric);\r\n            Assert.That(actual, Is.EqualTo(expected));\r\n        }\r\n    }\r\n\r\n    [TestCase(\"[0,1]:ka\")]\r\n    [TestCase(\"[1,0]:ka\")] // Should be able to convert even if time-tag is invalid.\r\n    [TestCase(\"[-1,1]:ka\")] // Should be able to convert even if time-tag is invalid.\r\n    public void TestToPositionText(string rubyTagStr)\r\n    {\r\n        var rubyTag = TestCaseTagHelper.ParseRubyTag(rubyTagStr);\r\n        var actual = RubyTagUtils.ToPositionText(rubyTag);\r\n\r\n        Assert.That(actual.Text, Is.EqualTo(rubyTag.Text));\r\n        Assert.That(actual.StartIndex, Is.EqualTo(rubyTag.StartIndex));\r\n        Assert.That(actual.EndIndex, Is.EqualTo(rubyTag.EndIndex));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Objects/Utils/RubyTagsUtilsTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Asserts;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Objects.Utils;\r\n\r\n[TestFixture]\r\npublic class RubyTagsUtilsTest\r\n{\r\n    [TestCase(new[] { \"[0]:ka\", \"[1]:ra\", \"[2]:o\" }, RubyTagsUtils.Sorting.Asc, new[] { \"[0]:ka\", \"[1]:ra\", \"[2]:o\" })]\r\n    [TestCase(new[] { \"[0]:ka\", \"[1]:ra\", \"[2]:o\" }, RubyTagsUtils.Sorting.Desc, new[] { \"[2]:o\", \"[1]:ra\", \"[0]:ka\" })]\r\n    [TestCase(new[] { \"[0]:ka\", \"[2]:o\", \"[1]:ra\" }, RubyTagsUtils.Sorting.Asc, new[] { \"[0]:ka\", \"[1]:ra\", \"[2]:o\" })]\r\n    [TestCase(new[] { \"[0]:ka\", \"[2]:o\", \"[1]:ra\" }, RubyTagsUtils.Sorting.Desc, new[] { \"[2]:o\", \"[1]:ra\", \"[0]:ka\" })]\r\n    public void TestSort(string[] rubyTags, RubyTagsUtils.Sorting sorting, string[] expectedRubyTags)\r\n    {\r\n        var expected = TestCaseTagHelper.ParseRubyTags(expectedRubyTags);\r\n        var actual = RubyTagsUtils.Sort(TestCaseTagHelper.ParseRubyTags(rubyTags), sorting);\r\n        RubyTagAssert.ArePropertyEqual(expected, actual);\r\n    }\r\n\r\n    [TestCase(new[] { \"[0,6]:ka\" }, \"karaoke\", new string[] { })]\r\n    [TestCase(new[] { \"[-1]:ka\" }, \"karaoke\", new[] { \"[-1]:ka\" })]\r\n    [TestCase(new[] { \"[7]:ka\" }, \"karaoke\", new[] { \"[7]:ka\" })]\r\n    public void TestFindOutOfRange(string[] rubyTags, string lyric, string[] expectedRubyTags)\r\n    {\r\n        var expected = TestCaseTagHelper.ParseRubyTags(expectedRubyTags);\r\n        var actual = RubyTagsUtils.FindOutOfRange(TestCaseTagHelper.ParseRubyTags(rubyTags), lyric);\r\n        RubyTagAssert.ArePropertyEqual(expected, actual);\r\n    }\r\n\r\n    [TestCase(new[] { \"[0]:ka\", \"[1]:ra\", \"[2]:o\" }, RubyTagsUtils.Sorting.Asc, new string[] { })]\r\n    [TestCase(new[] { \"[0]:ka\", \"[2]:o\", \"[1]:ra\" }, RubyTagsUtils.Sorting.Asc, new string[] { })]\r\n    [TestCase(new[] { \"[1,0]:ka\" }, RubyTagsUtils.Sorting.Asc, new[] { \"[1,0]:ka\" })] // no need to fix the case if text-tag index is not ordered.\r\n    [TestCase(new[] { \"[0,1]:ka\", \"[1,2]:ra\" }, RubyTagsUtils.Sorting.Asc, new[] { \"[1,2]:ra\" })]\r\n    [TestCase(new[] { \"[0,1]:ka\", \"[1,2]:ra\" }, RubyTagsUtils.Sorting.Desc, new[] { \"[0,1]:ka\" })]\r\n    [TestCase(new[] { \"[0,2]:ka\", \"[1]:ra\" }, RubyTagsUtils.Sorting.Asc, new[] { \"[1]:ra\" })]\r\n    [TestCase(new[] { \"[0,2]:ka\", \"[1]:ra\" }, RubyTagsUtils.Sorting.Desc, new[] { \"[1]:ra\" })]\r\n    [TestCase(new[] { \"[0,1]:ka\", \"[1,2]:ra\", \"[2,3]:o\" }, RubyTagsUtils.Sorting.Asc, new[] { \"[1,2]:ra\" })]\r\n    [TestCase(new[] { \"[0,1]:ka\", \"[1,2]:ra\", \"[2,3]:o\" }, RubyTagsUtils.Sorting.Desc, new[] { \"[1,2]:ra\" })]\r\n    public void TestFindOverlapping(string[] rubyTags, RubyTagsUtils.Sorting sorting, string[] expectedRubyTags)\r\n    {\r\n        var expected = TestCaseTagHelper.ParseRubyTags(expectedRubyTags);\r\n        var actual = RubyTagsUtils.FindOverlapping(TestCaseTagHelper.ParseRubyTags(rubyTags), sorting);\r\n        RubyTagAssert.ArePropertyEqual(expected, actual);\r\n    }\r\n\r\n    [TestCase(new[] { \"[0]:ka\", \"[1]:ra\", \"[2]:o\" }, new string[] { })]\r\n    [TestCase(new string[] { }, new string[] { })]\r\n    [TestCase(new[] { \"[0]:\", \"[1]:ra\", \"[2]:o\" }, new[] { \"[0]:\" })]\r\n    [TestCase(new[] { \"[0]:\", \"[1]:\", \"[2]:\" }, new[] { \"[0]:\", \"[1]:\", \"[2]:\" })]\r\n    public void TestFindEmptyText(string[] rubyTags, string[] expectedRubyTags)\r\n    {\r\n        var expected = TestCaseTagHelper.ParseRubyTags(expectedRubyTags);\r\n        var actual = RubyTagsUtils.FindEmptyText(TestCaseTagHelper.ParseRubyTags(rubyTags));\r\n        RubyTagAssert.ArePropertyEqual(expected, actual);\r\n    }\r\n\r\n    [TestCase(new[] { \"[0]:ka\" }, \"[0]:ka\")]\r\n    [TestCase(new[] { \"[0]:ka\", \"[1]:ra\", \"[2]:o\", \"[3]:ke\" }, \"[0,3]:karaoke\")]\r\n    public void TestCombine(string[] tags, string expectRubyTag)\r\n    {\r\n        var rubyTags = TestCaseTagHelper.ParseRubyTags(tags);\r\n\r\n        var expected = TestCaseTagHelper.ParseRubyTag(expectRubyTag);\r\n        var actual = RubyTagsUtils.Combine(rubyTags);\r\n        RubyTagAssert.ArePropertyEqual(expected, actual);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Objects/Utils/TimeTagUtilsTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Objects.Utils;\r\n\r\npublic class TimeTagUtilsTest\r\n{\r\n    [TestCase(\"[1,start]:1000\", 2, \"[3,start]:1000\")]\r\n    [TestCase(\"[1,end]:1000\", 2, \"[3,end]:1000\")]\r\n    [TestCase(\"[1,start]\", 2, \"[3,start]\")]\r\n    [TestCase(\"[1,end]\", 2, \"[3,end]\")]\r\n    [TestCase(\"[1,start]:1000\", -2, \"[-1,start]:1000\")]\r\n    [TestCase(\"[1,end]:1000\", -2, \"[-1,end]:1000\")]\r\n    public void TestShiftingTimeTag(string shiftingTag, int offset, string expectedTimeTag)\r\n    {\r\n        var timeTag = TestCaseTagHelper.ParseTimeTag(shiftingTag);\r\n\r\n        var expected = TestCaseTagHelper.ParseTimeTag(expectedTimeTag);\r\n        var actual = TimeTagUtils.ShiftingTimeTag(timeTag, offset);\r\n        Assert.That(actual.Index, Is.EqualTo(expected.Index));\r\n        Assert.That(actual.Time, Is.EqualTo(expected.Time));\r\n    }\r\n\r\n    [TestCase(\"[1,start]:1000\", \"00:01:000\")]\r\n    [TestCase(\"[1,end]:1000\", \"00:01:000\")]\r\n    [TestCase(\"[-1,start]:1000\", \"00:01:000\")]\r\n    [TestCase(\"[-1,start]:-1000\", \"-00:01:000\")]\r\n    [TestCase(\"[-1,start]\", \"--:--:---\")]\r\n    public void TestFormattedString(string tag, string expected)\r\n    {\r\n        var timeTag = TestCaseTagHelper.ParseTimeTag(tag);\r\n\r\n        string actual = TimeTagUtils.FormattedString(timeTag);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Objects/Utils/TimeTagsUtilsTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Extensions;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Asserts;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Objects.Utils;\r\n\r\n[TestFixture]\r\npublic class TimeTagsUtilsTest\r\n{\r\n    [TestCase(\"[1,start]:1000\", \"[3,start]:3000\", 2, \"[2,start]:2000\")]\r\n    [TestCase(\"[1,start]:1000\", \"[3,end]:4000\", 2, \"[2,start]:2000\")]\r\n    [TestCase(\"[1,end]:2000\", \"[3,start]:3000\", 2, \"[2,start]:2000\")]\r\n    [TestCase(\"[1,start]\", \"[3,start]:3000\", 2, \"[2,start]\")]\r\n    [TestCase(\"[1,start]:1000\", \"[3,start]\", 2, \"[2,start]\")]\r\n    [TestCase(\"[1,start]\", \"[3,start]\", 2, \"[2,start]\")]\r\n    [TestCase(\"[0,start]\", \"[0,start]\", 0, \"[0,start]\")] // edge case, but it's valid.\r\n    [TestCase(\"[1,start]\", \"[3,start]\", 10, null)] // new index should be in the range.\r\n    [TestCase(\"[10,start]\", \"[3,start]\", 10, null)] // start index should be smaller then end index.\r\n    [TestCase(\"[1,start]\", null, 2, null)] // should not be null.\r\n    public void TestGenerateTimeTag(string startTag, string? endTag, int index, string? result)\r\n    {\r\n        var startTimeTag = TestCaseTagHelper.ParseTimeTag(startTag);\r\n        var endTimeTag = TestCaseTagHelper.ParseTimeTag(endTag);\r\n\r\n        if (result != null)\r\n        {\r\n            var expectedTimeTag = TestCaseTagHelper.ParseTimeTag(result);\r\n            var actualTimeTag = TimeTagsUtils.GenerateCenterTimeTag(startTimeTag, endTimeTag, index);\r\n\r\n            Assert.That(actualTimeTag.Index, Is.EqualTo(expectedTimeTag.Index));\r\n            Assert.That(actualTimeTag.Time, Is.EqualTo(expectedTimeTag.Time));\r\n        }\r\n        else\r\n        {\r\n            Assert.That(() => TimeTagsUtils.GenerateCenterTimeTag(startTimeTag, endTimeTag, index), Throws.Exception);\r\n        }\r\n    }\r\n\r\n    [TestCase(new[] { \"[0,start]:1100\", \"[0,end]:2000\", \"[1,start]:2100\", \"[1,end]:3000\" }, new[] { \"[0,start]:1100\", \"[0,end]:2000\", \"[1,start]:2100\", \"[1,end]:3000\" })]\r\n    [TestCase(new[] { \"[1,end]:3000\", \"[1,start]:2100\", \"[0,end]:2000\", \"[0,start]:1100\" }, new[] { \"[0,start]:1100\", \"[0,end]:2000\", \"[1,start]:2100\", \"[1,end]:3000\" })]\r\n    [TestCase(new[] { \"[0,start]\", \"[0,start]\", \"[0,end]:2000\", \"[0,start]:1100\" }, new[] { \"[0,start]\", \"[0,start]\", \"[0,start]:1100\", \"[0,end]:2000\" })]\r\n    [TestCase(new[] { \"[0,start]:1000\", \"[0,start]:1100\", \"[0,end]:2000\", \"[0,start]:1100\" }, new[] { \"[0,start]:1000\", \"[0,start]:1100\", \"[0,start]:1100\", \"[0,end]:2000\" })]\r\n    [TestCase(new[] { \"[0,start]\", \"[0,end]\", \"[0,start]\", \"[1,start]\", \"[1,end]\" }, new[] { \"[0,start]\", \"[0,start]\", \"[0,end]\", \"[1,start]\", \"[1,end]\" })]\r\n    public void TestSort(string[] timeTagTexts, string[] expectedTimeTags)\r\n    {\r\n        var timeTags = TestCaseTagHelper.ParseTimeTags(timeTagTexts);\r\n\r\n        var expected = TestCaseTagHelper.ParseTimeTags(expectedTimeTags);\r\n        var actual = TimeTagsUtils.Sort(timeTags);\r\n        TimeTagAssert.ArePropertyEqual(expected, actual);\r\n    }\r\n\r\n    [TestCase(\"カラオケ\", new[] { \"[0,start]:1000\", \"[1,start]:2000\", \"[2,start]:3000\", \"[3,start]:4000\", \"[3,end]:5000\" }, new string[] { })]\r\n    [TestCase(\"カラオケ\", new[] { \"[0,start]:1000\", \"[3,end]:5000\" }, new string[] { })]\r\n    [TestCase(\"カラオケ\", new[] { \"[-1,start]:1000\" }, new[] { \"[-1,start]:1000\" })]\r\n    [TestCase(\"カラオケ\", new[] { \"[4,start]:4000\" }, new[] { \"[4,start]:4000\" })]\r\n    public void TestFindOutOfRange(string text, string[] timeTagTexts, string[] invalidTimeTags)\r\n    {\r\n        var timeTags = TestCaseTagHelper.ParseTimeTags(timeTagTexts);\r\n\r\n        var expected = TestCaseTagHelper.ParseTimeTags(invalidTimeTags);\r\n        var actual = TimeTagsUtils.FindOutOfRange(timeTags, text);\r\n        TimeTagAssert.ArePropertyEqual(expected, actual);\r\n    }\r\n\r\n    [TestCase(new[] { \"[0,start]:1000\", \"[1,start]\", \"[2,start]:3000\", \"[3,start]\", \"[3,end]:5000\" }, new[] { \"[1,start]\", \"[3,start]\" })]\r\n    [TestCase(new[] { \"[0,start]\", \"[3,end]\" }, new[] { \"[0,start]\", \"[3,end]\" })]\r\n    public void TestFindNoneTime(string[] timeTagTexts, string[] invalidTimeTags)\r\n    {\r\n        var timeTags = TestCaseTagHelper.ParseTimeTags(timeTagTexts);\r\n\r\n        var expected = TestCaseTagHelper.ParseTimeTags(invalidTimeTags);\r\n        var actual = TimeTagsUtils.FindNoneTime(timeTags);\r\n        TimeTagAssert.ArePropertyEqual(expected, actual);\r\n    }\r\n\r\n    [TestCase(\"カラオケ\", new[] { \"[0,start]:1000\", \"[3,end]:2000\" }, true)]\r\n    [TestCase(\"カラオケ\", new[] { \"[0,start]:2000\", \"[3,end]:3000\", \"[3,end]:4000\" }, true)]\r\n    [TestCase(\"カラオケ\", new[] { \"[0,start]:3000\", \"[0,start]:4000\" }, true)] // multiple start time-tag is ok.\r\n    [TestCase(\"カラオケ\", new[] { \"[3,end]:1000\" }, false)]\r\n    [TestCase(\"カラオケ\", new[] { \"[-1,start]:1000\", \"[3,end]:2000\" }, false)] // out of range end time-tag should be count as missing.\r\n    [TestCase(\"\", new[] { \"[0,start]:1000\", \"[0,end]:2000\" }, false)] // empty lyric should always count as missing.\r\n    [TestCase(\"カラオケ\", new string[] { }, false)] // empty time-tag should always count as missing.\r\n    public void TestHasStartTimeTagInLyric(string text, string[] timeTagTexts, bool expected)\r\n    {\r\n        var timeTags = TestCaseTagHelper.ParseTimeTags(timeTagTexts);\r\n\r\n        bool actual = TimeTagsUtils.HasStartTimeTagInLyric(timeTags, text);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(\"カラオケ\", new[] { \"[0,start]:1000\", \"[3,end]:2000\" }, true)]\r\n    [TestCase(\"カラオケ\", new[] { \"[3,start]:2000\", \"[3,start]:3000\", \"[3,end]:4000\" }, true)]\r\n    [TestCase(\"カラオケ\", new[] { \"[3,end]:3000\", \"[3,end]:4000\" }, true)] // multiple end time-tag is ok.\r\n    [TestCase(\"カラオケ\", new[] { \"[0,start]:1000\" }, false)]\r\n    [TestCase(\"カラオケ\", new[] { \"[0,start]:1000\", \"[5,end]:2000\" }, false)] // out of range end time-tag should be count as missing.\r\n    [TestCase(\"\", new[] { \"[0,start]:1000\", \"[0,end]:2000\" }, false)] // empty lyric should always count as missing.\r\n    public void TestHasEndTimeTagInLyric(string text, string[] timeTagTexts, bool expected)\r\n    {\r\n        var timeTags = TestCaseTagHelper.ParseTimeTags(timeTagTexts);\r\n\r\n        bool actual = TimeTagsUtils.HasEndTimeTagInLyric(timeTags, text);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(new[] { \"[0,start]:2000\", \"[0,end]:1000\" }, GroupCheck.Asc, SelfCheck.BasedOnStart, new[] { 1 })]\r\n    [TestCase(new[] { \"[0,start]:2000\", \"[0,end]:1000\" }, GroupCheck.Asc, SelfCheck.BasedOnEnd, new[] { 0 })]\r\n    [TestCase(new[] { \"[0,start]:1100\", \"[0,end]:2100\", \"[1,start]:2000\", \"[1,end]:3000\" }, GroupCheck.Asc, SelfCheck.BasedOnStart, new[] { 2 })]\r\n    [TestCase(new[] { \"[0,start]:1100\", \"[0,end]:2100\", \"[1,start]:2000\", \"[1,end]:3000\" }, GroupCheck.Desc, SelfCheck.BasedOnStart, new[] { 1 })]\r\n    [TestCase(new[] { \"[0,start]:1000\", \"[0,end]:5000\", \"[1,start]:2000\", \"[1,end]:3000\" }, GroupCheck.Asc, SelfCheck.BasedOnStart, new[] { 2, 3 })]\r\n    [TestCase(new[] { \"[0,start]:1000\", \"[0,end]:5000\", \"[1,start]:2000\", \"[1,end]:3000\" }, GroupCheck.Desc, SelfCheck.BasedOnStart, new[] { 1 })]\r\n    [TestCase(new[] { \"[0,start]:1000\", \"[0,end]:2000\", \"[1,start]:0\", \"[1,end]:3000\" }, GroupCheck.Asc, SelfCheck.BasedOnStart, new[] { 2 })]\r\n    [TestCase(new[] { \"[0,start]:1000\", \"[0,end]:2000\", \"[1,start]:0\", \"[1,end]:3000\" }, GroupCheck.Desc, SelfCheck.BasedOnStart, new[] { 0, 1 })]\r\n    [TestCase(new[] { \"[0,start]:4000\", \"[0,end]:3000\", \"[1,start]:2000\", \"[1,end]:1000\" }, GroupCheck.Asc, SelfCheck.BasedOnStart, new[] { 1, 2, 3 })]\r\n    [TestCase(new[] { \"[0,start]:4000\", \"[0,end]:3000\", \"[1,start]:2000\", \"[1,end]:1000\" }, GroupCheck.Asc, SelfCheck.BasedOnEnd, new[] { 0, 2, 3 })]\r\n    [TestCase(new[] { \"[0,start]:4000\", \"[0,end]:3000\", \"[1,start]:2000\", \"[1,end]:1000\" }, GroupCheck.Desc, SelfCheck.BasedOnStart, new[] { 0, 1, 3 })]\r\n    [TestCase(new[] { \"[0,start]:4000\", \"[0,end]:3000\", \"[1,start]:2000\", \"[1,end]:1000\" }, GroupCheck.Desc, SelfCheck.BasedOnEnd, new[] { 0, 1, 2 })]\r\n    public void TestFindOverlapping(string[] timeTagTexts, GroupCheck other, SelfCheck self, int[] expected)\r\n    {\r\n        // run all and find overlapping indexes.\r\n        var timeTags = TestCaseTagHelper.ParseTimeTags(timeTagTexts);\r\n        var overlappingTimeTags = TimeTagsUtils.FindOverlapping(timeTags, other, self);\r\n\r\n        int[] actual = overlappingTimeTags.Select(v => timeTags.IndexOf(v)).ToArray();\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(new[] { \"[0,start]:2000\", \"[0,end]:1000\" }, GroupCheck.Asc, SelfCheck.BasedOnStart, new[] { \"[0,start]:2000\", \"[0,end]:2000\" })]\r\n    [TestCase(new[] { \"[0,start]:2000\", \"[0,end]:1000\" }, GroupCheck.Asc, SelfCheck.BasedOnEnd, new[] { \"[0,start]:1000\", \"[0,end]:1000\" })]\r\n    [TestCase(new[] { \"[0,start]:1100\", \"[0,end]:2100\", \"[1,start]:2000\", \"[1,end]:3000\" }, GroupCheck.Asc, SelfCheck.BasedOnStart,\r\n        new[] { \"[0,start]:1100\", \"[0,end]:2100\", \"[1,start]:2100\", \"[1,end]:3000\" })]\r\n    [TestCase(new[] { \"[0,start]:1100\", \"[0,end]:2100\", \"[1,start]:2000\", \"[1,end]:3000\" }, GroupCheck.Desc, SelfCheck.BasedOnStart,\r\n        new[] { \"[0,start]:1100\", \"[0,end]:2000\", \"[1,start]:2000\", \"[1,end]:3000\" })]\r\n    [TestCase(new[] { \"[0,start]:1000\", \"[0,end]:5000\", \"[1,start]:2000\", \"[1,end]:3000\" }, GroupCheck.Asc, SelfCheck.BasedOnStart,\r\n        new[] { \"[0,start]:1000\", \"[0,end]:5000\", \"[1,start]:5000\", \"[1,end]:5000\" })]\r\n    [TestCase(new[] { \"[0,start]:1000\", \"[0,end]:5000\", \"[1,start]:2000\", \"[1,end]:3000\" }, GroupCheck.Desc, SelfCheck.BasedOnStart,\r\n        new[] { \"[0,start]:1000\", \"[0,end]:2000\", \"[1,start]:2000\", \"[1,end]:3000\" })]\r\n    [TestCase(new[] { \"[0,start]:1000\", \"[0,end]:2000\", \"[1,start]:0\", \"[1,end]:3000\" }, GroupCheck.Asc, SelfCheck.BasedOnStart,\r\n        new[] { \"[0,start]:1000\", \"[0,end]:2000\", \"[1,start]:2000\", \"[1,end]:3000\" })]\r\n    [TestCase(new[] { \"[0,start]:1000\", \"[0,end]:2000\", \"[1,start]:0\", \"[1,end]:3000\" }, GroupCheck.Desc, SelfCheck.BasedOnStart,\r\n        new[] { \"[0,start]:0\", \"[0,end]:0\", \"[1,start]:0\", \"[1,end]:3000\" })]\r\n    //[TestCase(new[] { \"[0,start]:4000\", \"[0,end]:3000\", \"[1,start]:2000\", \"[1,end]:1000\" }, GroupCheck.Asc, SelfCheck.BasedOnStart, new double[] { \"[0,start]:4000\", \"[0,end]:4000\", \"[1,start]:4000\", \"[1,end]:4000\" })]\r\n    //[TestCase(new[] { \"[0,start]:4000\", \"[0,end]:3000\", \"[1,start]:2000\", \"[1,end]:1000\" }, GroupCheck.Asc, SelfCheck.BasedOnEnd, new double[] { \"[0,start]:3000\", \"[0,end]:3000\", \"[1,start]:3000\", \"[1,end]:3000\" })]\r\n    //[TestCase(new[] { \"[0,start]:4000\", \"[0,end]:3000\", \"[1,start]:2000\", \"[1,end]:1000\" }, GroupCheck.Desc, SelfCheck.BasedOnStart, new double[] { \"[0,start]:2000\", \"[0,end]:2000\", \"[1,start]:2000\", \"[1,end]:2000\" })]\r\n    //[TestCase(new[] { \"[0,start]:4000\", \"[0,end]:3000\", \"[1,start]:2000\", \"[1,end]:1000\" }, GroupCheck.Desc, SelfCheck.BasedOnEnd, new double[] { \"[0,start]:1000\", \"[0,end]:1000\", \"[1,start]:1000\", \"[1,end]:1000\" })]\r\n    public void TestFixOverlapping(string[] timeTagTexts, GroupCheck other, SelfCheck self, string[] expectedTimeTagTexts)\r\n    {\r\n        // check which part is fixed, using list of time to check result.\r\n        var timeTags = TestCaseTagHelper.ParseTimeTags(timeTagTexts);\r\n\r\n        var expected = TestCaseTagHelper.ParseTimeTags(expectedTimeTagTexts);\r\n        var actual = TimeTagsUtils.FixOverlapping(timeTags, other, self);\r\n        TimeTagAssert.ArePropertyEqual(expected, actual);\r\n    }\r\n\r\n    [TestCase(new[] { \"[0,start]:1100\", \"[0,end]:2000\", \"[1,start]:2100\", \"[1,end]:3000\" }, new double[] { 1100, 2000, 2100, 3000 })]\r\n    [TestCase(new[] { \"[0,start]:3000\", \"[0,end]:2100\", \"[1,start]:2000\", \"[1,end]:1100\" }, new double[] { 1100, 2000, 2100, 3000 })] // will sort by time.\r\n    [TestCase(new[] { \"[0,start]\", \"[0,start]\", \"[0,end]:2000\", \"[0,start]:1100\" }, new double[] { 1100, 2000 })] // will remove the time-tag with no time.\r\n    [TestCase(new[] { \"[0,start]:1000\", \"[0,start]:1100\" }, new double[] { 1000, 1100 })] // will remain the duplicated time-tag index.\r\n    [TestCase(new[] { \"[0,start]:1000\", \"[1,start]:1000\" }, new double[] { 1000 })] // Should not add duplicated time.\r\n    public void TestToTimeBasedDictionary(string[] timeTagTexts, double[] expected)\r\n    {\r\n        var timeTags = TestCaseTagHelper.ParseTimeTags(timeTagTexts);\r\n\r\n        var actual = TimeTagsUtils.ToTimeBasedDictionary(timeTags).Keys;\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(new[] { \"[0,start]:1100\", \"[0,end]:2000\", \"[1,start]:2100\", \"[1,end]:3000\" }, 1100)]\r\n    [TestCase(new[] { \"[1,end]:3000\", \"[1,start]:2100\", \"[0,end]:2000\", \"[0,start]:1100\" }, 1100)]\r\n    [TestCase(new[] { \"[0,start]\", \"[0,start]\", \"[0,end]:2000\", \"[0,start]:1100\" }, 1100)]\r\n    [TestCase(new[] { \"[0,start]\" }, null)]\r\n    [TestCase(new string[] { }, null)]\r\n    public void TestGetStartTime(string[] timeTagTexts, double? expected)\r\n    {\r\n        var timeTags = TestCaseTagHelper.ParseTimeTags(timeTagTexts);\r\n\r\n        double? actual = TimeTagsUtils.GetStartTime(timeTags);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(new[] { \"[0,start]:1100\", \"[0,end]:2000\", \"[1,start]:2100\", \"[1,end]:3000\" }, 3000)]\r\n    [TestCase(new[] { \"[1,end]:3000\", \"[1,start]:2100\", \"[0,end]:2000\", \"[0,start]:1100\" }, 3000)]\r\n    [TestCase(new[] { \"[0,start]\", \"[0,start]\", \"[0,end]:2000\", \"[0,start]:1100\" }, 2000)]\r\n    [TestCase(new[] { \"[0,start]\" }, null)]\r\n    [TestCase(new string[] { }, null)]\r\n    public void TestGetEndTime(string[] timeTagTexts, double? expected)\r\n    {\r\n        var timeTags = TestCaseTagHelper.ParseTimeTags(timeTagTexts);\r\n\r\n        double? actual = TimeTagsUtils.GetEndTime(timeTags);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Objects/Workings/HitObjectWorkingPropertyValidatorTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Types;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Objects.Workings;\r\n\r\npublic abstract class HitObjectWorkingPropertyValidatorTest<THitObject, TFlag>\r\n    where TFlag : struct, Enum\r\n    where THitObject : KaraokeHitObject, IHasWorkingProperty<TFlag>, new()\r\n{\r\n    [Test]\r\n    public void TestInitialState([Values] TFlag flag)\r\n    {\r\n        // should be valid on the first load.\r\n        AssetInitialStateIsValid(new THitObject(), flag);\r\n    }\r\n\r\n    [Test]\r\n    public void TestAllInvalidateTest([Values] TFlag flag)\r\n    {\r\n        // run this test case just make sure that all working property are checked.\r\n        Assert.That(() => new THitObject().InvalidateWorkingProperty(flag), Throws.Nothing);\r\n    }\r\n\r\n    protected void AssetInitialStateIsValid(THitObject hitObject, TFlag flag)\r\n    {\r\n        bool isValid = IsInitialStateValid(flag);\r\n        AssetIsValid(hitObject, flag, isValid);\r\n    }\r\n\r\n    protected void AssetIsValid(THitObject hitObject, TFlag flag, bool isValid)\r\n    {\r\n        Assert.That(!hitObject.GetAllInvalidWorkingProperties().Contains(flag), Is.EqualTo(isValid));\r\n    }\r\n\r\n    protected abstract bool IsInitialStateValid(TFlag flag);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Objects/Workings/LyricWorkingPropertyValidatorTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Workings;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Extensions;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Objects.Workings;\r\n\r\npublic class LyricWorkingPropertyValidatorTest : HitObjectWorkingPropertyValidatorTest<Lyric, LyricWorkingProperty>\r\n{\r\n    [Test]\r\n    public void TestSingers()\r\n    {\r\n        var lyric = new Lyric();\r\n\r\n        var singerId1 = TestCaseElementIdHelper.CreateElementIdByNumber(1);\r\n        var singerId2 = TestCaseElementIdHelper.CreateElementIdByNumber(2);\r\n        var singerId3 = TestCaseElementIdHelper.CreateElementIdByNumber(3);\r\n\r\n        // should be valid if singer is empty.\r\n        Assert.That(() => lyric.SingerIds = new List<ElementId>(), Throws.Nothing);\r\n        AssetIsValid(lyric, LyricWorkingProperty.Singers, true);\r\n\r\n        // should be invalid if assign the singer.\r\n        Assert.That(() => lyric.SingerIds.Add(singerId1), Throws.Nothing);\r\n        AssetIsValid(lyric, LyricWorkingProperty.Singers, false);\r\n\r\n        // should be valid again if remove the singer\r\n        Assert.That(() => lyric.SingerIds.Remove(singerId1), Throws.Nothing);\r\n        AssetIsValid(lyric, LyricWorkingProperty.Singers, true);\r\n\r\n        // should be matched if include all singers\r\n        Assert.That(() => lyric.SingerIds = new List<ElementId> { singerId1, singerId2, singerId3 }, Throws.Nothing);\r\n        Assert.That(() => lyric.Singers = new Dictionary<Singer, SingerState[]>\r\n        {\r\n            { new Singer().ChangeId(singerId1), Array.Empty<SingerState>() },\r\n            { new Singer().ChangeId(singerId2), Array.Empty<SingerState>() },\r\n            { new Singer().ChangeId(singerId3), Array.Empty<SingerState>() },\r\n        }, Throws.Nothing);\r\n        AssetIsValid(lyric, LyricWorkingProperty.Singers, true);\r\n\r\n        // should be matched if include all singers\r\n        Assert.That(() => lyric.SingerIds = new List<ElementId> { singerId1, singerId2, singerId3 }, Throws.Nothing);\r\n        Assert.That(() => lyric.Singers = new Dictionary<Singer, SingerState[]>\r\n        {\r\n            { new Singer().ChangeId(singerId1), new[] { new SingerState(singerId1).ChangeId(singerId2), new SingerState(singerId1).ChangeId(singerId3) } },\r\n        }, Throws.Nothing);\r\n        AssetIsValid(lyric, LyricWorkingProperty.Singers, true);\r\n\r\n        // should works even id is not by order.\r\n        Assert.That(() => lyric.SingerIds = new List<ElementId> { singerId1, singerId2, singerId3 }, Throws.Nothing);\r\n        Assert.That(() => lyric.Singers = new Dictionary<Singer, SingerState[]>\r\n        {\r\n            { new Singer().ChangeId(singerId3), Array.Empty<SingerState>() },\r\n            { new Singer().ChangeId(singerId2), Array.Empty<SingerState>() },\r\n            { new Singer().ChangeId(singerId1), Array.Empty<SingerState>() },\r\n        }, Throws.Nothing);\r\n        AssetIsValid(lyric, LyricWorkingProperty.Singers, true);\r\n\r\n        // should works even id is not by order.\r\n        Assert.That(() => lyric.SingerIds = new List<ElementId> { singerId3, singerId2, singerId1 }, Throws.Nothing);\r\n        Assert.That(() => lyric.Singers = new Dictionary<Singer, SingerState[]>\r\n        {\r\n            { new Singer().ChangeId(singerId1), Array.Empty<SingerState>() },\r\n            { new Singer().ChangeId(singerId2), Array.Empty<SingerState>() },\r\n            { new Singer().ChangeId(singerId3), Array.Empty<SingerState>() },\r\n        }, Throws.Nothing);\r\n        AssetIsValid(lyric, LyricWorkingProperty.Singers, true);\r\n\r\n        // should works if id is duplicated\r\n        Assert.That(() => lyric.SingerIds = new List<ElementId> { singerId1, singerId1, singerId1 }, Throws.Nothing);\r\n        Assert.That(() => lyric.Singers = new Dictionary<Singer, SingerState[]>\r\n        {\r\n            { new Singer().ChangeId(singerId1), Array.Empty<SingerState>() },\r\n        }, Throws.Nothing);\r\n        AssetIsValid(lyric, LyricWorkingProperty.Singers, true);\r\n\r\n        // should works if id is duplicated\r\n        Assert.That(() => lyric.SingerIds = new List<ElementId> { singerId1 }, Throws.Nothing);\r\n        Assert.That(() => lyric.Singers = new Dictionary<Singer, SingerState[]>\r\n        {\r\n            { new Singer().ChangeId(singerId1), Array.Empty<SingerState>() },\r\n            { new Singer().ChangeId(singerId1), Array.Empty<SingerState>() },\r\n        }, Throws.Nothing);\r\n        AssetIsValid(lyric, LyricWorkingProperty.Singers, true);\r\n    }\r\n\r\n    [Test]\r\n    public void TestPage()\r\n    {\r\n        var lyric = new Lyric();\r\n\r\n        // page state is valid because assign the property.\r\n        Assert.That(() => lyric.PageIndex = 1, Throws.Nothing);\r\n        AssetIsValid(lyric, LyricWorkingProperty.Page, true);\r\n    }\r\n\r\n    [Test]\r\n    public void TestReferenceLyric()\r\n    {\r\n        var lyric = new Lyric();\r\n\r\n        // should be valid if change the reference lyric id.\r\n        Assert.That(() =>\r\n        {\r\n            lyric.ReferenceLyricId = null;\r\n            lyric.ReferenceLyric = null;\r\n        }, Throws.Nothing);\r\n        AssetIsValid(lyric, LyricWorkingProperty.ReferenceLyric, true);\r\n\r\n        // should be invalid if change the reference lyric id.\r\n        Assert.That(() =>\r\n        {\r\n            lyric.ReferenceLyricId = TestCaseElementIdHelper.CreateElementIdByNumber(1);\r\n        }, Throws.Nothing);\r\n        AssetIsValid(lyric, LyricWorkingProperty.ReferenceLyric, false);\r\n\r\n        // should be valid again if change the id back.\r\n        Assert.That(() =>\r\n        {\r\n            lyric.ReferenceLyricId = null;\r\n        }, Throws.Nothing);\r\n        AssetIsValid(lyric, LyricWorkingProperty.ReferenceLyric, true);\r\n\r\n        // should be valid if change the reference lyric id.\r\n        Assert.That(() =>\r\n        {\r\n            var referencedLyric = new Lyric();\r\n\r\n            lyric.ReferenceLyricId = referencedLyric.ID;\r\n            lyric.ReferenceLyric = referencedLyric;\r\n        }, Throws.Nothing);\r\n        AssetIsValid(lyric, LyricWorkingProperty.ReferenceLyric, true);\r\n\r\n        // should be invalid if change the reference lyric id.\r\n        Assert.That(() => lyric.ReferenceLyricId = TestCaseElementIdHelper.CreateElementIdByNumber(2), Throws.Nothing);\r\n        AssetIsValid(lyric, LyricWorkingProperty.ReferenceLyric, false);\r\n\r\n        // should be valid again if assign the reference lyric to the matched lyric.\r\n        Assert.That(() => lyric.ReferenceLyric = new Lyric().ChangeId(2), Throws.Nothing);\r\n        AssetIsValid(lyric, LyricWorkingProperty.ReferenceLyric, true);\r\n\r\n        // should throw the exception if assign the working reference lyric to the unmatched reference lyric id.\r\n        Assert.That(() => lyric.ReferenceLyric = new Lyric().ChangeId(3), Throws.TypeOf<InvalidWorkingPropertyAssignException>());\r\n        Assert.That(() => lyric.ReferenceLyric = null, Throws.TypeOf<InvalidWorkingPropertyAssignException>());\r\n    }\r\n\r\n    protected override bool IsInitialStateValid(LyricWorkingProperty flag)\r\n    {\r\n        return new LyricWorkingPropertyValidator(new Lyric()).IsValid(flag);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Objects/Workings/NoteWorkingPropertyValidatorTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Workings;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Extensions;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Objects.Workings;\r\n\r\npublic class NoteWorkingPropertyValidatorTest : HitObjectWorkingPropertyValidatorTest<Note, NoteWorkingProperty>\r\n{\r\n    [Test]\r\n    public void TestPage()\r\n    {\r\n        var note = new Note();\r\n\r\n        // page state is valid because assign the property.\r\n        Assert.That(() => note.PageIndex = 1, Throws.Nothing);\r\n        AssetIsValid(note, NoteWorkingProperty.Page, true);\r\n    }\r\n\r\n    [Test]\r\n    public void TestReferenceLyric()\r\n    {\r\n        var note = new Note();\r\n\r\n        // should be valid if change the reference lyric id.\r\n        Assert.That(() =>\r\n        {\r\n            note.ReferenceLyricId = null;\r\n            note.ReferenceLyric = null;\r\n        }, Throws.Nothing);\r\n        AssetIsValid(note, NoteWorkingProperty.ReferenceLyric, true);\r\n\r\n        // should be invalid if change the reference lyric id.\r\n        Assert.That(() =>\r\n        {\r\n            note.ReferenceLyricId = TestCaseElementIdHelper.CreateElementIdByNumber(1);\r\n        }, Throws.Nothing);\r\n        AssetIsValid(note, NoteWorkingProperty.ReferenceLyric, false);\r\n\r\n        // should be valid again if change the id back.\r\n        Assert.That(() =>\r\n        {\r\n            note.ReferenceLyricId = null;\r\n        }, Throws.Nothing);\r\n        AssetIsValid(note, NoteWorkingProperty.ReferenceLyric, true);\r\n\r\n        // should be valid if change the reference lyric id.\r\n        Assert.That(() =>\r\n        {\r\n            var lyric = new Lyric();\r\n\r\n            note.ReferenceLyricId = lyric.ID;\r\n            note.ReferenceLyric = lyric;\r\n        }, Throws.Nothing);\r\n        AssetIsValid(note, NoteWorkingProperty.ReferenceLyric, true);\r\n\r\n        // should be invalid if change the reference lyric id.\r\n        Assert.That(() => note.ReferenceLyricId = TestCaseElementIdHelper.CreateElementIdByNumber(2), Throws.Nothing);\r\n        AssetIsValid(note, NoteWorkingProperty.ReferenceLyric, false);\r\n\r\n        // should be valid again if assign the reference lyric to the matched lyric.\r\n        Assert.That(() => note.ReferenceLyric = new Lyric().ChangeId(2), Throws.Nothing);\r\n        AssetIsValid(note, NoteWorkingProperty.ReferenceLyric, true);\r\n\r\n        // should throw the exception if assign the working reference lyric to the unmatched reference lyric id.\r\n        Assert.That(() => note.ReferenceLyric = new Lyric().ChangeId(3), Throws.TypeOf<InvalidWorkingPropertyAssignException>());\r\n        Assert.That(() => note.ReferenceLyric = null, Throws.TypeOf<InvalidWorkingPropertyAssignException>());\r\n    }\r\n\r\n    protected override bool IsInitialStateValid(NoteWorkingProperty flag)\r\n    {\r\n        return new NoteWorkingPropertyValidator(new Note()).IsValid(flag);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Overlays/Changelog/ChangelogPullRequestInfoTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Overlays.Changelog;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Overlays.Changelog;\r\n\r\npublic class ChangelogPullRequestInfoTest\r\n{\r\n    [TestCase(\"#2152@andy840119\", new[] { 2152 }, new[] { \"andy840119\" })]\r\n    [TestCase(\"#2152\", new[] { 2152 }, new string[] { })]\r\n    [TestCase(\"@andy840119\", new int[] { }, new[] { \"andy840119\" })]\r\n    [TestCase(\"#2152#2153\", new[] { 2152, 2153 }, new string[] { })]\r\n    [TestCase(\"#2152#2152\", new[] { 2152 }, new string[] { })]\r\n    [TestCase(\"@andy@andy840119\", new int[] { }, new[] { \"andy\", \"andy840119\" })]\r\n    [TestCase(\"@andy840119@andy840119\", new int[] { }, new[] { \"andy840119\" })]\r\n    [TestCase(\"https://raw.githubusercontent.com/karaoke-dev/karaoke-dev.github.io/master/content/changelog/2023.1212/#2152@andy840119\", new[] { 2152 }, new[] { \"andy840119\" })] // the actual data that will be get in this method.\r\n    public void TestGetPullRequestInfoFromLink(string url, int[] expectedPrs, string[] expectedUserNames)\r\n    {\r\n        var result = ChangelogPullRequestInfo.GetPullRequestInfoFromLink(\"karaoke\", url)!;\r\n\r\n        Assert.That(result.PullRequests.Select(x => x.Number), Is.EqualTo(expectedPrs));\r\n        Assert.That(result.Users.Select(x => x.UserName), Is.EqualTo(expectedUserNames));\r\n    }\r\n\r\n    [TestCase(\"unknown_repo\", \"#2152@andy840119\")] // \"unknown_repo\" does not in the repo list.\r\n    [TestCase(\"karaoke\", \"\")] // there's no pr number or username.\r\n    [TestCase(\"karaoke\", \"hello\")] // invalid pr number or username.\r\n    [TestCase(\"karaoke\", \"#aaa\")] // invalid pr number.\r\n    public void TestTestGetPullRequestInfoFromLinkWithNull(string link, string url)\r\n    {\r\n        var result = ChangelogPullRequestInfo.GetPullRequestInfoFromLink(link, url);\r\n        Assert.That(result, Is.Null);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Overlays/Changelog/TestSceneKaraokeChangeLogMarkdownContainer.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Online.API.Requests.Responses;\r\nusing osu.Game.Rulesets.Karaoke.Overlays.Changelog;\r\nusing osu.Game.Tests.Visual;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Overlays.Changelog;\r\n\r\n[TestFixture]\r\npublic partial class TestSceneKaraokeChangeLogMarkdownContainer : OsuTestScene\r\n{\r\n    private ChangeLogMarkdownContainer markdownContainer = null!;\r\n\r\n    [Cached]\r\n    private readonly OverlayColourProvider overlayColour = new(OverlayColourScheme.Orange);\r\n\r\n    [SetUp]\r\n    public void SetUp() => Schedule(() =>\r\n    {\r\n        var build = new APIChangelogBuild\r\n        {\r\n            DocumentUrl = \"https://raw.githubusercontent.com/karaoke-dev/karaoke-dev.github.io/master/content/changelog/2020.0620\",\r\n            RootUrl = \"https://github.com/karaoke-dev/karaoke-dev.github.io/tree/master/content/changelog/2020.0620\",\r\n            Content = \"---\\ntitle: \\\"2020.0620\\\"\\ndate: 2020-06-20\\n---\\n\\n## Achievement\\n\\n- Might support convert `UTAU`/`SynthV` project into karaoke beatmap in future. [karaoke](#100#101@andy840119)\",\r\n        };\r\n\r\n        Children = new Drawable[]\r\n        {\r\n            new Box\r\n            {\r\n                Colour = overlayColour.Background5,\r\n                RelativeSizeAxes = Axes.Both,\r\n            },\r\n            new BasicScrollContainer\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n                Padding = new MarginPadding(20),\r\n                Child = markdownContainer = new ChangeLogMarkdownContainer(build)\r\n                {\r\n                    RelativeSizeAxes = Axes.X,\r\n                    AutoSizeAxes = Axes.Y,\r\n                },\r\n            },\r\n        };\r\n    });\r\n\r\n    [Test]\r\n    public void TestShowWithNoFetch()\r\n    {\r\n        AddStep(\"Show\", () => markdownContainer.Show());\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Overlays/Changelog/TestSceneKaraokeChangeLogOverlay.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Overlays;\r\nusing osu.Game.Tests.Visual;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Overlays.Changelog;\r\n\r\n[TestFixture]\r\npublic partial class TestSceneKaraokeChangeLogOverlay : OsuTestScene\r\n{\r\n    private TestChangelogOverlay changelog = null!;\r\n\r\n    [SetUp]\r\n    public void SetUp() => Schedule(() => { Child = changelog = new TestChangelogOverlay(); });\r\n\r\n    [Test]\r\n    public void TestShowWithNoFetch()\r\n    {\r\n        AddStep(\"Show\", () => changelog.Show());\r\n        AddAssert(\"listing displayed\", () => changelog.Current.Value == null);\r\n    }\r\n\r\n    private partial class TestChangelogOverlay : KaraokeChangelogOverlay\r\n    {\r\n        public TestChangelogOverlay()\r\n            : base(\"karaoke-dev\")\r\n        {\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Overlays/TestSceneOverlayColourProvider.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing System.Reflection;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Extensions.Color4Extensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Framework.Platform;\r\nusing osu.Game.Graphics.Containers;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Extensions;\r\nusing osu.Game.Tests.Visual;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Overlays;\r\n\r\n[TestFixture]\r\npublic partial class TestSceneOverlayColourProvider : OsuTestScene\r\n{\r\n    [Test]\r\n    public void TestShowWithNoFetch()\r\n    {\r\n        var providers = Enum.GetValues<OverlayColourScheme>()\r\n                            .Select(x => new OverlayColourProvider(x));\r\n\r\n        var colourProperties = typeof(OverlayColourProvider)\r\n                               .GetProperties(BindingFlags.Public | BindingFlags.Instance)\r\n                               .Where(x => x.PropertyType == typeof(Color4)).ToArray();\r\n\r\n        Schedule(() =>\r\n        {\r\n            var columns = colourProperties.Select(c => new TitleTableColumn(c.Name)).OfType<TableColumn>().ToArray();\r\n            var content = providers.Select(provider =>\r\n            {\r\n                if (provider == null)\r\n                    throw new ArgumentNullException(nameof(provider));\r\n\r\n                return colourProperties.Select(c =>\r\n                {\r\n                    object? value = c.GetValue(provider);\r\n                    if (value == null)\r\n                        throw new ArgumentNullException(nameof(value));\r\n\r\n                    var colour = (Color4)value;\r\n                    return new PreviewColourDrawable(colour);\r\n                }).OfType<Drawable>();\r\n            }).To2DArray();\r\n\r\n            Child = new OsuScrollContainer(Direction.Horizontal)\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n                Child = new TableContainer\r\n                {\r\n                    RelativeSizeAxes = Axes.Y,\r\n                    AutoSizeAxes = Axes.X,\r\n                    Columns = columns,\r\n                    Content = content,\r\n                },\r\n            };\r\n        });\r\n    }\r\n\r\n    private class TitleTableColumn : TableColumn\r\n    {\r\n        public TitleTableColumn(string title)\r\n            : base(title, Anchor.Centre, new Dimension(GridSizeMode.Absolute, 120))\r\n        {\r\n        }\r\n    }\r\n\r\n    private partial class PreviewColourDrawable : CompositeDrawable\r\n    {\r\n        [Resolved]\r\n        private Clipboard clipboard { get; set; } = null!;\r\n\r\n        private readonly Color4 color;\r\n\r\n        public PreviewColourDrawable(Color4 color)\r\n        {\r\n            this.color = color;\r\n\r\n            RelativeSizeAxes = Axes.Both;\r\n            InternalChildren = new Drawable[]\r\n            {\r\n                new Box\r\n                {\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    Colour = color,\r\n                },\r\n                new OsuSpriteText\r\n                {\r\n                    Anchor = Anchor.Centre,\r\n                    Origin = Anchor.Centre,\r\n                    Text = color.ToHex(),\r\n                },\r\n            };\r\n        }\r\n\r\n        protected override bool OnClick(ClickEvent e)\r\n        {\r\n            clipboard.SetText(color.ToHex());\r\n            return base.OnClick(e);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Ranking/TestKaraokeScoreInfo.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Game.Online.API.Requests.Responses;\r\nusing osu.Game.Rulesets.Karaoke.Mods;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Beatmaps;\r\nusing osu.Game.Rulesets.Mods;\r\nusing osu.Game.Rulesets.Scoring;\r\nusing osu.Game.Scoring;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Ranking;\r\n\r\npublic class TestKaraokeScoreInfo : ScoreInfo\r\n{\r\n    public TestKaraokeScoreInfo()\r\n    {\r\n        var ruleset = new KaraokeRuleset().RulesetInfo;\r\n\r\n        User = new APIUser\r\n        {\r\n            Id = 1030492,\r\n            Username = \"andy840119\",\r\n            CoverUrl = \"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg\",\r\n        };\r\n\r\n        BeatmapInfo = new TestKaraokeBeatmap(ruleset).BeatmapInfo;\r\n        Ruleset = ruleset;\r\n        Mods = new Mod[] { new KaraokeModFlashlight(), new KaraokeModSnow() };\r\n\r\n        TotalScore = 2845370;\r\n        Accuracy = 0.95;\r\n        MaxCombo = 999;\r\n        Rank = ScoreRank.S;\r\n        Date = DateTimeOffset.Now;\r\n\r\n        Statistics[HitResult.Miss] = 1;\r\n        Statistics[HitResult.Meh] = 50;\r\n        Statistics[HitResult.Good] = 100;\r\n        Statistics[HitResult.Great] = 300;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Ranking/TestSceneBeatmapMetadataGraph.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Extensions.Color4Extensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\nusing osu.Game.Rulesets.Karaoke.Statistics;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Beatmaps;\r\nusing osu.Game.Scoring;\r\nusing osu.Game.Tests.Visual;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Ranking;\r\n\r\npublic partial class TestSceneBeatmapMetadataGraph : OsuTestScene\r\n{\r\n    [Test]\r\n    public void TestBeatmapMetadataGraph()\r\n    {\r\n        var ruleset = new KaraokeRuleset().RulesetInfo;\r\n        var originBeatmap = new TestKaraokeBeatmap(ruleset);\r\n        if (new KaraokeBeatmapConverter(originBeatmap, new KaraokeRuleset()).Convert() is not KaraokeBeatmap karaokeBeatmap)\r\n            throw new InvalidCastException(nameof(karaokeBeatmap));\r\n\r\n        karaokeBeatmap.SingerInfo = createSingerInfo();\r\n        createTest(new ScoreInfo(), karaokeBeatmap);\r\n    }\r\n\r\n    private void createTest(ScoreInfo score, IBeatmap beatmap) => AddStep(\"create test\", () =>\r\n    {\r\n        Children = new Drawable[]\r\n        {\r\n            new Box\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n                Colour = Color4Extensions.FromHex(\"#333\"),\r\n            },\r\n            new BeatmapMetadataGraph(beatmap)\r\n            {\r\n                Anchor = Anchor.Centre,\r\n                Origin = Anchor.Centre,\r\n                Size = new Vector2(600, 200),\r\n            },\r\n        };\r\n    });\r\n\r\n    private static SingerInfo createSingerInfo()\r\n    {\r\n        var singerInfo = new SingerInfo();\r\n\r\n        for (int i = 0; i < 10; i++)\r\n        {\r\n            int singerIndex = i;\r\n            singerInfo.AddSinger(s =>\r\n            {\r\n                s.Name = $\"Singer{singerIndex}\";\r\n                s.Romanisation = $\"[Romanisation]Singer{singerIndex}\";\r\n                s.EnglishName = $\"[English]Singer{singerIndex}\";\r\n            });\r\n        }\r\n\r\n        return singerInfo;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Ranking/TestSceneHitEventTimingDistributionGraph.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Extensions.Color4Extensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Scoring;\r\nusing osu.Game.Screens.Ranking.Statistics;\r\nusing osu.Game.Tests.Visual;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Ranking;\r\n\r\npublic partial class TestSceneHitEventTimingDistributionGraph : OsuTestScene\r\n{\r\n    [Test]\r\n    public void TestManyDistributedEvents()\r\n    {\r\n        createTest(CreateDistributedHitEvents());\r\n    }\r\n\r\n    [Test]\r\n    public void TestZeroTimeOffset()\r\n    {\r\n        createTest(Enumerable.Range(0, 100).Select(_ => new HitEvent(0, 1.0, HitResult.Perfect, new Note(), new Note(), null)).ToList());\r\n    }\r\n\r\n    [Test]\r\n    public void TestNoEvents()\r\n    {\r\n        createTest(new List<HitEvent>());\r\n    }\r\n\r\n    private void createTest(IReadOnlyList<HitEvent> events) => AddStep(\"create test\", () =>\r\n    {\r\n        Children = new Drawable[]\r\n        {\r\n            new Box\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n                Colour = Color4Extensions.FromHex(\"#333\"),\r\n            },\r\n            new HitEventTimingDistributionGraph(events)\r\n            {\r\n                Anchor = Anchor.Centre,\r\n                Origin = Anchor.Centre,\r\n                Size = new Vector2(600, 130),\r\n            },\r\n        };\r\n    });\r\n\r\n    public static List<HitEvent> CreateDistributedHitEvents()\r\n    {\r\n        var hitEvents = new List<HitEvent>();\r\n\r\n        for (int i = 0; i < 50; i++)\r\n        {\r\n            int count = (int)Math.Pow(25 - Math.Abs(i - 25), 2);\r\n\r\n            for (int j = 0; j < count; j++)\r\n                hitEvents.Add(new HitEvent(i - 25, 1.0, HitResult.Perfect, new Note(), new Note(), null));\r\n        }\r\n\r\n        return hitEvents;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Ranking/TestSceneNotScorableGraph.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Framework.Extensions.Color4Extensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Game.Rulesets.Karaoke.Statistics;\r\nusing osu.Game.Tests.Visual;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Ranking;\r\n\r\npublic partial class TestSceneNotScorableGraph : OsuTestScene\r\n{\r\n    [Test]\r\n    public void TestBeatmapInfoGraph()\r\n    {\r\n        createTest();\r\n    }\r\n\r\n    private void createTest() => AddStep(\"create test\", () =>\r\n    {\r\n        Children = new Drawable[]\r\n        {\r\n            new Box\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n                Colour = Color4Extensions.FromHex(\"#333\"),\r\n            },\r\n            new NotScorableGraph\r\n            {\r\n                Anchor = Anchor.Centre,\r\n                Origin = Anchor.Centre,\r\n                Size = new Vector2(600, 130),\r\n            },\r\n        };\r\n    });\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Ranking/TestSceneScoringResultGraph.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Framework.Extensions.Color4Extensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Statistics;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Beatmaps;\r\nusing osu.Game.Scoring;\r\nusing osu.Game.Tests.Visual;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Ranking;\r\n\r\npublic partial class TestSceneScoringResultGraph : OsuTestScene\r\n{\r\n    [Test]\r\n    public void TestManyDistributedEvents()\r\n    {\r\n        var ruleset = new KaraokeRuleset().RulesetInfo;\r\n        var beatmap = new TestKaraokeBeatmap(ruleset);\r\n        createTest(new ScoreInfo(), beatmap);\r\n    }\r\n\r\n    private void createTest(ScoreInfo score, IBeatmap beatmap) => AddStep(\"create test\", () =>\r\n    {\r\n        Children = new Drawable[]\r\n        {\r\n            new Box\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n                Colour = Color4Extensions.FromHex(\"#333\"),\r\n            },\r\n            new ScoringResultGraph(score, beatmap)\r\n            {\r\n                Anchor = Anchor.Centre,\r\n                Origin = Anchor.Centre,\r\n                Size = new Vector2(600, 130),\r\n            },\r\n        };\r\n    });\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Ranking/TestSceneStatisticsPanel.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Scoring;\r\nusing osu.Game.Screens.Ranking.Statistics;\r\nusing osu.Game.Tests.Visual;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Ranking;\r\n\r\npublic partial class TestSceneStatisticsPanel : OsuTestScene\r\n{\r\n    [Test]\r\n    public void TestScoreWithStatistics()\r\n    {\r\n        var score = new TestKaraokeScoreInfo\r\n        {\r\n            HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(),\r\n        };\r\n\r\n        loadPanel(score);\r\n    }\r\n\r\n    [Test]\r\n    public void TestScoreWithoutStatistics()\r\n    {\r\n        loadPanel(new TestKaraokeScoreInfo());\r\n    }\r\n\r\n    [Test]\r\n    public void TestNullScore()\r\n    {\r\n        loadPanel(null);\r\n    }\r\n\r\n    private void loadPanel(ScoreInfo? score) => AddStep(\"load panel\", () =>\r\n    {\r\n        Child = new StatisticsPanel\r\n        {\r\n            RelativeSizeAxes = Axes.Both,\r\n            State = { Value = Visibility.Visible },\r\n            Score = { Value = score },\r\n        };\r\n    });\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Replays/TestSceneAutoGeneration.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Replays;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\nusing osu.Game.Rulesets.Replays;\r\nusing osu.Game.Tests.Visual;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Replays;\r\n\r\npublic partial class TestSceneAutoGeneration : OsuTestScene\r\n{\r\n    [Test]\r\n    public void TestSingleShortNote()\r\n    {\r\n        // |     |\r\n        // |  -  |\r\n        // |     |\r\n\r\n        var referencedLyric = TestCaseNoteHelper.CreateLyricForNote(2, \"karaoke!\", 1000, 50);\r\n\r\n        var beatmap = new KaraokeBeatmap();\r\n        beatmap.HitObjects.Add(new Note\r\n        {\r\n            Text = \"karaoke!\",\r\n            Display = true,\r\n            Tone = new Tone(0, true),\r\n            ReferenceLyricId = referencedLyric.ID,\r\n            ReferenceLyric = referencedLyric,\r\n        });\r\n\r\n        var generated = new KaraokeAutoGenerator(beatmap).Generate();\r\n\r\n        Assert.That(generated.Frames.Count == 2, \"Replay must have 2 frames, start and end.\");\r\n        Assert.That(generated.Frames[0].Time, Is.EqualTo(1000), \"Incorrect time\");\r\n        Assert.That(generated.Frames[1].Time, Is.EqualTo(1051), \"Incorrect time\");\r\n        Assert.That(checkMatching(generated.Frames[0], new Tone(0, true)), \"Frame1 should sing.\");\r\n        Assert.That(checkMatching(generated.Frames[1], null), \"Frame2 should release sing.\");\r\n    }\r\n\r\n    [Test]\r\n    public void TestSingleNoteWithLongTime()\r\n    {\r\n        // |     |\r\n        // | *** |\r\n        // |     |\r\n\r\n        var referencedLyric = TestCaseNoteHelper.CreateLyricForNote(2, \"karaoke!\", 1000, 1000);\r\n\r\n        var beatmap = new KaraokeBeatmap();\r\n        beatmap.HitObjects.Add(new Note\r\n        {\r\n            Text = \"karaoke!\",\r\n            Display = true,\r\n            Tone = new Tone(0, true),\r\n            ReferenceLyricId = referencedLyric.ID,\r\n            ReferenceLyric = referencedLyric,\r\n        });\r\n\r\n        var generated = new KaraokeAutoGenerator(beatmap).Generate();\r\n\r\n        Assert.That(generated.Frames.Count, Is.EqualTo(11), \"Replay must have 11 frames,Start, duration(9 frames) and end.\");\r\n        Assert.That(generated.Frames[0].Time, Is.EqualTo(1000), \"Incorrect hit time\");\r\n        Assert.That(generated.Frames[10].Time, Is.EqualTo(2001), \"Incorrect time\");\r\n        Assert.That(checkMatching(generated.Frames[0], new Tone(0, true)), \"Fist frame should sing.\");\r\n        Assert.That(checkMatching(generated.Frames[10], null), \"Last frame should not sing.\");\r\n    }\r\n\r\n    [Test]\r\n    public void TestNoteStair()\r\n    {\r\n        // |      |\r\n        // |    - |\r\n        // | -    |\r\n\r\n        var firstReferencedLyric = TestCaseNoteHelper.CreateLyricForNote(2, \"karaoke!\", 1000, 50);\r\n        var secondReferencedLyric = TestCaseNoteHelper.CreateLyricForNote(3, \"karaoke!\", 1050, 50);\r\n\r\n        var beatmap = new KaraokeBeatmap();\r\n        beatmap.HitObjects.Add(new Note\r\n        {\r\n            Text = \"kara\",\r\n            Display = true,\r\n            Tone = new Tone(0, true),\r\n            ReferenceLyricId = firstReferencedLyric.ID,\r\n            ReferenceLyric = firstReferencedLyric,\r\n        });\r\n        beatmap.HitObjects.Add(new Note\r\n        {\r\n            Text = \"oke!\",\r\n            Display = true,\r\n            Tone = new Tone(1, true),\r\n            ReferenceLyricId = secondReferencedLyric.ID,\r\n            ReferenceLyric = secondReferencedLyric,\r\n        });\r\n\r\n        var generated = new KaraokeAutoGenerator(beatmap).Generate();\r\n\r\n        Assert.That(generated.Frames.Count, Is.EqualTo(3), \"Replay must have 3 frames, note1's start, note2's start and note2's end.\");\r\n        Assert.That(generated.Frames[0].Time, Is.EqualTo(1000), \"Incorrect time\");\r\n        Assert.That(generated.Frames[1].Time, Is.EqualTo(1050), \"Incorrect time\");\r\n        Assert.That(generated.Frames[2].Time, Is.EqualTo(1101), \"Incorrect time\");\r\n        Assert.That(checkMatching(generated.Frames[0], new Tone(0, true)), \"Frame1 should sing.\");\r\n        Assert.That(checkMatching(generated.Frames[1], new Tone(1, true)), \"Frame2 should sing.\");\r\n        Assert.That(checkMatching(generated.Frames[2], null), \"Frame3 should release sing.\");\r\n    }\r\n\r\n    private static bool checkMatching(ReplayFrame frame, Tone? tone)\r\n    {\r\n        if (frame is not KaraokeReplayFrame karaokeReplayFrame)\r\n            throw new InvalidCastException($\"{nameof(frame)} is not karaoke replay frame.\");\r\n\r\n        if (!karaokeReplayFrame.Sound)\r\n            return !tone.HasValue;\r\n\r\n        if (tone == null)\r\n            throw new ArgumentNullException(nameof(tone));\r\n\r\n        return karaokeReplayFrame.Scale == tone.Value.Scale + (tone.Value.Half ? 0.5f : 0);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Replays/TestSceneAutoGenerationBySinger.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.IO;\r\nusing System.Linq;\r\nusing Newtonsoft.Json;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Replays;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Resources;\r\nusing osu.Game.Tests.Visual;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Replays;\r\n\r\npublic partial class TestSceneAutoGenerationBySinger : OsuTestScene\r\n{\r\n    [Test]\r\n    public void TestSingDemoSong()\r\n    {\r\n        var beatmap = new KaraokeBeatmap();\r\n\r\n        var data = TestResources.OpenTrackResource(\"demo\");\r\n        var generated = new KaraokeAutoGeneratorBySinger(beatmap, data).Generate();\r\n\r\n        // Get generated frame and compare frame\r\n        var expected = getCompareResultFromName(\"demo\");\r\n        var actual = generated.Frames.OfType<KaraokeReplayFrame>().ToList();\r\n\r\n        // Check total frames.\r\n        Assert.That(actual.Count, Is.EqualTo(expected.Count), $\"Replay frame should have {expected.Count}.\");\r\n\r\n        // Compare generated frame with result;\r\n        for (int i = 0; i < expected.Count; i++)\r\n        {\r\n            Assert.That(actual[i].Time, Is.EqualTo(expected[i].Time));\r\n            Assert.That(actual[i].Sound, Is.EqualTo(expected[i].Sound));\r\n\r\n            if (!expected[i].Sound)\r\n                continue;\r\n\r\n            float convertedScale = beatmap.PitchToScale(expected[i].Pitch);\r\n            Assert.That(actual[i].Scale, Is.EqualTo(convertedScale));\r\n        }\r\n    }\r\n\r\n    private static IReadOnlyList<TestKaraokeReplayFrame> getCompareResultFromName(string name)\r\n    {\r\n        var data = TestResources.OpenResource($\"Testing/Track/{name}.json\");\r\n\r\n        using var reader = new StreamReader(data);\r\n        string str = reader.ReadToEnd();\r\n        return JsonConvert.DeserializeObject<TestKaraokeReplayFrame[]>(str) ?? Array.Empty<TestKaraokeReplayFrame>();\r\n    }\r\n\r\n    private struct TestKaraokeReplayFrame\r\n    {\r\n        [JsonProperty]\r\n        public double Time { get; set; }\r\n\r\n        [JsonProperty]\r\n        public float Pitch { get; set; }\r\n\r\n        [JsonProperty]\r\n        public bool Sound { get; set; }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Resources/TestResources.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.IO;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Audio;\r\nusing osu.Framework.Audio.Track;\r\nusing osu.Framework.Graphics.Rendering;\r\nusing osu.Framework.Graphics.Rendering.Dummy;\r\nusing osu.Framework.Graphics.Textures;\r\nusing osu.Framework.IO.Stores;\r\nusing osu.Game.Database;\r\nusing osu.Game.IO;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Resources;\r\n\r\npublic static class TestResources\r\n{\r\n    public static DllResourceStore GetStore() => new(typeof(TestResources).Assembly);\r\n\r\n    public static Stream OpenResource(string name) => GetStore().GetStream($\"Resources/{name}\");\r\n\r\n    public static Stream OpenBeatmapResource(string name) => OpenResource($\"Testing/Beatmaps/{name}.osu\");\r\n\r\n    public static Stream OpenSkinResource(string name) => OpenResource($\"Testing/Skin/{name}.skin\");\r\n\r\n    public static Stream OpenKarResource(string name) => OpenResource($\"Testing/Kar/{name}.kar\");\r\n\r\n    public static string GetTestKarForImport(string name)\r\n    {\r\n        string tempPath = Path.GetTempFileName() + \".kar\";\r\n\r\n        using (var stream = OpenKarResource(name))\r\n        using (var newFile = File.Create(tempPath))\r\n            stream.CopyTo(newFile);\r\n\r\n        Assert.That(File.Exists(tempPath));\r\n        return tempPath;\r\n    }\r\n\r\n    public static Stream OpenTrackResource(string name) => OpenResource($\"Testing/Track/{name}.mp3\");\r\n\r\n    public static Track OpenTrackInfo(AudioManager audioManager, string name) => audioManager.GetTrackStore(GetStore()).Get($\"Resources/Testing/Track/{name}.mp3\");\r\n\r\n    public static IStorageResourceProvider CreateSkinStorageResourceProvider(string skinName = \"special-skin\") => new TestStorageResourceProvider(skinName);\r\n\r\n    private class TestStorageResourceProvider : IStorageResourceProvider\r\n    {\r\n        public TestStorageResourceProvider(string skinName)\r\n        {\r\n            Files = Resources = new NamespacedResourceStore<byte[]>(new DllResourceStore(GetType().Assembly), $\"Resources/{skinName}\");\r\n        }\r\n\r\n        public IRenderer Renderer => new DummyRenderer();\r\n\r\n        public AudioManager AudioManager => null!;\r\n        public IResourceStore<byte[]> Files { get; }\r\n        public IResourceStore<byte[]> Resources { get; }\r\n        public RealmAccess RealmAccess => null!;\r\n\r\n        public IResourceStore<TextureUpload> CreateTextureLoaderStore(IResourceStore<byte[]> underlyingStore) => null!;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Resources/Testing/Beatmaps/karaoke-file-samples-expected-conversion.json",
    "content": "﻿{\n  \"Mappings\": []\n}"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Resources/Testing/Beatmaps/karaoke-file-samples-without-note.osu",
    "content": "﻿karaoke file format v1\n\n[General]\nAudioFilename: song.mp3\nAudioLeadIn: 0\nPreviewTime: 71220\nCountdown: 0\nSampleSet: Normal\nStackLeniency: 0.7\nMode: 111\nLetterboxInBreaks: 0\nWidescreenStoryboard: 1\n\n[Editor]\nDistanceSpacing: 1.1\nBeatDivisor: 4\nGridSize: 4\nTimelineZoom: 1\n\n[Metadata]\nTitle:Shinning star\nTitleUnicode:シャイニングスター\nArtist:森田交一\nArtistUnicode:森田交一\nCreator:andy840119\nVersion:Normal\nSource:シャイニングスター\nTags:karaoke\nBeatmapID:1\nBeatmapSetID:1\n\n[Difficulty]\nHPDrainRate:2\nCircleSize:3\nOverallDifficulty:2\nApproachRate:4\nSliderMultiplier:0.8\nSliderTickRate:1\n\n[Events]\n//Background and Video events\nVideo,0,\"video.mp4\"\n0,0,\"bg.png\",0,0\n\n[Colours]\n\n[HitObjects]\n@[00:18:58]た[00:18:81]だ[00:19:36]風[00:20:09]に[00:20:29]揺[00:20:49]ら[00:20:68]れ[00:20:89]て[00:20:93]\n@[00:21:61]何[00:22:00]も[00:22:19]考[00:23:14]え[00:23:53]ず[00:23:70]に[00:23:78]\n@[00:24:68]た[00:24:88]だ[00:25:45]雲[00:26:21]を[00:26:40]眺[00:26:78]め[00:26:96]て[00:27:03]\n@[00:27:70]過[00:27:90]ご[00:28:25]す[00:28:63]の[00:28:89]も[00:29:25]い[00:29:59]い[00:29:95]よ[00:30:14]ね[00:30:21]\n\n@[00:30:55]誰[00:31:53]し[00:31:72]も[00:32:25]何[00:32:64]か[00:32:83]し[00:33:02]ら[00:33:08]\n@[00:33:74]使[00:33:95]命[00:34:53]を[00:34:86]抱[00:35:43]え[00:35:76]て[00:35:92]る[00:36:00]\n@[00:36:78]た[00:36:97]だ[00:37:59]そ[00:37:78]れ[00:38:31]だ[00:38:51]け[00:38:69]の[00:38:89]こ[00:39:08]と[00:39:14]\n@[00:39:88]悩[00:40:39]む[00:40:58]の[00:40:93]は[00:41:33]も[00:41:70]う[00:42:06]や[00:42:27]め[00:42:42]た[00:42:48]\n\n@[00:42:99]さ[00:43:37]ざ[00:43:86]な[00:44:26]み[00:44:46]の[00:45:87]音[00:46:44]に[00:46:83]癒[00:47:25]や[00:47:58]さ[00:47:85]れ[00:48:03]て[00:48:20]く[00:48:24]\n@[00:49:09]軌[00:49:24]跡[00:49:76]を[00:49:93]運[00:50:47]ぶ[00:50:67]風[00:51:18]の[00:51:39]音[00:51:64]\n@[00:52:33]時[00:52:72]を[00:52:96]閉[00:53:21]じ[00:53:51]込[00:53:68]め[00:53:83]て[00:53:92]\n\n@[00:56:48]シャ[00:56:63]イ[00:56:77]ニ[00:57:07]ン[00:57:27]グ[00:57:57]ス[00:57:99]ター[00:58:23]綴[00:58:59]れ[00:58:73]ば[00:58:81]\n@[00:59:19]夢[00:59:58]に[00:59:79]眠[01:00:34]る[01:00:73]幻[01:01:84]が[01:02:39]掌[01:03:33]に[01:04:05]降[01:04:24]り[01:04:59]注[01:05:04]ぐ[01:05:17]\n@[01:05:61]新[01:06:15]た[01:06:64]な[01:06:84]世[01:07:48]界[01:07:67]へ[01:07:78]\n@[01:08:49]I'll [01:08:91]believe [01:09:28]of [01:09:91]my [01:10:28]sensation[01:10:35]\n@[01:11:40]果[01:11:54]て[01:11:71]し[01:11:89]な[01:12:27]い[01:12:48]道[01:12:88]の[01:13:31]向[01:13:65]こ[01:13:80]う[01:13:99]で[01:14:06]\n@[01:14:56]瞼[01:15:32]の[01:15:54]裏[01:16:29]に[01:16:66]映[01:17:20]る[01:17:26]\n@[01:17:78]一[01:18:17]滴[01:19:38]の[01:19:78]光[01:20:34]\n@[01:20:84]ト[01:21:05]キ[01:21:43]メ[01:21:78]キ[01:22:16]を[01:22:56]感[01:23:35]じ[01:23:69]て[01:23:75]\n\n@[01:40:75]貴[01:41:34]方[01:41:53]が[01:42:05]触[01:42:20]れ[01:42:36]る[01:42:51]全[01:42:90]て[01:43:00]\n@[01:43:51]幸[01:44:03]せ[01:44:38]で[01:44:51]あ[01:44:79]る[01:45:18]よ[01:45:52]う[01:45:68]に[01:45:75]\n@[01:46:67]今[01:47:37]生[01:47:54]き[01:47:83]る[01:48:25]喜[01:48:79]び[01:48:96]を[01:49:05]\n@[01:49:61]忘[01:49:93]れ[01:50:21]て[01:50:37]し[01:50:52]ま[01:50:86]わ[01:51:24]な[01:51:57]い[01:51:92]よ[01:52:12]う[01:52:22]\n\n@[01:52:84]月[01:53:32]の[01:53:68]光[01:54:34] [01:55:76]隠[01:56:29]す[01:56:69]雲[01:57:47]は[01:57:74]揺[01:57:91]ら[01:58:25]ぎ[01:58:32]\n@[01:58:83]自[01:59:03]由[01:59:61]の[01:59:77]翼[02:00:51]は[02:00:82]今[02:01:25]も[02:01:33]\n@[02:01:86]大[02:02:36]空[02:02:83]を[02:03:10]翔[02:03:57]る[02:03:64]\n\n@[02:06:36]シャ[02:06:51]イ[02:06:67]ニ[02:06:96]ン[02:07:17]グ[02:07:31]ス[02:07:60]ター[02:07:94]綴[02:08:50]れ[02:09:18]ば[02:09:24]\n@[02:09:34]無[02:09:49]限[02:10:00]の[02:10:16]イ[02:10:58]マ[02:10:95]ジ[02:11:30]ネー[02:11:47]ショ[02:11:65]ン[02:11:73]\n@[02:12:27]魔[02:12:43]法[02:13:04]が[02:13:21]使[02:14:06]え[02:14:41]る[02:14:56]よ[02:14:71]う[02:15:00]な[02:15:05]\n@[02:15:52]世[02:15:88]界[02:16:28]が[02:16:66]広[02:17:39]が[02:17:57]る[02:17:65]\n@[02:18:41]I'll [02:18:79]believe [02:19:48]of [02:19:76]my [02:20:63]sensation[02:20:72]\n@[02:21:27]今[02:21:63]も[02:21:80]降[02:22:15]り[02:22:34]募[02:23:09]る[02:23:46]想[02:23:85]い[02:23:99]よ[02:24:04]\n@[02:24:43]切[02:24:84]な[02:25:23]さ[02:25:40]や[02:25:80]ト[02:26:17]キ[02:26:55]メ[02:26:71]キ[02:27:04]が[02:27:14]\n@[02:27:84]心[02:28:56]の[02:28:84]真[02:29:16]ん[02:29:56]中[02:30:13]で[02:30:21]\n@[02:30:69]熱[02:31:23]い[02:31:62]メ[02:32:05]ロ[02:32:40]ディ[02:33:16]に[02:33:56]な[02:33:75]る[02:33:83]\n\n@[03:01:02]シャ[03:01:21]イ[03:01:40]ニ[03:01:74]ン[03:01:92]グ[03:02:25]ス[03:02:60]ター[03:02:75]綴[03:03:18]れ[03:03:32]ば[03:03:41]\n@[03:03:75]無[03:03:93]限[03:04:28]の[03:04:68]イ[03:04:88]マ[03:05:26]ジ[03:05:62]ネー[03:06:18]ショ[03:06:36]ン[03:06:44]\n@[03:06:94]魔[03:07:11]法[03:07:70]が[03:07:89]使[03:08:68]え[03:09:04]る[03:09:22]よ[03:09:37]う[03:09:67]な[03:09:73]\n@[03:10:19]世[03:10:54]界[03:10:92]が[03:11:29]広[03:12:06]が[03:12:24]る[03:12:33]\n@[03:13:05]I'll [03:13:44]believe [03:13:86]of [03:14:19]my [03:14:71]sensation[03:14:78]\n@[03:15:87]今[03:16:26]も[03:16:47]降[03:16:83]り[03:17:04]募[03:17:79]る[03:18:15]想[03:18:50]い[03:18:68]よ[03:18:74]\n@[03:19:16]切[03:19:52]な[03:19:93]さ[03:20:12]や[03:20:52]ト[03:20:89]キ[03:21:25]メ[03:21:43]キ[03:21:81]が[03:22:35]輝[03:23:13]き[03:23:52]出[03:23:83]す[03:23:89]\n\n@[03:24:93]シャ[03:25:09]イ[03:25:27]ニ[03:25:98]ン[03:26:17]グ[03:26:53]ス[03:26:69]ター[03:26:91]綴[03:27:47]れ[03:27:79]ば[03:27:87]\n@[03:28:20]夢[03:28:61]に[03:28:97]眠[03:29:54]る[03:29:93]幻[03:31:00]が[03:31:35]掌[03:32:21]に[03:32:63]降[03:33:01]り[03:33:38]注[03:33:90]ぐ[03:33:99]\n@[03:34:49]新[03:34:99]た[03:35:28]な[03:35:64]世[03:36:34]界[03:36:55]へ[03:36:64]\n@[03:37:36]I'll [03:38:02]believe [03:38:70]of [03:39:04]my [03:39:57]sensation[03:39:66]\n@[03:40:23]果[03:40:41]て[03:40:58]し[03:40:78]な[03:41:14]い[03:41:35]道[03:42:13]の[03:42:47]向[03:42:65]こ[03:42:85]う[03:43:05]で[03:43:12]\n@[03:43:45]瞼[03:44:19]の[03:44:40]裏[03:45:12]に[03:45:49]映[03:46:06]る[03:46:12]\n@[03:46:70]一[03:47:09]滴[03:48:19]の[03:48:55]光[03:49:18]\n@[03:49:69]ト[03:49:88]キ[03:50:27]メ[03:50:63]キ[03:51:02]を[03:51:39]感[03:52:18]じ[03:52:78]て[03:52:86]\n\n@[03:56:25]La [03:56:42]La [03:56:57]La [03:56:76]La [03:56:92]La [03:57:10]La [03:57:31]La~~[03:57:35]\n@[03:59:18]La [03:59:37]La [03:59:55]La [03:59:72]La [03:59:90]La [04:00:09]La [04:00:30]La~~ [04:00:69]La [04:00:90]La [04:01:11]La~[04:01:16]\n@[04:02:21]La [04:02:38]La [04:02:58]La [04:02:78]La [04:02:99]La [04:03:17]La [04:03:37]La~~[04:03:44]\n@[04:05:28]La [04:05:46]La [04:05:65]La [04:05:85]La [04:06:01]La [04:06:20]La [04:06:40]La~~ [04:07:51]La [04:08:13]La~~~~~~~~~~~[04:11:52]\n\n@Ruby1=た,た\n@Ruby2=だ,だ\n@Ruby3=風,か[00:00:21]ぜ,,[00:50:67]\n@Ruby4=に,に\n@Ruby5=揺,ゆ\n@Ruby6=ら,ら\n@Ruby7=れ,れ\n@Ruby8=て,て\n@Ruby9=何,な[00:00:17]に\n@Ruby10=も,も\n@Ruby11=考,か[00:00:18]ん[00:00:52]が\n@Ruby12=え,え\n@Ruby13=ず,ず\n@Ruby14=雲,く[00:00:35]も,,[01:56:69]\n@Ruby15=を,を\n@Ruby16=眺,な[00:00:20]が\n@Ruby17=め,め\n@Ruby18=過,す\n@Ruby19=ご,ご\n@Ruby20=す,す\n@Ruby21=の,の\n@Ruby22=い,い\n@Ruby23=よ,よ\n@Ruby24=ね,ね\n@Ruby25=誰,だ[00:00:47]れ\n@Ruby26=し,し\n@Ruby27=か,か\n@Ruby28=使,し,,[02:13:21]\n@Ruby29=命,め[00:00:39]い\n@Ruby30=抱,か[00:00:20]か\n@Ruby31=る,る\n@Ruby32=そ,そ\n@Ruby33=け,け\n@Ruby34=こ,こ\n@Ruby35=と,と\n@Ruby36=悩,な[00:00:16]や\n@Ruby37=む,む\n@Ruby38=は,は\n@Ruby39=う,う\n@Ruby40=や,や\n@Ruby41=さ,さ\n@Ruby42=ざ,ざ\n@Ruby43=な,な\n@Ruby44=み,み\n@Ruby45=音,お[00:00:18]と,,[00:51:39]\n@Ruby46=癒,い\n@Ruby47=く,く\n@Ruby48=軌,き\n@Ruby49=跡,せ[00:00:31]き\n@Ruby50=運,は[00:00:26]こ\n@Ruby51=ぶ,ぶ\n@Ruby52=風,か[00:00:28]ぜ,[00:50:67]\n@Ruby53=音,お[00:00:19]と,[00:51:39]\n@Ruby54=時,と[00:00:19]き\n@Ruby55=閉,と\n@Ruby56=じ,じ\n@Ruby57=込,こ\n@Ruby58=シャ,しゃ\n@Ruby59=イ,い\n@Ruby60=ニ,に\n@Ruby61=ン,ん\n@Ruby62=グ,ぐ\n@Ruby63=ス,す\n@Ruby64=タ,た\n@Ruby65=綴,つ[00:00:21]づ,,[02:07:94]\n@Ruby66=ば,ば\n@Ruby67=夢,ゆ[00:00:19]め,,[03:28:20]\n@Ruby68=眠,ね[00:00:37]む,,[03:28:97]\n@Ruby69=幻,ま[00:00:35]ぼ[00:00:73]ろ[00:00:89]し,,[03:29:93]\n@Ruby70=が,が\n@Ruby71=掌,て[00:00:17]の[00:00:39]ひ[00:00:74]ら,,[03:31:35]\n@Ruby72=降,ふ\n@Ruby73=り,り\n@Ruby74=注,そ[00:00:14]そ,,[03:33:38]\n@Ruby75=ぐ,ぐ\n@Ruby76=新,あ[00:00:21]ら\n@Ruby77=世,せ[00:00:28]か,,[02:15:52]\n@Ruby78=界,い\n@Ruby79=へ,へ\n@Ruby80=果,は\n@Ruby81=道,み[00:00:13]ち,,[03:41:35]\n@Ruby82=向,む\n@Ruby83=で,で\n@Ruby84=瞼,ま[00:00:20]ぶ[00:00:44]た,,[03:43:45]\n@Ruby85=裏,う[00:00:39]ら,,[03:44:40]\n@Ruby86=映,う[00:00:15]つ,,[03:45:49]\n@Ruby87=一,ひ[00:00:19]と,,[03:46:70]\n@Ruby88=滴,し[00:00:21]ず[00:00:73]く,,[03:47:09]\n@Ruby89=光,ひ[00:00:15]か[00:00:49]り,,[01:53:68]\n@Ruby90=ト,と\n@Ruby91=キ,き\n@Ruby92=メ,め\n@Ruby93=感,か[00:00:48]ん,,[03:51:39]\n@Ruby94=貴,あ[00:00:12]な\n@Ruby95=方,た\n@Ruby96=触,ふ\n@Ruby97=全,す[00:00:17]べ\n@Ruby98=幸,し[00:00:15]あ[00:00:31]わ\n@Ruby99=せ,せ\n@Ruby100=あ,あ\n@Ruby101=今,い[00:00:19]ま,,[02:00:82]\n@Ruby102=生,い\n@Ruby103=き,き\n@Ruby104=喜,よ[00:00:17]ろ[00:00:34]こ\n@Ruby105=び,び\n@Ruby106=忘,わ[00:00:16]す\n@Ruby107=ま,ま\n@Ruby108=わ,わ\n@Ruby109=月,つ[00:00:14]き\n@Ruby110=光,ひ[00:00:38]か[00:00:59]り,[01:53:68],[03:48:55]\n@Ruby111=隠,か[00:00:19]く\n@Ruby112=雲,く[00:00:39]も,[01:56:69]\n@Ruby113=ぎ,ぎ\n@Ruby114=自,じ\n@Ruby115=由,ゆ[00:00:27]う\n@Ruby116=翼,つ[00:00:29]ば[00:00:54]さ\n@Ruby117=今,い[00:00:27]ま,[02:00:82],[02:21:27]\n@Ruby118=大,お[00:00:18]お\n@Ruby119=空,ぞ[00:00:24]ら\n@Ruby120=翔,か[00:00:27]け\n@Ruby121=綴,つ[00:00:37]づ,[02:07:94],[03:02:75]\n@Ruby122=無,む\n@Ruby123=限,げ[00:00:17]ん,,[03:03:93]\n@Ruby124=マ,ま\n@Ruby125=ジ,じ\n@Ruby126=ネ,ね\n@Ruby127=ショ,しょ\n@Ruby128=魔,ま\n@Ruby129=法,ほ[00:00:23]う,,[03:07:11]\n@Ruby130=使,つ[00:00:38]か,[02:13:21],[03:07:89]\n@Ruby131=世,せ[00:00:18]か,[02:15:52],[03:35:64]\n@Ruby132=広,ひ[00:00:35]ろ,,[03:11:29]\n@Ruby133=今,い[00:00:17]ま,[02:21:27],[03:15:87]\n@Ruby134=募,つ[00:00:37]の\n@Ruby135=想,お[00:00:15]も,,[03:18:15]\n@Ruby136=切,せ[00:00:19]つ,,[03:19:16]\n@Ruby137=心,こ[00:00:17]こ[00:00:36]ろ\n@Ruby138=真,ま\n@Ruby139=ん,ん\n@Ruby140=中,な[00:00:20]か\n@Ruby141=熱,あ[00:00:22]つ\n@Ruby142=ロ,ろ\n@Ruby143=ディ,でぃ\n@Ruby144=綴,つ[00:00:27]づ,[03:02:75],[03:26:91]\n@Ruby145=限,げ[00:00:16]ん,[03:03:93]\n@Ruby146=法,ほ[00:00:22]う,[03:07:11]\n@Ruby147=使,つ[00:00:41]か,[03:07:89]\n@Ruby148=広,ひ[00:00:37]ろ,[03:11:29]\n@Ruby149=今,い[00:00:20]ま,[03:15:87]\n@Ruby150=想,お[00:00:17]も,[03:18:15]\n@Ruby151=切,せ[00:00:16]つ,[03:19:16]\n@Ruby152=輝,か[00:00:21]が[00:00:40]や\n@Ruby153=出,だ\n@Ruby154=綴,つ[00:00:35]づ,[03:26:91]\n@Ruby155=夢,ゆ[00:00:15]め,[03:28:20]\n@Ruby156=眠,ね[00:00:21]む,[03:28:97]\n@Ruby157=幻,ま[00:00:39]ぼ[00:00:56]ろ[00:00:75]し,[03:29:93]\n@Ruby158=掌,て[00:00:17]の[00:00:36]ひ[00:00:66]ら,[03:31:35]\n@Ruby159=注,そ[00:00:16]そ,[03:33:38]\n@Ruby160=世,せ[00:00:35]か,[03:35:64]\n@Ruby161=道,み[00:00:40]ち,[03:41:35]\n@Ruby162=瞼,ま[00:00:18]ぶ[00:00:38]た,[03:43:45]\n@Ruby163=裏,う[00:00:35]ら,[03:44:40]\n@Ruby164=映,う[00:00:18]つ,[03:45:49]\n@Ruby165=一,ひ[00:00:17]と,[03:46:70]\n@Ruby166=滴,し[00:00:33]ず[00:00:71]く,[03:47:09]\n@Ruby167=光,ひ[00:00:16]か[00:00:55]り,[03:48:55]\n@Ruby168=感,か[00:00:40]ん,[03:51:39]\n\nend\n\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Resources/Testing/Beatmaps/karaoke-file-samples.osu",
    "content": "﻿karaoke file format v1\n\n[General]\nAudioFilename: song.mp3\nAudioLeadIn: 0\nPreviewTime: 71220\nCountdown: 0\nSampleSet: Normal\nStackLeniency: 0.7\nMode: 111\nLetterboxInBreaks: 0\nWidescreenStoryboard: 1\n\n[Editor]\nDistanceSpacing: 1.1\nBeatDivisor: 4\nGridSize: 4\nTimelineZoom: 1\n\n[Metadata]\nTitle:Shinning star\nTitleUnicode:シャイニングスター\nArtist:森田交一\nArtistUnicode:森田交一\nCreator:andy840119\nVersion:Normal\nSource:シャイニングスター\nTags:karaoke\nBeatmapID:1\nBeatmapSetID:1\n\n[Difficulty]\nHPDrainRate:2\nCircleSize:3\nOverallDifficulty:2\nApproachRate:4\nSliderMultiplier:0.8\nSliderTickRate:1\n\n[Events]\n//Background and Video events\nVideo,0,\"video.mp4\"\n0,0,\"bg.png\",0,0\n\n[Colours]\n\n[HitObjects]\n@[00:18:58]た[00:18:81]だ[00:19:36]風[00:20:09]に[00:20:29]揺[00:20:49]ら[00:20:68]れ[00:20:89]て[00:20:93]\n@note1=0,0,(0|0),1,2,3,4,3\n@[00:21:61]何[00:22:00]も[00:22:19]考[00:23:14]え[00:23:53]ず[00:23:70]に[00:23:78]\n@note2=(1|1#),2,(2|1|1#),1,0.5,1\n@[00:24:68]た[00:24:88]だ[00:25:45]雲[00:26:21]を[00:26:40]眺[00:26:78]め[00:26:96]て[00:27:03]\n@note3=0,0,(0|0),1,(2|1#),2,3\n@[00:27:70]過[00:27:90]ご[00:28:25]す[00:28:63]の[00:28:89]も[00:29:25]い[00:29:59]い[00:29:95]よ[00:30:14]ね[00:30:21]\n@note4=2,3,2,1,0,-1,-1,0,1\n\n@[00:30:55]誰[00:31:53]し[00:31:72]も[00:32:25]何[00:32:64]か[00:32:83]し[00:33:02]ら[00:33:08]\n@[00:33:74]使[00:33:95]命[00:34:53]を[00:34:86]抱[00:35:43]え[00:35:76]て[00:35:92]る[00:36:00]\n@[00:36:78]た[00:36:97]だ[00:37:59]そ[00:37:78]れ[00:38:31]だ[00:38:51]け[00:38:69]の[00:38:89]こ[00:39:08]と[00:39:14]\n@[00:39:88]悩[00:40:39]む[00:40:58]の[00:40:93]は[00:41:33]も[00:41:70]う[00:42:06]や[00:42:27]め[00:42:42]た[00:42:48]\n\n@[00:42:99]さ[00:43:37]ざ[00:43:86]な[00:44:26]み[00:44:46]の[00:45:87]音[00:46:44]に[00:46:83]癒[00:47:25]や[00:47:58]さ[00:47:85]れ[00:48:03]て[00:48:20]く[00:48:24]\n@[00:49:09]軌[00:49:24]跡[00:49:76]を[00:49:93]運[00:50:47]ぶ[00:50:67]風[00:51:18]の[00:51:39]音[00:51:64]\n@[00:52:33]時[00:52:72]を[00:52:96]閉[00:53:21]じ[00:53:51]込[00:53:68]め[00:53:83]て[00:53:92]\n\n@[00:56:48]シャ[00:56:63]イ[00:56:77]ニ[00:57:07]ン[00:57:27]グ[00:57:57]ス[00:57:99]ター[00:58:23]綴[00:58:59]れ[00:58:73]ば[00:58:81]\n@[00:59:19]夢[00:59:58]に[00:59:79]眠[01:00:34]る[01:00:73]幻[01:01:84]が[01:02:39]掌[01:03:33]に[01:04:05]降[01:04:24]り[01:04:59]注[01:05:04]ぐ[01:05:17]\n@[01:05:61]新[01:06:15]た[01:06:64]な[01:06:84]世[01:07:48]界[01:07:67]へ[01:07:78]\n@[01:08:49]I'll [01:08:91]believe [01:09:28]of [01:09:91]my [01:10:28]sensation[01:10:35]\n@[01:11:40]果[01:11:54]て[01:11:71]し[01:11:89]な[01:12:27]い[01:12:48]道[01:12:88]の[01:13:31]向[01:13:65]こ[01:13:80]う[01:13:99]で[01:14:06]\n@[01:14:56]瞼[01:15:32]の[01:15:54]裏[01:16:29]に[01:16:66]映[01:17:20]る[01:17:26]\n@[01:17:78]一[01:18:17]滴[01:19:38]の[01:19:78]光[01:20:34]\n@[01:20:84]ト[01:21:05]キ[01:21:43]メ[01:21:78]キ[01:22:16]を[01:22:56]感[01:23:35]じ[01:23:69]て[01:23:75]\n\n@[01:40:75]貴[01:41:34]方[01:41:53]が[01:42:05]触[01:42:20]れ[01:42:36]る[01:42:51]全[01:42:90]て[01:43:00]\n@[01:43:51]幸[01:44:03]せ[01:44:38]で[01:44:51]あ[01:44:79]る[01:45:18]よ[01:45:52]う[01:45:68]に[01:45:75]\n@[01:46:67]今[01:47:37]生[01:47:54]き[01:47:83]る[01:48:25]喜[01:48:79]び[01:48:96]を[01:49:05]\n@[01:49:61]忘[01:49:93]れ[01:50:21]て[01:50:37]し[01:50:52]ま[01:50:86]わ[01:51:24]な[01:51:57]い[01:51:92]よ[01:52:12]う[01:52:22]\n\n@[01:52:84]月[01:53:32]の[01:53:68]光[01:54:34] [01:55:76]隠[01:56:29]す[01:56:69]雲[01:57:47]は[01:57:74]揺[01:57:91]ら[01:58:25]ぎ[01:58:32]\n@[01:58:83]自[01:59:03]由[01:59:61]の[01:59:77]翼[02:00:51]は[02:00:82]今[02:01:25]も[02:01:33]\n@[02:01:86]大[02:02:36]空[02:02:83]を[02:03:10]翔[02:03:57]る[02:03:64]\n\n@[02:06:36]シャ[02:06:51]イ[02:06:67]ニ[02:06:96]ン[02:07:17]グ[02:07:31]ス[02:07:60]ター[02:07:94]綴[02:08:50]れ[02:09:18]ば[02:09:24]\n@[02:09:34]無[02:09:49]限[02:10:00]の[02:10:16]イ[02:10:58]マ[02:10:95]ジ[02:11:30]ネー[02:11:47]ショ[02:11:65]ン[02:11:73]\n@[02:12:27]魔[02:12:43]法[02:13:04]が[02:13:21]使[02:14:06]え[02:14:41]る[02:14:56]よ[02:14:71]う[02:15:00]な[02:15:05]\n@[02:15:52]世[02:15:88]界[02:16:28]が[02:16:66]広[02:17:39]が[02:17:57]る[02:17:65]\n@[02:18:41]I'll [02:18:79]believe [02:19:48]of [02:19:76]my [02:20:63]sensation[02:20:72]\n@[02:21:27]今[02:21:63]も[02:21:80]降[02:22:15]り[02:22:34]募[02:23:09]る[02:23:46]想[02:23:85]い[02:23:99]よ[02:24:04]\n@[02:24:43]切[02:24:84]な[02:25:23]さ[02:25:40]や[02:25:80]ト[02:26:17]キ[02:26:55]メ[02:26:71]キ[02:27:04]が[02:27:14]\n@[02:27:84]心[02:28:56]の[02:28:84]真[02:29:16]ん[02:29:56]中[02:30:13]で[02:30:21]\n@[02:30:69]熱[02:31:23]い[02:31:62]メ[02:32:05]ロ[02:32:40]ディ[02:33:16]に[02:33:56]な[02:33:75]る[02:33:83]\n\n@[03:01:02]シャ[03:01:21]イ[03:01:40]ニ[03:01:74]ン[03:01:92]グ[03:02:25]ス[03:02:60]ター[03:02:75]綴[03:03:18]れ[03:03:32]ば[03:03:41]\n@[03:03:75]無[03:03:93]限[03:04:28]の[03:04:68]イ[03:04:88]マ[03:05:26]ジ[03:05:62]ネー[03:06:18]ショ[03:06:36]ン[03:06:44]\n@[03:06:94]魔[03:07:11]法[03:07:70]が[03:07:89]使[03:08:68]え[03:09:04]る[03:09:22]よ[03:09:37]う[03:09:67]な[03:09:73]\n@[03:10:19]世[03:10:54]界[03:10:92]が[03:11:29]広[03:12:06]が[03:12:24]る[03:12:33]\n@[03:13:05]I'll [03:13:44]believe [03:13:86]of [03:14:19]my [03:14:71]sensation[03:14:78]\n@[03:15:87]今[03:16:26]も[03:16:47]降[03:16:83]り[03:17:04]募[03:17:79]る[03:18:15]想[03:18:50]い[03:18:68]よ[03:18:74]\n@[03:19:16]切[03:19:52]な[03:19:93]さ[03:20:12]や[03:20:52]ト[03:20:89]キ[03:21:25]メ[03:21:43]キ[03:21:81]が[03:22:35]輝[03:23:13]き[03:23:52]出[03:23:83]す[03:23:89]\n\n@[03:24:93]シャ[03:25:09]イ[03:25:27]ニ[03:25:98]ン[03:26:17]グ[03:26:53]ス[03:26:69]ター[03:26:91]綴[03:27:47]れ[03:27:79]ば[03:27:87]\n@[03:28:20]夢[03:28:61]に[03:28:97]眠[03:29:54]る[03:29:93]幻[03:31:00]が[03:31:35]掌[03:32:21]に[03:32:63]降[03:33:01]り[03:33:38]注[03:33:90]ぐ[03:33:99]\n@[03:34:49]新[03:34:99]た[03:35:28]な[03:35:64]世[03:36:34]界[03:36:55]へ[03:36:64]\n@[03:37:36]I'll [03:38:02]believe [03:38:70]of [03:39:04]my [03:39:57]sensation[03:39:66]\n@[03:40:23]果[03:40:41]て[03:40:58]し[03:40:78]な[03:41:14]い[03:41:35]道[03:42:13]の[03:42:47]向[03:42:65]こ[03:42:85]う[03:43:05]で[03:43:12]\n@[03:43:45]瞼[03:44:19]の[03:44:40]裏[03:45:12]に[03:45:49]映[03:46:06]る[03:46:12]\n@[03:46:70]一[03:47:09]滴[03:48:19]の[03:48:55]光[03:49:18]\n@[03:49:69]ト[03:49:88]キ[03:50:27]メ[03:50:63]キ[03:51:02]を[03:51:39]感[03:52:18]じ[03:52:78]て[03:52:86]\n\n@[03:56:25]La [03:56:42]La [03:56:57]La [03:56:76]La [03:56:92]La [03:57:10]La [03:57:31]La~~[03:57:35]\n@[03:59:18]La [03:59:37]La [03:59:55]La [03:59:72]La [03:59:90]La [04:00:09]La [04:00:30]La~~ [04:00:69]La [04:00:90]La [04:01:11]La~[04:01:16]\n@[04:02:21]La [04:02:38]La [04:02:58]La [04:02:78]La [04:02:99]La [04:03:17]La [04:03:37]La~~[04:03:44]\n@[04:05:28]La [04:05:46]La [04:05:65]La [04:05:85]La [04:06:01]La [04:06:20]La [04:06:40]La~~ [04:07:51]La [04:08:13]La~~~~~~~~~~~[04:11:52]\n\n@Ruby1=た,た\n@Ruby2=だ,だ\n@Ruby3=風,か[00:00:21]ぜ,,[00:50:67]\n@Ruby4=に,に\n@Ruby5=揺,ゆ\n@Ruby6=ら,ら\n@Ruby7=れ,れ\n@Ruby8=て,て\n@Ruby9=何,な[00:00:17]に\n@Ruby10=も,も\n@Ruby11=考,か[00:00:18]ん[00:00:52]が\n@Ruby12=え,え\n@Ruby13=ず,ず\n@Ruby14=雲,く[00:00:35]も,,[01:56:69]\n@Ruby15=を,を\n@Ruby16=眺,な[00:00:20]が\n@Ruby17=め,め\n@Ruby18=過,す\n@Ruby19=ご,ご\n@Ruby20=す,す\n@Ruby21=の,の\n@Ruby22=い,い\n@Ruby23=よ,よ\n@Ruby24=ね,ね\n@Ruby25=誰,だ[00:00:47]れ\n@Ruby26=し,し\n@Ruby27=か,か\n@Ruby28=使,し,,[02:13:21]\n@Ruby29=命,め[00:00:39]い\n@Ruby30=抱,か[00:00:20]か\n@Ruby31=る,る\n@Ruby32=そ,そ\n@Ruby33=け,け\n@Ruby34=こ,こ\n@Ruby35=と,と\n@Ruby36=悩,な[00:00:16]や\n@Ruby37=む,む\n@Ruby38=は,は\n@Ruby39=う,う\n@Ruby40=や,や\n@Ruby41=さ,さ\n@Ruby42=ざ,ざ\n@Ruby43=な,な\n@Ruby44=み,み\n@Ruby45=音,お[00:00:18]と,,[00:51:39]\n@Ruby46=癒,い\n@Ruby47=く,く\n@Ruby48=軌,き\n@Ruby49=跡,せ[00:00:31]き\n@Ruby50=運,は[00:00:26]こ\n@Ruby51=ぶ,ぶ\n@Ruby52=風,か[00:00:28]ぜ,[00:50:67]\n@Ruby53=音,お[00:00:19]と,[00:51:39]\n@Ruby54=時,と[00:00:19]き\n@Ruby55=閉,と\n@Ruby56=じ,じ\n@Ruby57=込,こ\n@Ruby58=シャ,しゃ\n@Ruby59=イ,い\n@Ruby60=ニ,に\n@Ruby61=ン,ん\n@Ruby62=グ,ぐ\n@Ruby63=ス,す\n@Ruby64=タ,た\n@Ruby65=綴,つ[00:00:21]づ,,[02:07:94]\n@Ruby66=ば,ば\n@Ruby67=夢,ゆ[00:00:19]め,,[03:28:20]\n@Ruby68=眠,ね[00:00:37]む,,[03:28:97]\n@Ruby69=幻,ま[00:00:35]ぼ[00:00:73]ろ[00:00:89]し,,[03:29:93]\n@Ruby70=が,が\n@Ruby71=掌,て[00:00:17]の[00:00:39]ひ[00:00:74]ら,,[03:31:35]\n@Ruby72=降,ふ\n@Ruby73=り,り\n@Ruby74=注,そ[00:00:14]そ,,[03:33:38]\n@Ruby75=ぐ,ぐ\n@Ruby76=新,あ[00:00:21]ら\n@Ruby77=世,せ[00:00:28]か,,[02:15:52]\n@Ruby78=界,い\n@Ruby79=へ,へ\n@Ruby80=果,は\n@Ruby81=道,み[00:00:13]ち,,[03:41:35]\n@Ruby82=向,む\n@Ruby83=で,で\n@Ruby84=瞼,ま[00:00:20]ぶ[00:00:44]た,,[03:43:45]\n@Ruby85=裏,う[00:00:39]ら,,[03:44:40]\n@Ruby86=映,う[00:00:15]つ,,[03:45:49]\n@Ruby87=一,ひ[00:00:19]と,,[03:46:70]\n@Ruby88=滴,し[00:00:21]ず[00:00:73]く,,[03:47:09]\n@Ruby89=光,ひ[00:00:15]か[00:00:49]り,,[01:53:68]\n@Ruby90=ト,と\n@Ruby91=キ,き\n@Ruby92=メ,め\n@Ruby93=感,か[00:00:48]ん,,[03:51:39]\n@Ruby94=貴,あ[00:00:12]な\n@Ruby95=方,た\n@Ruby96=触,ふ\n@Ruby97=全,す[00:00:17]べ\n@Ruby98=幸,し[00:00:15]あ[00:00:31]わ\n@Ruby99=せ,せ\n@Ruby100=あ,あ\n@Ruby101=今,い[00:00:19]ま,,[02:00:82]\n@Ruby102=生,い\n@Ruby103=き,き\n@Ruby104=喜,よ[00:00:17]ろ[00:00:34]こ\n@Ruby105=び,び\n@Ruby106=忘,わ[00:00:16]す\n@Ruby107=ま,ま\n@Ruby108=わ,わ\n@Ruby109=月,つ[00:00:14]き\n@Ruby110=光,ひ[00:00:38]か[00:00:59]り,[01:53:68],[03:48:55]\n@Ruby111=隠,か[00:00:19]く\n@Ruby112=雲,く[00:00:39]も,[01:56:69]\n@Ruby113=ぎ,ぎ\n@Ruby114=自,じ\n@Ruby115=由,ゆ[00:00:27]う\n@Ruby116=翼,つ[00:00:29]ば[00:00:54]さ\n@Ruby117=今,い[00:00:27]ま,[02:00:82],[02:21:27]\n@Ruby118=大,お[00:00:18]お\n@Ruby119=空,ぞ[00:00:24]ら\n@Ruby120=翔,か[00:00:27]け\n@Ruby121=綴,つ[00:00:37]づ,[02:07:94],[03:02:75]\n@Ruby122=無,む\n@Ruby123=限,げ[00:00:17]ん,,[03:03:93]\n@Ruby124=マ,ま\n@Ruby125=ジ,じ\n@Ruby126=ネ,ね\n@Ruby127=ショ,しょ\n@Ruby128=魔,ま\n@Ruby129=法,ほ[00:00:23]う,,[03:07:11]\n@Ruby130=使,つ[00:00:38]か,[02:13:21],[03:07:89]\n@Ruby131=世,せ[00:00:18]か,[02:15:52],[03:35:64]\n@Ruby132=広,ひ[00:00:35]ろ,,[03:11:29]\n@Ruby133=今,い[00:00:17]ま,[02:21:27],[03:15:87]\n@Ruby134=募,つ[00:00:37]の\n@Ruby135=想,お[00:00:15]も,,[03:18:15]\n@Ruby136=切,せ[00:00:19]つ,,[03:19:16]\n@Ruby137=心,こ[00:00:17]こ[00:00:36]ろ\n@Ruby138=真,ま\n@Ruby139=ん,ん\n@Ruby140=中,な[00:00:20]か\n@Ruby141=熱,あ[00:00:22]つ\n@Ruby142=ロ,ろ\n@Ruby143=ディ,でぃ\n@Ruby144=綴,つ[00:00:27]づ,[03:02:75],[03:26:91]\n@Ruby145=限,げ[00:00:16]ん,[03:03:93]\n@Ruby146=法,ほ[00:00:22]う,[03:07:11]\n@Ruby147=使,つ[00:00:41]か,[03:07:89]\n@Ruby148=広,ひ[00:00:37]ろ,[03:11:29]\n@Ruby149=今,い[00:00:20]ま,[03:15:87]\n@Ruby150=想,お[00:00:17]も,[03:18:15]\n@Ruby151=切,せ[00:00:16]つ,[03:19:16]\n@Ruby152=輝,か[00:00:21]が[00:00:40]や\n@Ruby153=出,だ\n@Ruby154=綴,つ[00:00:35]づ,[03:26:91]\n@Ruby155=夢,ゆ[00:00:15]め,[03:28:20]\n@Ruby156=眠,ね[00:00:21]む,[03:28:97]\n@Ruby157=幻,ま[00:00:39]ぼ[00:00:56]ろ[00:00:75]し,[03:29:93]\n@Ruby158=掌,て[00:00:17]の[00:00:36]ひ[00:00:66]ら,[03:31:35]\n@Ruby159=注,そ[00:00:16]そ,[03:33:38]\n@Ruby160=世,せ[00:00:35]か,[03:35:64]\n@Ruby161=道,み[00:00:40]ち,[03:41:35]\n@Ruby162=瞼,ま[00:00:18]ぶ[00:00:38]た,[03:43:45]\n@Ruby163=裏,う[00:00:35]ら,[03:44:40]\n@Ruby164=映,う[00:00:18]つ,[03:45:49]\n@Ruby165=一,ひ[00:00:17]と,[03:46:70]\n@Ruby166=滴,し[00:00:33]ず[00:00:71]く,[03:47:09]\n@Ruby167=光,ひ[00:00:16]か[00:00:55]り,[03:48:55]\n@Ruby168=感,か[00:00:40]ん,[03:51:39]\n\nend\n\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Resources/Testing/Beatmaps/karaoke-note-samples.osu",
    "content": "﻿karaoke file format v1\n\n[HitObjects]\n@[00:01.00]か[00:02.00]ら[00:03.00]お[00:04.00]け[00:05.00]\n@note1=1,2#,3,(3#|4)\n\nend"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Resources/Testing/Beatmaps/karaoke-translation-samples.osu",
    "content": "﻿karaoke file format v1\n\n[HitObjects]\n@[00:01.00]か[00:02.00]ら[00:03.00]お[00:04.00]け[00:05.00]\n@[00:06.00]大[00:07.00]好[00:08.00]き[00:09.00]\n\n@tr[zh-TW]=卡拉OK\n@tr[zh-TW]=喜歡\n\n@tr[en-US]=karaoke\n@tr[en-US]=like it\n\nend"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Resources/Testing/Fonts/Fnt/OpenSans/LICENSE.txt",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Resources/Testing/Kar/default.kar",
    "content": "[00:52:28]重[00:52:96]な[00:53:17]り[00:53:86]合[00:54:32]う[00:56:52]想[00:57:03]い[00:57:31]な[00:57:78]ん[00:57:98]て[00:58:48]幻[00:59:65]だっ[01:00:54]た[01:01:95]\n[01:02:14]何[01:02:61]度[01:02:84]も[01:03:36]目[01:03:96]が[01:04:45]覚[01:04:89]め[01:05:12]て[01:06:76]\n[01:07:04]枯[01:07:24]れ[01:07:63]葉[01:07:93]は[01:08:63]ま[01:09:08]た[01:11:27]切[01:11:76]な[01:12:03]さ[01:12:52]と[01:13:16]後[01:13:72]悔[01:14:13]だ[01:15:11]け[01:15:39]を[01:16:69]\n[01:16:86]強[01:17:37]め[01:17:65]て[01:18:07]セ[01:18:33]ピ[01:18:83]ア[01:19:24]に[01:19:63]す[01:20:02]る[01:21:61]\n\n[01:22:07]ね[01:22:25]え[01:22:73]二[01:23:26]人[01:23:69]運[01:24:26]命[01:24:71]の[01:25:46]歯[01:25:66]車[01:26:31]同[01:27:21]じ[01:27:44]じゃ[01:27:80]な[01:28:15]い[01:28:41]の？[01:28:92]\n[01:29:15]叫[01:29:67]ん[01:29:96]で[01:30:15]も[01:31:53]も[01:31:81]う[01:32:08]届[01:32:59]か[01:32:86]な[01:33:10]い[01:35:71]\n\n\n[01:36:20]泡[01:36:70]沫[01:37:58]の[01:38:06]レ[01:38:30]ク[01:38:56]エ[01:38:98]ル[01:39:47]ド[01:39:78]\n[01:39:93]時[01:40:39]の[01:40:84]砂[01:41:53]は[01:41:74]さ[01:42:01]び[01:42:46]付[01:42:71]い[01:43:17]て[01:43:40]\n[01:43:60]頬[01:44:11]伝[01:45:00]う[01:45:47]涙[01:46:44]だ[01:46:94]け[01:47:15]\n[01:47:35]た[01:47:59]だ[01:47:83]静[01:48:67]か[01:49:20]に[01:49:41]感[01:50:07]じ[01:50:29]て[01:50:53]る[01:51:94]\n\n[02:06:27]僕[02:06:77]た[02:07:65]ち[02:08:11]は[02:10:37]い[02:10:61]つ[02:10:88]の[02:11:16]日[02:11:63]に[02:12:24]間[02:12:74]違[02:13:02]え[02:13:50]た[02:14:20]だ[02:14:42]ろ[02:14:75]う[02:15:72]\n[02:15:93]叶[02:16:40]わ[02:16:71]な[02:17:10]い[02:17:37]約[02:17:95]束[02:18:70]だ[02:19:02]け[02:20:57]\n\n[02:21:03]何[02:21:77]度[02:22:16]歌[02:22:70]い[02:22:91]叫[02:23:55]ん[02:23:80]で[02:24:19]も[02:24:35]\n[02:24:53]あ[02:24:74]な[02:25:11]た[02:25:44]に[02:25:67]届[02:26:33]か[02:26:58]な[02:26:89]い[02:27:24]ま[02:27:47]ま[02:27:96]\n[02:28:28]無[02:28:51]情[02:29:18]な[02:30:62]時[02:31:02]を[02:31:21]眺[02:31:84]め[02:32:08]て[02:34:05]\n\n[02:35:29]泡[02:35:75]沫[02:36:68]の[02:37:13]レ[02:37:39]ク[02:37:82]エ[02:38:23]ル[02:38:65]ド[02:38:86]\n[02:39:17]時[02:39:59]の[02:40:05]砂[02:40:64]は[02:40:88]さ[02:41:13]び[02:41:57]付[02:41:81]い[02:42:23]て[02:42:62]\n[02:42:66]頬[02:43:17]伝[02:44:09]う[02:44:54]涙[02:45:44]だ[02:45:92]け[02:46:23]\n[02:46:40]た[02:46:61]だ[02:46:92]静[02:47:80]か[02:48:36]に[02:48:54]感[02:49:12]じ[02:49:34]て[02:49:62]る[02:50:69]\n\n[03:04:90]こ[03:05:12]の[03:05:37]想[03:06:27]い[03:06:68]風[03:07:14]に[03:07:61]乗[03:08:07]せ[03:08:27]\n[03:08:50]消[03:08:65]え[03:08:84]て[03:09:05]な[03:09:47]く[03:09:93]な[03:10:14]れ[03:10:42]ば[03:10:66]い[03:11:08]い[03:11:33]の[03:11:77]に[03:12:11]\n[03:12:29]今[03:12:73]も[03:13:20]ま[03:13:63]だ[03:13:86] [03:14:10]今[03:14:54]も[03:15:02]ま[03:15:44]だ[03:15:75]\n[03:15:93]あ[03:16:16]な[03:16:38]た[03:16:91]が[03:17:33]胸[03:18:00]を[03:18:26]苦[03:18:93]し[03:18:99]め[03:19:73]\n\n[03:19:82]気[03:20:03]が[03:20:17]つ[03:20:58]け[03:21:05]ば[03:21:48]あ[03:21:68]な[03:22:00]た[03:22:36]だ[03:22:83]け[03:22:93]\n[03:23:31]気[03:23:54]が[03:23:79]つ[03:24:26]く[03:24:71]の[03:24:93]が[03:25:18]遅[03:25:85]す[03:26:08]ぎ[03:26:53]た[03:26:79]\n[03:26:97]戻[03:27:47]ら[03:27:96]な[03:28:39]い[03:28:59]季[03:28:97]節[03:29:38]を[03:29:81]た[03:30:24]だ[03:30:46]\n[03:30:47]眺[03:31:18]め[03:31:64]る[03:32:08]こ[03:32:35]と[03:32:81]し[03:33:18]か[03:33:42]で[03:33:60]き[03:33:80]な[03:34:03]い[03:34:48]\n\n[03:34:74]泡[03:35:29]沫[03:35:78]の[03:36:23]レ[03:36:44]ク[03:36:74]エ[03:37:19]ル[03:37:63]ド[03:38:77]\n[03:45:74]時[03:46:06]間[03:46:44]が[03:46:89]戻[03:47:54]る[03:47:80]の[03:48:22]な[03:48:53]ら[03:48:75]ば[03:50:53]"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Resources/Testing/Kar/light.kar",
    "content": "﻿枯れた世界に輝く　名もない永遠の華\n強く生きていけるよう　私を変えていく\n\nあぁ、わかっていたの　本当は\n光なんて求めちゃいけないってこと\n\nあぁ、それでもきっと人は\n輝く未来なんてものを求めてさまよう\n\n女神なんて　信じてるわけじゃない\n未来をこの　手で切り開く勇気が　強さがほしい\nあなたを守りたい\n\n枯れた世界に輝く　名もない永遠の華\n強く生きていけるよう　私を変えていく\n\nあぁ、例えこの世が暗く\n絶望に飲み込まれても　私は大丈夫\n\nあなたがいればそこは\n灰色(いろ)(いろ)の景色(いろ)(いろ)さえ色づき始める\n\n華もきっと　咲き誇るときを終え\nいつかきっと　また蕾をつけるだろう　\n終わることない\n生命（いのち）が続くまでは\n\nあなたの手を離さない　瞳に誓う　永遠の恋\n強く儚い思いを　守り続けていたい\n\n枯れた世界に輝く　名もない永遠の華\n強く生き抜きけるように　私を変えていく\nあなたの手を離さない　瞳に誓う　永遠の恋\n強く儚い思いを　守り続けていたい"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Resources/Testing/Track/demo.json",
    "content": "﻿[\n  {\n    \"Time\": 222.85714285714283,\n    \"Pitch\": 130.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 297.1428571428571,\n    \"Pitch\": 130.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 371.4285714285714,\n    \"Pitch\": 130.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 445.71428571428567,\n    \"Pitch\": 132.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 520.0,\n    \"Pitch\": 131.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 594.2857142857142,\n    \"Pitch\": 128.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 668.5714285714286,\n    \"Pitch\": 134.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 742.8571428571428,\n    \"Pitch\": 147.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 817.1428571428571,\n    \"Pitch\": 147.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 891.4285714285713,\n    \"Pitch\": 147.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 965.7142857142857,\n    \"Pitch\": 147.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 1040.0,\n    \"Pitch\": 146.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 1114.2857142857142,\n    \"Pitch\": 144.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 1188.5714285714284,\n    \"Pitch\": 154.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 1262.8571428571427,\n    \"Pitch\": 166.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 1337.142857142857,\n    \"Pitch\": 165.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 1411.4285714285713,\n    \"Pitch\": 166.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 1485.7142857142856,\n    \"Pitch\": 166.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 1560.0,\n    \"Pitch\": 163.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 1634.2857142857142,\n    \"Pitch\": 163.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 1708.5714285714287,\n    \"Pitch\": 172.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 1782.8571428571427,\n    \"Pitch\": 175.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 1857.1428571428569,\n    \"Pitch\": 175.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 1931.4285714285713,\n    \"Pitch\": 176.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 2005.7142857142856,\n    \"Pitch\": 176.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 2080.0,\n    \"Pitch\": 172.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 2154.285714285714,\n    \"Pitch\": 176.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 2228.5714285714284,\n    \"Pitch\": 195.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 2302.8571428571427,\n    \"Pitch\": 196.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 2377.142857142857,\n    \"Pitch\": 196.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 2451.4285714285716,\n    \"Pitch\": 197.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 2525.7142857142853,\n    \"Pitch\": 196.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 2600.0,\n    \"Pitch\": 193.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 2674.285714285714,\n    \"Pitch\": 202.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 2748.5714285714284,\n    \"Pitch\": 221.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 2822.8571428571427,\n    \"Pitch\": 220.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 2897.1428571428573,\n    \"Pitch\": 221.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 2971.428571428571,\n    \"Pitch\": 221.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 3045.7142857142853,\n    \"Pitch\": 219.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 3120.0,\n    \"Pitch\": 217.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 3194.2857142857138,\n    \"Pitch\": 234.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 3268.5714285714284,\n    \"Pitch\": 249.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 3342.8571428571427,\n    \"Pitch\": 247.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 3417.1428571428573,\n    \"Pitch\": 249.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 3491.428571428571,\n    \"Pitch\": 249.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 3565.7142857142853,\n    \"Pitch\": 243.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 3640.0,\n    \"Pitch\": 246.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 3714.2857142857138,\n    \"Pitch\": 260.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 3788.5714285714284,\n    \"Pitch\": 264.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 3862.8571428571427,\n    \"Pitch\": 262.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 3937.1428571428573,\n    \"Pitch\": 262.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 4011.428571428571,\n    \"Pitch\": 264.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 4085.7142857142853,\n    \"Pitch\": 256.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 4160.0,\n    \"Pitch\": 267.0,\n    \"Sound\": true\n  },\n  {\n    \"Time\": 4234.285714285714,\n    \"Pitch\": 0.0,\n    \"Sound\": false\n  }\n]"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Resources/special-skin/default.json",
    "content": "{\n  \"lyric_font_info\": {\n    \"$type\": 0,\n    \"name\": \"Default lyric config\",\n    \"smart_horizon\": 2,\n    \"lyrics_interval\": 4,\n    \"ruby_interval\": 2,\n    \"ruby_margin\": 4,\n    \"main_text_font\": {\n      \"family\": \"Torus\",\n      \"weight\": \"Bold\",\n      \"size\": 48.0\n    },\n    \"ruby_text_font\": {\n      \"family\": \"Torus\",\n      \"weight\": \"Bold\"\n    },\n    \"romanisation_text_font\": {\n      \"family\": \"Torus\",\n      \"weight\": \"Bold\"\n    }\n  },\n  \"lyric_style\": {\n    \"$type\": 2,\n    \"name\": \"Default lyric style\",\n    \"left_lyric_text_shaders\": [\n      {\n        \"$type\": \"StepShader\",\n        \"name\": \"HelloShader\",\n        \"draw\": true,\n        \"step_shaders\": [\n          {\n            \"$type\": \"OutlineShader\",\n            \"radius\": 3,\n            \"outline_colour\": \"#CCA532\"\n          },\n          {\n            \"$type\": \"ShadowShader\",\n            \"shadow_colour\": \"#6B5B2D\",\n            \"shadow_offset\": {\n              \"x\": 3.0,\n              \"y\": 3.0\n            }\n          }\n        ]\n      }\n    ],\n    \"right_lyric_text_shaders\": [\n      {\n        \"$type\": \"StepShader\",\n        \"name\": \"HelloShader\",\n        \"draw\": true,\n        \"step_shaders\": [\n          {\n            \"$type\": \"OutlineShader\",\n            \"radius\": 3,\n            \"outline_colour\": \"#5932CC\"\n          },\n          {\n            \"$type\": \"ShadowShader\",\n            \"shadow_colour\": \"#3D2D6B\",\n            \"shadow_offset\": {\n              \"x\": 3.0,\n              \"y\": 3.0\n            }\n          }\n        ]\n      }\n    ]\n  },\n  \"note_style\": {\n    \"$type\": 3,\n    \"name\": \"Default note style\",\n    \"note_color\": \"#44AADD\",\n    \"blink_color\": \"#FF66AA\",\n    \"text_color\": \"#FFFFFF\",\n    \"bold_text\": true\n  }\n}"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Resources/special-skin/lyric-font-infos.json",
    "content": "[]"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Resources/special-skin/note-styles.json",
    "content": "[\n  {\n    \"$type\": 3,\n    \"id\": 1,\n    \"name\": \"Customized note style\",\n    \"note_color\": \"#C944DD\",\n    \"blink_color\": \"#FF66AA\",\n    \"text_color\": \"#FFFFFF\",\n    \"bold_text\": true\n  }\n]"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Screens/Edit/Beatmap/BeatmapEditorScreenTestScene.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Screens.Edit.Beatmap;\r\n\r\npublic abstract partial class BeatmapEditorScreenTestScene<T> : GenericEditorScreenTestScene<T, KaraokeBeatmapEditorScreenMode>\r\n    where T : BeatmapEditorScreen;\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Screens/Edit/Beatmap/Components/TestSceneLyricSelector.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Components.UserInterfaceV2;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Beatmaps;\r\nusing osu.Game.Screens.Edit;\r\nusing osu.Game.Tests.Visual;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Screens.Edit.Beatmap.Components;\r\n\r\npublic partial class TestSceneLyricSelector : OsuManualInputManagerTestScene\r\n{\r\n    [Cached(typeof(EditorBeatmap))]\r\n    private readonly EditorBeatmap editorBeatmap;\r\n\r\n    public TestSceneLyricSelector()\r\n    {\r\n        var beatmap = new TestKaraokeBeatmap(new KaraokeRuleset().RulesetInfo);\r\n        var karaokeBeatmap = new KaraokeBeatmapConverter(beatmap, new KaraokeRuleset()).Convert() as KaraokeBeatmap;\r\n        editorBeatmap = new EditorBeatmap(karaokeBeatmap);\r\n    }\r\n\r\n    [Test]\r\n    public void TestAllFiles()\r\n    {\r\n        AddStep(\"show the selector\", () =>\r\n        {\r\n            var language = new Bindable<Lyric?>();\r\n            Child = new LyricSelector\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n                Size = new Vector2(0.5f, 0.8f),\r\n                Anchor = Anchor.Centre,\r\n                Origin = Anchor.Centre,\r\n                Current = language,\r\n            };\r\n        });\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Screens/Edit/Beatmap/Lyrics/CaretPosition/Algorithms/BaseCaretPositionAlgorithmTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Reflection;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition.Algorithms;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Screens.Edit.Beatmap.Lyrics.CaretPosition.Algorithms;\r\n\r\npublic abstract class BaseCaretPositionAlgorithmTest<TAlgorithm, TCaret> where TAlgorithm : ICaretPositionAlgorithm where TCaret : struct, ICaretPosition\r\n{\r\n    protected static void TestPositionMovable(Lyric[] lyrics, TCaret caret, bool expected, Action<TAlgorithm>? invokeAlgorithm = null)\r\n    {\r\n        var algorithm = CreateAlgorithm(lyrics);\r\n\r\n        invokeAlgorithm?.Invoke(algorithm);\r\n\r\n        // because we made the position movable into the protected, so use another way to test it.\r\n        var method = algorithm.GetType().GetMethod(\"PositionMovable\", BindingFlags.Instance | BindingFlags.NonPublic);\r\n        object? result = method?.Invoke(algorithm, new object[] { caret });\r\n        if (result is not bool actual)\r\n            throw new InvalidCastException();\r\n\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    protected static void TestMoveToPreviousLyric(Lyric[] lyrics, TCaret caret, TCaret? expected, Action<TAlgorithm>? invokeAlgorithm = null)\r\n    {\r\n        var algorithm = CreateAlgorithm(lyrics);\r\n\r\n        invokeAlgorithm?.Invoke(algorithm);\r\n\r\n        var actual = algorithm.MoveToPreviousLyric(caret) as TCaret?;\r\n        AssertEqual(expected, actual);\r\n    }\r\n\r\n    protected static void TestMoveToNextLyric(Lyric[] lyrics, TCaret caret, TCaret? expected, Action<TAlgorithm>? invokeAlgorithm = null)\r\n    {\r\n        var algorithm = CreateAlgorithm(lyrics);\r\n\r\n        invokeAlgorithm?.Invoke(algorithm);\r\n\r\n        var actual = algorithm.MoveToNextLyric(caret) as TCaret?;\r\n        AssertEqual(expected, actual);\r\n    }\r\n\r\n    protected static void TestMoveToFirstLyric(Lyric[] lyrics, TCaret? expected, Action<TAlgorithm>? invokeAlgorithm = null)\r\n    {\r\n        var algorithm = CreateAlgorithm(lyrics);\r\n\r\n        invokeAlgorithm?.Invoke(algorithm);\r\n\r\n        var actual = algorithm.MoveToFirstLyric() as TCaret?;\r\n        AssertEqual(expected, actual);\r\n    }\r\n\r\n    protected static void TestMoveToLastLyric(Lyric[] lyrics, TCaret? expected, Action<TAlgorithm>? invokeAlgorithm = null)\r\n    {\r\n        var algorithm = CreateAlgorithm(lyrics);\r\n\r\n        invokeAlgorithm?.Invoke(algorithm);\r\n\r\n        var actual = algorithm.MoveToLastLyric() as TCaret?;\r\n        AssertEqual(expected, actual);\r\n    }\r\n\r\n    protected static void TestMoveToTargetLyric(Lyric[] lyrics, Lyric lyric, TCaret? expected, Action<TAlgorithm>? invokeAlgorithm = null)\r\n    {\r\n        var algorithm = CreateAlgorithm(lyrics);\r\n\r\n        invokeAlgorithm?.Invoke(algorithm);\r\n\r\n        var actual = algorithm.MoveToTargetLyric(lyric) as TCaret?;\r\n        AssertEqual(expected, actual);\r\n    }\r\n\r\n    protected static TAlgorithm CreateAlgorithm(Lyric[] lyrics)\r\n        => ActivatorUtils.CreateInstance<TAlgorithm>(new object[] { lyrics });\r\n\r\n    protected static void AssertEqual(TCaret? expected, TCaret? actual)\r\n    {\r\n        if (expected == null || actual == null)\r\n        {\r\n            Assert.That(expected, Is.Null);\r\n            Assert.That(actual, Is.Null);\r\n        }\r\n        else\r\n        {\r\n            Assert.That(actual.Value, Is.EqualTo(expected.Value));\r\n        }\r\n    }\r\n\r\n    protected Lyric[] GetLyricsByMethodName(string methodName)\r\n    {\r\n        var thisType = GetType();\r\n        var theMethod = getMethod(thisType, methodName);\r\n        if (theMethod == null)\r\n            throw new MissingMethodException(\"Test method is not exist.\");\r\n\r\n        return (Lyric[])theMethod.GetValue(this)!;\r\n    }\r\n\r\n    private static PropertyInfo? getMethod(Type type, string methodName)\r\n    {\r\n        Type? targetType = type;\r\n\r\n        while (targetType != null)\r\n        {\r\n            var theMethod = targetType.GetProperty(methodName, BindingFlags.NonPublic | BindingFlags.Static);\r\n            if (theMethod != null)\r\n                return theMethod;\r\n\r\n            targetType = targetType.BaseType;\r\n        }\r\n\r\n        return null;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Screens/Edit/Beatmap/Lyrics/CaretPosition/Algorithms/BaseCharIndexCaretPositionAlgorithmTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition.Algorithms;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Screens.Edit.Beatmap.Lyrics.CaretPosition.Algorithms;\r\n\r\npublic abstract class BaseCharIndexCaretPositionAlgorithmTest<TAlgorithm, TCaret> : BaseIndexCaretPositionAlgorithmTest<TAlgorithm, TCaret>\r\n    where TAlgorithm : CharIndexCaretPositionAlgorithm<TCaret> where TCaret : struct, ICharIndexCaretPosition\r\n{\r\n    #region Lyric\r\n\r\n    [TestCase(nameof(singleLyric), 0, 0, true)]\r\n    [TestCase(nameof(singleLyric), 0, 3, true)]\r\n    [TestCase(nameof(singleLyric), 0, 4, false)]\r\n    [TestCase(nameof(singleLyric), 0, -1, false)]\r\n    [TestCase(nameof(singleLyricWithOneText), 0, 0, true)]\r\n    [TestCase(nameof(singleLyricWithNoText), 0, 0, false)] // Should have at least one char in the lyric.\r\n    [TestCase(nameof(singleLyricWithNoText), 0, 1, false)]\r\n    public void TestPositionMovable(string sourceName, int lyricIndex, int index, bool movable)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var caret = createCaretPosition(lyrics, lyricIndex, index);\r\n\r\n        // Check is movable\r\n        TestPositionMovable(lyrics, caret, movable);\r\n    }\r\n\r\n    [TestCase(nameof(singleLyric), 0, 0, null, null)] // cannot move up if at top index.\r\n    [TestCase(nameof(singleLyricWithOneText), 0, 0, null, null)]\r\n    [TestCase(nameof(twoLyricsWithText), 1, 0, 0, 0)]\r\n    [TestCase(nameof(threeLyricsWithSpacing), 2, 0, 0, 0)]\r\n    [TestCase(nameof(threeLyricsWithSpacing), 2, 2, 0, 2)]\r\n    public void TestMoveToPreviousLyric(string sourceName, int lyricIndex, int index, int? expectedLyricIndex, int? expectedIndex)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var caret = createCaretPosition(lyrics, lyricIndex, index);\r\n        var expected = createExpectedCaretPosition(lyrics, expectedLyricIndex, expectedIndex);\r\n\r\n        // Check is movable\r\n        TestMoveToPreviousLyric(lyrics, caret, expected);\r\n    }\r\n\r\n    [TestCase(nameof(singleLyric), 0, 0, null, null)] // cannot move down if at bottom index.\r\n    [TestCase(nameof(singleLyricWithOneText), 0, 0, null, null)]\r\n    [TestCase(nameof(twoLyricsWithText), 0, 0, 1, 0)]\r\n    [TestCase(nameof(threeLyricsWithSpacing), 0, 0, 2, 0)]\r\n    [TestCase(nameof(threeLyricsWithSpacing), 0, 3, 2, 2)]\r\n    public void TestMoveToNextLyric(string sourceName, int lyricIndex, int index, int? expectedLyricIndex, int? expectedIndex)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var caret = createCaretPosition(lyrics, lyricIndex, index);\r\n        var expected = createExpectedCaretPosition(lyrics, expectedLyricIndex, expectedIndex);\r\n\r\n        // Check is movable\r\n        TestMoveToNextLyric(lyrics, caret, expected);\r\n    }\r\n\r\n    [TestCase(nameof(singleLyric), 0, 0)]\r\n    [TestCase(nameof(singleLyricWithOneText), 0, 0)]\r\n    [TestCase(nameof(twoLyricsWithText), 0, 0)]\r\n    [TestCase(nameof(threeLyricsWithSpacing), 0, 0)]\r\n    public void TestMoveToFirstLyric(string sourceName, int? expectedLyricIndex, int? expectedIndex)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var expected = createExpectedCaretPosition(lyrics, expectedLyricIndex, expectedIndex);\r\n\r\n        // Check first position\r\n        TestMoveToFirstLyric(lyrics, expected);\r\n    }\r\n\r\n    [TestCase(nameof(singleLyric), 0, 3)]\r\n    [TestCase(nameof(singleLyricWithOneText), 0, 0)]\r\n    [TestCase(nameof(twoLyricsWithText), 1, 2)]\r\n    [TestCase(nameof(threeLyricsWithSpacing), 2, 2)]\r\n    public void TestMoveToLastLyric(string sourceName, int? expectedLyricIndex, int? expectedIndex)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var expected = createExpectedCaretPosition(lyrics, expectedLyricIndex, expectedIndex);\r\n\r\n        // Check last position\r\n        TestMoveToLastLyric(lyrics, expected);\r\n    }\r\n\r\n    [TestCase(nameof(singleLyric), 0, 0)]\r\n    [TestCase(nameof(singleLyricWithOneText), 0, 0)]\r\n    public void TestMoveToTargetLyric(string sourceName, int lyricIndex, int? expectedIndex)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var lyric = lyrics[lyricIndex];\r\n        var expected = createExpectedCaretPosition(lyrics, lyricIndex, expectedIndex);\r\n\r\n        // Check move to target position.\r\n        TestMoveToTargetLyric(lyrics, lyric, expected);\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Lyric index\r\n\r\n    [TestCase(nameof(singleLyric), 0, 0, null, null)]\r\n    [TestCase(nameof(singleLyric), 0, 1, 0, 0)]\r\n    [TestCase(nameof(singleLyricWithOneText), 0, 0, null, null)]\r\n    public void TestMoveToPreviousIndex(string sourceName, int lyricIndex, int index, int? expectedLyricIndex, int? expectedIndex)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var caret = createCaretPosition(lyrics, lyricIndex, index);\r\n        var expected = createExpectedCaretPosition(lyrics, expectedLyricIndex, expectedIndex);\r\n\r\n        // Check is movable\r\n        TestMoveToPreviousIndex(lyrics, caret, expected);\r\n    }\r\n\r\n    [TestCase(nameof(singleLyric), 0, 3, null, null)]\r\n    [TestCase(nameof(singleLyric), 0, 2, 0, 3)]\r\n    [TestCase(nameof(singleLyricWithOneText), 0, 0, null, null)]\r\n    public void TestMoveToNextIndex(string sourceName, int lyricIndex, int index, int? expectedLyricIndex, int? expectedIndex)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var caret = createCaretPosition(lyrics, lyricIndex, index);\r\n        var expected = createExpectedCaretPosition(lyrics, expectedLyricIndex, expectedIndex);\r\n\r\n        // Check is movable\r\n        TestMoveToNextIndex(lyrics, caret, expected);\r\n    }\r\n\r\n    [TestCase(nameof(singleLyric), 0, 0, 0)]\r\n    [TestCase(nameof(singleLyricWithOneText), 0, 0, 0)]\r\n    public void TestMoveToFirstIndex(string sourceName, int lyricIndex, int? expectedLyricIndex, int? expectedIndex)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var lyric = lyrics[lyricIndex];\r\n        var expected = createExpectedCaretPosition(lyrics, expectedLyricIndex, expectedIndex);\r\n\r\n        // Check is movable\r\n        TestMoveToFirstIndex(lyrics, lyric, expected);\r\n    }\r\n\r\n    [TestCase(nameof(singleLyric), 0, 0, 3)]\r\n    [TestCase(nameof(singleLyricWithOneText), 0, 0, 0)]\r\n    public void TestMoveToLastIndex(string sourceName, int lyricIndex, int? expectedLyricIndex, int? expectedIndex)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var lyric = lyrics[lyricIndex];\r\n        var expected = createExpectedCaretPosition(lyrics, expectedLyricIndex, expectedIndex);\r\n\r\n        // Check is movable\r\n        TestMoveToLastIndex(lyrics, lyric, expected);\r\n    }\r\n\r\n    [TestCase(nameof(singleLyric), 0, 0, 0)]\r\n    [TestCase(nameof(singleLyric), 0, 3, 3)]\r\n    [TestCase(nameof(singleLyric), 0, -1, null)] // will check the invalid case.\r\n    [TestCase(nameof(singleLyric), 0, 5, null)]\r\n    public void TestMoveToTargetLyric(string sourceName, int lyricIndex, int textIndex, int? expectedTextIndex)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var lyric = lyrics[lyricIndex];\r\n        var expected = createExpectedCaretPosition(lyrics, lyricIndex, expectedTextIndex);\r\n\r\n        // Check move to target position.\r\n        TestMoveToTargetLyric(lyrics, lyric, textIndex, expected);\r\n    }\r\n\r\n    #endregion\r\n\r\n    protected abstract TCaret CreateCaret(Lyric lyric, int index);\r\n\r\n    private TCaret createCaretPosition(IEnumerable<Lyric> lyrics, int lyricIndex, int index)\r\n    {\r\n        var lyric = lyrics.ElementAtOrDefault(lyricIndex);\r\n        if (lyric == null)\r\n            throw new ArgumentNullException();\r\n\r\n        return CreateCaret(lyric, index);\r\n    }\r\n\r\n    private TCaret? createExpectedCaretPosition(IEnumerable<Lyric> lyrics, int? lyricIndex, int? index)\r\n    {\r\n        if (lyricIndex == null || index == null)\r\n            return null;\r\n\r\n        return createCaretPosition(lyrics, lyricIndex.Value, index.Value);\r\n    }\r\n\r\n    #region source\r\n\r\n    private static Lyric[] singleLyric => new[]\r\n    {\r\n        new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n        },\r\n    };\r\n\r\n    private static Lyric[] singleLyricWithOneText => new[]\r\n    {\r\n        new Lyric\r\n        {\r\n            Text = \"A\",\r\n        },\r\n    };\r\n\r\n    private static Lyric[] singleLyricWithNoText => new[]\r\n    {\r\n        new Lyric(),\r\n    };\r\n\r\n    private static Lyric[] twoLyricsWithText => new[]\r\n    {\r\n        new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n        },\r\n        new Lyric\r\n        {\r\n            Text = \"大好き\",\r\n        },\r\n    };\r\n\r\n    private static Lyric[] threeLyricsWithSpacing => new[]\r\n    {\r\n        new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n        },\r\n        new Lyric(),\r\n        new Lyric\r\n        {\r\n            Text = \"大好き\",\r\n        },\r\n    };\r\n\r\n    #endregion\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Screens/Edit/Beatmap/Lyrics/CaretPosition/Algorithms/BaseIndexCaretPositionAlgorithmTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition.Algorithms;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Screens.Edit.Beatmap.Lyrics.CaretPosition.Algorithms;\r\n\r\npublic abstract class BaseIndexCaretPositionAlgorithmTest<TAlgorithm, TCaret> : BaseCaretPositionAlgorithmTest<TAlgorithm, TCaret>\r\n    where TAlgorithm : IIndexCaretPositionAlgorithm where TCaret : struct, IIndexCaretPosition\r\n{\r\n    protected static void TestMoveToPreviousIndex(Lyric[] lyrics, TCaret caret, TCaret? expected, Action<TAlgorithm>? invokeAlgorithm = null)\r\n    {\r\n        var algorithm = CreateAlgorithm(lyrics);\r\n\r\n        invokeAlgorithm?.Invoke(algorithm);\r\n\r\n        var actual = algorithm.MoveToPreviousIndex(caret) as TCaret?;\r\n        AssertEqual(expected, actual);\r\n    }\r\n\r\n    protected static void TestMoveToNextIndex(Lyric[] lyrics, TCaret caret, TCaret? expected, Action<TAlgorithm>? invokeAlgorithm = null)\r\n    {\r\n        var algorithm = CreateAlgorithm(lyrics);\r\n\r\n        invokeAlgorithm?.Invoke(algorithm);\r\n\r\n        var actual = algorithm.MoveToNextIndex(caret) as TCaret?;\r\n        AssertEqual(expected, actual);\r\n    }\r\n\r\n    protected static void TestMoveToFirstIndex(Lyric[] lyrics, Lyric lyric, TCaret? expected, Action<TAlgorithm>? invokeAlgorithm = null)\r\n    {\r\n        var algorithm = CreateAlgorithm(lyrics);\r\n\r\n        invokeAlgorithm?.Invoke(algorithm);\r\n\r\n        var actual = algorithm.MoveToFirstIndex(lyric) as TCaret?;\r\n        AssertEqual(expected, actual);\r\n    }\r\n\r\n    protected static void TestMoveToLastIndex(Lyric[] lyrics, Lyric lyric, TCaret? expected, Action<TAlgorithm>? invokeAlgorithm = null)\r\n    {\r\n        var algorithm = CreateAlgorithm(lyrics);\r\n\r\n        invokeAlgorithm?.Invoke(algorithm);\r\n\r\n        var actual = algorithm.MoveToLastIndex(lyric) as TCaret?;\r\n        AssertEqual(expected, actual);\r\n    }\r\n\r\n    protected static void TestMoveToTargetLyric<TIndex>(Lyric[] lyrics, Lyric lyric, TIndex index, TCaret? expected, Action<TAlgorithm>? invokeAlgorithm = null)\r\n        where TIndex : notnull\r\n    {\r\n        var algorithm = CreateAlgorithm(lyrics);\r\n\r\n        invokeAlgorithm?.Invoke(algorithm);\r\n\r\n        var actual = algorithm.MoveToTargetLyric(lyric, index) as TCaret?;\r\n        AssertEqual(expected, actual);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Screens/Edit/Beatmap/Lyrics/CaretPosition/Algorithms/ClickingCaretPositionAlgorithmTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition.Algorithms;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Screens.Edit.Beatmap.Lyrics.CaretPosition.Algorithms;\r\n\r\npublic class ClickingCaretPositionAlgorithmTest : BaseCaretPositionAlgorithmTest<ClickingCaretPositionAlgorithm, ClickingCaretPosition>\r\n{\r\n    #region Lyric\r\n\r\n    [TestCase(nameof(singleLyric), 0, true)]\r\n    [TestCase(nameof(singleLyricWithNoText), 0, true)]\r\n    public void TestPositionMovable(string sourceName, int lyricIndex, bool movable)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var caret = createCaretPosition(lyrics, lyricIndex);\r\n\r\n        // Check is movable, will always be true in this algorithm.\r\n        TestPositionMovable(lyrics, caret, movable);\r\n    }\r\n\r\n    [TestCase(nameof(singleLyric), 0, null)] // should always not movable.\r\n    [TestCase(nameof(singleLyricWithNoText), 0, null)]\r\n    [TestCase(nameof(twoLyricsWithText), 1, null)]\r\n    [TestCase(nameof(threeLyricsWithSpacing), 2, null)]\r\n    public void TestMoveToPreviousLyric(string sourceName, int lyricIndex, int? expectedLyricIndex)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var caret = createCaretPosition(lyrics, lyricIndex);\r\n        var expected = createExpectedCaretPosition(lyrics, expectedLyricIndex);\r\n\r\n        // Check is movable\r\n        TestMoveToPreviousLyric(lyrics, caret, expected);\r\n    }\r\n\r\n    [TestCase(nameof(singleLyric), 0, null)] // should always not movable.\r\n    [TestCase(nameof(singleLyricWithNoText), 0, null)]\r\n    [TestCase(nameof(twoLyricsWithText), 0, null)]\r\n    [TestCase(nameof(threeLyricsWithSpacing), 0, null)]\r\n    public void TestMoveToNextLyric(string sourceName, int lyricIndex, int? expectedLyricIndex)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var caret = createCaretPosition(lyrics, lyricIndex);\r\n        var expected = createExpectedCaretPosition(lyrics, expectedLyricIndex);\r\n\r\n        // Check is movable\r\n        TestMoveToNextLyric(lyrics, caret, expected);\r\n    }\r\n\r\n    [TestCase(nameof(singleLyric), null)] // should always not movable.\r\n    [TestCase(nameof(singleLyricWithNoText), null)]\r\n    [TestCase(nameof(twoLyricsWithText), null)]\r\n    [TestCase(nameof(threeLyricsWithSpacing), null)]\r\n    public void TestMoveToFirstLyric(string sourceName, int? expectedLyricIndex)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var expected = createExpectedCaretPosition(lyrics, expectedLyricIndex);\r\n\r\n        // Check first position\r\n        TestMoveToFirstLyric(lyrics, expected);\r\n    }\r\n\r\n    [TestCase(nameof(singleLyric), null)] // should always not movable.\r\n    [TestCase(nameof(singleLyricWithNoText), null)]\r\n    [TestCase(nameof(twoLyricsWithText), null)]\r\n    [TestCase(nameof(threeLyricsWithSpacing), null)]\r\n    public void TestMoveToLastLyric(string sourceName, int? expectedLyricIndex)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var expected = createExpectedCaretPosition(lyrics, expectedLyricIndex);\r\n\r\n        // Check last position\r\n        TestMoveToLastLyric(lyrics, expected);\r\n    }\r\n\r\n    [TestCase(nameof(singleLyric), 0)]\r\n    [TestCase(nameof(singleLyricWithNoText), 0)]\r\n    public void TestMoveToTargetLyric(string sourceName, int expectedLyricIndex)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var lyric = lyrics[expectedLyricIndex];\r\n        var expected = createExpectedCaretPosition(lyrics, expectedLyricIndex);\r\n\r\n        // Check move to target position.\r\n        TestMoveToTargetLyric(lyrics, lyric, expected);\r\n    }\r\n\r\n    #endregion\r\n\r\n    private static ClickingCaretPosition createCaretPosition(IEnumerable<Lyric> lyrics, int lyricIndex)\r\n    {\r\n        var lyric = lyrics.ElementAtOrDefault(lyricIndex);\r\n        if (lyric == null)\r\n            throw new ArgumentNullException();\r\n\r\n        return new ClickingCaretPosition(lyric);\r\n    }\r\n\r\n    private static ClickingCaretPosition? createExpectedCaretPosition(IEnumerable<Lyric> lyrics, int? lyricIndex)\r\n    {\r\n        if (lyricIndex == null)\r\n            return null;\r\n\r\n        return createCaretPosition(lyrics, lyricIndex.Value);\r\n    }\r\n\r\n    #region source\r\n\r\n    private static Lyric[] singleLyric => new[]\r\n    {\r\n        new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n        },\r\n    };\r\n\r\n    private static Lyric[] singleLyricWithNoText => new[]\r\n    {\r\n        new Lyric(),\r\n    };\r\n\r\n    private static Lyric[] twoLyricsWithText => new[]\r\n    {\r\n        new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n        },\r\n        new Lyric\r\n        {\r\n            Text = \"大好き\",\r\n        },\r\n    };\r\n\r\n    private static Lyric[] threeLyricsWithSpacing => new[]\r\n    {\r\n        new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n        },\r\n        new Lyric(),\r\n        new Lyric\r\n        {\r\n            Text = \"大好き\",\r\n        },\r\n    };\r\n\r\n    #endregion\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Screens/Edit/Beatmap/Lyrics/CaretPosition/Algorithms/CreateRemoveTimeTagCaretPositionAlgorithmTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition.Algorithms;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Screens.Edit.Beatmap.Lyrics.CaretPosition.Algorithms;\r\n\r\n[TestFixture]\r\npublic class CreateRemoveTimeTagCaretPositionAlgorithmTest : BaseCharIndexCaretPositionAlgorithmTest<CreateRemoveTimeTagCaretPositionAlgorithm, CreateRemoveTimeTagCaretPosition>\r\n{\r\n    protected override CreateRemoveTimeTagCaretPosition CreateCaret(Lyric lyric, int index)\r\n        => new(lyric, index);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Screens/Edit/Beatmap/Lyrics/CaretPosition/Algorithms/CreateRubyTagCaretPositionAlgorithmTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition.Algorithms;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Screens.Edit.Beatmap.Lyrics.CaretPosition.Algorithms;\r\n\r\n[TestFixture]\r\npublic class CreateRubyTagCaretPositionAlgorithmTest : BaseCharIndexCaretPositionAlgorithmTest<CreateRubyTagCaretPositionAlgorithm, CreateRubyTagCaretPosition>\r\n{\r\n    protected override CreateRubyTagCaretPosition CreateCaret(Lyric lyric, int index)\r\n        => new(lyric, index);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Screens/Edit/Beatmap/Lyrics/CaretPosition/Algorithms/CuttingCaretPositionAlgorithmTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition.Algorithms;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Screens.Edit.Beatmap.Lyrics.CaretPosition.Algorithms;\r\n\r\n[TestFixture]\r\npublic class CuttingCaretPositionAlgorithmTest : BaseIndexCaretPositionAlgorithmTest<CuttingCaretPositionAlgorithm, CuttingCaretPosition>\r\n{\r\n    #region Lyric\r\n\r\n    [TestCase(nameof(singleLyric), 0, 1, true)]\r\n    [TestCase(nameof(singleLyric), 0, 3, true)]\r\n    [TestCase(nameof(singleLyric), 0, 0, false)]\r\n    [TestCase(nameof(singleLyric), 0, 4, false)]\r\n    [TestCase(nameof(singleLyric), 0, 5, false)]\r\n    [TestCase(nameof(singleLyric), 0, -1, false)]\r\n    [TestCase(nameof(singleLyricWithTwoText), 0, 1, true)]\r\n    [TestCase(nameof(singleLyricWithOneText), 0, 1, false)] // Should be able to hover only has at least two chars.\r\n    [TestCase(nameof(singleLyricWithNoText), 0, 0, false)] // It's not able to cut if no lyric text.\r\n    [TestCase(nameof(singleLyricWithNoText), 0, 1, false)]\r\n    [TestCase(nameof(singleLyricWithNoText), 0, -1, false)]\r\n    public void TestPositionMovable(string sourceName, int lyricIndex, int index, bool movable)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var caret = createCaretPosition(lyrics, lyricIndex, index);\r\n\r\n        // Check is movable\r\n        TestPositionMovable(lyrics, caret, movable);\r\n    }\r\n\r\n    [TestCase(nameof(singleLyric), 0, 1, null, null)] // cannot move up if at top index.\r\n    [TestCase(nameof(singleLyricWithTwoText), 0, 1, null, null)]\r\n    [TestCase(nameof(twoLyricsWithText), 1, 1, 0, 1)]\r\n    [TestCase(nameof(threeLyricsWithSpacing), 2, 1, 0, 1)]\r\n    [TestCase(nameof(threeLyricsWithSpacing), 2, 2, 0, 2)]\r\n    public void TestMoveToPreviousLyric(string sourceName, int lyricIndex, int index, int? expectedLyricIndex, int? expectedIndex)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var caret = createCaretPosition(lyrics, lyricIndex, index);\r\n        var expected = createExpectedCaretPosition(lyrics, expectedLyricIndex, expectedIndex);\r\n\r\n        // Check is movable\r\n        TestMoveToPreviousLyric(lyrics, caret, expected);\r\n    }\r\n\r\n    [TestCase(nameof(singleLyric), 0, 1, null, null)] // cannot move down if at bottom index.\r\n    [TestCase(nameof(twoLyricsWithText), 0, 1, 1, 1)]\r\n    [TestCase(nameof(twoLyricsWithText), 0, 3, 1, 2)]\r\n    [TestCase(nameof(threeLyricsWithSpacing), 0, 1, 2, 1)]\r\n    [TestCase(nameof(threeLyricsWithSpacing), 0, 3, 2, 2)]\r\n    public void TestMoveToNextLyric(string sourceName, int lyricIndex, int index, int? expectedLyricIndex, int? expectedIndex)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var caret = createCaretPosition(lyrics, lyricIndex, index);\r\n        var expected = createExpectedCaretPosition(lyrics, expectedLyricIndex, expectedIndex);\r\n\r\n        // Check is movable\r\n        TestMoveToNextLyric(lyrics, caret, expected);\r\n    }\r\n\r\n    [TestCase(nameof(singleLyric), 0, 1)]\r\n    [TestCase(nameof(singleLyricWithNoText), null, null)]\r\n    [TestCase(nameof(twoLyricsWithText), 0, 1)]\r\n    [TestCase(nameof(threeLyricsWithSpacing), 0, 1)]\r\n    public void TestMoveToFirstLyric(string sourceName, int? expectedLyricIndex, int? expectedIndex)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var expected = createExpectedCaretPosition(lyrics, expectedLyricIndex, expectedIndex);\r\n\r\n        // Check first position\r\n        TestMoveToFirstLyric(lyrics, expected);\r\n    }\r\n\r\n    [TestCase(nameof(singleLyric), 0, 3)]\r\n    [TestCase(nameof(singleLyricWithNoText), null, null)]\r\n    [TestCase(nameof(twoLyricsWithText), 1, 2)]\r\n    [TestCase(nameof(threeLyricsWithSpacing), 2, 2)]\r\n    public void TestMoveToLastLyric(string sourceName, int? expectedLyricIndex, int? expectedIndex)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var expected = createExpectedCaretPosition(lyrics, expectedLyricIndex, expectedIndex);\r\n\r\n        // Check last position\r\n        TestMoveToLastLyric(lyrics, expected);\r\n    }\r\n\r\n    [TestCase(nameof(singleLyric), 0, 1)]\r\n    [TestCase(nameof(singleLyricWithTwoText), 0, 1)]\r\n    public void TestMoveToTargetLyric(string sourceName, int lyricIndex, int expectedTextIndex)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var lyric = lyrics[lyricIndex];\r\n        var expected = createExpectedCaretPosition(lyrics, lyricIndex, expectedTextIndex);\r\n\r\n        // Check move to target position.\r\n        TestMoveToTargetLyric(lyrics, lyric, expected);\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Lyric index\r\n\r\n    [TestCase(nameof(singleLyric), 0, 1, null, null)]\r\n    [TestCase(nameof(singleLyric), 0, 2, 0, 1)]\r\n    [TestCase(nameof(singleLyricWithTwoText), 0, 1, null, null)]\r\n    public void TestMoveToPreviousIndex(string sourceName, int lyricIndex, int index, int? expectedLyricIndex, int? expectedIndex)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var caret = createCaretPosition(lyrics, lyricIndex, index);\r\n        var expected = createExpectedCaretPosition(lyrics, expectedLyricIndex, expectedIndex);\r\n\r\n        // Check is movable\r\n        TestMoveToPreviousIndex(lyrics, caret, expected);\r\n    }\r\n\r\n    [TestCase(nameof(singleLyric), 0, 3, null, null)]\r\n    [TestCase(nameof(singleLyric), 0, 2, 0, 3)]\r\n    [TestCase(nameof(singleLyricWithTwoText), 0, 1, null, null)]\r\n    public void TestMoveToNextIndex(string sourceName, int lyricIndex, int index, int? expectedLyricIndex, int? expectedIndex)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var caret = createCaretPosition(lyrics, lyricIndex, index);\r\n        var expected = createExpectedCaretPosition(lyrics, expectedLyricIndex, expectedIndex);\r\n\r\n        // Check is movable\r\n        TestMoveToNextIndex(lyrics, caret, expected);\r\n    }\r\n\r\n    [TestCase(nameof(singleLyric), 0, 0, 1)]\r\n    [TestCase(nameof(singleLyricWithTwoText), 0, 0, 1)]\r\n    public void TestMoveToFirstIndex(string sourceName, int lyricIndex, int? expectedLyricIndex, int? expectedIndex)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var lyric = lyrics[lyricIndex];\r\n        var expected = createExpectedCaretPosition(lyrics, expectedLyricIndex, expectedIndex);\r\n\r\n        // Check is movable\r\n        TestMoveToFirstIndex(lyrics, lyric, expected);\r\n    }\r\n\r\n    [TestCase(nameof(singleLyric), 0, 0, 3)]\r\n    [TestCase(nameof(singleLyricWithTwoText), 0, 0, 1)]\r\n    public void TestMoveToLastIndex(string sourceName, int lyricIndex, int? expectedLyricIndex, int? expectedIndex)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var lyric = lyrics[lyricIndex];\r\n        var expected = createExpectedCaretPosition(lyrics, expectedLyricIndex, expectedIndex);\r\n\r\n        // Check is movable\r\n        TestMoveToLastIndex(lyrics, lyric, expected);\r\n    }\r\n\r\n    [TestCase(nameof(singleLyric), 0, 1, 1)]\r\n    [TestCase(nameof(singleLyric), 0, 3, 3)]\r\n    [TestCase(nameof(singleLyric), 0, 0, null)] // will check the invalid case.\r\n    [TestCase(nameof(singleLyric), 0, 4, null)]\r\n    public void TestMoveToTargetLyric(string sourceName, int lyricIndex, int textIndex, int? expectedTextIndex)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var lyric = lyrics[lyricIndex];\r\n        var expected = createExpectedCaretPosition(lyrics, lyricIndex, expectedTextIndex);\r\n\r\n        // Check move to target position.\r\n        TestMoveToTargetLyric(lyrics, lyric, textIndex, expected);\r\n    }\r\n\r\n    #endregion\r\n\r\n    private static CuttingCaretPosition createCaretPosition(IEnumerable<Lyric> lyrics, int lyricIndex, int index)\r\n    {\r\n        var lyric = lyrics.ElementAtOrDefault(lyricIndex);\r\n        if (lyric == null)\r\n            throw new ArgumentNullException();\r\n\r\n        return new CuttingCaretPosition(lyric, index);\r\n    }\r\n\r\n    private static CuttingCaretPosition? createExpectedCaretPosition(IEnumerable<Lyric> lyrics, int? lyricIndex, int? index)\r\n    {\r\n        if (lyricIndex == null || index == null)\r\n            return null;\r\n\r\n        return createCaretPosition(lyrics, lyricIndex.Value, index.Value);\r\n    }\r\n\r\n    #region source\r\n\r\n    private static Lyric[] singleLyric => new[]\r\n    {\r\n        new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n        },\r\n    };\r\n\r\n    private static Lyric[] singleLyricWithTwoText => new[]\r\n    {\r\n        new Lyric\r\n        {\r\n            Text = \"AA\",\r\n        },\r\n    };\r\n\r\n    private static Lyric[] singleLyricWithOneText => new[]\r\n    {\r\n        new Lyric\r\n        {\r\n            Text = \"A\",\r\n        },\r\n    };\r\n\r\n    private static Lyric[] singleLyricWithNoText => new[]\r\n    {\r\n        new Lyric(),\r\n    };\r\n\r\n    private static Lyric[] twoLyricsWithText => new[]\r\n    {\r\n        new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n        },\r\n        new Lyric\r\n        {\r\n            Text = \"大好き\",\r\n        },\r\n    };\r\n\r\n    private static Lyric[] threeLyricsWithSpacing => new[]\r\n    {\r\n        new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n        },\r\n        new Lyric(),\r\n        new Lyric\r\n        {\r\n            Text = \"大好き\",\r\n        },\r\n    };\r\n\r\n    #endregion\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Screens/Edit/Beatmap/Lyrics/CaretPosition/Algorithms/NavigateCaretPositionAlgorithmTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition.Algorithms;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Screens.Edit.Beatmap.Lyrics.CaretPosition.Algorithms;\r\n\r\npublic class NavigateCaretPositionAlgorithmTest : BaseCaretPositionAlgorithmTest<NavigateCaretPositionAlgorithm, NavigateCaretPosition>\r\n{\r\n    #region Lyric\r\n\r\n    [TestCase(nameof(singleLyric), 0, true)]\r\n    [TestCase(nameof(singleLyricWithNoText), 0, true)]\r\n    public void TestPositionMovable(string sourceName, int lyricIndex, bool movable)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var caret = createCaretPosition(lyrics, lyricIndex);\r\n\r\n        // Check is movable, will always be true in this algorithm.\r\n        TestPositionMovable(lyrics, caret, movable);\r\n    }\r\n\r\n    [TestCase(nameof(singleLyric), 0, null)] // cannot move up if at top index.\r\n    [TestCase(nameof(singleLyricWithNoText), 0, null)]\r\n    [TestCase(nameof(twoLyricsWithText), 1, 0)]\r\n    [TestCase(nameof(threeLyricsWithSpacing), 2, 1)]\r\n    public void TestMoveToPreviousLyric(string sourceName, int lyricIndex, int? expectedLyricIndex)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var caret = createCaretPosition(lyrics, lyricIndex);\r\n        var expected = createExpectedCaretPosition(lyrics, expectedLyricIndex);\r\n\r\n        // Check is movable\r\n        TestMoveToPreviousLyric(lyrics, caret, expected);\r\n    }\r\n\r\n    [TestCase(nameof(singleLyric), 0, null)] // cannot move down if at bottom index.\r\n    [TestCase(nameof(singleLyricWithNoText), 0, null)]\r\n    [TestCase(nameof(twoLyricsWithText), 0, 1)]\r\n    [TestCase(nameof(threeLyricsWithSpacing), 0, 1)]\r\n    public void TestMoveToNextLyric(string sourceName, int lyricIndex, int? expectedLyricIndex)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var caret = createCaretPosition(lyrics, lyricIndex);\r\n        var expected = createExpectedCaretPosition(lyrics, expectedLyricIndex);\r\n\r\n        // Check is movable\r\n        TestMoveToNextLyric(lyrics, caret, expected);\r\n    }\r\n\r\n    [TestCase(nameof(singleLyric), 0)]\r\n    [TestCase(nameof(singleLyricWithNoText), 0)]\r\n    [TestCase(nameof(twoLyricsWithText), 0)]\r\n    [TestCase(nameof(threeLyricsWithSpacing), 0)]\r\n    public void TestMoveToFirstLyric(string sourceName, int? expectedLyricIndex)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var expected = createExpectedCaretPosition(lyrics, expectedLyricIndex);\r\n\r\n        // Check first position\r\n        TestMoveToFirstLyric(lyrics, expected);\r\n    }\r\n\r\n    [TestCase(nameof(singleLyric), 0)]\r\n    [TestCase(nameof(singleLyricWithNoText), 0)]\r\n    [TestCase(nameof(twoLyricsWithText), 1)]\r\n    [TestCase(nameof(threeLyricsWithSpacing), 2)]\r\n    public void TestMoveToLastLyric(string sourceName, int? expectedLyricIndex)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var expected = createExpectedCaretPosition(lyrics, expectedLyricIndex);\r\n\r\n        // Check last position\r\n        TestMoveToLastLyric(lyrics, expected);\r\n    }\r\n\r\n    [TestCase(nameof(singleLyric), 0)]\r\n    [TestCase(nameof(singleLyricWithNoText), 0)]\r\n    public void TestMoveToTargetLyric(string sourceName, int expectedLyricIndex)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var lyric = lyrics[expectedLyricIndex];\r\n        var expected = createExpectedCaretPosition(lyrics, expectedLyricIndex);\r\n\r\n        // Check move to target position.\r\n        TestMoveToTargetLyric(lyrics, lyric, expected);\r\n    }\r\n\r\n    #endregion\r\n\r\n    private static NavigateCaretPosition createCaretPosition(IEnumerable<Lyric> lyrics, int lyricIndex)\r\n    {\r\n        var lyric = lyrics.ElementAtOrDefault(lyricIndex);\r\n        if (lyric == null)\r\n            throw new ArgumentNullException();\r\n\r\n        return new NavigateCaretPosition(lyric);\r\n    }\r\n\r\n    private static NavigateCaretPosition? createExpectedCaretPosition(IEnumerable<Lyric> lyrics, int? lyricIndex)\r\n    {\r\n        if (lyricIndex == null)\r\n            return null;\r\n\r\n        return createCaretPosition(lyrics, lyricIndex.Value);\r\n    }\r\n\r\n    #region source\r\n\r\n    private static Lyric[] singleLyric => new[]\r\n    {\r\n        new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n        },\r\n    };\r\n\r\n    private static Lyric[] singleLyricWithNoText => new[]\r\n    {\r\n        new Lyric(),\r\n    };\r\n\r\n    private static Lyric[] twoLyricsWithText => new[]\r\n    {\r\n        new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n        },\r\n        new Lyric\r\n        {\r\n            Text = \"大好き\",\r\n        },\r\n    };\r\n\r\n    private static Lyric[] threeLyricsWithSpacing => new[]\r\n    {\r\n        new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n        },\r\n        new Lyric(),\r\n        new Lyric\r\n        {\r\n            Text = \"大好き\",\r\n        },\r\n    };\r\n\r\n    #endregion\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Screens/Edit/Beatmap/Lyrics/CaretPosition/Algorithms/RecordingTimeTagCaretPositionAlgorithmTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition.Algorithms;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Screens.Edit.Beatmap.Lyrics.CaretPosition.Algorithms;\r\n\r\n[TestFixture]\r\npublic class RecordingTimeTagCaretPositionAlgorithmTest : BaseIndexCaretPositionAlgorithmTest<RecordingTimeTagCaretPositionAlgorithm, RecordingTimeTagCaretPosition>\r\n{\r\n    private const int not_exist_tag = -1;\r\n\r\n    #region Lyric\r\n\r\n    [TestCase(nameof(singleLyric), RecordingTimeTagCaretMoveMode.None, 0, 0, true)]\r\n    [TestCase(nameof(singleLyric), RecordingTimeTagCaretMoveMode.OnlyStartTag, 0, 0, true)]\r\n    [TestCase(nameof(singleLyric), RecordingTimeTagCaretMoveMode.OnlyEndTag, 0, 0, false)]\r\n    [TestCase(nameof(singleLyricWithoutTimeTag), RecordingTimeTagCaretMoveMode.None, 0, not_exist_tag, false)]\r\n    [TestCase(nameof(singleLyricWithoutTimeTag), RecordingTimeTagCaretMoveMode.OnlyStartTag, 0, not_exist_tag, false)]\r\n    [TestCase(nameof(singleLyricWithoutTimeTag), RecordingTimeTagCaretMoveMode.OnlyEndTag, 0, not_exist_tag, false)]\r\n    [TestCase(nameof(singleLyricWithNoText), RecordingTimeTagCaretMoveMode.None, 0, not_exist_tag, false)]\r\n    [TestCase(nameof(singleLyricWithNoText), RecordingTimeTagCaretMoveMode.OnlyStartTag, 0, not_exist_tag, false)]\r\n    [TestCase(nameof(singleLyricWithNoText), RecordingTimeTagCaretMoveMode.OnlyEndTag, 0, not_exist_tag, false)]\r\n    public void TestPositionMovable(string sourceName, RecordingTimeTagCaretMoveMode mode, int lyricIndex, int timeTagIndex, bool movable)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var caret = createCaretPosition(lyrics, lyricIndex, timeTagIndex);\r\n\r\n        // Check is movable\r\n        TestPositionMovable(lyrics, caret, movable, algorithms => algorithms.Mode = mode);\r\n    }\r\n\r\n    [TestCase(nameof(singleLyric), RecordingTimeTagCaretMoveMode.None, 0, 0, null, null)]\r\n    [TestCase(nameof(twoLyricsWithText), RecordingTimeTagCaretMoveMode.None, 1, 0, 0, 0)]\r\n    [TestCase(nameof(twoLyricsWithText), RecordingTimeTagCaretMoveMode.None, 1, 3, 0, 0)] // should move to first start index.\r\n    [TestCase(nameof(twoLyricsWithText), RecordingTimeTagCaretMoveMode.OnlyStartTag, 1, 3, 0, 0)] // should move to first start index.\r\n    [TestCase(nameof(twoLyricsWithText), RecordingTimeTagCaretMoveMode.OnlyEndTag, 1, 3, 0, 4)] // should move to first end index.\r\n    [TestCase(nameof(threeLyricsWithSpacing), RecordingTimeTagCaretMoveMode.None, 2, 0, 0, 0)]\r\n    [TestCase(nameof(threeLyricsWithSpacing), RecordingTimeTagCaretMoveMode.None, 2, 3, 0, 0)]\r\n    [TestCase(nameof(threeLyricsWithSpacing), RecordingTimeTagCaretMoveMode.OnlyStartTag, 2, 3, 0, 0)]\r\n    [TestCase(nameof(threeLyricsWithSpacing), RecordingTimeTagCaretMoveMode.OnlyEndTag, 2, 3, 0, 4)]\r\n    public void TestMoveToPreviousLyric(string sourceName, RecordingTimeTagCaretMoveMode mode, int lyricIndex, int index, int? expectedLyricIndex, int? expectedTimeTagIndex)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var caret = createCaretPosition(lyrics, lyricIndex, index);\r\n        var expected = createExpectedCaretPosition(lyrics, expectedLyricIndex, expectedTimeTagIndex);\r\n\r\n        // Check is movable\r\n        TestMoveToPreviousLyric(lyrics, caret, expected, algorithms => algorithms.Mode = mode);\r\n    }\r\n\r\n    [TestCase(nameof(singleLyric), RecordingTimeTagCaretMoveMode.None, 0, 0, null, null)]\r\n    [TestCase(nameof(twoLyricsWithText), RecordingTimeTagCaretMoveMode.None, 0, 0, 1, 0)]\r\n    [TestCase(nameof(twoLyricsWithText), RecordingTimeTagCaretMoveMode.None, 0, 2, 1, 0)] // should move to first start index.\r\n    [TestCase(nameof(twoLyricsWithText), RecordingTimeTagCaretMoveMode.OnlyStartTag, 0, 2, 1, 0)] // should move to first start index.\r\n    [TestCase(nameof(twoLyricsWithText), RecordingTimeTagCaretMoveMode.OnlyEndTag, 0, 2, 1, 3)] // should move to first end index.\r\n    [TestCase(nameof(threeLyricsWithSpacing), RecordingTimeTagCaretMoveMode.None, 0, 0, 2, 0)]\r\n    [TestCase(nameof(threeLyricsWithSpacing), RecordingTimeTagCaretMoveMode.None, 0, 2, 2, 0)]\r\n    [TestCase(nameof(threeLyricsWithSpacing), RecordingTimeTagCaretMoveMode.OnlyStartTag, 0, 2, 2, 0)]\r\n    [TestCase(nameof(threeLyricsWithSpacing), RecordingTimeTagCaretMoveMode.OnlyEndTag, 0, 2, 2, 3)]\r\n    public void TestMoveToNextLyric(string sourceName, RecordingTimeTagCaretMoveMode mode, int lyricIndex, int index, int? expectedLyricIndex, int? expectedTimeTagIndex)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var caret = createCaretPosition(lyrics, lyricIndex, index);\r\n        var expected = createExpectedCaretPosition(lyrics, expectedLyricIndex, expectedTimeTagIndex);\r\n\r\n        // Check is movable\r\n        TestMoveToNextLyric(lyrics, caret, expected, algorithms => algorithms.Mode = mode);\r\n    }\r\n\r\n    [TestCase(nameof(singleLyric), RecordingTimeTagCaretMoveMode.None, 0, 0)]\r\n    [TestCase(nameof(singleLyric), RecordingTimeTagCaretMoveMode.OnlyStartTag, 0, 0)]\r\n    [TestCase(nameof(singleLyric), RecordingTimeTagCaretMoveMode.OnlyEndTag, 0, 4)]\r\n    [TestCase(nameof(singleLyricWithoutTimeTag), RecordingTimeTagCaretMoveMode.None, null, null)]\r\n    [TestCase(nameof(singleLyricWithoutTimeTag), RecordingTimeTagCaretMoveMode.OnlyStartTag, null, null)]\r\n    [TestCase(nameof(singleLyricWithoutTimeTag), RecordingTimeTagCaretMoveMode.OnlyEndTag, null, null)]\r\n    [TestCase(nameof(twoLyricsWithText), RecordingTimeTagCaretMoveMode.None, 0, 0)]\r\n    [TestCase(nameof(twoLyricsWithText), RecordingTimeTagCaretMoveMode.OnlyStartTag, 0, 0)]\r\n    [TestCase(nameof(twoLyricsWithText), RecordingTimeTagCaretMoveMode.OnlyEndTag, 0, 4)]\r\n    [TestCase(nameof(threeLyricsWithSpacing), RecordingTimeTagCaretMoveMode.None, 0, 0)]\r\n    [TestCase(nameof(threeLyricsWithSpacing), RecordingTimeTagCaretMoveMode.OnlyStartTag, 0, 0)]\r\n    [TestCase(nameof(threeLyricsWithSpacing), RecordingTimeTagCaretMoveMode.OnlyEndTag, 0, 4)]\r\n    public void TestMoveToFirstLyric(string sourceName, RecordingTimeTagCaretMoveMode mode, int? expectedLyricIndex, int? expectedTimeTagIndex)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var expected = createExpectedCaretPosition(lyrics, expectedLyricIndex, expectedTimeTagIndex);\r\n\r\n        // Check is movable\r\n        TestMoveToFirstLyric(lyrics, expected, algorithms => algorithms.Mode = mode);\r\n    }\r\n\r\n    [TestCase(nameof(singleLyric), RecordingTimeTagCaretMoveMode.None, 0, 4)]\r\n    [TestCase(nameof(singleLyric), RecordingTimeTagCaretMoveMode.OnlyStartTag, 0, 3)]\r\n    [TestCase(nameof(singleLyric), RecordingTimeTagCaretMoveMode.OnlyEndTag, 0, 4)]\r\n    [TestCase(nameof(singleLyricWithoutTimeTag), RecordingTimeTagCaretMoveMode.None, null, null)]\r\n    [TestCase(nameof(singleLyricWithoutTimeTag), RecordingTimeTagCaretMoveMode.OnlyStartTag, null, null)]\r\n    [TestCase(nameof(singleLyricWithoutTimeTag), RecordingTimeTagCaretMoveMode.OnlyEndTag, null, null)]\r\n    [TestCase(nameof(twoLyricsWithText), RecordingTimeTagCaretMoveMode.None, 1, 3)]\r\n    [TestCase(nameof(twoLyricsWithText), RecordingTimeTagCaretMoveMode.OnlyStartTag, 1, 2)]\r\n    [TestCase(nameof(twoLyricsWithText), RecordingTimeTagCaretMoveMode.OnlyEndTag, 1, 3)]\r\n    [TestCase(nameof(threeLyricsWithSpacing), RecordingTimeTagCaretMoveMode.None, 2, 3)]\r\n    [TestCase(nameof(threeLyricsWithSpacing), RecordingTimeTagCaretMoveMode.OnlyStartTag, 2, 2)]\r\n    [TestCase(nameof(threeLyricsWithSpacing), RecordingTimeTagCaretMoveMode.OnlyEndTag, 2, 3)]\r\n    public void TestMoveToLastLyric(string sourceName, RecordingTimeTagCaretMoveMode mode, int? expectedLyricIndex, int? expectedTimeTagIndex)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var expected = createExpectedCaretPosition(lyrics, expectedLyricIndex, expectedTimeTagIndex);\r\n\r\n        // Check is movable\r\n        TestMoveToLastLyric(lyrics, expected, algorithms => algorithms.Mode = mode);\r\n    }\r\n\r\n    [TestCase(nameof(singleLyric), RecordingTimeTagCaretMoveMode.None, 0, 0, 0)]\r\n    [TestCase(nameof(singleLyric), RecordingTimeTagCaretMoveMode.OnlyStartTag, 0, 0, 0)]\r\n    [TestCase(nameof(singleLyric), RecordingTimeTagCaretMoveMode.OnlyEndTag, 0, 0, 4)]\r\n    [TestCase(nameof(singleLyricWithoutTimeTag), RecordingTimeTagCaretMoveMode.None, 0, null, null)] // should not hover to the lyric if contains no time-tag in the lyric.\r\n    [TestCase(nameof(singleLyricWithoutTimeTag), RecordingTimeTagCaretMoveMode.OnlyStartTag, 0, null, null)]\r\n    [TestCase(nameof(singleLyricWithoutTimeTag), RecordingTimeTagCaretMoveMode.OnlyEndTag, 0, null, null)]\r\n    [TestCase(nameof(singleLyricWithNoText), RecordingTimeTagCaretMoveMode.None, 0, null, null)] // should not hover to the lyric if contains no text and no time-tag in the lyric\r\n    public void TestMoveToTargetLyric(string sourceName, RecordingTimeTagCaretMoveMode mode, int lyricIndex, int? expectedLyricIndex, int? expectedTimeTagIndex)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var lyric = lyrics[lyricIndex];\r\n        var expected = createExpectedCaretPosition(lyrics, expectedLyricIndex, expectedTimeTagIndex);\r\n\r\n        // Check move to target position.\r\n        TestMoveToTargetLyric(lyrics, lyric, expected, algorithms => algorithms.Mode = mode);\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Lyric index\r\n\r\n    [TestCase(nameof(singleLyric), RecordingTimeTagCaretMoveMode.None, 0, 0, null, null)]\r\n    [TestCase(nameof(singleLyric), RecordingTimeTagCaretMoveMode.None, 0, 4, 0, 3)]\r\n    [TestCase(nameof(singleLyric), RecordingTimeTagCaretMoveMode.OnlyStartTag, 0, 4, 0, 3)]\r\n    [TestCase(nameof(singleLyric), RecordingTimeTagCaretMoveMode.OnlyEndTag, 0, 4, null, null)]\r\n    public void TestMoveToPreviousIndex(string sourceName, RecordingTimeTagCaretMoveMode mode, int lyricIndex, int index, int? expectedLyricIndex, int? expectedTimeTagIndex)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var caret = createCaretPosition(lyrics, lyricIndex, index);\r\n        var expected = createExpectedCaretPosition(lyrics, expectedLyricIndex, expectedTimeTagIndex);\r\n\r\n        // Check is movable\r\n        TestMoveToPreviousIndex(lyrics, caret, expected, algorithms => algorithms.Mode = mode);\r\n    }\r\n\r\n    [TestCase(nameof(singleLyric), RecordingTimeTagCaretMoveMode.None, 0, 4, null, null)]\r\n    [TestCase(nameof(singleLyric), RecordingTimeTagCaretMoveMode.None, 0, 0, 0, 1)]\r\n    [TestCase(nameof(singleLyric), RecordingTimeTagCaretMoveMode.OnlyStartTag, 0, 0, 0, 1)]\r\n    [TestCase(nameof(singleLyric), RecordingTimeTagCaretMoveMode.OnlyEndTag, 0, 0, 0, 4)]\r\n    public void TestMoveToNextIndex(string sourceName, RecordingTimeTagCaretMoveMode mode, int lyricIndex, int index, int? expectedLyricIndex, int? expectedTimeTagIndex)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var caret = createCaretPosition(lyrics, lyricIndex, index);\r\n        var expected = createExpectedCaretPosition(lyrics, expectedLyricIndex, expectedTimeTagIndex);\r\n\r\n        // Check is movable\r\n        TestMoveToNextIndex(lyrics, caret, expected, algorithms => algorithms.Mode = mode);\r\n    }\r\n\r\n    [TestCase(nameof(singleLyric), RecordingTimeTagCaretMoveMode.None, 0, 0, 0)]\r\n    [TestCase(nameof(singleLyric), RecordingTimeTagCaretMoveMode.OnlyStartTag, 0, 0, 0)]\r\n    [TestCase(nameof(singleLyric), RecordingTimeTagCaretMoveMode.OnlyEndTag, 0, 0, 4)]\r\n    [TestCase(nameof(singleLyricWithNoText), RecordingTimeTagCaretMoveMode.None, 0, null, null)]\r\n    [TestCase(nameof(singleLyricWithNoText), RecordingTimeTagCaretMoveMode.OnlyStartTag, 0, null, null)]\r\n    [TestCase(nameof(singleLyricWithNoText), RecordingTimeTagCaretMoveMode.OnlyEndTag, 0, null, null)]\r\n    public void TestMoveToFirstIndex(string sourceName, RecordingTimeTagCaretMoveMode mode, int lyricIndex, int? expectedLyricIndex, int? expectedTimeTagIndex)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var lyric = lyrics[lyricIndex];\r\n        var expected = createExpectedCaretPosition(lyrics, expectedLyricIndex, expectedTimeTagIndex);\r\n\r\n        // Check is movable\r\n        TestMoveToFirstIndex(lyrics, lyric, expected, algorithms => algorithms.Mode = mode);\r\n    }\r\n\r\n    [TestCase(nameof(singleLyric), RecordingTimeTagCaretMoveMode.None, 0, 0, 4)]\r\n    [TestCase(nameof(singleLyric), RecordingTimeTagCaretMoveMode.OnlyStartTag, 0, 0, 3)]\r\n    [TestCase(nameof(singleLyric), RecordingTimeTagCaretMoveMode.OnlyEndTag, 0, 0, 4)]\r\n    [TestCase(nameof(singleLyricWithNoText), RecordingTimeTagCaretMoveMode.None, 0, null, null)]\r\n    [TestCase(nameof(singleLyricWithNoText), RecordingTimeTagCaretMoveMode.OnlyStartTag, 0, null, null)]\r\n    [TestCase(nameof(singleLyricWithNoText), RecordingTimeTagCaretMoveMode.OnlyEndTag, 0, null, null)]\r\n    public void TestMoveToLastIndex(string sourceName, RecordingTimeTagCaretMoveMode mode, int lyricIndex, int? expectedLyricIndex, int? expectedTimeTagIndex)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var lyric = lyrics[lyricIndex];\r\n        var expected = createExpectedCaretPosition(lyrics, expectedLyricIndex, expectedTimeTagIndex);\r\n\r\n        // Check is movable\r\n        TestMoveToLastIndex(lyrics, lyric, expected, algorithms => algorithms.Mode = mode);\r\n    }\r\n\r\n    [TestCase(nameof(singleLyric), RecordingTimeTagCaretMoveMode.None, 0, 0, 0, 0)]\r\n    [TestCase(nameof(singleLyric), RecordingTimeTagCaretMoveMode.None, 0, 4, 0, 4)]\r\n    public void TestMoveToTargetLyric(string sourceName, RecordingTimeTagCaretMoveMode mode, int lyricIndex, int timeTagIndex, int? expectedLyricIndex, int? expectedTimeTagIndex)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var lyric = lyrics[lyricIndex];\r\n        var timeTag = lyric.TimeTags.ElementAt(timeTagIndex);\r\n        var expected = createExpectedCaretPosition(lyrics, expectedLyricIndex, expectedTimeTagIndex);\r\n\r\n        // Check move to target position.\r\n        TestMoveToTargetLyric(lyrics, lyric, timeTag, expected, algorithms => algorithms.Mode = mode);\r\n    }\r\n\r\n    #endregion\r\n\r\n    private static RecordingTimeTagCaretPosition createCaretPosition(IEnumerable<Lyric> lyrics, int lyricIndex, int timeTagIndex)\r\n    {\r\n        var lyric = lyrics.ElementAtOrDefault(lyricIndex);\r\n        var timeTag = timeTagIndex == not_exist_tag\r\n            ? new TimeTag(new TextIndex(not_exist_tag))\r\n            : lyric?.TimeTags.ElementAtOrDefault(timeTagIndex);\r\n\r\n        if (lyric == null || timeTag == null)\r\n            throw new ArgumentNullException();\r\n\r\n        return new RecordingTimeTagCaretPosition(lyric, timeTag);\r\n    }\r\n\r\n    private static RecordingTimeTagCaretPosition? createExpectedCaretPosition(IEnumerable<Lyric> lyrics, int? lyricIndex, int? timeTagIndex)\r\n    {\r\n        if (lyricIndex == null || timeTagIndex == null)\r\n            return null;\r\n\r\n        return createCaretPosition(lyrics, lyricIndex.Value, timeTagIndex.Value);\r\n    }\r\n\r\n    #region source\r\n\r\n    private static Lyric[] singleLyric => new[]\r\n    {\r\n        new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            TimeTags = TestCaseTagHelper.ParseTimeTags(new[]\r\n            {\r\n                \"[0,start]:1000\",\r\n                \"[1,start]:2000\",\r\n                \"[2,start]:3000\",\r\n                \"[3,start]:4000\",\r\n                \"[3,end]:5000\",\r\n            }),\r\n        },\r\n    };\r\n\r\n    private static Lyric[] singleLyricWithoutTimeTag => new[]\r\n    {\r\n        new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n        },\r\n    };\r\n\r\n    private static Lyric[] singleLyricWithNoText => new[]\r\n    {\r\n        new Lyric(),\r\n    };\r\n\r\n    private static Lyric[] twoLyricsWithText => new[]\r\n    {\r\n        new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            TimeTags = TestCaseTagHelper.ParseTimeTags(new[]\r\n            {\r\n                \"[0,start]:1000\",\r\n                \"[1,start]:2000\",\r\n                \"[2,start]:3000\",\r\n                \"[3,start]:4000\",\r\n                \"[3,end]:5000\",\r\n            }),\r\n        },\r\n        new Lyric\r\n        {\r\n            Text = \"大好き\",\r\n            TimeTags = TestCaseTagHelper.ParseTimeTags(new[]\r\n            {\r\n                \"[0,start]:1000\",\r\n                \"[1,start]:2000\",\r\n                \"[2,start]:3000\",\r\n                \"[2,end]:5000\",\r\n            }),\r\n        },\r\n    };\r\n\r\n    private static Lyric[] threeLyricsWithSpacing => new[]\r\n    {\r\n        new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n            TimeTags = TestCaseTagHelper.ParseTimeTags(new[]\r\n            {\r\n                \"[0,start]:1000\",\r\n                \"[1,start]:2000\",\r\n                \"[2,start]:3000\",\r\n                \"[3,start]:4000\",\r\n                \"[3,end]:5000\",\r\n            }),\r\n        },\r\n        new Lyric(),\r\n        new Lyric\r\n        {\r\n            Text = \"大好き\",\r\n            TimeTags = TestCaseTagHelper.ParseTimeTags(new[]\r\n            {\r\n                \"[0,start]:1000\",\r\n                \"[1,start]:2000\",\r\n                \"[2,start]:3000\",\r\n                \"[2,end]:5000\",\r\n            }),\r\n        },\r\n    };\r\n\r\n    #endregion\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Screens/Edit/Beatmap/Lyrics/CaretPosition/Algorithms/TypingCaretPositionAlgorithmTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition.Algorithms;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Screens.Edit.Beatmap.Lyrics.CaretPosition.Algorithms;\r\n\r\n[TestFixture]\r\npublic class TypingCaretPositionAlgorithmTest : BaseIndexCaretPositionAlgorithmTest<TypingCaretPositionAlgorithm, TypingCaretPosition>\r\n{\r\n    #region Lyric\r\n\r\n    [TestCase(nameof(singleLyric), 0, 0, true)]\r\n    [TestCase(nameof(singleLyric), 0, 4, true)]\r\n    [TestCase(nameof(singleLyric), 0, 5, false)]\r\n    [TestCase(nameof(singleLyric), 0, -1, false)]\r\n    [TestCase(nameof(singleLyricWithNoText), 0, 0, true)] // it's still movable even text is empty or null.\r\n    [TestCase(nameof(singleLyricWithNoText), 0, 1, false)]\r\n    public void TestPositionMovable(string sourceName, int lyricIndex, int index, bool movable)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var caret = createCaretPosition(lyrics, lyricIndex, index);\r\n\r\n        // Check is movable\r\n        TestPositionMovable(lyrics, caret, movable);\r\n    }\r\n\r\n    [TestCase(nameof(singleLyric), 0, 0, null, null)] // cannot move up if at top index.\r\n    [TestCase(nameof(singleLyricWithNoText), 0, 0, null, null)]\r\n    [TestCase(nameof(twoLyricsWithText), 1, 0, 0, 0)]\r\n    [TestCase(nameof(threeLyricsWithSpacing), 2, 0, 1, 0)]\r\n    [TestCase(nameof(threeLyricsWithSpacing), 2, 3, 1, 0)]\r\n    public void TestMoveToPreviousLyric(string sourceName, int lyricIndex, int index, int? expectedLyricIndex, int? expectedIndex)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var caret = createCaretPosition(lyrics, lyricIndex, index);\r\n        var expected = createExpectedCaretPosition(lyrics, expectedLyricIndex, expectedIndex);\r\n\r\n        // Check is movable\r\n        TestMoveToPreviousLyric(lyrics, caret, expected);\r\n    }\r\n\r\n    [TestCase(nameof(singleLyric), 0, 0, null, null)] // cannot move down if at bottom index.\r\n    [TestCase(nameof(singleLyricWithNoText), 0, 0, null, null)]\r\n    [TestCase(nameof(twoLyricsWithText), 0, 0, 1, 0)]\r\n    [TestCase(nameof(threeLyricsWithSpacing), 0, 0, 1, 0)]\r\n    [TestCase(nameof(threeLyricsWithSpacing), 0, 4, 1, 0)]\r\n    public void TestMoveToNextLyric(string sourceName, int lyricIndex, int index, int? expectedLyricIndex, int? expectedIndex)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var caret = createCaretPosition(lyrics, lyricIndex, index);\r\n        var expected = createExpectedCaretPosition(lyrics, expectedLyricIndex, expectedIndex);\r\n\r\n        // Check is movable\r\n        TestMoveToNextLyric(lyrics, caret, expected);\r\n    }\r\n\r\n    [TestCase(nameof(singleLyric), 0, 0)]\r\n    [TestCase(nameof(singleLyricWithNoText), 0, 0)]\r\n    [TestCase(nameof(twoLyricsWithText), 0, 0)]\r\n    [TestCase(nameof(threeLyricsWithSpacing), 0, 0)]\r\n    public void TestMoveToFirstLyric(string sourceName, int? expectedLyricIndex, int? expectedIndex)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var expected = createExpectedCaretPosition(lyrics, expectedLyricIndex, expectedIndex);\r\n\r\n        // Check first position\r\n        TestMoveToFirstLyric(lyrics, expected);\r\n    }\r\n\r\n    [TestCase(nameof(singleLyric), 0, 4)]\r\n    [TestCase(nameof(singleLyricWithNoText), 0, 0)]\r\n    [TestCase(nameof(twoLyricsWithText), 1, 3)]\r\n    [TestCase(nameof(threeLyricsWithSpacing), 2, 3)]\r\n    public void TestMoveToLastLyric(string sourceName, int? expectedLyricIndex, int? expectedIndex)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var expected = createExpectedCaretPosition(lyrics, expectedLyricIndex, expectedIndex);\r\n\r\n        // Check last position\r\n        TestMoveToLastLyric(lyrics, expected);\r\n    }\r\n\r\n    [TestCase(nameof(singleLyric), 0, 0)]\r\n    [TestCase(nameof(singleLyricWithNoText), 0, 0)]\r\n    public void TestMoveToTargetLyric(string sourceName, int lyricIndex, int? expectedIndex)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var lyric = lyrics[lyricIndex];\r\n        var expected = createExpectedCaretPosition(lyrics, lyricIndex, expectedIndex);\r\n\r\n        // Check move to target position.\r\n        TestMoveToTargetLyric(lyrics, lyric, expected);\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Lyric index\r\n\r\n    [TestCase(nameof(singleLyric), 0, 0, null, null)]\r\n    [TestCase(nameof(singleLyric), 0, 1, 0, 0)]\r\n    [TestCase(nameof(singleLyricWithNoText), 0, 0, null, null)]\r\n    public void TestMoveToPreviousIndex(string sourceName, int lyricIndex, int index, int? expectedLyricIndex, int? expectedIndex)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var caret = createCaretPosition(lyrics, lyricIndex, index);\r\n        var expected = createExpectedCaretPosition(lyrics, expectedLyricIndex, expectedIndex);\r\n\r\n        // Check is movable\r\n        TestMoveToPreviousIndex(lyrics, caret, expected);\r\n    }\r\n\r\n    [TestCase(nameof(singleLyric), 0, 4, null, null)]\r\n    [TestCase(nameof(singleLyric), 0, 3, 0, 4)]\r\n    [TestCase(nameof(singleLyricWithNoText), 0, 0, null, null)]\r\n    public void TestMoveToNextIndex(string sourceName, int lyricIndex, int index, int? expectedLyricIndex, int? expectedIndex)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var caret = createCaretPosition(lyrics, lyricIndex, index);\r\n        var expected = createExpectedCaretPosition(lyrics, expectedLyricIndex, expectedIndex);\r\n\r\n        // Check is movable\r\n        TestMoveToNextIndex(lyrics, caret, expected);\r\n    }\r\n\r\n    [TestCase(nameof(singleLyric), 0, 0, 0)]\r\n    [TestCase(nameof(singleLyricWithNoText), 0, 0, 0)]\r\n    public void TestMoveToFirstIndex(string sourceName, int lyricIndex, int? expectedLyricIndex, int? expectedIndex)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var lyric = lyrics[lyricIndex];\r\n        var expected = createExpectedCaretPosition(lyrics, expectedLyricIndex, expectedIndex);\r\n\r\n        // Check is movable\r\n        TestMoveToFirstIndex(lyrics, lyric, expected);\r\n    }\r\n\r\n    [TestCase(nameof(singleLyric), 0, 0, 4)]\r\n    [TestCase(nameof(singleLyricWithNoText), 0, 0, 0)]\r\n    public void TestMoveToLastIndex(string sourceName, int lyricIndex, int? expectedLyricIndex, int? expectedIndex)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var lyric = lyrics[lyricIndex];\r\n        var expected = createExpectedCaretPosition(lyrics, expectedLyricIndex, expectedIndex);\r\n\r\n        // Check is movable\r\n        TestMoveToLastIndex(lyrics, lyric, expected);\r\n    }\r\n\r\n    [TestCase(nameof(singleLyric), 0, 0, 0)]\r\n    [TestCase(nameof(singleLyric), 0, 4, 4)]\r\n    [TestCase(nameof(singleLyric), 0, -1, null)] // will check the invalid case.\r\n    [TestCase(nameof(singleLyric), 0, 5, null)]\r\n    public void TestMoveToTargetLyric(string sourceName, int lyricIndex, int textIndex, int? expectedTextIndex)\r\n    {\r\n        var lyrics = GetLyricsByMethodName(sourceName);\r\n        var lyric = lyrics[lyricIndex];\r\n        var expected = createExpectedCaretPosition(lyrics, lyricIndex, expectedTextIndex);\r\n\r\n        // Check move to target position.\r\n        TestMoveToTargetLyric(lyrics, lyric, textIndex, expected);\r\n    }\r\n\r\n    #endregion\r\n\r\n    private static TypingCaretPosition createCaretPosition(IEnumerable<Lyric> lyrics, int lyricIndex, int index)\r\n    {\r\n        var lyric = lyrics.ElementAtOrDefault(lyricIndex);\r\n        if (lyric == null)\r\n            throw new ArgumentNullException();\r\n\r\n        return new TypingCaretPosition(lyric, index);\r\n    }\r\n\r\n    private static TypingCaretPosition? createExpectedCaretPosition(IEnumerable<Lyric> lyrics, int? lyricIndex, int? index)\r\n    {\r\n        if (lyricIndex == null || index == null)\r\n            return null;\r\n\r\n        return createCaretPosition(lyrics, lyricIndex.Value, index.Value);\r\n    }\r\n\r\n    #region source\r\n\r\n    private static Lyric[] singleLyric => new[]\r\n    {\r\n        new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n        },\r\n    };\r\n\r\n    private static Lyric[] singleLyricWithNoText => new[]\r\n    {\r\n        new Lyric(),\r\n    };\r\n\r\n    private static Lyric[] twoLyricsWithText => new[]\r\n    {\r\n        new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n        },\r\n        new Lyric\r\n        {\r\n            Text = \"大好き\",\r\n        },\r\n    };\r\n\r\n    private static Lyric[] threeLyricsWithSpacing => new[]\r\n    {\r\n        new Lyric\r\n        {\r\n            Text = \"カラオケ\",\r\n        },\r\n        new Lyric(),\r\n        new Lyric\r\n        {\r\n            Text = \"大好き\",\r\n        },\r\n    };\r\n\r\n    #endregion\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Screens/Edit/Beatmap/Lyrics/CaretPosition/IndexCaretPositionTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Screens.Edit.Beatmap.Lyrics.CaretPosition;\r\n\r\n[TestFixture(typeof(CreateRubyTagCaretPosition))]\r\n[TestFixture(typeof(CuttingCaretPosition))]\r\n[TestFixture(typeof(RecordingTimeTagCaretPosition))]\r\n[TestFixture(typeof(CreateRemoveTimeTagCaretPosition))]\r\n[TestFixture(typeof(TypingCaretPosition))]\r\npublic class IndexCaretPositionTest<TIndexCaretPosition> where TIndexCaretPosition : IIndexCaretPosition\r\n{\r\n    [Test]\r\n    public void TestCompareWithLargerIndex()\r\n    {\r\n        var lyric = new Lyric();\r\n\r\n        var caretPosition = createSmallerCaretPosition(lyric);\r\n        var comparedCaretPosition = createBiggerCaretPosition(lyric);\r\n\r\n        Assert.That(caretPosition < comparedCaretPosition);\r\n        Assert.That(caretPosition <= comparedCaretPosition);\r\n        Assert.That(caretPosition >= comparedCaretPosition, Is.False);\r\n        Assert.That(caretPosition > comparedCaretPosition, Is.False);\r\n    }\r\n\r\n    [Test]\r\n    public void TestCompareEqualIndex()\r\n    {\r\n        var lyric = new Lyric();\r\n\r\n        var caretPosition = createSmallerCaretPosition(lyric);\r\n        var comparedCaretPosition = createSmallerCaretPosition(lyric);\r\n\r\n        Assert.That(caretPosition < comparedCaretPosition, Is.False);\r\n        Assert.That(caretPosition <= comparedCaretPosition);\r\n        Assert.That(caretPosition >= comparedCaretPosition);\r\n        Assert.That(caretPosition > comparedCaretPosition, Is.False);\r\n    }\r\n\r\n    [Test]\r\n    public void TestCompareWithSmallerIndex()\r\n    {\r\n        var lyric = new Lyric();\r\n\r\n        var caretPosition = createBiggerCaretPosition(lyric);\r\n        var comparedCaretPosition = createSmallerCaretPosition(lyric);\r\n\r\n        Assert.That(caretPosition < comparedCaretPosition, Is.False);\r\n        Assert.That(caretPosition <= comparedCaretPosition, Is.False);\r\n        Assert.That(caretPosition >= comparedCaretPosition);\r\n        Assert.That(caretPosition > comparedCaretPosition);\r\n    }\r\n\r\n    [Test]\r\n    public void TestCompareWithDifferentLyric()\r\n    {\r\n        var lyric1 = new Lyric();\r\n        var lyric2 = new Lyric();\r\n\r\n        var caretPosition = createBiggerCaretPosition(lyric1);\r\n        var comparedCaretPosition = createSmallerCaretPosition(lyric2);\r\n\r\n        Assert.Throws<InvalidOperationException>(() =>\r\n        {\r\n            int _ = caretPosition.CompareTo(comparedCaretPosition);\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestCompareDifferentType()\r\n    {\r\n        var lyric = new Lyric();\r\n\r\n        var caretPosition = createBiggerCaretPosition(lyric);\r\n        var comparedCaretPosition = new FakeCaretPosition(lyric);\r\n\r\n        Assert.Throws<InvalidOperationException>(() =>\r\n        {\r\n            int _ = caretPosition.CompareTo(comparedCaretPosition);\r\n        });\r\n    }\r\n\r\n    private static IIndexCaretPosition createSmallerCaretPosition(Lyric lyric) =>\r\n        typeof(TIndexCaretPosition) switch\r\n        {\r\n            Type t when t == typeof(CreateRubyTagCaretPosition) => new CreateRubyTagCaretPosition(lyric, 0),\r\n            Type t when t == typeof(CuttingCaretPosition) => new CuttingCaretPosition(lyric, 0),\r\n            Type t when t == typeof(RecordingTimeTagCaretPosition) => new RecordingTimeTagCaretPosition(lyric, new TimeTag(new TextIndex())),\r\n            Type t when t == typeof(CreateRemoveTimeTagCaretPosition) => new CreateRemoveTimeTagCaretPosition(lyric, 0),\r\n            Type t when t == typeof(TypingCaretPosition) => new TypingCaretPosition(lyric, 0),\r\n            _ => throw new NotSupportedException(),\r\n        };\r\n\r\n    private static IIndexCaretPosition createBiggerCaretPosition(Lyric lyric) =>\r\n        typeof(TIndexCaretPosition) switch\r\n        {\r\n            Type t when t == typeof(CreateRubyTagCaretPosition) => new CreateRubyTagCaretPosition(lyric, 1),\r\n            Type t when t == typeof(CuttingCaretPosition) => new CuttingCaretPosition(lyric, 1),\r\n            Type t when t == typeof(RecordingTimeTagCaretPosition) => new RecordingTimeTagCaretPosition(lyric, new TimeTag(new TextIndex(1))),\r\n            Type t when t == typeof(CreateRemoveTimeTagCaretPosition) => new CreateRemoveTimeTagCaretPosition(lyric, 1),\r\n            Type t when t == typeof(TypingCaretPosition) => new TypingCaretPosition(lyric, 1),\r\n            _ => throw new NotSupportedException(),\r\n        };\r\n\r\n    private struct FakeCaretPosition : IIndexCaretPosition\r\n    {\r\n        public FakeCaretPosition(Lyric lyric)\r\n        {\r\n            Lyric = lyric;\r\n        }\r\n\r\n        public Lyric Lyric { get; }\r\n\r\n        public int CompareTo(IIndexCaretPosition? other)\r\n        {\r\n            throw new NotImplementedException();\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Screens/Edit/Beatmap/Lyrics/Content/SingleLyricEditorTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Components.Lyrics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Screens.Edit.Beatmap.Lyrics.Content;\r\n\r\npublic class SingleLyricEditorTest\r\n{\r\n    [Test]\r\n    public void TestLockMessage()\r\n    {\r\n        var lyric = new Lyric();\r\n        Assert.That(InteractableLyric.GetLyricPropertyLockedReason(lyric, LyricEditorMode.View), Is.Null);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Screens/Edit/Beatmap/Lyrics/Content/TestSceneInteractableLyric.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Cursor;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.IO.Stores;\r\nusing osu.Framework.Testing;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Edit.ChangeHandlers.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Components.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\nusing osu.Game.Screens.Edit;\r\nusing osu.Game.Skinning;\r\nusing osu.Game.Tests.Visual;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Screens.Edit.Beatmap.Lyrics.Content;\r\n\r\npublic partial class TestSceneInteractableLyric : EditorClockTestScene\r\n{\r\n    private const int border = 36;\r\n\r\n    private static readonly Lyric lyric = new()\r\n    {\r\n        Text = \"カラオケ\",\r\n        TimeTags = TestCaseTagHelper.ParseTimeTags(new[] { \"[0,start]:1000#^ka\", \"[1,start]:2000#ra\", \"[2,start]:3000#o\", \"[3,start]:4000#ke\", \"[3,end]:5000\" }),\r\n        RubyTags = TestCaseTagHelper.ParseRubyTags(new[] { \"[0]:か\", \"[1]:ら\", \"[2]:お\", \"[3]:け\" }),\r\n    };\r\n\r\n    [Resolved]\r\n    private OsuColour colour { get; set; } = null!;\r\n\r\n    [Cached(typeof(EditorBeatmap))]\r\n    private readonly EditorBeatmap editorBeatmap = new(new KaraokeBeatmap\r\n    {\r\n        BeatmapInfo =\r\n        {\r\n            Ruleset = new KaraokeRuleset().RulesetInfo,\r\n        },\r\n    });\r\n\r\n    public TestSceneInteractableLyric()\r\n    {\r\n        editorBeatmap.Add(lyric);\r\n        editorBeatmap.SelectedHitObjects.Add(lyric);\r\n    }\r\n\r\n    [Test]\r\n    public void TestGridLayer()\r\n    {\r\n        AddToggleStep(\"Add/remove the grid layer\", value =>\r\n        {\r\n            if (value)\r\n            {\r\n                addLoader(new LayerLoader<GridLayer>\r\n                {\r\n                    OnLoad = layer =>\r\n                    {\r\n                        layer.Spacing = 10;\r\n                    },\r\n                });\r\n            }\r\n            else\r\n            {\r\n                removeLoader<GridLayer>();\r\n            }\r\n        });\r\n\r\n        AddSliderStep(\"Change the spacing of the grid\", 0, 100, 10, value =>\r\n        {\r\n            changeLayerProperty<GridLayer>(layer =>\r\n            {\r\n                layer.Spacing = value;\r\n            });\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestEditLyricLayer()\r\n    {\r\n        AddToggleStep(\"Add/remove the edit lyric layer\", value =>\r\n        {\r\n            if (value)\r\n            {\r\n                addLoader<EditLyricLayer>();\r\n            }\r\n            else\r\n            {\r\n                removeLoader<EditLyricLayer>();\r\n            }\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestTimeTagLayer()\r\n    {\r\n        AddToggleStep(\"Add/remove the time-tag layer\", value =>\r\n        {\r\n            if (value)\r\n            {\r\n                addLoader<TimeTagLayer>();\r\n            }\r\n            else\r\n            {\r\n                removeLoader<TimeTagLayer>();\r\n            }\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestCaretLayer()\r\n    {\r\n        AddToggleStep(\"Add/remove the caret layer\", value =>\r\n        {\r\n            if (value)\r\n            {\r\n                addLoader<CaretLayer>();\r\n            }\r\n            else\r\n            {\r\n                removeLoader<CaretLayer>();\r\n            }\r\n        });\r\n\r\n        AddStep(\"View mode\", () => switchMode(LyricEditorMode.View));\r\n        AddStep(\"Typing mode\", () => switchMode(LyricEditorMode.EditText, TextEditStep.Typing));\r\n        AddStep(\"Cutting mode\", () => switchMode(LyricEditorMode.EditText, TextEditStep.Split));\r\n        AddStep(\"Edit ruby mode\", () => switchMode(LyricEditorMode.EditRuby, RubyTagEditMode.Create));\r\n        AddStep(\"Edit time-tag mode\", () => switchMode(LyricEditorMode.EditTimeTag, TimeTagEditStep.Create));\r\n        AddStep(\"Record time-tag mode\", () => switchMode(LyricEditorMode.EditTimeTag, TimeTagEditStep.Recording));\r\n    }\r\n\r\n    [Test]\r\n    public void TestBlueprintLayer()\r\n    {\r\n        AddToggleStep(\"Add/remove the blueprint layer\", value =>\r\n        {\r\n            if (value)\r\n            {\r\n                addLoader<BlueprintLayer>();\r\n            }\r\n            else\r\n            {\r\n                removeLoader<BlueprintLayer>();\r\n            }\r\n        });\r\n\r\n        AddStep(\"Ruby blueprint\", () =>\r\n        {\r\n            switchMode(LyricEditorMode.EditRuby, RubyTagEditMode.Create);\r\n            switchEditRubyModeState(RubyTagEditMode.Modify);\r\n        });\r\n        AddStep(\"Time-tag adjust blueprint\", () =>\r\n        {\r\n            switchMode(LyricEditorMode.EditTimeTag, TimeTagEditStep.Adjust);\r\n        });\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OsuGameBase game)\r\n    {\r\n        Dependencies.CacheAs<ILyricsProvider>(new LyricsProvider().With(Add));\r\n        Dependencies.CacheAs<ILyricsChangeHandler>(new LyricsChangeHandler().With(Add));\r\n        Dependencies.CacheAs<ILyricTextChangeHandler>(new LyricTextChangeHandler().With(Add));\r\n        Dependencies.CacheAs<ILyricRubyTagsChangeHandler>(new LyricRubyTagsChangeHandler().With(Add));\r\n        Dependencies.CacheAs<ILyricTimeTagsChangeHandler>(new LyricTimeTagsChangeHandler().With(Add));\r\n        Dependencies.Cache(new KaraokeRulesetLyricEditorConfigManager());\r\n\r\n        game.Resources.AddStore(new NamespacedResourceStore<byte[]>(new ShaderResourceStore(), \"Resources\"));\r\n    }\r\n\r\n    #region Testing tools\r\n\r\n    private void switchMode(LyricEditorMode mode, Enum? step = null)\r\n    {\r\n        var editorState = this.ChildrenOfType<ILyricEditorState>().First();\r\n        editorState.SwitchMode(mode);\r\n\r\n        if (step != null)\r\n        {\r\n            editorState.SwitchEditStep(step);\r\n        }\r\n    }\r\n\r\n    private void switchEditRubyModeState(RubyTagEditMode mode)\r\n    {\r\n        var editRubyModeState = this.ChildrenOfType<IEditRubyModeState>().First();\r\n        editRubyModeState.BindableRubyTagEditMode.Value = mode;\r\n    }\r\n\r\n    private readonly List<LayerLoader> loaders = new()\r\n    {\r\n        // note: lyric layer should always in the loader and never be removed.\r\n        new LayerLoader<LyricLayer>\r\n        {\r\n            OnLoad = layer =>\r\n            {\r\n                layer.LyricPosition = new Vector2(border);\r\n            },\r\n        },\r\n    };\r\n\r\n    private LayerLoader? getLoader<TLayer>()\r\n        => loaders.FirstOrDefault(l => l.GetType().GenericTypeArguments.First() == typeof(TLayer));\r\n\r\n    private void addLoader<TLayer>(bool reloadView = true) where TLayer : Layer\r\n    {\r\n        addLoader(new LayerLoader<TLayer>(), reloadView);\r\n    }\r\n\r\n    private void addLoader<TLayer>(LayerLoader<TLayer> instance, bool reloadView = true) where TLayer : Layer\r\n    {\r\n        if (getLoader<TLayer>() != null)\r\n        {\r\n            return;\r\n        }\r\n\r\n        loaders.Add(instance);\r\n\r\n        if (reloadView)\r\n        {\r\n            updateInteractableLyric();\r\n        }\r\n    }\r\n\r\n    private void removeLoader<TLoader>(bool reloadView = true)\r\n    {\r\n        var loader = getLoader<TLoader>();\r\n\r\n        if (loader != null)\r\n        {\r\n            loaders.Remove(loader);\r\n        }\r\n\r\n        if (reloadView)\r\n        {\r\n            updateInteractableLyric();\r\n        }\r\n    }\r\n\r\n    private void changeLayerProperty<TLayer>(Action<TLayer> action, bool reloadView = true) where TLayer : Layer\r\n    {\r\n        if (getLoader<TLayer>() == null)\r\n        {\r\n            return;\r\n        }\r\n\r\n        removeLoader<TLayer>(false);\r\n        addLoader(new LayerLoader<TLayer>\r\n        {\r\n            OnLoad = action,\r\n        }, false);\r\n\r\n        if (reloadView)\r\n        {\r\n            updateInteractableLyric();\r\n        }\r\n    }\r\n\r\n    private void updateInteractableLyric()\r\n    {\r\n        RemoveAll(x => x is PopoverContainer, true);\r\n        Add(new PopoverContainer\r\n        {\r\n            Anchor = Anchor.Centre,\r\n            Origin = Anchor.Centre,\r\n            AutoSizeAxes = Axes.Both,\r\n            Children = new Drawable[]\r\n            {\r\n                new Box\r\n                {\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    Colour = colour.BlueDark,\r\n                },\r\n                new MockLyricEditorState\r\n                {\r\n                    AutoSizeAxes = Axes.Both,\r\n                    Padding = new MarginPadding(48),\r\n                    Child = new SkinProvidingContainer(new LyricEditorSkin(null)\r\n                    {\r\n                        FontSize = 48,\r\n                    })\r\n                    {\r\n                        RelativeSizeAxes = Axes.None,\r\n                        AutoSizeAxes = Axes.Both,\r\n                        Child = createInteractableLyric(loaders.ToArray()),\r\n                    },\r\n                },\r\n            },\r\n        });\r\n    }\r\n\r\n    private static InteractableLyric createInteractableLyric(LayerLoader[] loaders)\r\n    {\r\n        return new InteractableLyric(lyric)\r\n        {\r\n            Anchor = Anchor.CentreLeft,\r\n            Origin = Anchor.CentreLeft,\r\n            TextSizeChanged = (self, size) =>\r\n            {\r\n                self.Width = size.X + border * 2;\r\n                self.Height = size.Y + border * 2;\r\n            },\r\n            Loaders = reorderLoaders(loaders),\r\n        };\r\n    }\r\n\r\n    private static LayerLoader[] reorderLoaders(LayerLoader[] loaders)\r\n    {\r\n        // follow how LyricEditor to change the sort from input loaders\r\n        return loaders.OrderBy(x =>\r\n        {\r\n            var type = x.GetType().GenericTypeArguments.First();\r\n            return type.Name switch\r\n            {\r\n                nameof(GridLayer) => 0,\r\n                nameof(LyricLayer) => 1,\r\n                nameof(EditLyricLayer) => 2,\r\n                nameof(TimeTagLayer) => 3,\r\n                nameof(CaretLayer) => 4,\r\n                nameof(BlueprintLayer) => 5,\r\n                _ => throw new InvalidOperationException(),\r\n            };\r\n        }).ToArray();\r\n    }\r\n\r\n    #endregion\r\n\r\n    /// <summary>\r\n    /// Follow <see cref=\"LyricEditor\"/> to create the component with necessary DI.\r\n    /// </summary>\r\n    [Cached(typeof(ILyricEditorState))]\r\n    private partial class MockLyricEditorState : Container, ILyricEditorState\r\n    {\r\n        private readonly Bindable<LyricEditorMode> bindableMode = new();\r\n        private readonly Bindable<EditorModeWithEditStep> bindableModeWithEditStep = new();\r\n\r\n        public IBindable<LyricEditorMode> BindableMode => bindableMode;\r\n\r\n        public IBindable<EditorModeWithEditStep> BindableModeWithEditStep => bindableModeWithEditStep;\r\n        public LyricEditorMode Mode => LyricEditorMode.View;\r\n\r\n        [Cached]\r\n        private readonly LyricEditorColourProvider colourProvider = new();\r\n\r\n        [Cached(typeof(ILyricCaretState))]\r\n        private readonly LyricCaretState lyricCaretState = new();\r\n\r\n        [Cached(typeof(IEditRubyModeState))]\r\n        private readonly EditRubyModeState editRubyModeState = new();\r\n\r\n        [Cached(typeof(IEditTimeTagModeState))]\r\n        private readonly EditTimeTagModeState editTimeTagModeState = new();\r\n\r\n        /// <summary>\r\n        /// Add the DI into children here for prevent child is removed if call Children = [...] outside.\r\n        /// </summary>\r\n        protected override void LoadComplete()\r\n        {\r\n            base.LoadComplete();\r\n\r\n            // global state\r\n            AddInternal(lyricCaretState);\r\n\r\n            // state for target mode only.\r\n            AddInternal(editRubyModeState);\r\n            AddInternal(editTimeTagModeState);\r\n        }\r\n\r\n        public void SwitchMode(LyricEditorMode mode)\r\n            => bindableMode.Value = mode;\r\n\r\n        public void SwitchEditStep<TEditStep>(TEditStep editStep) where TEditStep : Enum\r\n        {\r\n            bindableModeWithEditStep.Value = new EditorModeWithEditStep\r\n            {\r\n                Mode = bindableMode.Value,\r\n                EditStep = editStep,\r\n                Default = false,\r\n            };\r\n        }\r\n\r\n        public void NavigateToFix(LyricEditorMode mode)\r\n            => bindableMode.Value = mode;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Screens/Edit/Beatmap/Lyrics/Content/TestScenePreviewKaraokeSpriteText.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Primitives;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.IO.Stores;\r\nusing osu.Game.Graphics;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Utils;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Content.Components.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\nusing osu.Game.Tests.Visual;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Screens.Edit.Beatmap.Lyrics.Content;\r\n\r\npublic partial class TestScenePreviewKaraokeSpriteText : OsuTestScene\r\n{\r\n    private PreviewKaraokeSpriteText karaokeSpriteText = null!;\r\n    private Container mask = null!;\r\n    private OsuSpriteText spriteText = null!;\r\n\r\n    private Action? updateAction;\r\n\r\n    private readonly Lyric lyric = new()\r\n    {\r\n        Text = \"カラオケ\",\r\n        TimeTags = TestCaseTagHelper.ParseTimeTags(new[] { \"[0,start]:1000#^ka\", \"[1,start]:2000#ra\", \"[2,start]:3000#o\", \"[3,start]:4000#ke\", \"[3,end]:5000\" }),\r\n        RubyTags = TestCaseTagHelper.ParseRubyTags(new[] { \"[0]:か\", \"[1]:ら\", \"[2]:お\", \"[3]:け\" }),\r\n    };\r\n\r\n    protected override void Update()\r\n    {\r\n        updateAction?.Invoke();\r\n        base.Update();\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(OsuGameBase game, OsuColour colour)\r\n    {\r\n        game.Resources.AddStore(new NamespacedResourceStore<byte[]>(new ShaderResourceStore(), \"Resources\"));\r\n\r\n        Child = new Container\r\n        {\r\n            Anchor = Anchor.Centre,\r\n            Origin = Anchor.Centre,\r\n            Scale = new Vector2(2),\r\n            Width = 200,\r\n            Height = 100,\r\n            Children = new Drawable[]\r\n            {\r\n                new Box\r\n                {\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    Colour = colour.BlueDarker,\r\n                },\r\n                new Container\r\n                {\r\n                    Padding = new MarginPadding\r\n                    {\r\n                        Vertical = 8,\r\n                        Horizontal = 24,\r\n                    },\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    Children = new Drawable[]\r\n                    {\r\n                        karaokeSpriteText = new PreviewKaraokeSpriteText(lyric),\r\n                        mask = new Container\r\n                        {\r\n                            Masking = true,\r\n                            BorderThickness = 1,\r\n                            BorderColour = colour.RedDarker,\r\n                            Child = new Box\r\n                            {\r\n                                RelativeSizeAxes = Axes.Both,\r\n                                Colour = colour.RedDarker,\r\n                                Alpha = 0.3f,\r\n                            },\r\n                        },\r\n                    },\r\n                },\r\n                spriteText = new OsuSpriteText\r\n                {\r\n                    Anchor = Anchor.BottomLeft,\r\n                    Origin = Anchor.BottomLeft,\r\n                    Font = OsuFont.GetFont(size: 20),\r\n                },\r\n            },\r\n        };\r\n    }\r\n\r\n    [SetUp]\r\n    public void Setup()\r\n    {\r\n        Schedule(() =>\r\n        {\r\n            updateAction = null;\r\n\r\n            mask.Hide();\r\n            spriteText.Hide();\r\n        });\r\n    }\r\n\r\n    #region Text char index\r\n\r\n    [Test]\r\n    public void TestGetCharIndexByPosition()\r\n    {\r\n        AddStep(\"Show Char index Position\", () =>\r\n        {\r\n            triggerUpdate(() =>\r\n            {\r\n                var mousePosition = getMousePosition();\r\n                int? charIndex = karaokeSpriteText.GetCharIndexByPosition(mousePosition);\r\n                updateText(charIndex.ToString());\r\n\r\n                if (charIndex == null)\r\n                {\r\n                    hidePosition();\r\n                }\r\n                else\r\n                {\r\n                    var position = karaokeSpriteText.GetRectByCharIndex(charIndex.Value);\r\n                    showPosition(position);\r\n                }\r\n            });\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestGetRectByCharIndex()\r\n    {\r\n        for (int i = 0; i < lyric.Text.Length; i++)\r\n        {\r\n            int charIndex = i;\r\n            AddStep($\"Show Char index Position {i}\", () =>\r\n            {\r\n                var position = karaokeSpriteText.GetRectByCharIndex(charIndex);\r\n                showPosition(position);\r\n            });\r\n        }\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Text indicator\r\n\r\n    [Test]\r\n    public void TestGetCharIndicatorByPosition()\r\n    {\r\n        AddStep(\"Show char indicator position\", () =>\r\n        {\r\n            triggerUpdate(() =>\r\n            {\r\n                var mousePosition = getMousePosition();\r\n                int? charIndex = karaokeSpriteText.GetCharIndicatorByPosition(mousePosition);\r\n                updateText(charIndex.ToString());\r\n\r\n                if (charIndex == null)\r\n                {\r\n                    hidePosition();\r\n                }\r\n                else\r\n                {\r\n                    var position = karaokeSpriteText.GetRectByCharIndicator(charIndex.Value);\r\n                    showPosition(position);\r\n                }\r\n            });\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestGetRectByCharIndicator()\r\n    {\r\n        for (int i = 0; i <= lyric.Text.Length; i++)\r\n        {\r\n            int charIndex = i;\r\n            AddStep($\"Show char indicator position: {i}\", () =>\r\n            {\r\n                var position = karaokeSpriteText.GetRectByCharIndicator(charIndex);\r\n                showPosition(position);\r\n            });\r\n        }\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Ruby tag\r\n\r\n    [Test]\r\n    public void TestGetRubyPosition()\r\n    {\r\n        foreach (var rubyTag in lyric.RubyTags)\r\n        {\r\n            AddStep($\"Show ruby-tag position: {RubyTagUtils.PositionFormattedString(rubyTag)}\", () =>\r\n            {\r\n                var position = karaokeSpriteText.GetRubyTagByPosition(rubyTag);\r\n                showPosition(position);\r\n            });\r\n        }\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Time tag\r\n\r\n    [Test]\r\n    public void TestGetTimeTagByPosition()\r\n    {\r\n        AddStep(\"Show time-tag position\", () =>\r\n        {\r\n            triggerUpdate(() =>\r\n            {\r\n                var mousePosition = getMousePosition();\r\n                var timeTag = karaokeSpriteText.GetTimeTagByPosition(mousePosition);\r\n                updateText(timeTag != null ? TimeTagUtils.FormattedString(timeTag) : null);\r\n\r\n                if (timeTag == null)\r\n                {\r\n                    hidePosition();\r\n                }\r\n                else\r\n                {\r\n                    var position = karaokeSpriteText.GetPositionByTimeTag(timeTag);\r\n                    showPosition(position);\r\n                }\r\n            });\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestGetPositionByTimeTag()\r\n    {\r\n        foreach (var timeTag in lyric.TimeTags)\r\n        {\r\n            AddStep($\"Show time-tag position {TimeTagUtils.FormattedString(timeTag)}\", () =>\r\n            {\r\n                var position = karaokeSpriteText.GetPositionByTimeTag(timeTag);\r\n                showPosition(position);\r\n            });\r\n        }\r\n    }\r\n\r\n    #endregion\r\n\r\n    [TearDown]\r\n    public void TearDown()\r\n    {\r\n        Schedule(() =>\r\n        {\r\n            updateAction = null;\r\n        });\r\n    }\r\n\r\n    private Vector2 getMousePosition()\r\n    {\r\n        var position = GetContainingInputManager().CurrentState.Mouse.Position;\r\n        return karaokeSpriteText.ToLocalSpace(position);\r\n    }\r\n\r\n    private void triggerUpdate(Action action)\r\n    {\r\n        updateAction = action;\r\n    }\r\n\r\n    private void updateText(string? text)\r\n    {\r\n        spriteText.Text = text ?? \"-\";\r\n\r\n        spriteText.Show();\r\n    }\r\n\r\n    private void hidePosition()\r\n    {\r\n        mask.Hide();\r\n    }\r\n\r\n    private void showPosition(Vector2 position)\r\n    {\r\n        const float sizing = 5;\r\n        showPosition(new RectangleF(position.X - sizing / 2, position.Y - sizing / 2, sizing, sizing));\r\n    }\r\n\r\n    private void showPosition(RectangleF? position)\r\n    {\r\n        if (position == null)\r\n        {\r\n            mask.Hide();\r\n        }\r\n        else\r\n        {\r\n            mask.Position = position.Value.TopLeft;\r\n            mask.Size = position.Value.Size;\r\n\r\n            mask.Show();\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Screens/Edit/Beatmap/Lyrics/LyricEditorTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Screens.Edit.Beatmap.Lyrics;\r\n\r\npublic class LyricEditorTest\r\n{\r\n    [TestCase(LyricEditorLayout.List, LyricEditorLayout.List, LyricEditorLayout.List)]\r\n    [TestCase(LyricEditorLayout.Compose, LyricEditorLayout.Compose, LyricEditorLayout.Compose)]\r\n    [TestCase(LyricEditorLayout.List | LyricEditorLayout.Compose, LyricEditorLayout.List, LyricEditorLayout.List)]\r\n    [TestCase(LyricEditorLayout.List | LyricEditorLayout.Compose, LyricEditorLayout.Compose, LyricEditorLayout.Compose)]\r\n    [TestCase(LyricEditorLayout.List, LyricEditorLayout.Compose, LyricEditorLayout.List)] // should use the support layout if prefer layout is not matched.\r\n    [TestCase(LyricEditorLayout.Compose, LyricEditorLayout.List, LyricEditorLayout.Compose)]\r\n    public void TestGetSuitableLayout(LyricEditorLayout supportedLayout, LyricEditorLayout preferLayout, LyricEditorLayout actualLayout)\r\n    {\r\n        var expectedLayout = LyricEditor.GetSuitableLayout(supportedLayout, preferLayout);\r\n        Assert.That(actualLayout, Is.EqualTo(expectedLayout));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Screens/Edit/Beatmap/Lyrics/LyricEditorVerifierTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Globalization;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Framework.Testing;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics;\r\nusing osu.Game.Screens.Edit;\r\nusing osu.Game.Tests.Visual;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Screens.Edit.Beatmap.Lyrics;\r\n\r\n[HeadlessTest]\r\npublic partial class LyricEditorVerifierTest : EditorClockTestScene\r\n{\r\n    private Lyric internalLyric = null!;\r\n\r\n    private LyricEditorVerifier verifier = null!;\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        var beatmap = new KaraokeBeatmap\r\n        {\r\n            BeatmapInfo =\r\n            {\r\n                Ruleset = new KaraokeRuleset().RulesetInfo,\r\n            },\r\n        };\r\n        var editorBeatmap = new EditorBeatmap(beatmap);\r\n        Dependencies.Cache(editorBeatmap);\r\n\r\n        Add(editorBeatmap);\r\n    }\r\n\r\n    [SetUp]\r\n    public virtual void SetUp()\r\n    {\r\n        internalLyric = createLyricWithLanguageIssueOnly();\r\n\r\n        AddStep(\"Setup\", () =>\r\n        {\r\n            // Reset editor beatmap.\r\n            var editorBeatmap = Dependencies.Get<EditorBeatmap>();\r\n            editorBeatmap.Clear();\r\n            editorBeatmap.SelectedHitObjects.Clear();\r\n            editorBeatmap.Add(internalLyric);\r\n\r\n            // Initialize the verifier.\r\n            RemoveAll(x => x is LyricEditorVerifier, true);\r\n            Add(verifier = new LyricEditorVerifier());\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestFirstLoad()\r\n    {\r\n        // Check should contains language-related issue in the verifier.\r\n        assertHitObjectIssueAmount(internalLyric, 1);\r\n        assertEditModeIssueAmount(LyricEditorMode.EditLanguage, 1);\r\n\r\n        // Should not contains issue in other edit mode.\r\n        assertEditModeIssueAmount(LyricEditorMode.EditText, 0);\r\n    }\r\n\r\n    [Test]\r\n    public void TestAdd()\r\n    {\r\n        var newLyric = createLyricWithLanguageIssueOnly();\r\n\r\n        updateEditorBeatmap(editorBeatmap =>\r\n        {\r\n            editorBeatmap.Add(newLyric);\r\n        });\r\n\r\n        assertHitObjectIssueAmount(internalLyric, 1);\r\n        assertHitObjectIssueAmount(newLyric, 1);\r\n        assertEditModeIssueAmount(LyricEditorMode.EditLanguage, 2);\r\n    }\r\n\r\n    [Test]\r\n    public void TestRemove()\r\n    {\r\n        updateEditorBeatmap(editorBeatmap =>\r\n        {\r\n            editorBeatmap.Remove(internalLyric);\r\n        });\r\n\r\n        assertEditModeIssueAmount(LyricEditorMode.EditLanguage, 0);\r\n    }\r\n\r\n    [Test]\r\n    public void TestUpdate()\r\n    {\r\n        updateEditorBeatmap(editorBeatmap =>\r\n        {\r\n            internalLyric.Language = new CultureInfo(\"ja-JP\");\r\n            editorBeatmap.Update(internalLyric);\r\n        });\r\n\r\n        assertHitObjectIssueAmount(internalLyric, 0);\r\n        assertEditModeIssueAmount(LyricEditorMode.EditLanguage, 0);\r\n    }\r\n\r\n    [Test]\r\n    public void TestRefresh()\r\n    {\r\n        AddStep(\"Fix the language issue and refresh.\", () =>\r\n        {\r\n            internalLyric.Language = new CultureInfo(\"ja-JP\");\r\n            verifier.Refresh();\r\n        });\r\n\r\n        assertHitObjectIssueAmount(internalLyric, 0);\r\n        assertEditModeIssueAmount(LyricEditorMode.EditLanguage, 0);\r\n    }\r\n\r\n    [Test]\r\n    public void TestRefreshByHitObject()\r\n    {\r\n        AddStep(\"Fix the language issue and refresh.\", () =>\r\n        {\r\n            internalLyric.Language = new CultureInfo(\"ja-JP\");\r\n            verifier.RefreshByHitObject(internalLyric);\r\n        });\r\n\r\n        assertHitObjectIssueAmount(internalLyric, 0);\r\n        assertEditModeIssueAmount(LyricEditorMode.EditLanguage, 0);\r\n    }\r\n\r\n    #region Tool\r\n\r\n    private static Lyric createLyricWithLanguageIssueOnly()\r\n        => new()\r\n        {\r\n            Text = \"カラオケ！\",\r\n            TimeTags = new List<TimeTag>\r\n            {\r\n                new(new TextIndex(0), 500),\r\n                new(new TextIndex(4, TextIndex.IndexState.End), 2000),\r\n            },\r\n        };\r\n\r\n    private void updateEditorBeatmap(Action<EditorBeatmap> action)\r\n    {\r\n        AddStep(\"Prepare testing beatmap\", () =>\r\n        {\r\n            var editorBeatmap = Dependencies.Get<EditorBeatmap>();\r\n            action(editorBeatmap);\r\n        });\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Assertion\r\n\r\n    private void assertHitObjectIssueAmount(KaraokeHitObject karaokeHitObject, int issueCount)\r\n    {\r\n        AddStep(\"Check hit-object issue amount.\", () =>\r\n        {\r\n            var editModeIssues = verifier.GetBindable(karaokeHitObject);\r\n            Assert.That(editModeIssues.Count, Is.EqualTo(issueCount));\r\n        });\r\n    }\r\n\r\n    private void assertEditModeIssueAmount(LyricEditorMode editMode, int issueCount)\r\n    {\r\n        AddStep(\"Check edit mode issue amount.\", () =>\r\n        {\r\n            var editModeIssues = verifier.GetIssueByType(editMode);\r\n            Assert.That(editModeIssues.Count, Is.EqualTo(issueCount));\r\n        });\r\n    }\r\n\r\n    #endregion\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Screens/Edit/Beatmap/Lyrics/Settings/TestSceneLyricEditorDescriptionTextFlowContainer.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Cursor;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings.Components.Markdown;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Markdown;\r\nusing osu.Game.Tests.Visual;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Screens.Edit.Beatmap.Lyrics.Settings;\r\n\r\n[TestFixture]\r\npublic partial class TestSceneDescriptionTextFlowContainer : OsuTestScene\r\n{\r\n    [Cached]\r\n    private readonly OverlayColourProvider overlayColourProvider = new(OverlayColourScheme.Blue);\r\n\r\n    private LyricEditorDescriptionTextFlowContainer lyricEditorDescriptionTextFlowContainer = null!;\r\n\r\n    [SetUp]\r\n    public void SetUp() => Schedule(() =>\r\n    {\r\n        Child = new PopoverContainer\r\n        {\r\n            RelativeSizeAxes = Axes.Both,\r\n            Child = lyricEditorDescriptionTextFlowContainer = new LyricEditorDescriptionTextFlowContainer\r\n            {\r\n                Anchor = Anchor.Centre,\r\n                Origin = Anchor.Centre,\r\n                Size = new Vector2(100),\r\n            },\r\n        };\r\n    });\r\n\r\n    [Test]\r\n    public void TestDisplayDescriptionWithEditMode()\r\n    {\r\n        AddStep(\"Markdown description\", () =>\r\n        {\r\n            lyricEditorDescriptionTextFlowContainer.Description = new DescriptionFormat\r\n            {\r\n                Text = $\"Test description with [{DescriptionFormat.LINK_KEY_ACTION}](singer_mode)\",\r\n                Actions = new Dictionary<string, IDescriptionAction>\r\n                {\r\n                    {\r\n                        \"singer_mode\", new SwitchModeDescriptionAction\r\n                        {\r\n                            Text = \"edit text mode\",\r\n                            Mode = LyricEditorMode.EditSinger,\r\n                        }\r\n                    },\r\n                },\r\n            };\r\n        });\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Screens/Edit/Beatmap/Lyrics/States/BaseLyricCaretStateTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Testing;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition.Algorithms;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\nusing osu.Game.Screens.Edit;\r\nusing osu.Game.Tests.Visual;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Screens.Edit.Beatmap.Lyrics.States;\r\n\r\n[HeadlessTest]\r\npublic abstract partial class BaseLyricCaretStateTest : OsuTestScene\r\n{\r\n    private TestLyricEditorState state = null!;\r\n    private EditorBeatmap editorBeatmap = null!;\r\n\r\n    private LyricCaretState lyricCaretState = null!;\r\n\r\n    protected ILyricCaretState LyricCaretState => lyricCaretState;\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        var beatmap = new KaraokeBeatmap\r\n        {\r\n            BeatmapInfo =\r\n            {\r\n                Ruleset = new KaraokeRuleset().RulesetInfo,\r\n            },\r\n        };\r\n\r\n        Dependencies.Cache(editorBeatmap = new EditorBeatmap(beatmap));\r\n        Dependencies.Cache(new EditorClock());\r\n        Dependencies.CacheAs<ILyricEditorState>(state = new TestLyricEditorState());\r\n        Dependencies.CacheAs<IEditTextModeState>(new EditTextModeState());\r\n        Dependencies.CacheAs<IEditRubyModeState>(new EditRubyModeState());\r\n        Dependencies.CacheAs<IEditTimeTagModeState>(new EditTimeTagModeState());\r\n        Dependencies.Cache(new KaraokeRulesetLyricEditorConfigManager());\r\n\r\n        var lyricsProvider = new LyricsProvider();\r\n        Dependencies.CacheAs<ILyricsProvider>(lyricsProvider);\r\n\r\n        Children = new Drawable[]\r\n        {\r\n            state,\r\n            lyricsProvider,\r\n            lyricCaretState = new LyricCaretState(),\r\n        };\r\n    }\r\n\r\n    [SetUp]\r\n    public void Setup()\r\n    {\r\n        AddStep(\"Set-up\", () =>\r\n        {\r\n            state.SwitchMode(LyricEditorMode.View);\r\n        });\r\n    }\r\n\r\n    #region Test utility\r\n\r\n    public void PrepareLyrics(IEnumerable<string> lyricTexts)\r\n    {\r\n        AddStep(\"Prepare lyrics\", () =>\r\n        {\r\n            var lyrics = lyricTexts.Select((x, i) => new Lyric { Text = x, Order = i });\r\n\r\n            editorBeatmap.Clear();\r\n            editorBeatmap.AddRange(lyrics);\r\n        });\r\n    }\r\n\r\n    protected void ChangeMode(TestCaretType type)\r\n    {\r\n        AddStep(\"Switch to edit mode\", () =>\r\n        {\r\n            switch (type)\r\n            {\r\n                case TestCaretType.ViewOnly:\r\n                    state.SwitchMode(LyricEditorMode.View);\r\n                    return;\r\n\r\n                case TestCaretType.CaretEnable:\r\n                    state.SwitchMode(LyricEditorMode.EditReferenceLyric);\r\n                    break;\r\n\r\n                case TestCaretType.CaretWithIndex:\r\n                    state.SwitchMode(LyricEditorMode.EditText);\r\n                    state.SwitchEditStep(TextEditStep.Split);\r\n                    break;\r\n\r\n                case TestCaretType.CaretDraggable:\r\n                    state.SwitchMode(LyricEditorMode.EditText);\r\n                    state.SwitchEditStep(TextEditStep.Typing);\r\n                    break;\r\n\r\n                default:\r\n                    throw new ArgumentOutOfRangeException(nameof(type), type, null);\r\n            }\r\n        });\r\n    }\r\n\r\n    protected Lyric GetLyric(int index)\r\n    {\r\n        var hitObjects = editorBeatmap.HitObjects[index];\r\n\r\n        if (hitObjects is not Lyric lyric)\r\n            throw new InvalidCastException();\r\n\r\n        return lyric;\r\n    }\r\n\r\n    public void PrepareHoverCaretPosition(Func<ICaretPosition?> getPosition)\r\n    {\r\n        AddStep(\"Set hover caret position\", () =>\r\n        {\r\n            if (LyricCaretState.BindableHoverCaretPosition is not Bindable<ICaretPosition?> bindable)\r\n                throw new InvalidOperationException();\r\n\r\n            bindable.Value = getPosition();\r\n        });\r\n    }\r\n\r\n    public void PrepareCaretPosition(Func<ICaretPosition?> getPosition)\r\n    {\r\n        AddStep(\"Set caret position\", () =>\r\n        {\r\n            if (LyricCaretState.BindableCaretPosition is not Bindable<ICaretPosition?> bindable)\r\n                throw new InvalidOperationException();\r\n\r\n            bindable.Value = getPosition();\r\n        });\r\n    }\r\n\r\n    public void PrepareRangeCaretPosition(Func<RangeCaretPosition?> getPosition)\r\n    {\r\n        AddStep(\"Set range caret position\", () =>\r\n        {\r\n            if (LyricCaretState.BindableRangeCaretPosition is not Bindable<RangeCaretPosition?> bindable)\r\n                throw new InvalidOperationException();\r\n\r\n            bindable.Value = getPosition();\r\n        });\r\n    }\r\n\r\n    protected void AssertHoverCaretPosition(Func<ICaretPosition?> getPosition)\r\n    {\r\n        AddAssert(\"Assert hover caret position\", () => EqualityComparer<ICaretPosition?>.Default.Equals(getPosition(), LyricCaretState.HoverCaretPosition));\r\n    }\r\n\r\n    protected void AssertCaretPosition(Func<ICaretPosition?> getPosition)\r\n    {\r\n        AddAssert(\"Assert caret position\", () => EqualityComparer<ICaretPosition?>.Default.Equals(getPosition(), LyricCaretState.CaretPosition));\r\n    }\r\n\r\n    protected void AssertDraggableCaretPosition(Func<RangeCaretPosition?> getPosition)\r\n    {\r\n        AddAssert(\"Assert range caret position\", () => EqualityComparer<RangeCaretPosition?>.Default.Equals(getPosition(), LyricCaretState.RangeCaretPosition));\r\n    }\r\n\r\n    #endregion\r\n\r\n    private partial class TestLyricEditorState : Component, ILyricEditorState\r\n    {\r\n        private readonly Bindable<LyricEditorMode> bindableMode = new();\r\n\r\n        private readonly Bindable<EditorModeWithEditStep> bindableModeWithEditStep = new();\r\n\r\n        public IBindable<LyricEditorMode> BindableMode => bindableMode;\r\n\r\n        public IBindable<EditorModeWithEditStep> BindableModeWithEditStep => bindableModeWithEditStep;\r\n\r\n        public LyricEditorMode Mode => bindableMode.Value;\r\n\r\n        [Resolved]\r\n        private IEditTextModeState editTextModeState { get; set; } = null!;\r\n\r\n        public void SwitchMode(LyricEditorMode mode)\r\n        {\r\n            bindableMode.Value = mode;\r\n\r\n            bindableMode.BindValueChanged(_ =>\r\n            {\r\n                updateModeWithEditStep();\r\n            }, true);\r\n            editTextModeState.BindableEditStep.BindValueChanged(e =>\r\n            {\r\n                updateModeWithEditStep();\r\n            });\r\n        }\r\n\r\n        private void updateModeWithEditStep()\r\n        {\r\n            bindableModeWithEditStep.Value = new EditorModeWithEditStep\r\n            {\r\n                Mode = bindableMode.Value,\r\n                EditStep = getTheEditStep(bindableMode.Value),\r\n                Default = false,\r\n            };\r\n\r\n            Enum? getTheEditStep(LyricEditorMode mode) =>\r\n                mode switch\r\n                {\r\n                    LyricEditorMode.View => null,\r\n                    LyricEditorMode.EditText => editTextModeState.EditStep,\r\n                    LyricEditorMode.EditReferenceLyric => null,\r\n                    LyricEditorMode.EditLanguage => throw new NotSupportedException(),\r\n                    LyricEditorMode.EditRuby => throw new NotSupportedException(),\r\n                    LyricEditorMode.EditTimeTag => throw new NotSupportedException(),\r\n                    LyricEditorMode.EditRomanisation => throw new NotSupportedException(),\r\n                    LyricEditorMode.EditNote => throw new NotSupportedException(),\r\n                    LyricEditorMode.EditSinger => throw new NotSupportedException(),\r\n                    _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null),\r\n                };\r\n        }\r\n\r\n        public void SwitchEditStep<TEditStep>(TEditStep editStep) where TEditStep : Enum\r\n        {\r\n            if (editStep is not TextEditStep textEditStep)\r\n                throw new NotSupportedException();\r\n\r\n            editTextModeState.BindableEditStep.Value = textEditStep;\r\n        }\r\n\r\n        public void NavigateToFix(LyricEditorMode mode)\r\n            => throw new NotSupportedException();\r\n    }\r\n}\r\n\r\n/// <summary>\r\n/// The main goal of the test case is testing the behavior if using different <see cref=\"ICaretPositionAlgorithm\"/>\r\n/// Not test all the <see cref=\"ICaretPositionAlgorithm\"/>\r\n/// So we choose the some representative <see cref=\"ICaretPositionAlgorithm\"/>.\r\n/// </summary>\r\npublic enum TestCaretType\r\n{\r\n    ViewOnly,\r\n\r\n    CaretEnable,\r\n\r\n    CaretWithIndex,\r\n\r\n    CaretDraggable,\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Screens/Edit/Beatmap/Lyrics/States/LyricCaretStateActionTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Screens.Edit.Beatmap.Lyrics.States;\r\n\r\npublic partial class LyricCaretStateActionTest : BaseLyricCaretStateTest\r\n{\r\n    #region Action with lyric\r\n\r\n    [Test]\r\n    public void TestGetCaretPositionByActionWithViewOnlyMode()\r\n    {\r\n        PrepareLyrics(new[] { \"Lyric1\" });\r\n        ChangeMode(TestCaretType.ViewOnly);\r\n\r\n        // All caret position should be null.\r\n        AssertGetCaretPositionByAction(MovingCaretAction.PreviousLyric, () => null);\r\n        AssertGetCaretPositionByAction(MovingCaretAction.NextLyric, () => null);\r\n        AssertGetCaretPositionByAction(MovingCaretAction.FirstLyric, () => null);\r\n        AssertGetCaretPositionByAction(MovingCaretAction.LastLyric, () => null);\r\n        AssertGetCaretPositionByAction(MovingCaretAction.PreviousIndex, () => null);\r\n        AssertGetCaretPositionByAction(MovingCaretAction.NextIndex, () => null);\r\n        AssertGetCaretPositionByAction(MovingCaretAction.FirstIndex, () => null);\r\n        AssertGetCaretPositionByAction(MovingCaretAction.LastIndex, () => null);\r\n    }\r\n\r\n    [Test]\r\n    public void TestGetCaretPositionByActionWithCaretEnableMode()\r\n    {\r\n        PrepareLyrics(new[] { \"Lyric1\", \"Lyric2\" });\r\n        ChangeMode(TestCaretType.CaretEnable);\r\n\r\n        // previous lyric.\r\n        PrepareCaretPosition(() => new NavigateCaretPosition(GetLyric(0)));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.PreviousLyric, () => null);\r\n        PrepareCaretPosition(() => new NavigateCaretPosition(GetLyric(1)));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.PreviousLyric, () => new NavigateCaretPosition(GetLyric(0)));\r\n\r\n        // next lyric.\r\n        PrepareCaretPosition(() => new NavigateCaretPosition(GetLyric(0)));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.NextLyric, () => new NavigateCaretPosition(GetLyric(1)));\r\n        PrepareCaretPosition(() => new NavigateCaretPosition(GetLyric(1)));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.NextLyric, () => null);\r\n\r\n        // first lyric.\r\n        PrepareCaretPosition(() => new NavigateCaretPosition(GetLyric(0)));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.FirstLyric, () => new NavigateCaretPosition(GetLyric(0)));\r\n        PrepareCaretPosition(() => new NavigateCaretPosition(GetLyric(1)));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.FirstLyric, () => new NavigateCaretPosition(GetLyric(0)));\r\n\r\n        // last lyric.\r\n        PrepareCaretPosition(() => new NavigateCaretPosition(GetLyric(0)));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.LastLyric, () => new NavigateCaretPosition(GetLyric(1)));\r\n        PrepareCaretPosition(() => new NavigateCaretPosition(GetLyric(1)));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.LastLyric, () => new NavigateCaretPosition(GetLyric(1)));\r\n\r\n        // should not have value in the previous index.\r\n        PrepareCaretPosition(() => new NavigateCaretPosition(GetLyric(0)));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.PreviousIndex, () => null);\r\n\r\n        // should not have value in the next index.\r\n        PrepareCaretPosition(() => new NavigateCaretPosition(GetLyric(0)));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.NextIndex, () => null);\r\n\r\n        // should not have value in the first index.\r\n        PrepareCaretPosition(() => new NavigateCaretPosition(GetLyric(0)));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.FirstIndex, () => null);\r\n\r\n        // should not have value in the last index.\r\n        PrepareCaretPosition(() => new NavigateCaretPosition(GetLyric(0)));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.LastIndex, () => null);\r\n    }\r\n\r\n    [Test]\r\n    public void TestGetCaretPositionByActionWithCaretWithIndexMode()\r\n    {\r\n        PrepareLyrics(new[] { \"Lyric1\", \"Lyric2\" });\r\n        ChangeMode(TestCaretType.CaretWithIndex);\r\n\r\n        // previous lyric.\r\n        PrepareCaretPosition(() => new CuttingCaretPosition(GetLyric(0), 1));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.PreviousLyric, () => null);\r\n        PrepareCaretPosition(() => new CuttingCaretPosition(GetLyric(1), 1));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.PreviousLyric, () => new CuttingCaretPosition(GetLyric(0), 1));\r\n\r\n        // next lyric.\r\n        PrepareCaretPosition(() => new CuttingCaretPosition(GetLyric(0), 1));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.NextLyric, () => new CuttingCaretPosition(GetLyric(1), 1));\r\n        PrepareCaretPosition(() => new CuttingCaretPosition(GetLyric(1), 1));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.NextLyric, () => null);\r\n\r\n        // first lyric.\r\n        PrepareCaretPosition(() => new CuttingCaretPosition(GetLyric(0), 1));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.FirstLyric, () => new CuttingCaretPosition(GetLyric(0), 1));\r\n        PrepareCaretPosition(() => new CuttingCaretPosition(GetLyric(1), 1));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.FirstLyric, () => new CuttingCaretPosition(GetLyric(0), 1));\r\n\r\n        // last lyric.\r\n        PrepareCaretPosition(() => new CuttingCaretPosition(GetLyric(0), 1));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.LastLyric, () => new CuttingCaretPosition(GetLyric(1), 5));\r\n        PrepareCaretPosition(() => new CuttingCaretPosition(GetLyric(1), 1));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.LastLyric, () => new CuttingCaretPosition(GetLyric(1), 5));\r\n\r\n        // previous index.\r\n        PrepareCaretPosition(() => new CuttingCaretPosition(GetLyric(0), 1));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.PreviousIndex, () => null);\r\n        PrepareCaretPosition(() => new CuttingCaretPosition(GetLyric(0), 5));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.PreviousIndex, () => new CuttingCaretPosition(GetLyric(0), 4));\r\n        PrepareCaretPosition(() => new CuttingCaretPosition(GetLyric(1), 1)); // should change to the previous lyric\r\n        AssertGetCaretPositionByAction(MovingCaretAction.PreviousIndex, () => new CuttingCaretPosition(GetLyric(0), 5));\r\n\r\n        // next index.\r\n        PrepareCaretPosition(() => new CuttingCaretPosition(GetLyric(1), 1));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.NextIndex, () => new CuttingCaretPosition(GetLyric(1), 2));\r\n        PrepareCaretPosition(() => new CuttingCaretPosition(GetLyric(1), 5));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.NextIndex, () => null);\r\n        PrepareCaretPosition(() => new CuttingCaretPosition(GetLyric(0), 5)); // should change to the next lyric\r\n        AssertGetCaretPositionByAction(MovingCaretAction.NextIndex, () => new CuttingCaretPosition(GetLyric(1), 1));\r\n\r\n        // first index.\r\n        PrepareCaretPosition(() => new CuttingCaretPosition(GetLyric(0), 1));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.FirstIndex, () => null);\r\n        PrepareCaretPosition(() => new CuttingCaretPosition(GetLyric(0), 5));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.FirstIndex, () => new CuttingCaretPosition(GetLyric(0), 1));\r\n        PrepareCaretPosition(() => new CuttingCaretPosition(GetLyric(1), 1)); // should change to the previous lyric\r\n        AssertGetCaretPositionByAction(MovingCaretAction.FirstIndex, () => new CuttingCaretPosition(GetLyric(0), 1));\r\n\r\n        // last index.\r\n        PrepareCaretPosition(() => new CuttingCaretPosition(GetLyric(1), 1));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.LastIndex, () => new CuttingCaretPosition(GetLyric(1), 5));\r\n        PrepareCaretPosition(() => new CuttingCaretPosition(GetLyric(1), 5));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.LastIndex, () => null);\r\n        PrepareCaretPosition(() => new CuttingCaretPosition(GetLyric(0), 5)); // should change to the next lyric\r\n        AssertGetCaretPositionByAction(MovingCaretAction.LastIndex, () => new CuttingCaretPosition(GetLyric(1), 5));\r\n    }\r\n\r\n    [Test]\r\n    public void TestGetCaretPositionByActionWithCaretDraggableMode()\r\n    {\r\n        PrepareLyrics(new[] { \"Lyric1\", \"Lyric2\" });\r\n        ChangeMode(TestCaretType.CaretDraggable);\r\n\r\n        // previous lyric.\r\n        PrepareCaretPosition(() => new TypingCaretPosition(GetLyric(0), 0));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.PreviousLyric, () => null);\r\n        PrepareCaretPosition(() => new TypingCaretPosition(GetLyric(1), 0));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.PreviousLyric, () => new TypingCaretPosition(GetLyric(0), 0));\r\n\r\n        // next lyric.\r\n        PrepareCaretPosition(() => new TypingCaretPosition(GetLyric(0), 0));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.NextLyric, () => new TypingCaretPosition(GetLyric(1), 0));\r\n        PrepareCaretPosition(() => new TypingCaretPosition(GetLyric(1), 0));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.NextLyric, () => null);\r\n\r\n        // first lyric.\r\n        PrepareCaretPosition(() => new TypingCaretPosition(GetLyric(0), 0));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.FirstLyric, () => new TypingCaretPosition(GetLyric(0), 0));\r\n        PrepareCaretPosition(() => new TypingCaretPosition(GetLyric(1), 0));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.FirstLyric, () => new TypingCaretPosition(GetLyric(0), 0));\r\n\r\n        // last lyric.\r\n        PrepareCaretPosition(() => new TypingCaretPosition(GetLyric(0), 0));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.LastLyric, () => new TypingCaretPosition(GetLyric(1), 6));\r\n        PrepareCaretPosition(() => new TypingCaretPosition(GetLyric(1), 0));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.LastLyric, () => new TypingCaretPosition(GetLyric(1), 6));\r\n\r\n        // previous index.\r\n        PrepareCaretPosition(() => new TypingCaretPosition(GetLyric(0), 0));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.PreviousIndex, () => null);\r\n        PrepareCaretPosition(() => new TypingCaretPosition(GetLyric(0), 6));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.PreviousIndex, () => new TypingCaretPosition(GetLyric(0), 5));\r\n        PrepareCaretPosition(() => new TypingCaretPosition(GetLyric(1), 0)); // should change to the previous lyric\r\n        AssertGetCaretPositionByAction(MovingCaretAction.PreviousIndex, () => new TypingCaretPosition(GetLyric(0), 6));\r\n\r\n        // next index.\r\n        PrepareCaretPosition(() => new TypingCaretPosition(GetLyric(1), 0));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.NextIndex, () => new TypingCaretPosition(GetLyric(1), 1));\r\n        PrepareCaretPosition(() => new TypingCaretPosition(GetLyric(1), 6));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.NextIndex, () => null);\r\n        PrepareCaretPosition(() => new TypingCaretPosition(GetLyric(0), 6)); // should change to the next lyric\r\n        AssertGetCaretPositionByAction(MovingCaretAction.NextIndex, () => new TypingCaretPosition(GetLyric(1), 0));\r\n\r\n        // first index.\r\n        PrepareCaretPosition(() => new TypingCaretPosition(GetLyric(0), 0));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.FirstIndex, () => null);\r\n        PrepareCaretPosition(() => new TypingCaretPosition(GetLyric(0), 6));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.FirstIndex, () => new TypingCaretPosition(GetLyric(0), 0));\r\n        PrepareCaretPosition(() => new TypingCaretPosition(GetLyric(1), 0)); // should change to the previous lyric\r\n        AssertGetCaretPositionByAction(MovingCaretAction.FirstIndex, () => new TypingCaretPosition(GetLyric(0), 0));\r\n\r\n        // last index.\r\n        PrepareCaretPosition(() => new TypingCaretPosition(GetLyric(1), 0));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.LastIndex, () => new TypingCaretPosition(GetLyric(1), 6));\r\n        PrepareCaretPosition(() => new TypingCaretPosition(GetLyric(1), 6));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.LastIndex, () => null);\r\n        PrepareCaretPosition(() => new TypingCaretPosition(GetLyric(0), 6)); // should change to the next lyric\r\n        AssertGetCaretPositionByAction(MovingCaretAction.LastIndex, () => new TypingCaretPosition(GetLyric(1), 6));\r\n    }\r\n\r\n    [Test]\r\n    public void TestGetCaretPositionByActionWithCaretDraggableModeWithDragRange()\r\n    {\r\n        PrepareLyrics(new[] { \"Lyric1\", \"Lyric2\", \"Lyric3\" });\r\n        ChangeMode(TestCaretType.CaretDraggable);\r\n\r\n        // Testing the case without selecting the whole lyric.\r\n        PrepareRangeCaretPosition(() =>\r\n        {\r\n            var startIndex = new TypingCaretPosition(GetLyric(1), 1);\r\n            var endIndex = new TypingCaretPosition(GetLyric(1), 5);\r\n\r\n            return new RangeCaretPosition(startIndex, endIndex, RangeCaretDraggingState.EndDrag);\r\n        });\r\n        AssertGetCaretPositionByAction(MovingCaretAction.PreviousLyric, () => new TypingCaretPosition(GetLyric(0), 1));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.NextLyric, () => new TypingCaretPosition(GetLyric(2), 1));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.FirstLyric, () => new TypingCaretPosition(GetLyric(0), 0));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.LastLyric, () => new TypingCaretPosition(GetLyric(2), 6));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.PreviousIndex, () => new TypingCaretPosition(GetLyric(1), 1));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.NextIndex, () => new TypingCaretPosition(GetLyric(1), 5));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.FirstIndex, () => new TypingCaretPosition(GetLyric(1), 0));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.LastIndex, () => new TypingCaretPosition(GetLyric(1), 6));\r\n\r\n        // Testing the case with select the whole lyric.\r\n        PrepareRangeCaretPosition(() =>\r\n        {\r\n            var startIndex = new TypingCaretPosition(GetLyric(1), 0);\r\n            var endIndex = new TypingCaretPosition(GetLyric(1), 6);\r\n\r\n            return new RangeCaretPosition(startIndex, endIndex, RangeCaretDraggingState.EndDrag);\r\n        });\r\n        AssertGetCaretPositionByAction(MovingCaretAction.PreviousLyric, () => new TypingCaretPosition(GetLyric(0), 0));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.NextLyric, () => new TypingCaretPosition(GetLyric(2), 0));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.FirstLyric, () => new TypingCaretPosition(GetLyric(0), 0));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.LastLyric, () => new TypingCaretPosition(GetLyric(2), 6));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.PreviousIndex, () => new TypingCaretPosition(GetLyric(1), 0));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.NextIndex, () => new TypingCaretPosition(GetLyric(1), 6));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.FirstIndex, () => new TypingCaretPosition(GetLyric(1), 0));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.LastIndex, () => new TypingCaretPosition(GetLyric(1), 6));\r\n\r\n        // Now, test the case with single lyric.\r\n        PrepareLyrics(new[] { \"Lyric1\" });\r\n\r\n        // Testing the case without selecting the whole lyric.\r\n        PrepareRangeCaretPosition(() =>\r\n        {\r\n            var startIndex = new TypingCaretPosition(GetLyric(0), 1);\r\n            var endIndex = new TypingCaretPosition(GetLyric(0), 5);\r\n\r\n            return new RangeCaretPosition(startIndex, endIndex, RangeCaretDraggingState.EndDrag);\r\n        });\r\n        AssertGetCaretPositionByAction(MovingCaretAction.PreviousLyric, () => null);\r\n        AssertGetCaretPositionByAction(MovingCaretAction.NextLyric, () => null);\r\n        AssertGetCaretPositionByAction(MovingCaretAction.FirstLyric, () => new TypingCaretPosition(GetLyric(0), 0));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.LastLyric, () => new TypingCaretPosition(GetLyric(0), 6));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.PreviousIndex, () => new TypingCaretPosition(GetLyric(0), 1));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.NextIndex, () => new TypingCaretPosition(GetLyric(0), 5));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.FirstIndex, () => new TypingCaretPosition(GetLyric(0), 0));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.LastIndex, () => new TypingCaretPosition(GetLyric(0), 6));\r\n\r\n        // Testing the case with select the whole lyric.\r\n        PrepareRangeCaretPosition(() =>\r\n        {\r\n            var startIndex = new TypingCaretPosition(GetLyric(0), 0);\r\n            var endIndex = new TypingCaretPosition(GetLyric(0), 6);\r\n\r\n            return new RangeCaretPosition(startIndex, endIndex, RangeCaretDraggingState.EndDrag);\r\n        });\r\n        AssertGetCaretPositionByAction(MovingCaretAction.PreviousLyric, () => null);\r\n        AssertGetCaretPositionByAction(MovingCaretAction.NextLyric, () => null);\r\n        AssertGetCaretPositionByAction(MovingCaretAction.FirstLyric, () => new TypingCaretPosition(GetLyric(0), 0));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.LastLyric, () => new TypingCaretPosition(GetLyric(0), 6));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.PreviousIndex, () => new TypingCaretPosition(GetLyric(0), 0));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.NextIndex, () => new TypingCaretPosition(GetLyric(0), 6));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.FirstIndex, () => new TypingCaretPosition(GetLyric(0), 0));\r\n        AssertGetCaretPositionByAction(MovingCaretAction.LastIndex, () => new TypingCaretPosition(GetLyric(0), 6));\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Action with not lyric\r\n\r\n    [Test]\r\n    public void TestGetCaretPositionByActionWithNoLyric([Values] TestCaretType type, [Values] MovingCaretAction action)\r\n    {\r\n        PrepareLyrics(Array.Empty<string>());\r\n        ChangeMode(type);\r\n\r\n        // make sure that action should not cause the exception and should not change the caret position.\r\n        AssertGetCaretPositionByAction(action, () => null);\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Test utility\r\n\r\n    protected void AssertGetCaretPositionByAction(MovingCaretAction action, Func<ICaretPosition?> getPosition)\r\n    {\r\n        AddAssert(\"Assert caret position by action\", () =>\r\n        {\r\n            var expectedCaret = getPosition();\r\n            var actualCaret = LyricCaretState.GetCaretPositionByAction(action);\r\n\r\n            return EqualityComparer<ICaretPosition?>.Default.Equals(expectedCaret, actualCaret);\r\n        });\r\n    }\r\n\r\n    #endregion\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Screens/Edit/Beatmap/Lyrics/States/LyricCaretStateMoveCaretTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Screens.Edit.Beatmap.Lyrics.States;\r\n\r\npublic partial class LyricCaretStateMoveCaretTest : BaseLyricCaretStateTest\r\n{\r\n    #region Hover lyric\r\n\r\n    [Test]\r\n    public void TestMoveHoverCaretToTargetPosition()\r\n    {\r\n        PrepareLyrics(new[] { \"Lyric1\", \"Lyric2\" });\r\n\r\n        // view only.\r\n        ChangeMode(TestCaretType.ViewOnly);\r\n        MoveHoverCaretToTargetPosition(() => GetLyric(1));\r\n\r\n        AssertHoverCaretPosition(() => null);\r\n        AssertCaretPosition(() => null);\r\n        AssertDraggableCaretPosition(() => null);\r\n\r\n        // caret enable.\r\n        ChangeMode(TestCaretType.CaretEnable);\r\n        MoveHoverCaretToTargetPosition(() => GetLyric(1));\r\n\r\n        AssertHoverCaretPosition(() => new NavigateCaretPosition(GetLyric(1)));\r\n        AssertCaretPosition(() => new NavigateCaretPosition(GetLyric(0)));\r\n        AssertDraggableCaretPosition(() => null);\r\n\r\n        // caret with index.\r\n        ChangeMode(TestCaretType.CaretWithIndex);\r\n        MoveHoverCaretToTargetPosition(() => GetLyric(1));\r\n\r\n        AssertHoverCaretPosition(() => new CuttingCaretPosition(GetLyric(1), 1));\r\n        AssertCaretPosition(() => new CuttingCaretPosition(GetLyric(0), 1));\r\n        AssertDraggableCaretPosition(() => null);\r\n\r\n        // caret with index (2).\r\n        ChangeMode(TestCaretType.CaretWithIndex);\r\n        MoveHoverCaretToTargetPosition(() => GetLyric(1), () => 2);\r\n\r\n        AssertHoverCaretPosition(() => new CuttingCaretPosition(GetLyric(1), 2));\r\n        AssertCaretPosition(() => new CuttingCaretPosition(GetLyric(0), 1));\r\n        AssertDraggableCaretPosition(() => null);\r\n\r\n        // caret draggable.\r\n        ChangeMode(TestCaretType.CaretDraggable);\r\n        MoveHoverCaretToTargetPosition(() => GetLyric(1));\r\n\r\n        AssertHoverCaretPosition(() => new TypingCaretPosition(GetLyric(1), 0));\r\n        AssertCaretPosition(() => new TypingCaretPosition(GetLyric(0), 0));\r\n        AssertDraggableCaretPosition(() => null);\r\n\r\n        // caret draggable (2).\r\n        ChangeMode(TestCaretType.CaretDraggable);\r\n        MoveHoverCaretToTargetPosition(() => GetLyric(1), () => 1);\r\n\r\n        AssertHoverCaretPosition(() => new TypingCaretPosition(GetLyric(1), 1));\r\n        AssertCaretPosition(() => new TypingCaretPosition(GetLyric(0), 0));\r\n        AssertDraggableCaretPosition(() => null);\r\n    }\r\n\r\n    [Test]\r\n    public void TestConfirmHoverCaretPosition()\r\n    {\r\n        PrepareLyrics(new[] { \"Lyric1\", \"Lyric2\" });\r\n\r\n        // view only.\r\n        ChangeMode(TestCaretType.ViewOnly);\r\n        ConfirmHoverCaretPosition();\r\n\r\n        AssertHoverCaretPosition(() => null);\r\n        AssertCaretPosition(() => null);\r\n        AssertDraggableCaretPosition(() => null);\r\n\r\n        // caret enable.\r\n        ChangeMode(TestCaretType.CaretEnable);\r\n        PrepareHoverCaretPosition(() => new NavigateCaretPosition(GetLyric(1)));\r\n        ConfirmHoverCaretPosition();\r\n\r\n        AssertHoverCaretPosition(() => null);\r\n        AssertCaretPosition(() => new NavigateCaretPosition(GetLyric(1)));\r\n        AssertDraggableCaretPosition(() => null);\r\n\r\n        // caret with index.\r\n        ChangeMode(TestCaretType.CaretWithIndex);\r\n        PrepareHoverCaretPosition(() => new CuttingCaretPosition(GetLyric(1), 1));\r\n        ConfirmHoverCaretPosition();\r\n\r\n        AssertHoverCaretPosition(() => null);\r\n        AssertCaretPosition(() => new CuttingCaretPosition(GetLyric(1), 1));\r\n        AssertDraggableCaretPosition(() => null);\r\n\r\n        // caret draggable.\r\n        ChangeMode(TestCaretType.CaretDraggable);\r\n        PrepareHoverCaretPosition(() => new TypingCaretPosition(GetLyric(1), 0));\r\n        ConfirmHoverCaretPosition();\r\n\r\n        AssertHoverCaretPosition(() => null);\r\n        AssertCaretPosition(() => new TypingCaretPosition(GetLyric(1), 0));\r\n        AssertDraggableCaretPosition(() => null);\r\n    }\r\n\r\n    [Test]\r\n    public void TestClearHoverCaretPosition()\r\n    {\r\n        PrepareLyrics(new[] { \"Lyric1\", \"Lyric2\" });\r\n\r\n        // view only.\r\n        ChangeMode(TestCaretType.ViewOnly);\r\n        ClearHoverCaretPosition();\r\n\r\n        AssertHoverCaretPosition(() => null);\r\n        AssertCaretPosition(() => null);\r\n        AssertDraggableCaretPosition(() => null);\r\n\r\n        // caret enable.\r\n        ChangeMode(TestCaretType.CaretEnable);\r\n        PrepareHoverCaretPosition(() => new NavigateCaretPosition(GetLyric(1)));\r\n        ClearHoverCaretPosition();\r\n\r\n        AssertHoverCaretPosition(() => null);\r\n        AssertCaretPosition(() => new NavigateCaretPosition(GetLyric(0)));\r\n        AssertDraggableCaretPosition(() => null);\r\n\r\n        // caret with index.\r\n        ChangeMode(TestCaretType.CaretWithIndex);\r\n        PrepareHoverCaretPosition(() => new CuttingCaretPosition(GetLyric(1), 1));\r\n        ClearHoverCaretPosition();\r\n\r\n        AssertHoverCaretPosition(() => null);\r\n        AssertCaretPosition(() => new CuttingCaretPosition(GetLyric(0), 1));\r\n        AssertDraggableCaretPosition(() => null);\r\n\r\n        // caret draggable.\r\n        ChangeMode(TestCaretType.CaretDraggable);\r\n        PrepareHoverCaretPosition(() => new TypingCaretPosition(GetLyric(1), 0));\r\n        ClearHoverCaretPosition();\r\n\r\n        AssertHoverCaretPosition(() => null);\r\n        AssertCaretPosition(() => new TypingCaretPosition(GetLyric(0), 0));\r\n        AssertDraggableCaretPosition(() => null);\r\n    }\r\n\r\n    [Test]\r\n    public void TestMoveCaretToTargetPosition()\r\n    {\r\n        PrepareLyrics(new[] { \"Lyric1\", \"Lyric2\" });\r\n\r\n        // view only.\r\n        ChangeMode(TestCaretType.ViewOnly);\r\n        MoveCaretToTargetPosition(() => GetLyric(1));\r\n\r\n        AssertHoverCaretPosition(() => null);\r\n        AssertCaretPosition(() => null);\r\n        AssertDraggableCaretPosition(() => null);\r\n\r\n        // caret enable.\r\n        ChangeMode(TestCaretType.CaretEnable);\r\n        MoveCaretToTargetPosition(() => GetLyric(1));\r\n\r\n        AssertHoverCaretPosition(() => null);\r\n        AssertCaretPosition(() => new NavigateCaretPosition(GetLyric(1)));\r\n        AssertDraggableCaretPosition(() => null);\r\n\r\n        // caret with index.\r\n        ChangeMode(TestCaretType.CaretWithIndex);\r\n        MoveCaretToTargetPosition(() => GetLyric(1));\r\n\r\n        AssertHoverCaretPosition(() => null);\r\n        AssertCaretPosition(() => new CuttingCaretPosition(GetLyric(1), 1));\r\n        AssertDraggableCaretPosition(() => null);\r\n\r\n        // caret with index (2).\r\n        ChangeMode(TestCaretType.CaretWithIndex);\r\n        MoveCaretToTargetPosition(() => GetLyric(1), () => 2);\r\n\r\n        AssertHoverCaretPosition(() => null);\r\n        AssertCaretPosition(() => new CuttingCaretPosition(GetLyric(1), 2));\r\n        AssertDraggableCaretPosition(() => null);\r\n\r\n        // caret draggable.\r\n        ChangeMode(TestCaretType.CaretDraggable);\r\n        MoveCaretToTargetPosition(() => GetLyric(1));\r\n\r\n        AssertHoverCaretPosition(() => null);\r\n        AssertCaretPosition(() => new TypingCaretPosition(GetLyric(1), 0));\r\n        AssertDraggableCaretPosition(() => null);\r\n\r\n        // caret draggable (2).\r\n        ChangeMode(TestCaretType.CaretDraggable);\r\n        MoveCaretToTargetPosition(() => GetLyric(1), () => 1);\r\n\r\n        AssertHoverCaretPosition(() => null);\r\n        AssertCaretPosition(() => new TypingCaretPosition(GetLyric(1), 1));\r\n        AssertDraggableCaretPosition(() => null);\r\n    }\r\n\r\n    [Test]\r\n    public void TestMoveDraggingCaretIndex()\r\n    {\r\n        PrepareLyrics(new[] { \"Lyric1\", \"Lyric2\" });\r\n        ChangeMode(TestCaretType.CaretDraggable);\r\n        MoveHoverCaretToTargetPosition(() => GetLyric(1), () => 1);\r\n\r\n        // start dragging.\r\n        StartDragging();\r\n        AssertHoverCaretPosition(() => null);\r\n        AssertCaretPosition(() => null);\r\n        AssertDraggableCaretPosition(() =>\r\n        {\r\n            var startPosition = new TypingCaretPosition(GetLyric(1), 1);\r\n            var endPosition = new TypingCaretPosition(GetLyric(1), 1);\r\n\r\n            return new RangeCaretPosition(startPosition, endPosition, RangeCaretDraggingState.StartDrag);\r\n        });\r\n\r\n        // move dragging index.\r\n        MoveDraggingCaretIndex(() => 2);\r\n        AssertHoverCaretPosition(() => null);\r\n        AssertCaretPosition(() => null);\r\n        AssertDraggableCaretPosition(() =>\r\n        {\r\n            var startPosition = new TypingCaretPosition(GetLyric(1), 1);\r\n            var endPosition = new TypingCaretPosition(GetLyric(1), 2);\r\n\r\n            return new RangeCaretPosition(startPosition, endPosition, RangeCaretDraggingState.Dragging);\r\n        });\r\n\r\n        // end dragging.\r\n        EndDragging();\r\n        AssertHoverCaretPosition(() => null);\r\n        AssertCaretPosition(() => null);\r\n        AssertDraggableCaretPosition(() =>\r\n        {\r\n            var startPosition = new TypingCaretPosition(GetLyric(1), 1);\r\n            var endPosition = new TypingCaretPosition(GetLyric(1), 2);\r\n\r\n            return new RangeCaretPosition(startPosition, endPosition, RangeCaretDraggingState.EndDrag);\r\n        });\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Test utility\r\n\r\n    protected void MoveHoverCaretToTargetPosition(Func<Lyric> lyric)\r\n    {\r\n        AddStep(\"Moving hover caret to target position\", () =>\r\n        {\r\n            LyricCaretState.MoveHoverCaretToTargetPosition(lyric());\r\n        });\r\n    }\r\n\r\n    protected void MoveHoverCaretToTargetPosition<TIndex>(Func<Lyric> lyric, Func<TIndex> index)\r\n        where TIndex : notnull\r\n    {\r\n        AddStep(\"Moving hover caret to target position with index\", () =>\r\n        {\r\n            LyricCaretState.MoveHoverCaretToTargetPosition(lyric(), index());\r\n        });\r\n    }\r\n\r\n    protected void ConfirmHoverCaretPosition()\r\n    {\r\n        AddStep(\"Conform hover caret position\", () =>\r\n        {\r\n            LyricCaretState.ConfirmHoverCaretPosition();\r\n        });\r\n    }\r\n\r\n    protected void ClearHoverCaretPosition()\r\n    {\r\n        AddStep(\"Clear hover caret position\", () =>\r\n        {\r\n            LyricCaretState.ClearHoverCaretPosition();\r\n        });\r\n    }\r\n\r\n    protected void MoveCaretToTargetPosition(Func<Lyric> lyric)\r\n    {\r\n        AddStep(\"Move caret to target position\", () =>\r\n        {\r\n            LyricCaretState.MoveCaretToTargetPosition(lyric());\r\n        });\r\n    }\r\n\r\n    protected void MoveCaretToTargetPosition<TIndex>(Func<Lyric> lyric, Func<TIndex> index)\r\n        where TIndex : notnull\r\n    {\r\n        AddStep(\"Move caret to target position with index\", () =>\r\n        {\r\n            LyricCaretState.MoveCaretToTargetPosition(lyric(), index());\r\n        });\r\n    }\r\n\r\n    protected void StartDragging()\r\n    {\r\n        AddStep(\"Start dragging\", () =>\r\n        {\r\n            LyricCaretState.StartDragging();\r\n        });\r\n    }\r\n\r\n    protected void MoveDraggingCaretIndex<TIndex>(Func<TIndex> index)\r\n        where TIndex : notnull\r\n    {\r\n        AddStep(\"Moving dragging caret index\", () =>\r\n        {\r\n            LyricCaretState.MoveDraggingCaretIndex(index());\r\n        });\r\n    }\r\n\r\n    protected void EndDragging()\r\n    {\r\n        AddStep(\"End dragging\", () =>\r\n        {\r\n            LyricCaretState.EndDragging();\r\n        });\r\n    }\r\n\r\n    #endregion\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Screens/Edit/Beatmap/Lyrics/States/LyricCaretStateSwitchModeTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.CaretPosition.Algorithms;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Screens.Edit.Beatmap.Lyrics.States;\r\n\r\npublic partial class LyricCaretStateSwitchModeTest : BaseLyricCaretStateTest\r\n{\r\n    #region Default state\r\n\r\n    [Test]\r\n    public void TestViewOnlyMode()\r\n    {\r\n        PrepareLyrics(new[] { \"Lyric1\", \"Lyric2\" });\r\n        ChangeMode(TestCaretType.ViewOnly);\r\n\r\n        AssertHoverCaretPosition(() => null);\r\n        AssertCaretPosition(() => null);\r\n        AssertDraggableCaretPosition(() => null);\r\n\r\n        AssertCaretEnabled(false);\r\n        AssertCaretDraggable(false);\r\n\r\n        AssertCaretPositionAlgorithmIsNull();\r\n    }\r\n\r\n    [Test]\r\n    public void TestCaretEnableMode()\r\n    {\r\n        PrepareLyrics(new[] { \"Lyric1\", \"Lyric2\" });\r\n        ChangeMode(TestCaretType.CaretEnable);\r\n\r\n        AssertHoverCaretPosition(() => null);\r\n        AssertCaretPosition(() => new NavigateCaretPosition(GetLyric(0)));\r\n        AssertDraggableCaretPosition(() => null);\r\n\r\n        AssertCaretEnabled(true);\r\n        AssertCaretDraggable(false);\r\n\r\n        AssertCaretPositionAlgorithm<NavigateCaretPositionAlgorithm>();\r\n    }\r\n\r\n    [Test]\r\n    public void TestCaretWithIndexMode()\r\n    {\r\n        PrepareLyrics(new[] { \"Lyric1\", \"Lyric2\" });\r\n        ChangeMode(TestCaretType.CaretWithIndex);\r\n\r\n        AssertHoverCaretPosition(() => null);\r\n        AssertCaretPosition(() => new CuttingCaretPosition(GetLyric(0), 1));\r\n        AssertDraggableCaretPosition(() => null);\r\n\r\n        AssertCaretEnabled(true);\r\n        AssertCaretDraggable(false);\r\n\r\n        AssertCaretPositionAlgorithm<CuttingCaretPositionAlgorithm>();\r\n    }\r\n\r\n    [Test]\r\n    public void TestCaretDraggableMode()\r\n    {\r\n        PrepareLyrics(new[] { \"Lyric1\", \"Lyric2\" });\r\n        ChangeMode(TestCaretType.CaretDraggable);\r\n\r\n        AssertHoverCaretPosition(() => null);\r\n        AssertCaretPosition(() => new TypingCaretPosition(GetLyric(0), 0));\r\n        AssertDraggableCaretPosition(() => null);\r\n\r\n        AssertCaretEnabled(true);\r\n        AssertCaretDraggable(true);\r\n\r\n        AssertCaretPositionAlgorithm<TypingCaretPositionAlgorithm>();\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Default state with no lyric\r\n\r\n    [Test]\r\n    public void TestDefaultModeWithNoLyric([Values] TestCaretType type)\r\n    {\r\n        PrepareLyrics(Array.Empty<string>());\r\n        ChangeMode(type);\r\n\r\n        AssertHoverCaretPosition(() => null);\r\n        AssertCaretPosition(() => null);\r\n        AssertDraggableCaretPosition(() => null);\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Switch mode\r\n\r\n    [Test]\r\n    public void TestSwitchModeNotCauseBroken([Values] TestCaretType currentMode, [Values] TestCaretType nextEditMode)\r\n    {\r\n        PrepareLyrics(new[] { \"Lyric1\", \"Lyric2\" });\r\n        ChangeMode(currentMode);\r\n        ChangeMode(nextEditMode);\r\n    }\r\n\r\n    [Test]\r\n    public void TestSwitchTestFromRangeOfCaret()\r\n    {\r\n        PrepareLyrics(new[] { \"Lyric1\", \"Lyric2\" });\r\n        ChangeMode(TestCaretType.CaretDraggable);\r\n        PrepareCaretPosition(() => new TypingCaretPosition(GetLyric(0), 0));\r\n        PrepareRangeCaretPosition(() =>\r\n        {\r\n            var startIndex = new TypingCaretPosition(GetLyric(0), 0);\r\n            var endIndex = new TypingCaretPosition(GetLyric(0), 0);\r\n\r\n            return new RangeCaretPosition(startIndex, endIndex, RangeCaretDraggingState.EndDrag);\r\n        });\r\n\r\n        ChangeMode(TestCaretType.ViewOnly);\r\n\r\n        AssertHoverCaretPosition(() => null);\r\n        AssertCaretPosition(() => null);\r\n        AssertDraggableCaretPosition(() => null);\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Test utility\r\n\r\n    protected void AssertCaretEnabled(bool enabled)\r\n    {\r\n        AddAssert(\"Assert caret enabled\", () => LyricCaretState.CaretEnabled == enabled);\r\n    }\r\n\r\n    protected void AssertCaretDraggable(bool caretDraggable)\r\n    {\r\n        AddAssert(\"Assert caret draggable\", () => LyricCaretState.CaretDraggable == caretDraggable);\r\n    }\r\n\r\n    protected void AssertCaretPositionAlgorithmIsNull()\r\n    {\r\n        AddAssert(\"Assert caret position algorithm should be null\", () => LyricCaretState.CaretPositionAlgorithm == null);\r\n    }\r\n\r\n    protected void AssertCaretPositionAlgorithm<TCaretPositionAlgorithm>()\r\n        where TCaretPositionAlgorithm : ICaretPositionAlgorithm\r\n    {\r\n        AddAssert(\"Assert caret position algorithm\", () => LyricCaretState.CaretPositionAlgorithm?.GetType() == typeof(TCaretPositionAlgorithm));\r\n    }\r\n\r\n    #endregion\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Screens/Edit/Beatmap/Lyrics/TestSceneLyricEditorColourProvider.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing System.Reflection;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Extensions.Color4Extensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Framework.Input.Events;\r\nusing osu.Framework.Platform;\r\nusing osu.Game.Graphics.Containers;\r\nusing osu.Game.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Extensions;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics;\r\nusing osu.Game.Tests.Visual;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Screens.Edit.Beatmap.Lyrics;\r\n\r\npublic partial class TestSceneLyricEditorColourProvider : OsuTestScene\r\n{\r\n    [Test]\r\n    public void TestShowWithNoFetch()\r\n    {\r\n        var provider = new LyricEditorColourProvider();\r\n        var types = Enum.GetValues<LyricEditorMode>();\r\n\r\n        var colourMethods = typeof(LyricEditorColourProvider)\r\n                            .GetMethods(BindingFlags.Public | BindingFlags.Instance)\r\n                            .Where(x =>\r\n                            {\r\n                                var parameters = x.GetBaseDefinition().GetParameters();\r\n                                return parameters.Length == 1 && parameters[0].ParameterType == typeof(LyricEditorMode);\r\n                            }).ToArray();\r\n\r\n        Schedule(() =>\r\n        {\r\n            var editMOdeColumns = new TableColumn[]\r\n            {\r\n                new TitleTableColumn(\"Edit mode\"),\r\n            };\r\n            var editModeContent = types.Select(type =>\r\n            {\r\n                return new Drawable[]\r\n                {\r\n                    new OsuSpriteText\r\n                    {\r\n                        Text = type.ToString(),\r\n                    },\r\n                };\r\n            }).To2DArray();\r\n\r\n            var columns = colourMethods.Select(c => new TitleTableColumn(c.Name)).OfType<TableColumn>().ToArray();\r\n            var content = types.Select(type =>\r\n            {\r\n                return colourMethods.Select(c =>\r\n                {\r\n                    object? value = c.Invoke(provider, new object[] { type });\r\n                    if (value == null)\r\n                        throw new ArgumentNullException(nameof(value));\r\n\r\n                    var colour = (Color4)value;\r\n                    return new PreviewColourDrawable(colour);\r\n                }).OfType<Drawable>();\r\n            }).To2DArray();\r\n\r\n            const int edit_mode_name_width = 120;\r\n            Child = new OsuScrollContainer(Direction.Horizontal)\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n                Children = new Drawable[]\r\n                {\r\n                    new TableContainer\r\n                    {\r\n                        RelativeSizeAxes = Axes.Y,\r\n                        Width = edit_mode_name_width,\r\n                        Columns = editMOdeColumns,\r\n                        Content = editModeContent,\r\n                    },\r\n                    new TableContainer\r\n                    {\r\n                        RelativeSizeAxes = Axes.Y,\r\n                        AutoSizeAxes = Axes.X,\r\n                        Columns = columns,\r\n                        Content = content,\r\n                        Margin = new MarginPadding\r\n                        {\r\n                            Left = edit_mode_name_width,\r\n                        },\r\n                    },\r\n                },\r\n            };\r\n        });\r\n    }\r\n\r\n    private class TitleTableColumn : TableColumn\r\n    {\r\n        public TitleTableColumn(string title)\r\n            : base(title, Anchor.Centre, new Dimension(GridSizeMode.Absolute, 120))\r\n        {\r\n        }\r\n    }\r\n\r\n    private partial class PreviewColourDrawable : CompositeDrawable\r\n    {\r\n        [Resolved]\r\n        private Clipboard clipboard { get; set; } = null!;\r\n\r\n        private readonly Color4 color;\r\n\r\n        public PreviewColourDrawable(Color4 color)\r\n        {\r\n            this.color = color;\r\n\r\n            RelativeSizeAxes = Axes.Both;\r\n            InternalChildren = new Drawable[]\r\n            {\r\n                new Box\r\n                {\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    Colour = color,\r\n                },\r\n                new OsuSpriteText\r\n                {\r\n                    Anchor = Anchor.Centre,\r\n                    Origin = Anchor.Centre,\r\n                    Text = color.ToHex(),\r\n                },\r\n            };\r\n        }\r\n\r\n        protected override bool OnClick(ClickEvent e)\r\n        {\r\n            clipboard.SetText(color.ToHex());\r\n            return base.OnClick(e);\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Screens/Edit/Beatmap/TestSceneEditorMenuBar.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Graphics.UserInterface;\r\nusing osu.Game.Graphics.UserInterface;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Edit.Components.Menus;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Components.Menus;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics;\r\nusing osu.Game.Screens.Edit.Components.Menus;\r\nusing osu.Game.Tests.Visual;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Screens.Edit.Beatmap;\r\n\r\n[TestFixture]\r\npublic partial class TestSceneEditorMenuBar : OsuTestScene\r\n{\r\n    [Cached]\r\n    private readonly OverlayColourProvider overlayColour = new(OverlayColourScheme.Aquamarine);\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        var lyricEditorConfig = new KaraokeRulesetLyricEditorConfigManager();\r\n        Add(new Container\r\n        {\r\n            Anchor = Anchor.TopCentre,\r\n            Origin = Anchor.TopCentre,\r\n            RelativeSizeAxes = Axes.X,\r\n            Height = 50,\r\n            Y = 50,\r\n            Child = new EditorMenuBar\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n                Items = new[]\r\n                {\r\n                    new MenuItem(\"File\")\r\n                    {\r\n                        Items = new MenuItem[]\r\n                        {\r\n                            new ImportLyricMenu(null, \"Import from text\", null!),\r\n                            new ImportLyricMenu(null, \"Import from .kar file\", null!),\r\n                            new OsuMenuItemSpacer(),\r\n                            new EditorMenuItem(\"Export to .kar\", MenuItemType.Standard, () => { }),\r\n                            new EditorMenuItem(\"Export to text\", MenuItemType.Standard, () => { }),\r\n                            new EditorMenuItem(\"Export to json\", MenuItemType.Destructive, () => { }),\r\n                        },\r\n                    },\r\n                    new LyricEditorModeMenuItem(\"Mode\", new Bindable<LyricEditorMode>()),\r\n                    new MenuItem(\"View\")\r\n                    {\r\n                        Items = new MenuItem[]\r\n                        {\r\n                            new LyricEditorTextSizeMenu(lyricEditorConfig, \"Text size\"),\r\n                            new AutoFocusToEditLyricMenu(lyricEditorConfig, \"Auto focus to edit lyric\"),\r\n                        },\r\n                    },\r\n                    new MenuItem(\"Config\")\r\n                    {\r\n                        Items = new MenuItem[]\r\n                        {\r\n                            new EditorMenuItem(\"Lyric editor\"),\r\n                            new GeneratorConfigMenu(\"Auto-generator\"),\r\n                            new LockStateMenuItem(\"Lock\", lyricEditorConfig),\r\n                        },\r\n                    },\r\n                    new MenuItem(\"Tools\")\r\n                    {\r\n                        Items = new MenuItem[]\r\n                        {\r\n                            new KaraokeSkinEditorMenu(null!, null!, \"Skin editor\"),\r\n                        },\r\n                    },\r\n                },\r\n            },\r\n        });\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Screens/Edit/Beatmap/TestSceneKaraokeBeatmapEditor.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Screens.Edit.Beatmap;\r\n\r\npublic partial class TestSceneKaraokeBeatmapEditor : GenericEditorTestScene<KaraokeBeatmapEditor, KaraokeBeatmapEditorScreenMode>;\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Screens/Edit/Beatmap/TestSceneLyricEditorScreen.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Testing;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.Settings;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Lyrics.States.Modes;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Screens.Edit.Beatmap;\r\n\r\npublic partial class TestSceneLyricEditorScreen : BeatmapEditorScreenTestScene<LyricEditorScreen>\r\n{\r\n    [Cached]\r\n    private readonly Bindable<LyricEditorMode> bindableLyricEditorMode = new();\r\n\r\n    protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };\r\n\r\n    protected override LyricEditorScreen CreateEditorScreen() => new();\r\n\r\n    private DialogOverlay dialogOverlay = null!;\r\n    private LyricsProvider lyricsProvider = null!;\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        base.Content.AddRange(new Drawable[]\r\n        {\r\n            Content,\r\n            dialogOverlay = new DialogOverlay(),\r\n            lyricsProvider = new LyricsProvider(),\r\n        });\r\n\r\n        Dependencies.CacheAs<IDialogOverlay>(dialogOverlay);\r\n        Dependencies.CacheAs<ILyricsProvider>(lyricsProvider);\r\n        Dependencies.Cache(new KaraokeRulesetLyricEditorConfigManager());\r\n        Dependencies.Cache(new KaraokeRulesetEditGeneratorConfigManager());\r\n    }\r\n\r\n    [Test]\r\n    public void TestViewMode()\r\n    {\r\n        switchToMode(LyricEditorMode.View);\r\n    }\r\n\r\n    [Test]\r\n    public void TestEditTextMode()\r\n    {\r\n        switchToMode(LyricEditorMode.EditText);\r\n        clickEditStepButtons<TextEditStep>();\r\n    }\r\n\r\n    [Test]\r\n    public void TestEditReferenceMode()\r\n    {\r\n        switchToMode(LyricEditorMode.EditReferenceLyric);\r\n    }\r\n\r\n    [Test]\r\n    public void TestEditLanguageMode()\r\n    {\r\n        switchToMode(LyricEditorMode.EditLanguage);\r\n        clickEditStepButtons<LanguageEditStep>();\r\n    }\r\n\r\n    [Test]\r\n    public void TestEditRubyMode()\r\n    {\r\n        switchToMode(LyricEditorMode.EditRuby);\r\n        clickEditStepButtons<RubyTagEditStep>();\r\n    }\r\n\r\n    [Test]\r\n    public void TestEditTimeTagMode()\r\n    {\r\n        switchToMode(LyricEditorMode.EditTimeTag);\r\n        clickEditStepButtons<TimeTagEditStep>();\r\n    }\r\n\r\n    [Test]\r\n    public void TestEditRomanisation()\r\n    {\r\n        switchToMode(LyricEditorMode.EditRomanisation);\r\n        clickEditStepButtons<RomanisationTagEditStep>();\r\n    }\r\n\r\n    [Test]\r\n    public void TestEditNoteMode()\r\n    {\r\n        switchToMode(LyricEditorMode.EditNote);\r\n        clickEditStepButtons<NoteEditStep>();\r\n    }\r\n\r\n    [Test]\r\n    public void TestEditSingerMode()\r\n    {\r\n        switchToMode(LyricEditorMode.EditSinger);\r\n    }\r\n\r\n    private void switchToMode(LyricEditorMode mode)\r\n    {\r\n        AddStep($\"switch to mode {Enum.GetName(mode)}\", () =>\r\n        {\r\n            bindableLyricEditorMode.Value = mode;\r\n        });\r\n        AddWaitStep(\"wait for switch to new mode\", 10);\r\n    }\r\n\r\n    private void clickEditStepButtons<T>() where T : struct, Enum\r\n    {\r\n        foreach (var editMode in Enum.GetValues<T>())\r\n        {\r\n            clickTargetEditModeButton(editMode);\r\n        }\r\n    }\r\n\r\n    private void clickTargetEditModeButton<T>(T editMode) where T : struct, Enum\r\n    {\r\n        AddStep(\"Click the button\", () =>\r\n        {\r\n            var editStepSection = this.ChildrenOfType<LyricEditorSettingsHeader<T>>().Single();\r\n            editStepSection.Current.Value = editMode;\r\n        });\r\n        AddWaitStep(\"wait for switch to new edit mode.\", 10);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Screens/Edit/Beatmap/TestScenePageScreen.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps.Metadatas;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Pages;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Screens.Edit.Beatmap;\r\n\r\n[TestFixture]\r\npublic partial class TestScenePageScreen : BeatmapEditorScreenTestScene<PageScreen>\r\n{\r\n    protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };\r\n\r\n    protected override PageScreen CreateEditorScreen() => new();\r\n\r\n    protected override KaraokeBeatmap CreateBeatmap()\r\n    {\r\n        var karaokeBeatmap = base.CreateBeatmap();\r\n\r\n        karaokeBeatmap.PageInfo.Pages.AddRange(new Page[]\r\n        {\r\n            new()\r\n            {\r\n                Time = 1000,\r\n            },\r\n            new()\r\n            {\r\n                Time = 2000,\r\n            },\r\n            new()\r\n            {\r\n                Time = 3000,\r\n            },\r\n        });\r\n\r\n        return karaokeBeatmap;\r\n    }\r\n\r\n    private DialogOverlay dialogOverlay = null!;\r\n    private LyricsProvider lyricsProvider = null!;\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        base.Content.AddRange(new Drawable[]\r\n        {\r\n            Content,\r\n            dialogOverlay = new DialogOverlay(),\r\n            lyricsProvider = new LyricsProvider(),\r\n        });\r\n\r\n        Dependencies.CacheAs<IDialogOverlay>(dialogOverlay);\r\n        Dependencies.CacheAs<ILyricsProvider>(lyricsProvider);\r\n        Dependencies.Cache(new KaraokeRulesetEditGeneratorConfigManager());\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Screens/Edit/Beatmap/TestSceneSingerScreen.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Graphics.Cursor;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Singers;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Screens.Edit.Beatmap;\r\n\r\n[TestFixture]\r\npublic partial class TestSceneSingerScreen : BeatmapEditorScreenTestScene<SingerScreen>\r\n{\r\n    protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };\r\n\r\n    protected override SingerScreen CreateEditorScreen() => new();\r\n\r\n    protected override KaraokeBeatmap CreateBeatmap()\r\n    {\r\n        var karaokeBeatmap = base.CreateBeatmap();\r\n\r\n        var singerInfo = karaokeBeatmap.SingerInfo;\r\n\r\n        singerInfo.AddSinger(s =>\r\n        {\r\n            s.Order = 1;\r\n            s.Name = \"初音ミク\";\r\n            s.Romanisation = \"Hatsune Miku\";\r\n            s.EnglishName = \"Miku\";\r\n            s.Description = \"International superstar vocaloid Hatsune Miku.\";\r\n            s.Hue = 189 / 360f;\r\n        });\r\n\r\n        singerInfo.AddSinger(s =>\r\n        {\r\n            s.Order = 2;\r\n            s.Name = \"ハク\";\r\n            s.Romanisation = \"haku\";\r\n            s.EnglishName = \"andy840119\";\r\n            s.Description = \"Creator of this ruleset.\";\r\n            s.Hue = 46 / 360f;\r\n        });\r\n\r\n        singerInfo.AddSinger(s =>\r\n        {\r\n            s.Order = 3;\r\n            s.Name = \"ゴミパソコン\";\r\n            s.Romanisation = \"gomi-pasokonn\";\r\n            s.EnglishName = \"Miku\";\r\n            s.Description = \"My fucking slow desktop.\";\r\n            s.Hue = 290 / 360f;\r\n        });\r\n\r\n        return karaokeBeatmap;\r\n    }\r\n\r\n    private DialogOverlay dialogOverlay = null!;\r\n    private LyricsProvider lyricsProvider = null!;\r\n    private KaraokeBeatmapResourcesProvider karaokeBeatmapResourcesProvider = null!;\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        base.Content.AddRange(new Drawable[]\r\n        {\r\n            new OsuContextMenuContainer\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n                Child = Content,\r\n            },\r\n            dialogOverlay = new DialogOverlay(),\r\n            lyricsProvider = new LyricsProvider(),\r\n            karaokeBeatmapResourcesProvider = new KaraokeBeatmapResourcesProvider(),\r\n        });\r\n\r\n        var beatDivisor = new BindableBeatDivisor\r\n        {\r\n            Value = Beatmap.Value.BeatmapInfo.BeatDivisor,\r\n        };\r\n        var editorClock = new EditorClock(Beatmap.Value.Beatmap, beatDivisor);\r\n        Dependencies.CacheAs(editorClock);\r\n        Dependencies.Cache(beatDivisor);\r\n        Dependencies.CacheAs<IDialogOverlay>(dialogOverlay);\r\n        Dependencies.CacheAs<ILyricsProvider>(lyricsProvider);\r\n        Dependencies.CacheAs<IKaraokeBeatmapResourcesProvider>(karaokeBeatmapResourcesProvider);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Screens/Edit/Beatmap/TestSceneTranslationsScreen.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Globalization;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Beatmaps.Translations;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Screens.Edit.Beatmap;\r\n\r\n[TestFixture]\r\npublic partial class TestSceneTranslationsScreen : BeatmapEditorScreenTestScene<TranslationScreen>\r\n{\r\n    protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };\r\n\r\n    protected override TranslationScreen CreateEditorScreen() => new();\r\n\r\n    protected override KaraokeBeatmap CreateBeatmap()\r\n    {\r\n        var karaokeBeatmap = base.CreateBeatmap();\r\n\r\n        karaokeBeatmap.AvailableTranslationLanguages = new List<CultureInfo>\r\n        {\r\n            new(\"zh-TW\"),\r\n            new(\"en-US\"),\r\n            new(\"ja-JP\"),\r\n        };\r\n\r\n        return karaokeBeatmap;\r\n    }\r\n\r\n    private DialogOverlay dialogOverlay = null!;\r\n    private LyricsProvider lyricsProvider = null!;\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        base.Content.AddRange(new Drawable[]\r\n        {\r\n            Content,\r\n            dialogOverlay = new DialogOverlay(),\r\n            lyricsProvider = new LyricsProvider(),\r\n        });\r\n\r\n        Dependencies.CacheAs<IDialogOverlay>(dialogOverlay);\r\n        Dependencies.CacheAs<ILyricsProvider>(lyricsProvider);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Screens/Edit/Components/Issues/TestSceneIssueIcon.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Issues;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\nusing osu.Game.Tests.Visual;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Screens.Edit.Components.Issues;\r\n\r\n[TestFixture]\r\npublic partial class TestSceneIssueIcon : OsuTestScene\r\n{\r\n    private IssueIcon icon = null!;\r\n\r\n    [SetUp]\r\n    public void SetUp() => Schedule(() =>\r\n    {\r\n        Child = icon = new IssueIcon\r\n        {\r\n            Anchor = Anchor.Centre,\r\n            Origin = Anchor.Centre,\r\n            Size = new Vector2(64),\r\n        };\r\n    });\r\n\r\n    [Test]\r\n    public void TestDisplayIconByIssues()\r\n    {\r\n        var availableIssues = TestCaseCheckHelper.CreateAllAvailableIssues();\r\n\r\n        foreach (var (check, issues) in availableIssues)\r\n        {\r\n            AddLabel($\"Check: {check.Metadata.Description}\");\r\n\r\n            foreach (var issue in issues)\r\n            {\r\n                AddStep($\"Test lyric with template {issue.Template.UnformattedMessage}\", () =>\r\n                {\r\n                    icon.Issue = issue;\r\n                });\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Screens/Edit/Components/Issues/TestSceneIssuesToolTip.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Rulesets.Edit.Checks.Components;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Issues;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\nusing osu.Game.Tests.Visual;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Screens.Edit.Components.Issues;\r\n\r\n[TestFixture]\r\npublic partial class TestSceneIssuesToolTip : OsuTestScene\r\n{\r\n    private IssuesToolTip toolTip = null!;\r\n\r\n    [SetUp]\r\n    public void SetUp() => Schedule(() =>\r\n    {\r\n        Child = toolTip = new IssuesToolTip\r\n        {\r\n            Anchor = Anchor.Centre,\r\n            Origin = Anchor.Centre,\r\n        };\r\n        toolTip.Show();\r\n    });\r\n\r\n    [Test]\r\n    public void TestTooltipWithoutIssue()\r\n    {\r\n        AddStep(\"Test tooltip with no issue.\", () =>\r\n        {\r\n            toolTip.SetContent(Array.Empty<Issue>());\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestTooltipWithIssue()\r\n    {\r\n        var availableIssues = TestCaseCheckHelper.CreateAllAvailableIssues();\r\n\r\n        foreach (var (check, issues) in availableIssues)\r\n        {\r\n            AddStep($\"Check {check.Metadata.Description} has {issues.Length} issues.\", () =>\r\n            {\r\n                toolTip.SetContent(issues);\r\n            });\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Screens/Edit/Components/Markdown/TestSceneDescriptionTextFlowContainer.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Cursor;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Components.Markdown;\r\nusing osu.Game.Tests.Visual;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Screens.Edit.Components.Markdown;\r\n\r\n[TestFixture]\r\npublic partial class TestSceneDescriptionTextFlowContainer : OsuTestScene\r\n{\r\n    [Cached]\r\n    private readonly OverlayColourProvider overlayColourProvider = new(OverlayColourScheme.Blue);\r\n\r\n    private DescriptionTextFlowContainer lyricEditorDescriptionTextFlowContainer = null!;\r\n\r\n    [SetUp]\r\n    public void SetUp() => Schedule(() =>\r\n    {\r\n        Child = new PopoverContainer\r\n        {\r\n            RelativeSizeAxes = Axes.Both,\r\n            Child = lyricEditorDescriptionTextFlowContainer = new DescriptionTextFlowContainer\r\n            {\r\n                Anchor = Anchor.Centre,\r\n                Origin = Anchor.Centre,\r\n                Size = new Vector2(100),\r\n            },\r\n        };\r\n    });\r\n\r\n    [Test]\r\n    public void TestDisplayDescription()\r\n    {\r\n        AddStep(\"Markdown description\", () =>\r\n        {\r\n            lyricEditorDescriptionTextFlowContainer.Description = new DescriptionFormat\r\n            {\r\n                Text = \"Test description with `Markdown` format.\",\r\n            };\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestDisplayDescriptionWithKey()\r\n    {\r\n        AddStep(\"Markdown description with key\", () =>\r\n        {\r\n            lyricEditorDescriptionTextFlowContainer.Description = new DescriptionFormat\r\n            {\r\n                Text = $\"Test description with [{DescriptionFormat.LINK_KEY_ACTION}](set_time)\",\r\n                Actions = new Dictionary<string, IDescriptionAction>\r\n                {\r\n                    {\r\n                        \"set_time\", new InputKeyDescriptionAction\r\n                        {\r\n                            AdjustableActions = new[]\r\n                            {\r\n                                KaraokeEditAction.SetTime,\r\n                            },\r\n                        }\r\n                    },\r\n                },\r\n            };\r\n        });\r\n\r\n        AddStep(\"Markdown description with key text and tooltip\", () =>\r\n        {\r\n            lyricEditorDescriptionTextFlowContainer.Description = new DescriptionFormat\r\n            {\r\n                Text = $\"Test description with [{DescriptionFormat.LINK_KEY_ACTION}](set_time)\",\r\n                Actions = new Dictionary<string, IDescriptionAction>\r\n                {\r\n                    {\r\n                        \"set_time\", new InputKeyDescriptionAction\r\n                        {\r\n                            Text = \"set time key.\",\r\n                            AdjustableActions = new[]\r\n                            {\r\n                                KaraokeEditAction.SetTime,\r\n                            },\r\n                        }\r\n                    },\r\n                },\r\n            };\r\n        });\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Screens/Edit/GenericEditorScreenTestScene.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Edit;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Beatmaps;\r\nusing osu.Game.Screens.Edit;\r\nusing osu.Game.Tests.Visual;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Screens.Edit;\r\n\r\npublic abstract partial class GenericEditorScreenTestScene<TScreen, TType> : EditorClockTestScene\r\n    where TScreen : GenericEditorScreen<TType>\r\n{\r\n    [Cached(typeof(EditorBeatmap))]\r\n    [Cached(typeof(IBeatSnapProvider))]\r\n    private readonly EditorBeatmap editorBeatmap;\r\n\r\n    [Cached]\r\n    private readonly OverlayColourProvider colourProvider = new(OverlayColourScheme.Blue);\r\n\r\n    protected GenericEditorScreenTestScene()\r\n    {\r\n        editorBeatmap = new EditorBeatmap(CreateBeatmap());\r\n    }\r\n\r\n    protected override void LoadComplete()\r\n    {\r\n        editorBeatmap.BeatmapInfo.Ruleset = new KaraokeRuleset().RulesetInfo;\r\n\r\n        Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap);\r\n\r\n        Children = new Drawable[]\r\n        {\r\n            editorBeatmap,\r\n            CreateEditorScreen().With(x =>\r\n            {\r\n                x.State.Value = Visibility.Visible;\r\n            }),\r\n        };\r\n    }\r\n\r\n    protected abstract GenericEditorScreen<TType> CreateEditorScreen();\r\n\r\n    protected virtual KaraokeBeatmap CreateBeatmap()\r\n    {\r\n        var beatmap = new TestKaraokeBeatmap(new KaraokeRuleset().RulesetInfo);\r\n        if (new KaraokeBeatmapConverter(beatmap, new KaraokeRuleset()).Convert() is not KaraokeBeatmap karaokeBeatmap)\r\n            throw new ArgumentNullException(nameof(karaokeBeatmap));\r\n\r\n        return karaokeBeatmap;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Screens/Edit/GenericEditorTestScene.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Edit;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Beatmaps;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Screens.Edit;\r\n\r\npublic abstract partial class GenericEditorTestScene<TEditor, TScreenMode> : ScreenTestScene<TEditor>\r\n    where TEditor : GenericEditor<TScreenMode>, new()\r\n    where TScreenMode : Enum\r\n{\r\n    [Cached(typeof(EditorBeatmap))]\r\n    [Cached(typeof(IBeatSnapProvider))]\r\n    private readonly EditorBeatmap editorBeatmap;\r\n\r\n    protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };\r\n\r\n    protected override TEditor CreateScreen() => new();\r\n\r\n    private DialogOverlay dialogOverlay = null!;\r\n\r\n    protected GenericEditorTestScene()\r\n    {\r\n        var karaokeBeatmap = CreateBeatmap();\r\n        editorBeatmap = new EditorBeatmap(karaokeBeatmap);\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap);\r\n\r\n        base.Content.Add(new EditorBeatmapDependencyContainer(editorBeatmap, new BindableBeatDivisor())\r\n        {\r\n            RelativeSizeAxes = Axes.Both,\r\n            Children = new Drawable[]\r\n            {\r\n                editorBeatmap,\r\n                Content,\r\n                dialogOverlay = new DialogOverlay(),\r\n            },\r\n        });\r\n\r\n        Dependencies.CacheAs<IDialogOverlay>(dialogOverlay);\r\n    }\r\n\r\n    protected virtual KaraokeBeatmap CreateBeatmap()\r\n    {\r\n        var beatmap = new TestKaraokeBeatmap(new KaraokeRuleset().RulesetInfo);\r\n        if (new KaraokeBeatmapConverter(beatmap, new KaraokeRuleset()).Convert() is not KaraokeBeatmap karaokeBeatmap)\r\n            throw new ArgumentNullException(nameof(karaokeBeatmap));\r\n\r\n        return karaokeBeatmap;\r\n    }\r\n\r\n    private partial class EditorBeatmapDependencyContainer : Container\r\n    {\r\n        [Cached]\r\n        private readonly EditorClock editorClock;\r\n\r\n        [Cached]\r\n        private readonly BindableBeatDivisor beatDivisor;\r\n\r\n        protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };\r\n\r\n        public EditorBeatmapDependencyContainer(IBeatmap beatmap, BindableBeatDivisor beatDivisor)\r\n        {\r\n            this.beatDivisor = beatDivisor;\r\n\r\n            InternalChildren = new Drawable[]\r\n            {\r\n                editorClock = new EditorClock(beatmap, beatDivisor),\r\n                Content,\r\n            };\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Screens/Edit/Import/TestSceneLyricImporter.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.IO;\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Framework.Screens;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Import.Lyrics;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Import.Lyrics.DragFile;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Resources;\r\nusing osu.Game.Screens.Edit;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Screens.Edit.Import;\r\n\r\n[TestFixture]\r\npublic partial class TestSceneLyricImporter : ScreenTestScene<TestSceneLyricImporter.TestLyricImporter>\r\n{\r\n    [Cached]\r\n    private readonly OverlayColourProvider overlayColourProvider = new(OverlayColourScheme.Blue);\r\n\r\n    [Cached]\r\n    private readonly KaraokeRulesetLyricEditorConfigManager lyricEditorConfigManager = new();\r\n\r\n    protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };\r\n\r\n    protected override TestLyricImporter CreateScreen()\r\n    {\r\n        string temp = TestResources.GetTestKarForImport(\"light\");\r\n        return new TestLyricImporter(new FileInfo(temp));\r\n    }\r\n\r\n    private DialogOverlay dialogOverlay = null!;\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        base.Content.AddRange(new Drawable[]\r\n        {\r\n            Content,\r\n            dialogOverlay = new DialogOverlay(),\r\n        });\r\n\r\n        Dependencies.CacheAs<IDialogOverlay>(dialogOverlay);\r\n        Dependencies.Cache(new EditorClock());\r\n    }\r\n\r\n    [Test]\r\n    public void TestGoToStep()\r\n    {\r\n        var steps = Enum.GetValues<LyricImporterStep>();\r\n\r\n        foreach (var step in steps)\r\n        {\r\n            AddStep($\"go to step {Enum.GetName(step)}\", () => { Screen.GoToStep(step); });\r\n        }\r\n    }\r\n\r\n    public partial class TestLyricImporter : LyricImporter\r\n    {\r\n        protected LyricImporterSubScreenStack ScreenStack => (InternalChild as ImportLyricOverlay)!.Dependencies.Get<LyricImporterSubScreenStack>();\r\n\r\n        private readonly FileInfo fileInfo;\r\n\r\n        public TestLyricImporter(FileInfo fileInfo)\r\n        {\r\n            this.fileInfo = fileInfo;\r\n        }\r\n\r\n        protected override void LoadComplete()\r\n        {\r\n            base.LoadComplete();\r\n\r\n            if (ScreenStack.CurrentScreen is not DragFileStepScreen dragFileSubScreen)\r\n                throw new ScreenStack.ScreenNotInStackException($\"{nameof(DragFileStepScreen)} does not in the screen.\");\r\n\r\n            dragFileSubScreen.ImportLyricFile(fileInfo);\r\n        }\r\n\r\n        public void GoToStep(LyricImporterStep step)\r\n        {\r\n            var totalSteps = Enum.GetValues<LyricImporterStep>().Where(x => x > ScreenStack.CurrentStep && x <= step);\r\n\r\n            foreach (var gotoStep in totalSteps)\r\n            {\r\n                ScreenStack.Push(gotoStep);\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Screens/Edit/Stages/Classic/ClassicStageScreenTestScene.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Stages.Classic;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Screens.Edit.Stages.Classic;\r\n\r\npublic abstract partial class ClassicStageScreenTestScene<T> : GenericEditorScreenTestScene<T, ClassicStageEditorScreenMode>\r\n    where T : ClassicStageScreen\r\n{\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Screens/Edit/Stages/Classic/TestSceneClassicStageEditor.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Stages.Classic;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Screens.Edit.Stages.Classic;\r\n\r\npublic partial class TestSceneClassicStageEditor : GenericEditorTestScene<ClassicStageEditor, ClassicStageEditorScreenMode>\r\n{\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Screens/Edit/Stages/Classic/TestSceneConfigScreen.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Stages.Classic.Config;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Screens.Edit.Stages.Classic;\r\n\r\npublic partial class TestSceneConfigScreen : ClassicStageScreenTestScene<ConfigScreen>\r\n{\r\n    protected override ConfigScreen CreateEditorScreen() => new();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Screens/Edit/Stages/Classic/TestSceneStageScreen.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit.Stages.Classic.Stage;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Screens.Edit.Stages.Classic;\r\n\r\n[Ignore(\"Ingore this test until able to edit the stage.\")]\r\npublic partial class TestSceneStageScreen : ClassicStageScreenTestScene<StageScreen>\r\n{\r\n    protected override StageScreen CreateEditorScreen() => new();\r\n\r\n    [Test]\r\n    public void TestSwitchCategoryAndEditMode()\r\n    {\r\n        var stageScreen = Children.OfType<StageScreen>().First();\r\n\r\n        AddWaitStep(\"wait for editor to load\", 5);\r\n\r\n        foreach (var category in Enum.GetValues<StageEditorEditCategory>())\r\n        {\r\n            foreach (var editMode in Enum.GetValues<StageEditorEditMode>())\r\n            {\r\n                AddLabel($\"{Enum.GetName(category)} category with {Enum.GetName(editMode)} mode\");\r\n\r\n                AddStep($\"switch to mode {Enum.GetName(editMode)}\", () =>\r\n                {\r\n                    stageScreen.ChangeEditCategory(category);\r\n                    stageScreen.BindableEditMode.Value = editMode;\r\n                });\r\n                AddWaitStep(\"wait for switch to new mode\", 5);\r\n            }\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Screens/ScreenTestScene.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Screens;\r\nusing osu.Game.Tests.Visual;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Screens;\r\n\r\npublic abstract partial class ScreenTestScene<T> : ScreenTestScene where T : OsuScreen\r\n{\r\n    protected T Screen { get; private set; } = null!;\r\n\r\n    public override void SetUpSteps()\r\n    {\r\n        base.SetUpSteps();\r\n\r\n        AddStep(\"load screen\", LoadScreen);\r\n        AddUntilStep(\"wait for loaded\", () => Screen.IsLoaded);\r\n    }\r\n\r\n    protected virtual void LoadScreen()\r\n    {\r\n        LoadScreen(Screen = CreateScreen());\r\n    }\r\n\r\n    protected abstract T CreateScreen();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Screens/Settings/Previews/TestSceneMicrophoneSoundVisualizer.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Input;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Settings.Previews.Input;\r\nusing osu.Game.Tests.Visual;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Screens.Settings.Previews;\r\n\r\n[TestFixture]\r\npublic partial class TestSceneMicrophoneSoundVisualizer : OsuTestScene\r\n{\r\n    private MicrophoneSoundVisualizer preview = null!;\r\n\r\n    [SetUp]\r\n    public void SetUp() => Schedule(() =>\r\n    {\r\n        Child = new MicrophoneInputManager\r\n        {\r\n            Child = preview = new MicrophoneSoundVisualizer\r\n            {\r\n                Anchor = Anchor.Centre,\r\n                Origin = Anchor.Centre,\r\n                DeviceName = \"Super large microphone device name : )\",\r\n            },\r\n        };\r\n    });\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Screens/Settings/TestSceneKaraokeSettings.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Settings;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Screens.Settings;\r\n\r\n[TestFixture]\r\npublic partial class TestSceneKaraokeSettings : ScreenTestScene<KaraokeSettings>\r\n{\r\n    protected override KaraokeSettings CreateScreen() => new();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Screens/Skin/KaraokeSkinEditorScreenTestScene.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Edit;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Skin;\r\nusing osu.Game.Rulesets.Karaoke.Skinning;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Resources;\r\nusing osu.Game.Screens.Edit;\r\nusing osu.Game.Skinning;\r\nusing osu.Game.Tests.Visual;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Screens.Skin;\r\n\r\npublic abstract partial class KaraokeSkinEditorScreenTestScene<T> : EditorClockTestScene where T : KaraokeSkinEditorScreen\r\n{\r\n    [Cached(typeof(EditorBeatmap))]\r\n    [Cached(typeof(IBeatSnapProvider))]\r\n    private readonly EditorBeatmap editorBeatmap;\r\n\r\n    [Cached]\r\n    private readonly OverlayColourProvider colourProvider = new(OverlayColourScheme.Pink);\r\n\r\n    private readonly KaraokeBeatmapSkin karaokeSkin = new TestKaraokeBeatmapSkin();\r\n\r\n    protected KaraokeSkinEditorScreenTestScene()\r\n    {\r\n        // todo: skin editor might not need the editor beatmap.\r\n        editorBeatmap = new EditorBeatmap(createBeatmap());\r\n    }\r\n\r\n    protected override void LoadComplete()\r\n    {\r\n        Child = new SkinProvidingContainer(karaokeSkin)\r\n        {\r\n            RelativeSizeAxes = Axes.Both,\r\n            Children = new Drawable[]\r\n            {\r\n                editorBeatmap,\r\n                CreateEditorScreen(karaokeSkin).With(x =>\r\n                {\r\n                    x.State.Value = Visibility.Visible;\r\n                }),\r\n            },\r\n        };\r\n    }\r\n\r\n    protected abstract T CreateEditorScreen(KaraokeSkin karaokeSkin);\r\n\r\n    protected class TestKaraokeBeatmapSkin : KaraokeBeatmapSkin\r\n    {\r\n        public TestKaraokeBeatmapSkin()\r\n            : base(new SkinInfo(), TestResources.CreateSkinStorageResourceProvider())\r\n        {\r\n        }\r\n    }\r\n\r\n    private KaraokeBeatmap createBeatmap()\r\n    {\r\n        var beatmap = new TestKaraokeBeatmap(new KaraokeRuleset().RulesetInfo);\r\n        if (new KaraokeBeatmapConverter(beatmap, new KaraokeRuleset()).Convert() is not KaraokeBeatmap karaokeBeatmap)\r\n            throw new ArgumentNullException(nameof(karaokeBeatmap));\r\n\r\n        return karaokeBeatmap;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Screens/Skin/TestSceneConfigScreen.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Rulesets.Karaoke.Screens.Skin.Config;\r\nusing osu.Game.Rulesets.Karaoke.Skinning;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Screens.Skin;\r\n\r\npublic partial class TestSceneConfigScreen : KaraokeSkinEditorScreenTestScene<ConfigScreen>\r\n{\r\n    protected override ConfigScreen CreateEditorScreen(KaraokeSkin karaokeSkin) => new(karaokeSkin);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Screens/Skin/TestSceneKaraokeSkinEditor.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing JetBrains.Annotations;\r\nusing osu.Framework.Allocation;\r\nusing osu.Game.Database;\r\nusing osu.Game.IO;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Extensions;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Skin;\r\nusing osu.Game.Rulesets.Karaoke.Skinning;\r\nusing osu.Game.Rulesets.Karaoke.Skinning.Elements;\r\nusing osu.Game.Screens.Edit;\r\nusing osu.Game.Skinning;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Screens.Skin;\r\n\r\npublic partial class TestSceneKaraokeSkinEditor : ScreenTestScene<KaraokeSkinEditor>\r\n{\r\n    // todo: karaoke skin editor might not need editor beatmap, or at least it will be optional.\r\n    [Cached(typeof(EditorBeatmap))]\r\n    private readonly EditorBeatmap editorBeatmap = new(new KaraokeBeatmap\r\n    {\r\n        BeatmapInfo =\r\n        {\r\n            Ruleset = new KaraokeRuleset().RulesetInfo,\r\n        },\r\n    });\r\n\r\n    private KaraokeSkin karaokeSkin = null!;\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(SkinManager skinManager)\r\n    {\r\n        skinManager.CurrentSkinInfo.Value = TestingSkin.CreateInfo().ToLiveUnmanaged();\r\n\r\n        karaokeSkin = (KaraokeSkin)skinManager.CurrentSkin.Value;\r\n    }\r\n\r\n    protected override KaraokeSkinEditor CreateScreen() => new(karaokeSkin);\r\n\r\n    /// <summary>\r\n    /// todo: it's a tricky way to create ruleset's own skin class.\r\n    /// should use generic skin like <see cref=\"LegacySkin\"/> eventually.\r\n    /// </summary>\r\n    public class TestingSkin : KaraokeSkin\r\n    {\r\n        internal static readonly Guid DEFAULT_SKIN = new(\"FEC5A291-5709-11EC-9F10-0800200C9A66\");\r\n\r\n        public static SkinInfo CreateInfo() => new()\r\n        {\r\n            ID = DEFAULT_SKIN,\r\n            Name = \"karaoke! (default skin)\",\r\n            Creator = \"team karaoke!\",\r\n            Protected = true,\r\n            InstantiationInfo = typeof(TestingSkin).GetInvariantInstantiationInfo(),\r\n        };\r\n\r\n        public TestingSkin(IStorageResourceProvider? resources)\r\n            : this(CreateInfo(), resources)\r\n        {\r\n        }\r\n\r\n        [UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)]\r\n        public TestingSkin(SkinInfo skin, IStorageResourceProvider? resources)\r\n            : base(skin, resources)\r\n        {\r\n            DefaultElement[ElementType.LyricFontInfo] = LyricFontInfo.CreateDefault();\r\n            DefaultElement[ElementType.NoteStyle] = NoteStyle.CreateDefault();\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Screens/Skin/TestSceneStyleScreen.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Skin.Style;\r\nusing osu.Game.Rulesets.Karaoke.Skinning;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Screens.Skin;\r\n\r\n[TestFixture]\r\n[Ignore(\"Shader broken.\")]\r\npublic partial class TestSceneStyleScreen : KaraokeSkinEditorScreenTestScene<StyleScreen>\r\n{\r\n    protected override StyleScreen CreateEditorScreen(KaraokeSkin karaokeSkin) => new(karaokeSkin);\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Screens/TestManageFontPreview.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Textures;\r\nusing osu.Framework.IO.Stores;\r\nusing osu.Framework.Platform;\r\nusing osu.Framework.Timing;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Settings.Previews.Graphics;\r\nusing osu.Game.Tests.Visual;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Screens;\r\n\r\npublic partial class TestManageFontPreview : OsuTestScene\r\n{\r\n    private readonly NowPlayingOverlay np;\r\n\r\n    public TestManageFontPreview()\r\n    {\r\n        Clock = new FramedClock();\r\n        Clock.ProcessFrame();\r\n\r\n        Add(np = new NowPlayingOverlay\r\n        {\r\n            Origin = Anchor.TopRight,\r\n            Anchor = Anchor.TopRight,\r\n        });\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load(GameHost host)\r\n    {\r\n        var resources = new KaraokeRuleset().CreateResourceStore();\r\n        var textureStore = new TextureStore(host.Renderer, host.CreateTextureLoaderStore(new NamespacedResourceStore<byte[]>(resources, \"Textures\")));\r\n        Dependencies.CacheAs(textureStore);\r\n\r\n        Add(new ManageFontPreview\r\n        {\r\n            Anchor = Anchor.Centre,\r\n            Origin = Anchor.Centre,\r\n        });\r\n    }\r\n\r\n    protected override void LoadComplete()\r\n    {\r\n        base.LoadComplete();\r\n        np.ToggleVisibility();\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Screens/TestSceneGeneratorConfigPopover.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Allocation;\r\nusing osu.Game.Overlays;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Screens.Edit;\r\nusing osu.Game.Tests.Visual;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Screens;\r\n\r\n[TestFixture]\r\npublic partial class TestSceneGeneratorConfigPopover : OsuTestScene\r\n{\r\n    private KaraokeRulesetEditGeneratorConfigManager config = null!;\r\n    private readonly OverlayColourProvider colourProvider = new(OverlayColourScheme.Blue);\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        config = new KaraokeRulesetEditGeneratorConfigManager();\r\n        Dependencies.Cache(config);\r\n        Dependencies.Cache(colourProvider);\r\n    }\r\n\r\n    [Test]\r\n    public void TestPopover()\r\n    {\r\n        foreach (var setting in Enum.GetValues<KaraokeRulesetEditGeneratorSetting>())\r\n        {\r\n            AddLabel($\"Generate config: {nameof(setting)}\");\r\n            AddStep(\"Show dialog\", () =>\r\n            {\r\n                var popover = new GeneratorConfigPopover(setting);\r\n\r\n                Schedule(() =>\r\n                {\r\n                    Child = popover;\r\n                    Child.Show();\r\n                });\r\n            });\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Skinning/Fonts/BitmapFontCompressorTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Extensions;\r\nusing osu.Framework.Extensions.IEnumerableExtensions;\r\nusing osu.Framework.IO.Stores;\r\nusing osu.Game.Rulesets.Karaoke.IO.Stores;\r\nusing osu.Game.Rulesets.Karaoke.Skinning.Fonts;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Asserts;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Resources;\r\nusing SharpFNT;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Skinning.Fonts;\r\n\r\npublic class BitmapFontGeneratorTest\r\n{\r\n    private BitmapFont font = null!;\r\n\r\n    [OneTimeSetUp]\r\n    public void OneTimeSetUp()\r\n    {\r\n        var fontResourceStore = new NamespacedResourceStore<byte[]>(TestResources.GetStore(), \"Resources.Testing.Fonts.Fnt.OpenSans\");\r\n        var glyphStore = new FntGlyphStore(fontResourceStore, \"OpenSans-Regular\");\r\n        glyphStore.LoadFontAsync().WaitSafely();\r\n\r\n        // make sure glyph are loaded.\r\n        var normalGlyph = glyphStore.Get('a');\r\n        if (normalGlyph == null)\r\n            throw new ArgumentNullException(nameof(normalGlyph));\r\n\r\n        font = glyphStore.BitmapFont ?? throw new InvalidOperationException(\"Font should not be null in the test case.\");\r\n    }\r\n\r\n    [TestCase(\"A\", 1)]\r\n    [TestCase(\"a\", 1)]\r\n    [TestCase(\"カラオケ\", 0)]\r\n    [TestCase(\"\", 1)]\r\n    public void TestCompress(string chars, int charAmount)\r\n    {\r\n        var result = BitmapFontCompressor.Compress(font, chars.ToArray());\r\n\r\n        // info and common should just copy.\r\n        ObjectAssert.ArePropertyEqual(font.Info, result.Info);\r\n        ObjectAssert.ArePropertyEqual(font.Common, result.Common);\r\n\r\n        // should have page if have char.\r\n        Assert.That(result.Pages, Is.Not.Null);\r\n\r\n        if (!string.IsNullOrEmpty(chars) && charAmount > 0)\r\n        {\r\n            Assert.That(result.Pages.Count, Is.Not.EqualTo(0));\r\n        }\r\n\r\n        // should have chars if have char.\r\n        Assert.That(result.Characters, Is.Not.Null);\r\n\r\n        if (!string.IsNullOrEmpty(chars))\r\n        {\r\n        Assert.That(result.Characters.Count, Is.EqualTo(charAmount));\r\n        }\r\n\r\n        // kerning pairs amount might be zero but cannot be null.\r\n        Assert.That(result.KerningPairs, Is.Not.Null);\r\n    }\r\n\r\n    [TestCase(new int[] { }, new string[] { })]\r\n    [TestCase(new[] { 0 }, new[] { \"OpenSans_0.png\" })] // max store page is start from 0.\r\n    [TestCase(new[] { 0, 1 }, new[] { \"OpenSans_0.png\" })] // should not have the case that more then origin page number.\r\n    public void TestGeneratePage(int[] pages, string[] expected)\r\n    {\r\n        var characters = pages.Select(x => new Character { Page = x }).ToArray();\r\n\r\n        try\r\n        {\r\n            string[] actual = BitmapFontCompressor.GeneratePages(font.Pages, characters).Values.ToArray();\r\n            Assert.That(expected, Is.EqualTo(actual));\r\n        }\r\n        catch\r\n        {\r\n            int expectedPageSize = expected.Length;\r\n            int storePage = font.Pages.Max(x => x.Key);\r\n            Assert.That(expectedPageSize, Is.GreaterThan(storePage));\r\n        }\r\n    }\r\n\r\n    [TestCase(\"A\")]\r\n    [TestCase(\"ABC\")]\r\n    [TestCase(\"abc\")]\r\n    [TestCase(\"!!!!\")]\r\n    [TestCase(\"カラオケ\")] // should not have any text if cannot get character in origin font.\r\n    public void TestGenerateCharactersPropertyWithSingleLine(string chars)\r\n    {\r\n        var characters = font.Characters;\r\n        int spacing = font.Info.SpacingHorizontal;\r\n        int topPadding = font.Info.PaddingUp;\r\n\r\n        var result = BitmapFontCompressor.GenerateCharacters(font.Info, font.Common, font.Characters, chars.ToArray());\r\n\r\n        foreach ((int c, var character) in result)\r\n        {\r\n            // check some property should be same as origin character.\r\n            var expected = characters[c];\r\n            Assert.That(expected, Is.Not.Null);\r\n            Assert.That(character.Width, Is.EqualTo(expected.Width));\r\n            Assert.That(character.Height, Is.EqualTo(expected.Height));\r\n            Assert.That(character.XOffset, Is.EqualTo(expected.XOffset));\r\n            Assert.That(character.YOffset, Is.EqualTo(expected.YOffset));\r\n            Assert.That(character.XAdvance, Is.EqualTo(expected.XAdvance));\r\n            Assert.That(character.Channel, Is.EqualTo(expected.Channel));\r\n\r\n            // test previous position should smaller the current one.\r\n            var previousChar = result.Values.GetPrevious(character);\r\n            if (previousChar == null)\r\n                return;\r\n\r\n            // all the test case can be finished in single line.\r\n            Assert.That(character.X, Is.EqualTo(previousChar.X + previousChar.Width + spacing));\r\n            Assert.That(previousChar.Y, Is.EqualTo(topPadding));\r\n            Assert.That(previousChar.Page, Is.EqualTo(0));\r\n        }\r\n    }\r\n\r\n    [TestCase(\"A\")]\r\n    [TestCase(\"ABC\")]\r\n    [TestCase(\"abc\")]\r\n    [TestCase(\"1234567890\")]\r\n    [TestCase(\"!!!!\")]\r\n    [TestCase(\"カラオケ\")] // should not have any text if cannot get character in origin font.\r\n    public void TestGenerateCharactersPropertyWithMultiLine(string chars)\r\n    {\r\n        // make sure that will change new line if print next chars.\r\n        var bitmapFontCommon = new BitmapFontCommon\r\n        {\r\n            ScaleWidth = 0,\r\n            ScaleHeight = int.MaxValue,\r\n        };\r\n        int spacing = font.Info.SpacingVertical;\r\n        int leftPadding = font.Info.PaddingUp;\r\n\r\n        var result = BitmapFontCompressor.GenerateCharacters(font.Info, bitmapFontCommon, font.Characters, chars.ToArray());\r\n\r\n        foreach (var (_, character) in result)\r\n        {\r\n            // test previous position should smaller the current one.\r\n            var previousChar = result.Values.GetPrevious(character);\r\n            if (previousChar == null)\r\n                return;\r\n\r\n            // all the test case can be finished in different line.\r\n            Assert.That(previousChar.X, Is.EqualTo(leftPadding));\r\n            Assert.That(character.Y, Is.EqualTo(previousChar.Y + previousChar.Height + spacing));\r\n            Assert.That(previousChar.Page, Is.EqualTo(0));\r\n        }\r\n    }\r\n\r\n    [TestCase(\"A\")]\r\n    [TestCase(\"ABC\")]\r\n    [TestCase(\"abc\")]\r\n    [TestCase(\"1234567890\")]\r\n    [TestCase(\"!!!!\")]\r\n    [TestCase(\"カラオケ\")] // should not have any text if cannot get character in origin font.\r\n    public void TestGenerateCharactersPropertyWithMultiPage(string chars)\r\n    {\r\n        // make sure that will change new page if print next chars.\r\n        var bitmapFontCommon = new BitmapFontCommon\r\n        {\r\n            ScaleWidth = 0,\r\n            ScaleHeight = 0,\r\n        };\r\n        int page = 0;\r\n        int topPadding = font.Info.PaddingUp;\r\n        int leftPadding = font.Info.PaddingUp;\r\n\r\n        var result = BitmapFontCompressor.GenerateCharacters(font.Info, bitmapFontCommon, font.Characters, chars.ToArray());\r\n\r\n        foreach (var (_, character) in result)\r\n        {\r\n            // test previous position should smaller the current one.\r\n            var previousChar = result.Values.GetPrevious(character);\r\n            if (previousChar == null)\r\n                return;\r\n\r\n            // all the test case can be finished in single line, so just test x position.\r\n            Assert.That(previousChar.X, Is.EqualTo(leftPadding));\r\n            Assert.That(previousChar.Y, Is.EqualTo(topPadding));\r\n            Assert.That(previousChar.Page, Is.EqualTo(page));\r\n            page++;\r\n        }\r\n    }\r\n\r\n    [Test]\r\n    public void TestGenerateAllCharacters()\r\n    {\r\n        // make sure that no characters is missing.\r\n        // not checking position because algorithm might not save as original one.\r\n        char[] chars = font.Characters.Keys.Select(x => (char)x).ToArray();\r\n        var result = BitmapFontCompressor.GenerateCharacters(font.Info, font.Common, font.Characters, chars);\r\n\r\n        int expected = font.Characters.Count;\r\n        int actual = result.Count;\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(\"カラオケ\", 0)]\r\n    [TestCase(\"からおけ\", 0)]\r\n    [TestCase(\"カラオケ(karaoke)\", 7)]\r\n    public void TestGenerateCharactersIfNotExist(string chars, int expected)\r\n    {\r\n        var result = BitmapFontCompressor.GenerateCharacters(font.Info, font.Common, font.Characters, chars.ToArray());\r\n\r\n        int actual = result.Count;\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(\"\", 0)]\r\n    [TestCase(\"a\", 0)] // should not have any kerning pair if has zero or one char.\r\n    [TestCase(\"aaaaa\", 0)] // same char should not have kerning pair.\r\n    [TestCase(\"ab\", 0)] // don't worry. some of pairs does not have kerning pair.\r\n    [TestCase(\"AB\", 1)]\r\n    [TestCase(\"ABC\", 3)]\r\n    public void TestGenerateKerningPairs(string chars, int expected)\r\n    {\r\n        var result = BitmapFontCompressor.GenerateKerningPairs(font.KerningPairs, chars.ToArray());\r\n\r\n        int actual = result.Count;\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [Test]\r\n    public void TestGenerateKerningPairsWithAllChars()\r\n    {\r\n        // make sure that no kerning is missing.\r\n        char[] chars = font.Characters.Keys.Select(x => (char)x).ToArray();\r\n        var kerningPairs = font.KerningPairs;\r\n        var result = BitmapFontCompressor.GenerateKerningPairs(kerningPairs, chars);\r\n\r\n        int expected = kerningPairs.Count;\r\n        int actual = result.Count;\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Skinning/Fonts/BitmapFontImageGeneratorTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Extensions;\r\nusing osu.Framework.Graphics.Textures;\r\nusing osu.Framework.IO.Stores;\r\nusing osu.Game.Rulesets.Karaoke.IO.Stores;\r\nusing osu.Game.Rulesets.Karaoke.Skinning.Fonts;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Resources;\r\nusing SharpFNT;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Skinning.Fonts;\r\n\r\npublic class BitmapFontImageGeneratorTest\r\n{\r\n    private TestFntGlyphStore glyphStore = null!;\r\n\r\n    private BitmapFont font = null!;\r\n\r\n    [OneTimeSetUp]\r\n    public void OneTimeSetUp()\r\n    {\r\n        var fontResourceStore = new NamespacedResourceStore<byte[]>(TestResources.GetStore(), \"Resources.Testing.Fonts.Fnt.OpenSans\");\r\n        glyphStore = new TestFntGlyphStore(fontResourceStore, \"OpenSans-Regular\");\r\n        glyphStore.LoadFontAsync().WaitSafely();\r\n\r\n        // make sure glyph are loaded.\r\n        var normalGlyph = glyphStore.Get('a');\r\n        if (normalGlyph == null)\r\n            throw new ArgumentNullException(nameof(normalGlyph));\r\n\r\n        font = glyphStore.BitmapFont ?? throw new InvalidOperationException(\"Font should not be null in the test case.\");\r\n    }\r\n\r\n    [Test]\r\n    public void TestGenerate()\r\n    {\r\n        var generator = new BitmapFontImageGenerator(glyphStore);\r\n\r\n        var result = generator.Generate(font);\r\n        var originPage = glyphStore.GetPageImage(0);\r\n\r\n        // test should draw same image as origin resource in glyph store.\r\n        Assert.That(result.Length, Is.EqualTo(1));\r\n\r\n        // test should draw same image as origin resource in glyph store.\r\n        var expected = originPage.Data.ToArray();\r\n        var actual = result.FirstOrDefault()?.Data.ToArray();\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [Test]\r\n    public void TestGeneratePage()\r\n    {\r\n        var generator = new BitmapFontImageGenerator(glyphStore);\r\n        var characters = font.Characters.Where(x => x.Value.Page == 0)\r\n                             .ToDictionary(x => x.Key, x => x.Value);\r\n\r\n        // test generate first page.\r\n        var result = generator.GeneratePage(font.Info, font.Common, characters);\r\n        var originPage = glyphStore.GetPageImage(0);\r\n\r\n        // test should draw same image as origin resource in glyph store.\r\n        var expected = originPage.Data.ToArray();\r\n        var actual = result.Data.ToArray();\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    private class TestFntGlyphStore : FntGlyphStore\r\n    {\r\n        public TestFntGlyphStore(ResourceStore<byte[]> store, string assetName)\r\n            : base(store, assetName)\r\n        {\r\n        }\r\n\r\n        // should expose this image for testing.\r\n        public new TextureUpload GetPageImage(int page)\r\n            => base.GetPageImage(page);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Skinning/KaraokeBeatmapSkinDecodingTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Skinning;\r\nusing osu.Game.Rulesets.Karaoke.Skinning.Elements;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Resources;\r\nusing osu.Game.Skinning;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Skinning;\r\n\r\npublic class KaraokeBeatmapSkinDecodingTest\r\n{\r\n    [Test]\r\n    public void TestKaraokeBeatmapSkinDefaultValue()\r\n    {\r\n        var storage = TestResources.CreateSkinStorageResourceProvider();\r\n        var skin = new KaraokeBeatmapSkin(new SkinInfo { Name = \"special-skin\" }, storage);\r\n\r\n        var referencedLyric = new Lyric();\r\n        var testingNote = new Note\r\n        {\r\n            ReferenceLyricId = referencedLyric.ID,\r\n            ReferenceLyric = referencedLyric,\r\n        };\r\n\r\n        // try to get default value from the skin.\r\n        var defaultLyricFontInfo = skin.GetConfig<Lyric, LyricFontInfo>(referencedLyric)!.Value;\r\n        var defaultNoteStyle = skin.GetConfig<Note, NoteStyle>(testingNote)!.Value;\r\n\r\n        // should be able to get the default value.\r\n        Assert.That(defaultLyricFontInfo, Is.Not.Null);\r\n        Assert.That(defaultNoteStyle, Is.Not.Null);\r\n\r\n        // Check the content\r\n        Assert.That(defaultLyricFontInfo.Name, Is.Not.Null, \"Default lyric config\");\r\n        Assert.That(defaultNoteStyle.Name, Is.Not.Null, \"Default note style\");\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Skinning/KaraokeHitObjectTestScene.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Rulesets.Karaoke.UI.Components;\r\nusing osu.Game.Rulesets.Objects.Drawables;\r\nusing osu.Game.Rulesets.UI.Scrolling;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Skinning;\r\n\r\n/// <summary>\r\n/// A test scene for a karaoke hitObject.\r\n/// </summary>\r\npublic abstract partial class KaraokeHitObjectTestScene : KaraokeSkinnableColumnTestScene\r\n{\r\n    protected const float PADDING = 100;\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        SetContents(_ => new FillFlowContainer\r\n        {\r\n            Anchor = Anchor.Centre,\r\n            Origin = Anchor.Centre,\r\n            RelativeSizeAxes = Axes.Both,\r\n            Height = 0.7f,\r\n            Direction = FillDirection.Horizontal,\r\n            Padding = new MarginPadding { Left = PADDING, Right = PADDING },\r\n            Children = new[]\r\n            {\r\n                new NotePlayfieldTestContainer(COLUMNS)\r\n                {\r\n                    Anchor = Anchor.Centre,\r\n                    Origin = Anchor.Centre,\r\n                    RelativeSizeAxes = Axes.X,\r\n                    Height = DefaultColumnBackground.COLUMN_HEIGHT,\r\n                    Child = new ScrollingHitObjectContainer\r\n                    {\r\n                        RelativeSizeAxes = Axes.Both,\r\n                    }.With(c =>\r\n                    {\r\n                        c.Add(CreateHitObject().With(h =>\r\n                        {\r\n                            h.AccentColour.Value = Color4.Orange;\r\n                        }));\r\n                    }),\r\n                },\r\n            },\r\n        });\r\n    }\r\n\r\n    protected abstract DrawableHitObject CreateHitObject();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Skinning/KaraokeSkinDecodingTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Skinning;\r\nusing osu.Game.Rulesets.Karaoke.Skinning.Elements;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Resources;\r\nusing osu.Game.Skinning;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Skinning;\r\n\r\npublic class KaraokeSkinDecodingTest\r\n{\r\n    [Test]\r\n    public void TestKaraokeSkinDefaultValue()\r\n    {\r\n        var storage = TestResources.CreateSkinStorageResourceProvider();\r\n        var skin = new KaraokeSkin(new SkinInfo { Name = \"special-skin\" }, storage);\r\n\r\n        var referencedLyric = new Lyric();\r\n        var testingNote = new Note\r\n        {\r\n            ReferenceLyricId = referencedLyric.ID,\r\n            ReferenceLyric = referencedLyric,\r\n        };\r\n\r\n        // try to get default value from the skin.\r\n        var defaultLyricFontInfo = skin.GetConfig<Lyric, LyricFontInfo>(referencedLyric)!.Value;\r\n        var defaultNoteStyle = skin.GetConfig<Note, NoteStyle>(testingNote)!.Value;\r\n\r\n        // should be able to get the default value.\r\n        Assert.That(defaultLyricFontInfo, Is.Not.Null);\r\n        Assert.That(defaultNoteStyle, Is.Not.Null);\r\n\r\n        // Check the content\r\n        Assert.That(defaultLyricFontInfo.Name, Is.Not.Null, \"Default lyric config\");\r\n        Assert.That(defaultNoteStyle.Name, Is.Not.Null, \"Default note style\");\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Skinning/KaraokeSkinnableColumnTestScene.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Extensions.Color4Extensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.UI.Components;\r\nusing osu.Game.Rulesets.Karaoke.UI.Position;\r\nusing osu.Game.Rulesets.Karaoke.UI.Scrolling;\r\nusing osu.Game.Rulesets.UI.Scrolling;\r\nusing osu.Game.Rulesets.UI.Scrolling.Algorithms;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Skinning;\r\n\r\n/// <summary>\r\n/// A test scene for skinnable karaoke components.\r\n/// </summary>\r\npublic abstract partial class KaraokeSkinnableColumnTestScene : KaraokeSkinnableTestScene\r\n{\r\n    protected const double START_TIME = 1000000000;\r\n    protected const double DURATION = 1000000000;\r\n\r\n    protected const int COLUMNS = 9;\r\n\r\n    [Cached(typeof(IScrollingInfo))]\r\n    private readonly TestScrollingInfo scrollingInfo = new();\r\n\r\n    [Cached(typeof(INotePositionInfo))]\r\n    private readonly PreviewNotePositionInfo notePositionInfo = new();\r\n\r\n    protected KaraokeSkinnableColumnTestScene()\r\n    {\r\n        scrollingInfo.Direction.Value = ScrollingDirection.Left;\r\n\r\n        Add(new Box\r\n        {\r\n            RelativeSizeAxes = Axes.Both,\r\n            Colour = Color4.SlateGray.Opacity(0.2f),\r\n            Depth = 1,\r\n        });\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        // Cache session because karaoke input manager need it.\r\n        var config = Dependencies.Get<KaraokeRulesetConfigManager>();\r\n        var session = new KaraokeSessionStatics(config, null);\r\n\r\n        Dependencies.Cache(session);\r\n    }\r\n\r\n    [Test]\r\n    public void TestScrollingDown()\r\n    {\r\n        AddStep(\"change direction to left\", () => scrollingInfo.Direction.Value = ScrollingDirection.Left);\r\n    }\r\n\r\n    [Test]\r\n    public void TestScrollingUp()\r\n    {\r\n        AddStep(\"change direction to right\", () => scrollingInfo.Direction.Value = ScrollingDirection.Right);\r\n    }\r\n\r\n    private class TestScrollingInfo : IScrollingInfo\r\n    {\r\n        public readonly Bindable<ScrollingDirection> Direction = new();\r\n\r\n        IBindable<ScrollingDirection> IScrollingInfo.Direction => Direction;\r\n        IBindable<double> IScrollingInfo.TimeRange { get; } = new Bindable<double>(1000);\r\n        IBindable<IScrollAlgorithm> IScrollingInfo.Algorithm { get; } = new Bindable<IScrollAlgorithm>(new ZeroScrollAlgorithm());\r\n    }\r\n\r\n    private class ZeroScrollAlgorithm : IScrollAlgorithm\r\n    {\r\n        public double GetDisplayStartTime(double originTime, float offset, double timeRange, float scrollLength)\r\n            => double.MinValue;\r\n\r\n        public float GetLength(double startTime, double endTime, double timeRange, float scrollLength)\r\n            => scrollLength;\r\n\r\n        public float PositionAt(double time, double currentTime, double timeRange, float scrollLength, double? originTime = null)\r\n            => (float)((time - START_TIME) / timeRange) * scrollLength;\r\n\r\n        public double TimeAt(float position, double currentTime, double timeRange, float scrollLength)\r\n            => 0;\r\n\r\n        public void Reset()\r\n        {\r\n        }\r\n    }\r\n\r\n    private class PreviewNotePositionInfo : INotePositionInfo\r\n    {\r\n        public IBindable<NotePositionCalculator> Position { get; } =\r\n            new Bindable<NotePositionCalculator>(new NotePositionCalculator(COLUMNS, DefaultColumnBackground.COLUMN_HEIGHT, ScrollingNotePlayfield.COLUMN_SPACING));\r\n\r\n        public NotePositionCalculator Calculator => Position.Value;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Skinning/KaraokeSkinnableTestScene.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Beatmaps;\r\nusing osu.Game.Tests.Visual;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Skinning;\r\n\r\npublic abstract partial class KaraokeSkinnableTestScene : SkinnableTestScene\r\n{\r\n    protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestKaraokeBeatmap(ruleset);\r\n\r\n    protected override Ruleset CreateRulesetForSkinProvider() => new KaraokeRuleset();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Skinning/NotePlayfieldTestContainer.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Rulesets.Karaoke.UI;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Skinning;\r\n\r\n/// <summary>\r\n/// A container to be used in a <see cref=\"KaraokeSkinnableColumnTestScene\"/> to provide a resolvable <see cref=\"NotePlayfield\"/> dependency.\r\n/// </summary>\r\npublic partial class NotePlayfieldTestContainer : Container\r\n{\r\n    protected override Container<Drawable> Content => content;\r\n\r\n    private readonly Container content;\r\n\r\n    [Cached]\r\n    private readonly NotePlayfield playfield;\r\n\r\n    public NotePlayfieldTestContainer(int column)\r\n    {\r\n        playfield = new NotePlayfield(column);\r\n        InternalChild = content = new KaraokeInputManager(new KaraokeRuleset().RulesetInfo)\r\n        {\r\n            RelativeSizeAxes = Axes.Both,\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Skinning/TestSceneColumnBackground.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Rulesets.Karaoke.UI.Components;\r\nusing osu.Game.Skinning;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Skinning;\r\n\r\npublic partial class TestSceneColumnBackground : KaraokeSkinnableColumnTestScene\r\n{\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        SetContents(_ => new FillFlowContainer\r\n        {\r\n            Anchor = Anchor.Centre,\r\n            Origin = Anchor.Centre,\r\n            RelativeSizeAxes = Axes.Both,\r\n            Size = new Vector2(0.8f),\r\n            Direction = FillDirection.Vertical,\r\n            Spacing = new Vector2(20),\r\n            Children = new Drawable[]\r\n            {\r\n                new NotePlayfieldTestContainer(0)\r\n                {\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    Height = 0.5f,\r\n                    Child = new SkinnableDrawable(new KaraokeSkinComponentLookup(KaraokeSkinComponents.ColumnBackground), _ => new DefaultColumnBackground(0))\r\n                    {\r\n                        RelativeSizeAxes = Axes.Both,\r\n                    },\r\n                },\r\n                new NotePlayfieldTestContainer(1)\r\n                {\r\n                    RelativeSizeAxes = Axes.Both,\r\n                    Height = 0.5f,\r\n                    Child = new SkinnableDrawable(new KaraokeSkinComponentLookup(KaraokeSkinComponents.ColumnBackground), _ => new DefaultColumnBackground(1))\r\n                    {\r\n                        RelativeSizeAxes = Axes.Both,\r\n                    },\r\n                },\r\n            },\r\n        });\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Skinning/TestSceneDrawableJudgement.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing osu.Framework.Extensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Rulesets.Judgements;\r\nusing osu.Game.Rulesets.Karaoke.UI;\r\nusing osu.Game.Rulesets.Objects;\r\nusing osu.Game.Rulesets.Scoring;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Skinning;\r\n\r\npublic partial class TestSceneDrawableJudgement : KaraokeSkinnableTestScene\r\n{\r\n    public TestSceneDrawableJudgement()\r\n    {\r\n        foreach (var result in Enum.GetValues<HitResult>().Skip(1))\r\n        {\r\n            AddStep(\"Show \" + result.GetDescription(), () =>\r\n            {\r\n                SetContents(_ =>\r\n                {\r\n                    var drawableManiaJudgement = new DrawableNoteJudgement\r\n                    {\r\n                        Anchor = Anchor.Centre,\r\n                        Origin = Anchor.Centre,\r\n                    };\r\n\r\n                    drawableManiaJudgement.Apply(new JudgementResult(new HitObject { StartTime = Time.Current }, new Judgement())\r\n                    {\r\n                        Type = result,\r\n                    }, null);\r\n\r\n                    return drawableManiaJudgement;\r\n                });\r\n            });\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Skinning/TestSceneHitExplosion.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Extensions.IEnumerableExtensions;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Rulesets.Karaoke.UI;\r\nusing osu.Game.Rulesets.Karaoke.UI.Components;\r\nusing osu.Game.Skinning;\r\nusing osuTK;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Skinning;\r\n\r\n[TestFixture]\r\npublic partial class TestSceneHitExplosion : KaraokeSkinnableColumnTestScene\r\n{\r\n    public TestSceneHitExplosion()\r\n    {\r\n        int runCount = 0;\r\n\r\n        AddRepeatStep(\"explode\", () =>\r\n        {\r\n            runCount++;\r\n\r\n            if (runCount % 15 > 12)\r\n                return;\r\n\r\n            CreatedDrawables.OfType<Container>().ForEach(c =>\r\n            {\r\n                var colour = runCount / 15 % 2 == 0 ? new Color4(94, 0, 57, 255) : new Color4(6, 84, 0, 255);\r\n                c.Add(new SkinnableDrawable(new KaraokeSkinComponentLookup(KaraokeSkinComponents.HitExplosion),\r\n                    _ => new DefaultHitExplosion(colour, runCount % 6 != 0)\r\n                    {\r\n                        Anchor = Anchor.Centre,\r\n                        Origin = Anchor.Centre,\r\n                    }));\r\n            });\r\n        }, 100);\r\n    }\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        SetContents(_ => new NotePlayfieldTestContainer(0)\r\n        {\r\n            Anchor = Anchor.Centre,\r\n            Origin = Anchor.Centre,\r\n            RelativePositionAxes = Axes.Y,\r\n            Y = -0.25f,\r\n            Size = new Vector2(DefaultHitExplosion.EXPLOSION_SIZE, DefaultColumnBackground.COLUMN_HEIGHT),\r\n        });\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Skinning/TestSceneLyric.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Globalization;\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Beatmaps.ControlPoints;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Drawables;\r\nusing osu.Game.Rulesets.Mods;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Skinning;\r\n\r\n[TestFixture]\r\npublic partial class TestSceneLyric : KaraokeSkinnableTestScene\r\n{\r\n    private static CultureInfo cultureInfo { get; } = new(\"en-US\");\r\n\r\n    public TestSceneLyric()\r\n    {\r\n        AddStep(\"Default Lyric\", () => SetContents(_ => testSingle()));\r\n    }\r\n\r\n    private Drawable testSingle(double timeOffset = 0)\r\n    {\r\n        double startTime = Time.Current + 1000 + timeOffset;\r\n\r\n        var lyric = new Lyric\r\n        {\r\n            Text = \"カラオケ！\",\r\n            TimeTags = new List<TimeTag>\r\n            {\r\n                new(new TextIndex(0), startTime + 500),\r\n                new(new TextIndex(1), startTime + 600)\r\n                {\r\n                    FirstSyllable = true,\r\n                    RomanisedSyllable = \"ra\",\r\n                },\r\n                new(new TextIndex(2), startTime + 1000),\r\n                new(new TextIndex(3), startTime + 1500)\r\n                {\r\n                    FirstSyllable = true,\r\n                    RomanisedSyllable = \"ke\",\r\n                },\r\n                new(new TextIndex(4), startTime + 2000),\r\n            },\r\n            RubyTags = new[]\r\n            {\r\n                new RubyTag\r\n                {\r\n                    StartIndex = 0,\r\n                    EndIndex = 0,\r\n                    Text = \"か\",\r\n                },\r\n                new RubyTag\r\n                {\r\n                    StartIndex = 2,\r\n                    EndIndex = 2,\r\n                    Text = \"お\",\r\n                },\r\n            },\r\n        };\r\n\r\n        lyric.Translations.Add(cultureInfo, \"karaoke\");\r\n\r\n        lyric.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());\r\n\r\n        var drawable = CreateDrawableLyric(lyric);\r\n        foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObject>())\r\n            mod.ApplyToDrawableHitObject(drawable);\r\n\r\n        return drawable;\r\n    }\r\n\r\n    private int depthIndex;\r\n\r\n    protected virtual TestDrawableLyric CreateDrawableLyric(Lyric lyric)\r\n        => new(lyric)\r\n        {\r\n            Anchor = Anchor.Centre,\r\n            Origin = Anchor.Centre,\r\n            Depth = depthIndex++,\r\n        };\r\n\r\n    protected partial class TestDrawableLyric : DrawableLyric\r\n    {\r\n        public TestDrawableLyric(Lyric h)\r\n            : base(h)\r\n        {\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Skinning/TestSceneNote.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Testing;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Beatmaps.ControlPoints;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Drawables;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\nusing osu.Game.Rulesets.Objects.Drawables;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Skinning;\r\n\r\npublic partial class TestSceneNote : KaraokeHitObjectTestScene\r\n{\r\n    public TestSceneNote()\r\n    {\r\n        AddToggleStep(\"toggle hitting\", v =>\r\n        {\r\n            foreach (var holdNote in CreatedDrawables.SelectMany(d => d.ChildrenOfType<DrawableNote>()))\r\n            {\r\n                ((Bindable<bool>)holdNote.IsHitting).Value = v;\r\n            }\r\n        });\r\n    }\r\n\r\n    protected override DrawableHitObject CreateHitObject()\r\n    {\r\n        var referencedLyric = TestCaseNoteHelper.CreateLyricForNote(2, \"カラオケ\", 100, 800);\r\n        var note = new Note\r\n        {\r\n            Text = \"カラオケ\",\r\n            ReferenceLyricId = referencedLyric.ID,\r\n            ReferenceLyric = referencedLyric,\r\n        };\r\n        note.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());\r\n\r\n        return new DrawableNote(note);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Skinning/TestSceneNotePlayfield.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Game.Rulesets.Karaoke.UI;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Skinning;\r\n\r\npublic partial class TestSceneNotePlayfield : KaraokeSkinnableColumnTestScene\r\n{\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        SetContents(_ => new KaraokeInputManager(new KaraokeRuleset().RulesetInfo)\r\n        {\r\n            Child = new NotePlayfield(COLUMNS),\r\n        });\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Stages/Drawables/TestSceneDrawableStageBeatmapCoverInfo.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.Stages;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Drawables;\r\nusing osu.Game.Tests.Visual;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Stages.Drawables;\r\n\r\npublic partial class TestSceneDrawableStageBeatmapCoverInfo : OsuTestScene\r\n{\r\n    private readonly DrawableStageBeatmapCoverInfo beatmapCoverInfo;\r\n\r\n    public TestSceneDrawableStageBeatmapCoverInfo()\r\n    {\r\n        Add(beatmapCoverInfo = new DrawableStageBeatmapCoverInfo(new StageBeatmapCoverInfo())\r\n        {\r\n            Anchor = Anchor.Centre,\r\n            Origin = Anchor.Centre,\r\n        });\r\n    }\r\n\r\n    [Test]\r\n    public void TestSize()\r\n    {\r\n        AddStep(\"Small size\", () => { beatmapCoverInfo.Size = new Vector2(200); });\r\n        AddStep(\"Medium size\", () => { beatmapCoverInfo.Size = new Vector2(300); });\r\n        AddStep(\"Large size\", () => { beatmapCoverInfo.Size = new Vector2(400); });\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Stages/Infos/Classic/ClassicLyricTimingInfoTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos.Classic;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Stages.Infos.Classic;\r\n\r\npublic class ClassicLyricTimingInfoTest\r\n{\r\n    #region Edit\r\n\r\n    [Test]\r\n    public void TestAddTimingPoint()\r\n    {\r\n        var timingInfo = new ClassicLyricTimingInfo();\r\n\r\n        // Test add timing point.\r\n        timingInfo.AddTimingPoint(x =>\r\n        {\r\n            x.Time = 1000;\r\n        });\r\n\r\n        Assert.That(timingInfo.Timings.Count, Is.EqualTo(1));\r\n        Assert.That(timingInfo.Timings[0].ID.ToString(), Is.Not.Empty);\r\n        Assert.That(timingInfo.Timings[0].Time, Is.EqualTo(1000));\r\n    }\r\n\r\n    [Test]\r\n    public void TestRemoveTimingPoint()\r\n    {\r\n        var timingInfo = new ClassicLyricTimingInfo();\r\n        var timingPoint = timingInfo.AddTimingPoint(x =>\r\n        {\r\n            x.Time = 1000;\r\n        });\r\n\r\n        // Test remove timing point.\r\n        timingInfo.RemoveTimingPoint(timingPoint);\r\n\r\n        Assert.That(timingInfo.Timings, Is.Empty);\r\n    }\r\n\r\n    [Test]\r\n    public void TestAddToMapping()\r\n    {\r\n        var timingInfo = new ClassicLyricTimingInfo();\r\n        var mappingTimingPoint = timingInfo.AddTimingPoint(x =>\r\n        {\r\n            x.Time = 1000;\r\n        });\r\n        timingInfo.AddTimingPoint(x =>\r\n        {\r\n            x.Time = 2000;\r\n        });\r\n\r\n        var lyric = new Lyric();\r\n\r\n        // Test add to mapping.\r\n        timingInfo.AddToMapping(mappingTimingPoint, lyric);\r\n\r\n        var mappings = timingInfo.GetLyricTimingPoints(lyric).ToArray();\r\n        Assert.That(mappings.Length, Is.EqualTo(1));\r\n        Assert.That(mappings[0], Is.EqualTo(mappingTimingPoint));\r\n    }\r\n\r\n    [Test]\r\n    public void TestRemoveFromMapping()\r\n    {\r\n        var timingInfo = new ClassicLyricTimingInfo();\r\n        var mappingTimingPoint = timingInfo.AddTimingPoint(x =>\r\n        {\r\n            x.Time = 1000;\r\n        });\r\n        timingInfo.AddTimingPoint(x =>\r\n        {\r\n            x.Time = 2000;\r\n        });\r\n\r\n        var lyric = new Lyric();\r\n\r\n        timingInfo.AddToMapping(mappingTimingPoint, lyric);\r\n\r\n        // Test remove mapping.\r\n        timingInfo.RemoveFromMapping(mappingTimingPoint, lyric);\r\n\r\n        var mappingTimingPoints = timingInfo.GetLyricTimingPoints(lyric);\r\n        var mappingsIds = timingInfo.GetMatchedLyricIds(mappingTimingPoint);\r\n        Assert.That(mappingTimingPoints, Is.Empty);\r\n        Assert.That(mappingsIds, Is.Empty);\r\n    }\r\n\r\n    [Test]\r\n    public void TestClearTimingPointFromMapping()\r\n    {\r\n        var timingInfo = new ClassicLyricTimingInfo();\r\n        var mappingTimingPoint = timingInfo.AddTimingPoint(x =>\r\n        {\r\n            x.Time = 1000;\r\n        });\r\n        timingInfo.AddTimingPoint(x =>\r\n        {\r\n            x.Time = 2000;\r\n        });\r\n\r\n        var lyric = new Lyric();\r\n\r\n        timingInfo.AddToMapping(mappingTimingPoint, lyric);\r\n\r\n        // Should remove all mappings.\r\n        timingInfo.ClearTimingPointFromMapping(mappingTimingPoint);\r\n\r\n        var mappingTimingPoints = timingInfo.GetLyricTimingPoints(lyric);\r\n        var mappingsIds = timingInfo.GetMatchedLyricIds(mappingTimingPoint);\r\n        Assert.That(mappingTimingPoints, Is.Empty);\r\n        Assert.That(mappingsIds, Is.Empty);\r\n    }\r\n\r\n    [Test]\r\n    public void TestClearLyricFromMapping()\r\n    {\r\n        var timingInfo = new ClassicLyricTimingInfo();\r\n        var mappingTimingPoint = timingInfo.AddTimingPoint(x =>\r\n        {\r\n            x.Time = 1000;\r\n        });\r\n        timingInfo.AddTimingPoint(x =>\r\n        {\r\n            x.Time = 2000;\r\n        });\r\n\r\n        var lyric = new Lyric();\r\n\r\n        timingInfo.AddToMapping(mappingTimingPoint, lyric);\r\n\r\n        // Should remove all mappings.\r\n        timingInfo.ClearLyricFromMapping(lyric);\r\n\r\n        var mappingTimingPoints = timingInfo.GetLyricTimingPoints(lyric);\r\n        var mappingsIds = timingInfo.GetMatchedLyricIds(mappingTimingPoint);\r\n        Assert.That(mappingTimingPoints, Is.Empty);\r\n        Assert.That(mappingsIds, Is.Empty);\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Query\r\n\r\n    [Test]\r\n    public void TestGetTimingPointOrder()\r\n    {\r\n        var timingInfo = new ClassicLyricTimingInfo();\r\n        timingInfo.Timings.AddRange(new[] { new ClassicLyricTimingPoint { Time = 1000 } });\r\n\r\n        var existTimingPoint = timingInfo.Timings.First();\r\n        int? existTimingPointOrder = timingInfo.GetTimingPointOrder(existTimingPoint);\r\n        Assert.That(existTimingPointOrder, Is.EqualTo(1));\r\n\r\n        var notExistTimingPoint = new ClassicLyricTimingPoint { Time = 1000 };\r\n        int? notExistTimingPointOrder = timingInfo.GetTimingPointOrder(notExistTimingPoint);\r\n        Assert.That(notExistTimingPointOrder, Is.Null);\r\n    }\r\n\r\n    [Test]\r\n    public void TestGetLyricTimingPoints()\r\n    {\r\n        var timingInfo = new ClassicLyricTimingInfo();\r\n        var mappingTimingPoint = timingInfo.AddTimingPoint(x =>\r\n        {\r\n            x.Time = 1000;\r\n        });\r\n\r\n        var lyric = new Lyric();\r\n\r\n        timingInfo.AddToMapping(mappingTimingPoint, lyric);\r\n\r\n        // Get the mapping points.\r\n        var mappingTimingPoints = timingInfo.GetLyricTimingPoints(lyric).ToArray();\r\n        Assert.That(mappingTimingPoints.Length, Is.EqualTo(1));\r\n        Assert.That(mappingTimingPoints[0].ID, Is.EqualTo(mappingTimingPoint.ID));\r\n    }\r\n\r\n    [TestCase(\"[2000,3000]:カラオケ\", new double[] { 1000, 4000 }, 1000, 4000)]\r\n    [TestCase(\"[2000,3000]:カラオケ\", new double[] { 2000, 3000 }, 2000, 3000)]\r\n    [TestCase(\"[2000,3000]:カラオケ\", new double[] { 2001, 2999 }, 2001, 2999)]\r\n    public void TestGetStartAndEndTime(string lyricText, double[] times, double? expectedStartTime, double? expectedEndTime)\r\n    {\r\n        // todo: should test with non start time and end time.\r\n        var lyric = TestCaseTagHelper.ParseLyric(lyricText);\r\n        var timingInfo = new ClassicLyricTimingInfo();\r\n\r\n        foreach (double time in times)\r\n        {\r\n            var timingPoint = timingInfo.AddTimingPoint(x =>\r\n            {\r\n                x.Time = time;\r\n            });\r\n            timingInfo.AddToMapping(timingPoint, lyric);\r\n        }\r\n\r\n        // Test get timing info.\r\n        var result = timingInfo.GetStartAndEndTime(lyric);\r\n        Assert.That(result.Item1, Is.EqualTo(expectedStartTime));\r\n        Assert.That(result.Item2, Is.EqualTo(expectedEndTime));\r\n    }\r\n\r\n    [Test]\r\n    public void TestGetStartTime()\r\n    {\r\n        var timingInfo = new ClassicLyricTimingInfo();\r\n\r\n        Assert.That(timingInfo.GetStartTime(), Is.Null);\r\n\r\n        // Test add timing point.\r\n        timingInfo.AddTimingPoint(x =>\r\n        {\r\n            x.Time = 1000;\r\n        });\r\n\r\n        Assert.That(timingInfo.GetStartTime(), Is.EqualTo(1000));\r\n    }\r\n\r\n    [Test]\r\n    public void TestGetEndTime()\r\n    {\r\n        var timingInfo = new ClassicLyricTimingInfo();\r\n\r\n        Assert.That(timingInfo.GetEndTime(), Is.Null);\r\n\r\n        // Test add timing point.\r\n        timingInfo.AddTimingPoint(x =>\r\n        {\r\n            x.Time = 1000;\r\n        });\r\n\r\n        Assert.That(timingInfo.GetEndTime(), Is.EqualTo(1000));\r\n    }\r\n\r\n    [Test]\r\n    public void TestGetMatchedLyricIds()\r\n    {\r\n        var timingInfo = new ClassicLyricTimingInfo();\r\n        var mappingTimingPoint = timingInfo.AddTimingPoint(x =>\r\n        {\r\n            x.Time = 1000;\r\n        });\r\n\r\n        var lyric = new Lyric();\r\n\r\n        timingInfo.AddToMapping(mappingTimingPoint, lyric);\r\n\r\n        // Get the mapping points.\r\n        var mappingIds = timingInfo.GetMatchedLyricIds(mappingTimingPoint);\r\n        Assert.That(mappingIds, Is.EqualTo(new[] { lyric.ID }));\r\n    }\r\n\r\n    #endregion\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Stages/Infos/Preview/PreviewStageTimingCalculatorTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos.Preview;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Extensions;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\nusing osu.Game.Rulesets.Objects;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Stages.Infos.Preview;\r\n\r\npublic class PreviewStageTimingCalculatorTest\r\n{\r\n    private const int lyric_1_id = 1;\r\n    private const int lyric_2_id = 2;\r\n    private const int lyric_3_id = 3;\r\n    private const int lyric_4_id = 4;\r\n    private const int lyric_5_id = 5;\r\n\r\n    // lyrics in the stage.\r\n    private const int number_of_lyrics = 4;\r\n\r\n    // offset time in the fade in/out\r\n    private const double fading_time = 10;\r\n\r\n    // offset time in the Lyrics arrangement.\r\n    private const double line_moving_time = 20;\r\n    private const double line_moving_offset_time = 30;\r\n\r\n    [TestCase(lyric_1_id, 0)]\r\n    [TestCase(lyric_2_id, 0)]\r\n    [TestCase(lyric_3_id, 0)]\r\n    [TestCase(lyric_4_id, 0)]\r\n    [TestCase(lyric_5_id, 2000 + line_moving_offset_time * 4 + fading_time)] // it's the time first lyric should be disappeared.\r\n    public void TestGetStartTime(int lyricId, double expected)\r\n    {\r\n        var beatmap = createBeatmap();\r\n        var lyric = beatmap.HitObjects.OfType<Lyric>().Single(x => x.ID == TestCaseElementIdHelper.CreateElementIdByNumber(lyricId));\r\n\r\n        var calculator = createCalculator(beatmap);\r\n        double actual = calculator.CalculateStartTime(lyric);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(lyric_1_id, 2000)]\r\n    [TestCase(lyric_2_id, 3000)]\r\n    [TestCase(lyric_3_id, 4000)]\r\n    [TestCase(lyric_4_id, 5000)]\r\n    [TestCase(lyric_5_id, 6000)]\r\n    public void TestGetEndTime(int lyricId, double expected)\r\n    {\r\n        var beatmap = createBeatmap();\r\n        var lyric = beatmap.HitObjects.OfType<Lyric>().Single(x => x.ID == TestCaseElementIdHelper.CreateElementIdByNumber(lyricId));\r\n\r\n        var calculator = createCalculator(beatmap);\r\n        double actual = calculator.CalculateEndTime(lyric);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(lyric_1_id, new string[] { })]\r\n    [TestCase(lyric_2_id, new[] { \"0:2000\" })]\r\n    [TestCase(lyric_3_id, new[] { \"1:2030\", \"0:3000\" })]\r\n    [TestCase(lyric_4_id, new[] { \"2:2060\", \"1:3030\", \"0:4000\" })]\r\n    [TestCase(lyric_5_id, new[] { \"2:3060\", \"1:4030\", \"0:5000\" })]\r\n    public void TestGetTiming(int lyricId, string[] timing)\r\n    {\r\n        var beatmap = createBeatmap();\r\n        var lyric = beatmap.HitObjects.OfType<Lyric>().Single(x => x.ID == TestCaseElementIdHelper.CreateElementIdByNumber(lyricId));\r\n\r\n        var calculator = createCalculator(beatmap);\r\n        var expected = convertKeyToDictionary(timing);\r\n        var actual = calculator.CalculateTimings(lyric);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    private static IDictionary<int, double> convertKeyToDictionary(IEnumerable<string> values)\r\n        => values.ToDictionary(k => int.Parse(k.Split(':').First()), v => double.Parse(v.Split(':').Last()));\r\n\r\n    private PreviewStageTimingCalculator createCalculator(IBeatmap beatmap)\r\n    {\r\n        var definition = new PreviewStageDefinition\r\n        {\r\n            NumberOfLyrics = number_of_lyrics,\r\n            FadingTime = fading_time,\r\n            LineMovingTime = line_moving_time,\r\n            LineMovingOffsetTime = line_moving_offset_time,\r\n        };\r\n\r\n        return new PreviewStageTimingCalculator(beatmap, definition);\r\n    }\r\n\r\n    private IBeatmap createBeatmap()\r\n    {\r\n        var lyrics = new List<Lyric>\r\n        {\r\n            createLyric(lyric_1_id, 1000, 2000),\r\n            createLyric(lyric_2_id, 2100, 3000),\r\n            createLyric(lyric_3_id, 3100, 4000),\r\n            createLyric(lyric_4_id, 4100, 5000),\r\n            createLyric(lyric_5_id, 5100, 6000),\r\n        };\r\n\r\n        lyrics.Reverse();\r\n        return new Beatmap\r\n        {\r\n            HitObjects = lyrics.OfType<HitObject>().ToList(),\r\n        };\r\n    }\r\n\r\n    private static Lyric createLyric(int id, double startTime, double endTime)\r\n    {\r\n        return new Lyric\r\n        {\r\n            TimeTags = new List<TimeTag>\r\n            {\r\n                new(new TextIndex(), startTime),\r\n                new(new TextIndex(), endTime),\r\n            },\r\n        }.ChangeId(id);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Stages/Infos/StageElementCategoryTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Stages.Infos;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Stages.Infos;\r\n\r\npublic class StageElementCategoryTest\r\n{\r\n    #region Edit\r\n\r\n    [Test]\r\n    public void TestAddElement()\r\n    {\r\n        var category = new TestStageElementCategory();\r\n        var element = category.AddElement(x =>\r\n        {\r\n            x.Name = \"Element 1\";\r\n        });\r\n\r\n        Assert.That(category.AvailableElements.Count, Is.EqualTo(1));\r\n        Assert.That(category.AvailableElements[0].ID, Is.Not.EqualTo(ElementId.Empty));\r\n        Assert.That(category.AvailableElements[0].Name, Is.EqualTo(\"Element 1\"));\r\n\r\n        category.AddElement();\r\n\r\n        Assert.That(category.AvailableElements.Count, Is.EqualTo(2));\r\n        Assert.That(category.AvailableElements[1].ID, Is.Not.EqualTo(ElementId.Empty));\r\n    }\r\n\r\n    [Test]\r\n    public void TestEditElement()\r\n    {\r\n        var category = new TestStageElementCategory();\r\n        var element = category.AddElement();\r\n\r\n        var id = element.ID;\r\n        category.EditElement(id, x =>\r\n        {\r\n            x.Name = \"Element 1\";\r\n        });\r\n\r\n        Assert.That(category.AvailableElements.Count, Is.EqualTo(1));\r\n        Assert.That(category.AvailableElements[0].ID, Is.Not.EqualTo(ElementId.Empty));\r\n        Assert.That(category.AvailableElements[0].Name, Is.EqualTo(\"Element 1\"));\r\n    }\r\n\r\n    [Test]\r\n    public void TestRemoveElement()\r\n    {\r\n        var category = new TestStageElementCategory();\r\n\r\n        var element1 = category.AddElement();\r\n        var element2 = category.AddElement();\r\n        var lyric1 = new Lyric();\r\n        var lyric2 = new Lyric();\r\n        var lyric3 = new Lyric();\r\n\r\n        category.AddToMapping(element1, lyric1);\r\n        category.AddToMapping(element1, lyric2);\r\n        category.AddToMapping(element2, lyric3);\r\n\r\n        // Only remove element 1.\r\n        category.RemoveElement(element1);\r\n\r\n        // Should have only one element.\r\n        var defaultElement = category.DefaultElement;\r\n        Assert.That(category.AvailableElements.Count, Is.EqualTo(1));\r\n\r\n        // Should get the default element because mapping has been removed.\r\n        Assert.That(category.GetElementByItem(lyric1), Is.EqualTo(defaultElement));\r\n        Assert.That(category.GetElementByItem(lyric2), Is.EqualTo(defaultElement));\r\n        Assert.That(category.GetElementByItem(lyric3), Is.EqualTo(element2));\r\n    }\r\n\r\n    [Test]\r\n    public void TestClearElements()\r\n    {\r\n        var category = new TestStageElementCategory();\r\n\r\n        var element1 = category.AddElement();\r\n        var element2 = category.AddElement();\r\n        var lyric1 = new Lyric();\r\n        var lyric2 = new Lyric();\r\n        var lyric3 = new Lyric();\r\n\r\n        category.AddToMapping(element1, lyric1);\r\n        category.AddToMapping(element1, lyric2);\r\n        category.AddToMapping(element2, lyric3);\r\n\r\n        // Clear all elements.\r\n        category.ClearElements();\r\n\r\n        // Should clear everything.\r\n        Assert.That(category.AvailableElements.Count, Is.EqualTo(0));\r\n        Assert.That(category.Mappings.Count, Is.EqualTo(0));\r\n\r\n        // should get the default element.\r\n        var defaultElement = category.DefaultElement;\r\n        Assert.That(category.GetElementByItem(lyric1), Is.EqualTo(defaultElement));\r\n    }\r\n\r\n    [Test]\r\n    public void TestAddToMapping()\r\n    {\r\n        var category = new TestStageElementCategory();\r\n\r\n        var element1 = category.AddElement();\r\n        var element2 = category.AddElement();\r\n        var lyric1 = new Lyric();\r\n        var lyric2 = new Lyric();\r\n        var lyric3 = new Lyric();\r\n\r\n        category.AddToMapping(element1, lyric1);\r\n        category.AddToMapping(element1, lyric2);\r\n        category.AddToMapping(element2, lyric3);\r\n\r\n        // Should get the matched element.\r\n        Assert.That(category.GetElementByItem(lyric1), Is.EqualTo(element1));\r\n        Assert.That(category.GetElementByItem(lyric2), Is.EqualTo(element1));\r\n        Assert.That(category.GetElementByItem(lyric3), Is.EqualTo(element2));\r\n    }\r\n\r\n    [Test]\r\n    public void TestRemoveHitObjectFromMapping()\r\n    {\r\n        var category = new TestStageElementCategory();\r\n\r\n        var element1 = category.AddElement();\r\n        var lyric1 = new Lyric();\r\n\r\n        category.AddToMapping(element1, lyric1);\r\n        category.RemoveHitObjectFromMapping(lyric1);\r\n\r\n        // Should clear added mappings.\r\n        var mappings = category.Mappings;\r\n        Assert.That(mappings, Is.Empty);\r\n    }\r\n\r\n    [Test]\r\n    public void TestRemoveElementFromMapping()\r\n    {\r\n        var category = new TestStageElementCategory();\r\n\r\n        var element1 = category.AddElement();\r\n        var lyric1 = new Lyric();\r\n\r\n        category.AddToMapping(element1, lyric1);\r\n        category.RemoveElementFromMapping(element1);\r\n\r\n        // Should clear added mappings.\r\n        var mappings = category.Mappings;\r\n        Assert.That(mappings, Is.Empty);\r\n    }\r\n\r\n    [Test]\r\n    public void TestClearUnusedMapping()\r\n    {\r\n        var category = new TestStageElementCategory();\r\n\r\n        var element1 = category.AddElement();\r\n        var lyric1 = new Lyric();\r\n        var lyric2 = new Lyric();\r\n        var lyric3 = new Lyric();\r\n\r\n        category.AddToMapping(element1, lyric1);\r\n        category.AddToMapping(element1, lyric2);\r\n        category.AddToMapping(element1, lyric3);\r\n\r\n        var lyricsInTheBeatmap = new[] { lyric1, lyric2 };\r\n        category.ClearUnusedMapping(id => lyricsInTheBeatmap.Any(x => x.ID == id));\r\n\r\n        // Should get the matched element.\r\n        Assert.That(category.GetElementByItem(lyric1), Is.EqualTo(element1));\r\n        Assert.That(category.GetElementByItem(lyric2), Is.EqualTo(element1));\r\n\r\n        // should get the default element because lyric3 is clear in the mapping.\r\n        var defaultElement = category.DefaultElement;\r\n        Assert.That(category.GetElementByItem(lyric3), Is.EqualTo(defaultElement));\r\n    }\r\n\r\n    #endregion\r\n\r\n    #region Query\r\n\r\n    [Test]\r\n    public void TestGetElementByItem()\r\n    {\r\n        var category = new TestStageElementCategory();\r\n\r\n        var element1 = category.AddElement();\r\n        var lyric1 = new Lyric();\r\n        var lyric2 = new Lyric();\r\n\r\n        category.AddToMapping(element1, lyric1);\r\n\r\n        // Should get the matched element.\r\n        Assert.That(category.GetElementByItem(lyric1), Is.EqualTo(element1));\r\n\r\n        // Should get the default element because it's not in the mapping list.\r\n        var defaultElement = category.DefaultElement;\r\n        Assert.That(category.GetElementByItem(lyric2), Is.EqualTo(defaultElement));\r\n    }\r\n\r\n    [Test]\r\n    public void TestGetHitObjectIdsByElement()\r\n    {\r\n        var category = new TestStageElementCategory();\r\n\r\n        var element1 = category.AddElement();\r\n        var lyric1 = new Lyric();\r\n\r\n        category.AddToMapping(element1, lyric1);\r\n\r\n        // Should get the matched element.\r\n        Assert.That(category.GetHitObjectIdsByElement(element1), Is.EqualTo(new[] { lyric1.ID }));\r\n\r\n        // Should get the default element because it's not in the mapping list.\r\n        var defaultElement = category.DefaultElement;\r\n        Assert.That(category.GetHitObjectIdsByElement(defaultElement), Is.Empty);\r\n    }\r\n\r\n    #endregion\r\n\r\n    private class TestStageElement : StageElement, IComparable<TestStageElement>\r\n    {\r\n        public int CompareTo(TestStageElement? other)\r\n        {\r\n            return ComparableUtils.CompareByProperty(this, other,\r\n                x => x.Name,\r\n                x => x.ID);\r\n        }\r\n    }\r\n\r\n    private class TestStageElementCategory : StageElementCategory<TestStageElement, Lyric>\r\n    {\r\n        protected override TestStageElement CreateDefaultElement()\r\n            => new();\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/TestSceneOsuGame.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Shapes;\r\nusing osu.Game.Tests.Visual;\r\nusing osuTK.Graphics;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests;\r\n\r\npublic partial class TestSceneOsuGame : OsuTestScene\r\n{\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        Children = new Drawable[]\r\n        {\r\n            new Box\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n                Colour = Color4.Black,\r\n            },\r\n        };\r\n\r\n        AddGame(new OsuGame());\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/UI/Position/NotePositionCalculatorTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Replays;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\nusing osu.Game.Rulesets.Karaoke.UI.Position;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.UI.Position;\r\n\r\n[TestFixture]\r\npublic class NotePositionCalculatorTest\r\n{\r\n    private const int default_columns = 9;\r\n    private const float default_column_height = 20;\r\n    private const float default_spacing = 1;\r\n\r\n    [TestCase(0, 0)]\r\n    [TestCase(1, -21f)]\r\n    [TestCase(1.5, -31.5f)]\r\n    public void TestPositionAtTone(double scale, float expected)\r\n    {\r\n        var calculator = new NotePositionCalculator(default_columns, default_column_height, default_spacing);\r\n        var note = new Note\r\n        {\r\n            Tone = TestCaseToneHelper.NumberToTone(scale),\r\n        };\r\n\r\n        float actual = calculator.YPositionAt(note);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(0, 0)]\r\n    [TestCase(1, -21f)]\r\n    [TestCase(1.5, -31.5f)]\r\n    public void TestPositionAtNote(double scale, float expected)\r\n    {\r\n        var calculator = new NotePositionCalculator(default_columns, default_column_height, default_spacing);\r\n        var tone = TestCaseToneHelper.NumberToTone(scale);\r\n\r\n        float actual = calculator.YPositionAt(tone);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(0f, 0)]\r\n    [TestCase(1f, -21f)]\r\n    [TestCase(-1f, 21f)]\r\n    public void TestPositionAtScoringAction(float scale, float expected)\r\n    {\r\n        var calculator = new NotePositionCalculator(default_columns, default_column_height, default_spacing);\r\n        var action = new KaraokeScoringAction\r\n        {\r\n            Scale = scale,\r\n        };\r\n\r\n        float actual = calculator.YPositionAt(action);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(0f, 0)]\r\n    [TestCase(1f, -21f)]\r\n    [TestCase(-1f, 21f)]\r\n    public void TestPositionAtReplayFrame(float scale, float expected)\r\n    {\r\n        var calculator = new NotePositionCalculator(default_columns, default_column_height, default_spacing);\r\n        var frame = new KaraokeReplayFrame(0, scale);\r\n\r\n        float actual = calculator.YPositionAt(frame);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(0f, 0)]\r\n    [TestCase(1f, -21f)]\r\n    [TestCase(-1f, 21f)]\r\n    [TestCase(10f, -84f)] // should handle the case not out of the range.\r\n    [TestCase(-10f, 84f)]\r\n    public void TestPositionAtScale(float scale, float expected)\r\n    {\r\n        var calculator = new NotePositionCalculator(default_columns, default_column_height, default_spacing);\r\n\r\n        float actual = calculator.YPositionAt(scale);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(1, 0)]\r\n    [TestCase(3, 1)]\r\n    public void TestGetMaxTone(int columns, double actual)\r\n    {\r\n        var calculator = new NotePositionCalculator(columns, default_column_height, default_spacing);\r\n\r\n        var expected = calculator.MaxTone;\r\n        Assert.That(TestCaseToneHelper.NumberToTone(actual), Is.EqualTo(expected));\r\n    }\r\n\r\n    [TestCase(1, 0)]\r\n    [TestCase(3, -1)]\r\n    public void TestGetMinTone(int columns, double actual)\r\n    {\r\n        var calculator = new NotePositionCalculator(columns, default_column_height, default_spacing);\r\n\r\n        var expected = calculator.MinTone;\r\n        Assert.That(TestCaseToneHelper.NumberToTone(actual), Is.EqualTo(expected));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/UI/TestSceneControlLayer.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.UI.HUD;\r\nusing osu.Game.Tests.Visual;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.UI;\r\n\r\n[TestFixture]\r\npublic partial class TestSceneControlLayer : OsuTestScene\r\n{\r\n    public SettingOverlayContainer SettingOverlayContainer { get; set; } = null!;\r\n\r\n    protected override Ruleset CreateRuleset() => new KaraokeRuleset();\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        var config = Dependencies.Get<KaraokeRulesetConfigManager>();\r\n        Dependencies.Cache(new KaraokeSessionStatics(config, null));\r\n\r\n        // Cannot work now because it need extra BDL in child\r\n        Add(new Container\r\n        {\r\n            RelativeSizeAxes = Axes.Both,\r\n            Child = SettingOverlayContainer = new SettingOverlayContainer\r\n            {\r\n                RelativeSizeAxes = Axes.Both,\r\n            },\r\n        });\r\n\r\n        AddStep(\"Toggle setting\", SettingOverlayContainer.ToggleGeneralSettingsOverlay);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/UI/TestSceneKaraokePlayer.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Beatmaps;\r\nusing osu.Game.Tests.Visual;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.UI;\r\n\r\n[TestFixture]\r\npublic partial class TestSceneKaraokePlayer : PlayerTestScene\r\n{\r\n    protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestKaraokeBeatmap(ruleset);\r\n\r\n    protected override Ruleset CreatePlayerRuleset() => new KaraokeRuleset();\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/UI/TestSceneNotePlayfield.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Allocation;\r\nusing osu.Framework.Bindables;\r\nusing osu.Framework.Graphics;\r\nusing osu.Framework.Graphics.Containers;\r\nusing osu.Game.Beatmaps;\r\nusing osu.Game.Beatmaps.ControlPoints;\r\nusing osu.Game.Rulesets.Karaoke.Configuration;\r\nusing osu.Game.Rulesets.Karaoke.Objects;\r\nusing osu.Game.Rulesets.Karaoke.Objects.Drawables;\r\nusing osu.Game.Rulesets.Karaoke.Replays;\r\nusing osu.Game.Rulesets.Karaoke.Tests.Helper;\r\nusing osu.Game.Rulesets.Karaoke.UI;\r\nusing osu.Game.Rulesets.Karaoke.UI.Components;\r\nusing osu.Game.Rulesets.Karaoke.UI.Position;\r\nusing osu.Game.Rulesets.Karaoke.UI.Scrolling;\r\nusing osu.Game.Rulesets.Mods;\r\nusing osu.Game.Rulesets.UI.Scrolling;\r\nusing osu.Game.Tests.Visual;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.UI;\r\n\r\n[TestFixture]\r\npublic partial class TestSceneNotePlayfield : OsuTestScene\r\n{\r\n    public const int COLUMNS = 9;\r\n\r\n    [Cached(typeof(IReadOnlyList<Mod>))]\r\n    private IReadOnlyList<Mod> mods { get; set; } = Array.Empty<Mod>();\r\n\r\n    [Cached(typeof(INotePositionInfo))]\r\n    private readonly PreviewNotePositionInfo notePositionInfo = new();\r\n\r\n    private readonly List<NotePlayfield> notePlayfields = new();\r\n\r\n    protected override Ruleset CreateRuleset() => new KaraokeRuleset();\r\n\r\n    [BackgroundDependencyLoader]\r\n    private void load()\r\n    {\r\n        var config = Dependencies.Get<KaraokeRulesetConfigManager>();\r\n        Dependencies.Cache(new KaraokeSessionStatics(config, null));\r\n\r\n        Child = new GridContainer\r\n        {\r\n            RelativeSizeAxes = Axes.Both,\r\n            Anchor = Anchor.Centre,\r\n            Origin = Anchor.Centre,\r\n            Content = new[]\r\n            {\r\n                new[]\r\n                {\r\n                    createColumn(ScrollingDirection.Left, COLUMNS),\r\n                },\r\n                new[]\r\n                {\r\n                    createColumn(ScrollingDirection.Right, COLUMNS),\r\n                },\r\n            },\r\n        };\r\n    }\r\n\r\n    protected override void LoadComplete()\r\n    {\r\n        base.LoadComplete();\r\n\r\n        AddStep(\"note\", () =>\r\n        {\r\n            createBar(true);\r\n            createNote();\r\n            createBar(true, 3000);\r\n        });\r\n        AddStep(\"multi note\", () =>\r\n        {\r\n            createBar(true);\r\n            createNote(2000, 100, -4);\r\n            createNote(2100, 100, -3);\r\n            createNote(2200, 100, -2);\r\n            createNote(2300, 100, -1);\r\n            createNote(2400, 100);\r\n            createNote(2500, 100, 1);\r\n            createNote(2600, 100, 2);\r\n            createNote(2700, 100, 3);\r\n            createNote(2800, 100, 4);\r\n            createBar(true, 2900);\r\n        });\r\n        AddStep(\"scoring\", () =>\r\n        {\r\n            createBar(true);\r\n            createNote(2000, 100, 4, true);\r\n            createNote(2100, 100, 3, true);\r\n            createNote(2200, 100, 2, true);\r\n            createNote(2300, 100, 1, true);\r\n            createNote(2400, 100, 0, true);\r\n            createNote(2500, 100, -1, true);\r\n            createNote(2600, 100, -2, true);\r\n            createNote(2700, 100, -3, true);\r\n            createNote(2800, 100, -4, true);\r\n            createBar(true, 2900);\r\n        });\r\n        AddStep(\"bar\", () => createBar(false));\r\n        AddStep(\"major bar\", () => createBar(true));\r\n    }\r\n\r\n    private void createNote(double increaseTime = 2000, double duration = 1000, int tone = 0, bool scoring = false)\r\n    {\r\n        notePlayfields.ForEach(x =>\r\n        {\r\n            var referencedLyric = TestCaseNoteHelper.CreateLyricForNote(2, \"Here\", Time.Current + increaseTime, duration);\r\n            var note = new Note\r\n            {\r\n                Text = \"Here\",\r\n                Display = true,\r\n                Tone = new Tone { Scale = tone },\r\n                ReferenceLyricId = referencedLyric.ID,\r\n                ReferenceLyric = referencedLyric,\r\n                ReferenceTimeTagIndex = 0,\r\n            };\r\n            note.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());\r\n\r\n            x.Add(new DrawableNote(note));\r\n        });\r\n\r\n        if (scoring)\r\n            createScoringPath(increaseTime, duration, tone);\r\n    }\r\n\r\n    private void createScoringPath(double increaseTime = 2000, double duration = 1000, int scale = 0)\r\n    {\r\n        notePlayfields.ForEach(x =>\r\n        {\r\n            // Start frame\r\n            x.AddReplay(new KaraokeReplayFrame(Time.Current + increaseTime, scale));\r\n\r\n            // End frame\r\n            x.AddReplay(new KaraokeReplayFrame(Time.Current + increaseTime + duration - 2, scale));\r\n\r\n            // Stop point\r\n            x.AddReplay(new KaraokeReplayFrame(Time.Current + increaseTime + duration - 1));\r\n        });\r\n    }\r\n\r\n    private void createBar(bool isMajor, double increaseTime = 2000)\r\n    {\r\n        notePlayfields.ForEach(x =>\r\n        {\r\n            var bar = new BarLine { StartTime = Time.Current + increaseTime, Major = isMajor };\r\n            bar.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());\r\n\r\n            x.Add(bar);\r\n        });\r\n    }\r\n\r\n    private Drawable createColumn(ScrollingDirection direction, int column)\r\n    {\r\n        var playfield = new NotePlayfield(column)\r\n        {\r\n            Anchor = Anchor.Centre,\r\n            Origin = Anchor.Centre,\r\n        };\r\n\r\n        notePlayfields.Add(playfield);\r\n\r\n        return new ScrollingTestContainer(direction)\r\n        {\r\n            Anchor = Anchor.Centre,\r\n            Origin = Anchor.Centre,\r\n            Padding = new MarginPadding(20),\r\n            RelativeSizeAxes = Axes.Both,\r\n            TimeRange = 2000,\r\n            Child = playfield,\r\n        };\r\n    }\r\n\r\n    private class PreviewNotePositionInfo : INotePositionInfo\r\n    {\r\n        public IBindable<NotePositionCalculator> Position { get; } =\r\n            new Bindable<NotePositionCalculator>(new NotePositionCalculator(COLUMNS, DefaultColumnBackground.COLUMN_HEIGHT, ScrollingNotePlayfield.COLUMN_SPACING));\r\n\r\n        public NotePositionCalculator Calculator => Position.Value;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/UI/TestSceneRulesetIcon.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Graphics.Containers;\r\nusing osu.Game.Tests.Visual;\r\nusing osuTK;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.UI;\r\n\r\n[TestFixture]\r\npublic partial class TestSceneRulesetIcon : OsuTestScene\r\n{\r\n    public TestSceneRulesetIcon()\r\n    {\r\n        Child = new ConstrainedIconContainer\r\n        {\r\n            Anchor = Anchor.Centre,\r\n            Origin = Anchor.Centre,\r\n            Icon = new KaraokeRuleset().CreateIcon(),\r\n            Size = new Vector2(40),\r\n        };\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/UI/TestSceneScoringStatus.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Framework.Graphics;\r\nusing osu.Game.Rulesets.Karaoke.UI.Components;\r\nusing osu.Game.Tests.Visual;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.UI;\r\n\r\n[TestFixture]\r\npublic partial class TestSceneScoringStatus : OsuTestScene\r\n{\r\n    [TestCase(ScoringStatusMode.AndroidMicrophonePermissionDeclined)]\r\n    [TestCase(ScoringStatusMode.AndroidDoesNotSupported)]\r\n    [TestCase(ScoringStatusMode.IOSMicrophonePermissionDeclined)]\r\n    [TestCase(ScoringStatusMode.IOSDoesNotSupported)]\r\n    [TestCase(ScoringStatusMode.OSXMicrophonePermissionDeclined)]\r\n    [TestCase(ScoringStatusMode.OSXDoesNotSupported)]\r\n    [TestCase(ScoringStatusMode.WindowsMicrophonePermissionDeclined)]\r\n    [TestCase(ScoringStatusMode.NoMicrophoneDevice)]\r\n    [TestCase(ScoringStatusMode.NotScoring)]\r\n    [TestCase(ScoringStatusMode.AutoPlay)]\r\n    [TestCase(ScoringStatusMode.Edit)]\r\n    [TestCase(ScoringStatusMode.Scoring)]\r\n    [TestCase(ScoringStatusMode.NotInitialized)]\r\n    public void TestMode(ScoringStatusMode mode)\r\n    {\r\n        AddStep(\"create mod display\", () =>\r\n        {\r\n            Child = new ScoringStatus(mode)\r\n            {\r\n                Anchor = Anchor.Centre,\r\n                Origin = Anchor.Centre,\r\n            };\r\n        });\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Utils/BindablesUtilsTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Bindables;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Utils;\r\n\r\npublic class BindablesUtilsTest\r\n{\r\n    [TestCase(null, null, new[] { 1 }, null, new[] { 1 }, new[] { 1 })]\r\n    [TestCase(null, null, null, new[] { 1 }, new[] { 1 }, new[] { 1 })]\r\n    [TestCase(null, null, new[] { 1 }, new[] { 2 }, new[] { 1, 2 }, new[] { 1, 2 })]\r\n    [TestCase(new[] { 1 }, new[] { 2 }, null, null, new[] { 1, 2 }, new[] { 2, 1 })]\r\n    [TestCase(new[] { 1 }, new[] { 2 }, new[] { 3 }, null, new[] { 1, 2, 3 }, new[] { 2, 1, 3 })]\r\n    [TestCase(new[] { 1 }, new[] { 2 }, null, new[] { 3 }, new[] { 1, 2, 3 }, new[] { 2, 1, 3 })]\r\n    public void TestSyncWithAddItems(int[] firstDefaultValues, int[] secondDefaultValues, int[]? firstNewValues, int[]? secondNewValues, int[] expectedFirstValues, int[] expectedSecondValues)\r\n    {\r\n        var firstBindableList = new BindableList<int>(firstDefaultValues);\r\n        var secondBindableList = new BindableList<int>(secondDefaultValues);\r\n\r\n        BindablesUtils.Sync(firstBindableList, secondBindableList);\r\n\r\n        if (firstNewValues != null)\r\n            firstBindableList.AddRange(firstNewValues);\r\n\r\n        if (secondNewValues != null)\r\n            secondBindableList.AddRange(secondNewValues);\r\n\r\n        Assert.That(expectedFirstValues, Is.EqualTo(firstBindableList.ToArray()));\r\n        Assert.That(expectedSecondValues, Is.EqualTo(secondBindableList.ToArray()));\r\n    }\r\n\r\n    [TestCase(null, null, new[] { 1 }, null, new int[] { }, new int[] { })] // should not clear if has no values.\r\n    [TestCase(null, null, null, new[] { 1 }, new int[] { }, new int[] { })]\r\n    [TestCase(null, null, new[] { 1 }, new[] { 2 }, new int[] { }, new int[] { })]\r\n    [TestCase(new[] { 1 }, new[] { 2 }, new[] { 1 }, null, new[] { 2 }, new[] { 2 })] // matched value should be clear\r\n    [TestCase(new[] { 1 }, new[] { 2 }, null, new[] { 1 }, new[] { 2 }, new[] { 2 })]\r\n    [TestCase(new[] { 1 }, new[] { 2 }, new[] { 1 }, new[] { 2 }, new int[] { }, new int[] { })] // all value should be clear\r\n    [TestCase(new[] { 1 }, new[] { 2 }, new[] { 2 }, new[] { 1 }, new int[] { }, new int[] { })]\r\n    [TestCase(new[] { 1 }, new[] { 2 }, new[] { 3 }, null, new[] { 1, 2 }, new[] { 2, 1 })] // should not clear value if not contains.\r\n    [TestCase(new[] { 1 }, new[] { 2 }, null, new[] { 3 }, new[] { 1, 2 }, new[] { 2, 1 })]\r\n    public void TestSyncWithRemoveItems(int[]? firstDefaultValues, int[]? secondDefaultValues, int[]? firstRemoveValues, int[]? secondRemoveValues, int[] expectedFirstValues,\r\n                                        int[] expectedSecondValues)\r\n    {\r\n        var firstBindableList = new BindableList<int>(firstDefaultValues);\r\n        var secondBindableList = new BindableList<int>(secondDefaultValues);\r\n\r\n        BindablesUtils.Sync(firstBindableList, secondBindableList);\r\n\r\n        if (firstRemoveValues != null)\r\n            firstBindableList.RemoveAll(firstRemoveValues.Contains);\r\n\r\n        if (secondRemoveValues != null)\r\n            secondBindableList.RemoveAll(secondRemoveValues.Contains);\r\n\r\n        Assert.That(expectedFirstValues, Is.EqualTo(firstBindableList.ToArray()));\r\n        Assert.That(expectedSecondValues, Is.EqualTo(secondBindableList.ToArray()));\r\n    }\r\n\r\n    [TestCase(new[] { 1 }, null, null, new[] { 1 })] // should sync default value also.\r\n    [TestCase(new[] { 1 }, null, new[] { 2 }, new[] { 1, 2 })]\r\n    [TestCase(null, null, new[] { 2 }, new[] { 2 })]\r\n    [TestCase(new[] { 1 }, new[] { 1 }, null, new[] { 1 })]\r\n    [TestCase(new[] { 1, 2 }, new[] { 1 }, null, new[] { 1, 2 })]\r\n    [TestCase(new[] { 1, 1 }, new[] { 1 }, null, new[] { 1 })]\r\n    [TestCase(new[] { 1, 1 }, new[] { 2 }, null, new[] { 2, 1 })] // it's ok if has duplicated value in default value.\r\n    [TestCase(new[] { 1 }, new[] { 1 }, new[] { 1 }, new[] { 1 })] // should not sync to second list if add the same value.\r\n    public void TestOnyWaySyncWithAddItems(int[]? defaultValuesInFirstBindable, int[]? defaultValuesInSecondBindable, int[]? newValues, int[] expectedValuesInSecondBindable)\r\n    {\r\n        var firstBindableList = new BindableList<int>(defaultValuesInFirstBindable);\r\n        var secondBindableList = new BindableList<int>(defaultValuesInSecondBindable);\r\n\r\n        BindablesUtils.OnyWaySync(firstBindableList, secondBindableList);\r\n\r\n        if (newValues != null)\r\n            firstBindableList.AddRange(newValues);\r\n\r\n        Assert.That(expectedValuesInSecondBindable, Is.EqualTo(secondBindableList.ToArray()));\r\n    }\r\n\r\n    [TestCase(new[] { 1, 2, 3 }, null, new[] { 1 }, new[] { 2, 3 })]\r\n    [TestCase(new[] { 1 }, null, new[] { 1 }, new int[] { })] // remove all values\r\n    [TestCase(null, null, new[] { 2 }, new int[] { })] // remove value that is not exist.\r\n    [TestCase(new[] { 1, 2 }, new[] { 3 }, new[] { 3 }, new[] { 3, 1, 2 })] // cannot remove value from second list if remove value is not in the first list.\r\n    public void TestOnyWaySyncWithRemoveItems(int[]? defaultValuesInFirstBindable, int[]? defaultValuesInSecondBindable, int[] removeValues, int[] expectedValuesInSecondBindable)\r\n    {\r\n        var firstBindableList = new BindableList<int>(defaultValuesInFirstBindable);\r\n        var secondBindableList = new BindableList<int>(defaultValuesInSecondBindable);\r\n\r\n        BindablesUtils.OnyWaySync(firstBindableList, secondBindableList);\r\n\r\n        firstBindableList.RemoveAll(removeValues.Contains);\r\n\r\n        Assert.That(expectedValuesInSecondBindable, Is.EqualTo(secondBindableList.ToArray()));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Utils/CharUtilsTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Utils;\r\n\r\n[TestFixture]\r\npublic class CharUtilsTest\r\n{\r\n    [TestCase(' ', true)]\r\n    [TestCase('　', true)]\r\n    [TestCase('ぴ', false)]\r\n    public void TestIsSpacing(char c, bool expected)\r\n    {\r\n        bool actual = CharUtils.IsSpacing(c);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase('ひ', true)]\r\n    [TestCase('び', true)]\r\n    [TestCase('ぴ', true)]\r\n    [TestCase('カ', true)]\r\n    [TestCase('ガ', true)]\r\n    [TestCase('゠', true)]\r\n    [TestCase('・', true)]\r\n    [TestCase('ー', true)]\r\n    [TestCase('a', false)]\r\n    [TestCase('1', false)]\r\n    public void TestIsKana(char c, bool expected)\r\n    {\r\n        bool actual = CharUtils.IsKana(c);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase('A', true)]\r\n    [TestCase('a', true)]\r\n    [TestCase('Ｚ', true)]\r\n    [TestCase('ｚ', true)]\r\n    [TestCase('1', false)]\r\n    [TestCase('文', false)]\r\n    public void TestIsEnglish(char c, bool expected)\r\n    {\r\n        bool actual = CharUtils.IsEnglish(c);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase(':', true)]\r\n    [TestCase('\"', true)]\r\n    [TestCase('&', true)]\r\n    [TestCase('#', true)]\r\n    [TestCase('@', true)]\r\n    [TestCase('A', false)]\r\n    public void TestIsAsciiSymbol(char c, bool expected)\r\n    {\r\n        bool actual = CharUtils.IsAsciiSymbol(c);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase('你', true)]\r\n    [TestCase('好', true)]\r\n    [TestCase('世', true)]\r\n    [TestCase('界', true)]\r\n    [TestCase('A', false)]\r\n    [TestCase('a', false)]\r\n    [TestCase('Ａ', false)]\r\n    [TestCase('ａ', false)]\r\n    [TestCase('~', false)]\r\n    [TestCase('～', false)]\r\n    [TestCase('ハ', false)]\r\n    [TestCase('は', false)]\r\n    [TestCase('ハ', false)]\r\n    public void TestIsChinese(char c, bool expected)\r\n    {\r\n        bool actual = CharUtils.IsChinese(c);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [TestCase('A', true)]\r\n    [TestCase('A', true)]\r\n    [TestCase('Ḁ', true)]\r\n    [TestCase('ỿ', true)]\r\n    [TestCase('Ｚ', false)]\r\n    [TestCase('ｚ', false)]\r\n    [TestCase('は', false)]\r\n    [TestCase('^', false)]\r\n    [TestCase(' ', false)]\r\n    public void TestIsLatin(char c, bool expected)\r\n    {\r\n        bool actual = CharUtils.IsLatin(c);\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Utils/ComparableUtilsTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing Newtonsoft.Json;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Utils;\r\n\r\npublic class ComparableUtilsTest\r\n{\r\n    [TestCase(\"{\\\"A\\\":0,\\\"B\\\":0,\\\"C\\\":\\\" \\\"}\", \"{\\\"A\\\":0,\\\"B\\\":0,\\\"C\\\":\\\" \\\"}\", 0)] // should be the same if two values are the same.\r\n    [TestCase(\"{\\\"A\\\":1,\\\"B\\\":1,\\\"C\\\":\\\"1\\\"}\", \"{\\\"A\\\":1,\\\"B\\\":1,\\\"C\\\":\\\"1\\\"}\", 0)] // should be the same if two values are the same.\r\n    [TestCase(\"{\\\"A\\\":0,\\\"B\\\":1,\\\"C\\\":\\\"1\\\"}\", \"{\\\"A\\\":1,\\\"B\\\":1,\\\"C\\\":\\\"1\\\"}\", -1)]\r\n    [TestCase(\"{\\\"A\\\":1,\\\"B\\\":0,\\\"C\\\":\\\"1\\\"}\", \"{\\\"A\\\":1,\\\"B\\\":1,\\\"C\\\":\\\"1\\\"}\", -1)]\r\n    [TestCase(\"{\\\"A\\\":1,\\\"B\\\":1,\\\"C\\\":\\\"\\\"}\", \"{\\\"A\\\":1,\\\"B\\\":1,\\\"C\\\":\\\"1\\\"}\", -49)]\r\n    [TestCase(\"{\\\"A\\\":1,\\\"B\\\":1,\\\"C\\\":\\\"1\\\"}\", \"{\\\"A\\\":0,\\\"B\\\":1,\\\"C\\\":\\\"1\\\"}\", 1)]\r\n    [TestCase(\"{\\\"A\\\":1,\\\"B\\\":1,\\\"C\\\":\\\"1\\\"}\", \"{\\\"A\\\":1,\\\"B\\\":0,\\\"C\\\":\\\"1\\\"}\", 1)]\r\n    [TestCase(\"{\\\"A\\\":1,\\\"B\\\":1,\\\"C\\\":\\\"1\\\"}\", \"{\\\"A\\\":1,\\\"B\\\":1,\\\"C\\\":\\\"\\\"}\", 49)]\r\n    public void TestCompare(string leftObjectProperty, string rightObjectProperty, int expected)\r\n    {\r\n        var leftObject = JsonConvert.DeserializeObject<TestObject>(leftObjectProperty);\r\n        var rightObject = JsonConvert.DeserializeObject<TestObject>(rightObjectProperty);\r\n        int actual = ComparableUtils.Compare(leftObject, rightObject,\r\n            (left, right) => left.A.CompareTo(right.A),\r\n            (left, right) => left.B.CompareTo(right.B),\r\n            (left, right) => string.Compare(left.C, right.C, StringComparison.Ordinal)); // using different comparator might get different compare number.\r\n        Assert.That(actual, Is.EqualTo(expected));\r\n    }\r\n\r\n    [TestCase(\"{\\\"A\\\":0,\\\"B\\\":0,\\\"C\\\":\\\" \\\"}\", \"{\\\"A\\\":0,\\\"B\\\":0,\\\"C\\\":\\\" \\\"}\", 0)] // should be the same if two values are the same.\r\n    [TestCase(\"{\\\"A\\\":1,\\\"B\\\":1,\\\"C\\\":\\\"1\\\"}\", \"{\\\"A\\\":1,\\\"B\\\":1,\\\"C\\\":\\\"1\\\"}\", 0)] // should be the same if two values are the same.\r\n    [TestCase(\"{\\\"A\\\":0,\\\"B\\\":1,\\\"C\\\":\\\"1\\\"}\", \"{\\\"A\\\":1,\\\"B\\\":1,\\\"C\\\":\\\"1\\\"}\", -1)]\r\n    [TestCase(\"{\\\"A\\\":1,\\\"B\\\":0,\\\"C\\\":\\\"1\\\"}\", \"{\\\"A\\\":1,\\\"B\\\":1,\\\"C\\\":\\\"1\\\"}\", -1)]\r\n    [TestCase(\"{\\\"A\\\":1,\\\"B\\\":1,\\\"C\\\":\\\"\\\"}\", \"{\\\"A\\\":1,\\\"B\\\":1,\\\"C\\\":\\\"1\\\"}\", -1)]\r\n    [TestCase(\"{\\\"A\\\":1,\\\"B\\\":1,\\\"C\\\":\\\"1\\\"}\", \"{\\\"A\\\":0,\\\"B\\\":1,\\\"C\\\":\\\"1\\\"}\", 1)]\r\n    [TestCase(\"{\\\"A\\\":1,\\\"B\\\":1,\\\"C\\\":\\\"1\\\"}\", \"{\\\"A\\\":1,\\\"B\\\":0,\\\"C\\\":\\\"1\\\"}\", 1)]\r\n    [TestCase(\"{\\\"A\\\":1,\\\"B\\\":1,\\\"C\\\":\\\"1\\\"}\", \"{\\\"A\\\":1,\\\"B\\\":1,\\\"C\\\":\\\"\\\"}\", 1)]\r\n    public void TestCompareByProperty(string leftObjectProperty, string rightObjectProperty, int expected)\r\n    {\r\n        var leftObject = JsonConvert.DeserializeObject<TestObject>(leftObjectProperty);\r\n        var rightObject = JsonConvert.DeserializeObject<TestObject>(rightObjectProperty);\r\n        int actual = ComparableUtils.CompareByProperty(leftObject, rightObject,\r\n            t => t.A,\r\n            t => t.B,\r\n            t => t.C);\r\n        Assert.That(actual, Is.EqualTo(expected));\r\n    }\r\n\r\n    private class TestObject\r\n    {\r\n        [JsonProperty]\r\n        public int A { get; set; }\r\n\r\n        [JsonProperty]\r\n        public double B { get; set; }\r\n\r\n        [JsonProperty]\r\n        public string C { get; set; } = string.Empty;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Utils/CultureInfoUtilsTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System.Globalization;\r\nusing System.Linq;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Utils;\r\n\r\npublic class CultureInfoUtilsTest\r\n{\r\n    [Test]\r\n    [Ignore(\"Cannot run those test cases right now because will got different results in the different platform...\")]\r\n    public void TestGetAvailableLanguages()\r\n    {\r\n        // seems there are 276 languages in the world.\r\n        const int expected = 276;\r\n        int actual = CultureInfoUtils.GetAvailableLanguages().Length;\r\n        Assert.That(expected, Is.EqualTo(actual));\r\n    }\r\n\r\n    [Test]\r\n    [Ignore(\"Cannot run those test cases right now because will got different results in the different platform...\")]\r\n    public void TestGetAvailableLanguagesWithUniqueLcid()\r\n    {\r\n        var languages = CultureInfoUtils.GetAvailableLanguages();\r\n\r\n        int uniqueLcidAmount = languages.Select(x => x.LCID).Where(x => x > 0).Distinct().Count();\r\n        int uniqueTwoLetterIsoLanguageNameAmount = languages.Select(x => x.TwoLetterISOLanguageName).Where(x => !string.IsNullOrEmpty(x)).Distinct().Count();\r\n        int uniqueThreeLetterIsoLanguageNameAmount = languages.Select(x => x.ThreeLetterISOLanguageName).Where(x => !string.IsNullOrEmpty(x)).Distinct().Count();\r\n\r\n        // todo: we should make sure that all the language code is not duplicated.\r\n        Assert.That(uniqueLcidAmount, Is.EqualTo(160));\r\n        Assert.That(uniqueTwoLetterIsoLanguageNameAmount, Is.EqualTo(244));\r\n        Assert.That(uniqueThreeLetterIsoLanguageNameAmount, Is.EqualTo(244));\r\n    }\r\n\r\n    [TestCase(\"zh-Hans\", true)] // 中文（简体）, 4\r\n    [TestCase(\"zh-Hant\", true)] // 中文（繁體）, 31748\r\n    [TestCase(\"zh\", true)] // 中文, 30724\r\n    [TestCase(\"zh-TW\", false)] // 中文（台灣）, 1028\r\n    [TestCase(\"zh-Hant-TW\", false)] // 中文（繁體，台灣）, 4096\r\n    [TestCase(\"zh-Hans-HK\", false)] // 中文（简体，香港特别行政区）, 4096\r\n    public void TestIsLanguage(string name, bool isLanguage)\r\n    {\r\n        var cultureInfo = new CultureInfo(name);\r\n        bool actual = CultureInfoUtils.IsLanguage(cultureInfo);\r\n        Assert.That(actual, Is.EqualTo(isLanguage));\r\n    }\r\n\r\n    [TestCase(4, true)] // 中文（简体）\r\n    [TestCase(31748, true)] // 中文（繁體）\r\n    [TestCase(30724, true)] // 中文\r\n    [TestCase(1028, false)] // 中文（台灣）\r\n    public void TestIsLanguage(int lcid, bool isLanguage)\r\n    {\r\n        var cultureInfo = new CultureInfo(lcid);\r\n        bool actual = CultureInfoUtils.IsLanguage(cultureInfo);\r\n        Assert.That(actual, Is.EqualTo(isLanguage));\r\n    }\r\n\r\n    [TestCase(4, \"中文（简体）\")]\r\n    [TestCase(31748, \"中文（繁體）\")]\r\n    [TestCase(30724, \"中文\")]\r\n    [TestCase(1028, \"中文（台灣）\")]\r\n    [Ignore(\"Cannot run those test cases right now because will got different results in the different platform...\")]\r\n    public void TestGetLanguageDisplayText(int lcid, string displayText)\r\n    {\r\n        var cultureInfo = new CultureInfo(lcid);\r\n        string actual = CultureInfoUtils.GetLanguageDisplayText(cultureInfo);\r\n        Assert.That(actual, Is.EqualTo(displayText));\r\n    }\r\n\r\n    [Test]\r\n    public void TestSaveAndLoadCultureInfoById()\r\n    {\r\n        var cultureInfos = CultureInfo.GetCultures(CultureTypes.AllCultures);\r\n\r\n        foreach (var cultureInfo in cultureInfos)\r\n        {\r\n            // this weird cultureInfo will let test case failed.\r\n            if (cultureInfo.LCID is 4096 or 4 or 31748)\r\n                continue;\r\n\r\n            // get the lcid and convert back to culture info again.\r\n            int lcid = CultureInfoUtils.GetSaveCultureInfoId(cultureInfo);\r\n            var actual = CultureInfoUtils.CreateLoadCultureInfoById(lcid);\r\n            Assert.That(actual, Is.EqualTo(cultureInfo));\r\n        }\r\n    }\r\n\r\n    [Test]\r\n    public void TestSaveAndLoadCultureInfoByCode()\r\n    {\r\n        var cultureInfos = CultureInfo.GetCultures(CultureTypes.AllCultures);\r\n\r\n        foreach (var cultureInfo in cultureInfos)\r\n        {\r\n            // this weird cultureInfo will let test case failed.\r\n            if (cultureInfo.LCID is 4096 or 4 or 31748)\r\n                continue;\r\n\r\n            // get the code and convert back to culture info again.\r\n            string lcid = CultureInfoUtils.GetSaveCultureInfoCode(cultureInfo);\r\n            var actual = CultureInfoUtils.CreateLoadCultureInfoByCode(lcid);\r\n            Assert.That(actual, Is.EqualTo(cultureInfo));\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Utils/EnumUtilsTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Utils;\r\n\r\n[TestFixture]\r\npublic class EnumUtilsTest\r\n{\r\n    [TestCase(TestEnum.Enum1, TestEnum.Enum3)]\r\n    [TestCase(TestEnum.Enum2, TestEnum.Enum1)]\r\n    [TestCase(TestEnum.Enum3, TestEnum.Enum2)]\r\n    public void TestGetPreviousValue(TestEnum current, TestEnum expected)\r\n    {\r\n        var actual = EnumUtils.GetPreviousValue(current);\r\n        Assert.That(actual, Is.EqualTo(expected));\r\n    }\r\n\r\n    [TestCase(TestEnum.Enum1, TestEnum.Enum2)]\r\n    [TestCase(TestEnum.Enum2, TestEnum.Enum3)]\r\n    [TestCase(TestEnum.Enum3, TestEnum.Enum1)]\r\n    public void TestGetNextValue(TestEnum current, TestEnum expected)\r\n    {\r\n        var actual = EnumUtils.GetNextValue(current);\r\n        Assert.That(actual, Is.EqualTo(expected));\r\n    }\r\n\r\n    [TestCase(TestEnum.Enum1, TestEnum.Enum1)]\r\n    [TestCase(TestEnum.Enum2, TestEnum.Enum2)]\r\n    [TestCase(TestEnum2.Enum1, TestEnum.Enum1)]\r\n    [TestCase(TestEnum2.Enum2, TestEnum.Enum2)]\r\n    public void TestCasting(Enum current, TestEnum? expected)\r\n    {\r\n        var actual = EnumUtils.Casting<TestEnum>(current);\r\n        Assert.That(actual, Is.EqualTo(expected));\r\n    }\r\n\r\n    public enum TestEnum\r\n    {\r\n        Enum1,\r\n\r\n        Enum2,\r\n\r\n        Enum3,\r\n    }\r\n\r\n    public enum TestEnum2\r\n    {\r\n        Enum1,\r\n\r\n        Enum2,\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Utils/FontUsageUtilsTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Skinning.Fonts;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Utils;\r\n\r\n[TestFixture]\r\npublic class FontUsageUtilsTest\r\n{\r\n    [TestCase(\"OpenSans\", null, false, \"OpenSans\")]\r\n    [TestCase(\"OpenSans\", \"Regular\", false, \"OpenSans-Regular\")]\r\n    [TestCase(\"OpenSans\", \"Regular\", true, \"OpenSans-RegularItalic\")]\r\n    public void TestToFontInfo(string expectedFamily, string? expectedWeight, bool italics, string expectedFontName)\r\n    {\r\n        var fontUsage = new FontUsage(expectedFontName);\r\n        var fontInfo = FontUsageUtils.ToFontInfo(fontUsage, FontFormat.Internal);\r\n        Assert.That(fontInfo.FontName, Is.EqualTo(expectedFontName));\r\n        Assert.That(fontInfo.Family, Is.EqualTo(expectedFamily));\r\n\r\n        // note: font info should not follow rules as fontUsage.\r\n        if (!italics)\r\n        {\r\n            Assert.That(fontInfo.Weight, Is.EqualTo(expectedWeight));\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Utils/FontUtilsTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Utils;\r\n\r\n[TestFixture]\r\npublic class FontUtilsTest\r\n{\r\n    [TestCase(10, \"10 px\")]\r\n    [TestCase(10.5f, \"10.5 px\")]\r\n    [TestCase(-3, \"-3 px\")]\r\n    public void TestGetText(float font, string expected)\r\n    {\r\n        string actual = FontUtils.GetText(font);\r\n        Assert.That(actual, Is.EqualTo(expected));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Utils/JpStringUtilsTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Utils;\r\n\r\n[TestFixture]\r\npublic class JpStringUtilsTest\r\n{\r\n    [TestCase(\"ハナビ\", \"はなび\")]\r\n    [TestCase(\"タイカイ\", \"たいかい\")]\r\n    [TestCase(\"花火大会\", \"花火大会\")]\r\n    public void TestToHiragana(string text, string expected)\r\n    {\r\n        string actual = JpStringUtils.ToHiragana(text);\r\n        Assert.That(actual, Is.EqualTo(expected));\r\n    }\r\n\r\n    [TestCase(\"はなび\", \"ハナビ\")]\r\n    [TestCase(\"たいかい\", \"タイカイ\")]\r\n    [TestCase(\"花火大会\", \"花火大会\")]\r\n    public void TestToKatakana(string text, string expected)\r\n    {\r\n        string actual = JpStringUtils.ToKatakana(text);\r\n        Assert.That(actual, Is.EqualTo(expected));\r\n    }\r\n\r\n    [TestCase(\"はなび\", \"hanabi\")]\r\n    [TestCase(\"たいかい\", \"taikai\")]\r\n    [TestCase(\"ハナビ\", \"hanabi\")]\r\n    [TestCase(\"タイカイ\", \"taikai\")]\r\n    [TestCase(\"花火大会\", \"花火大会\")] // cannot convert kanji to romaji.\r\n    [TestCase(\"ハナビ wo miru\", \"hanabi wo miru\")]\r\n    [TestCase(\"タイカイー☆\", \"taikaii☆\")] // it's converted by package, let's skip this checking.\r\n    [TestCase(\"タイカイ ー☆\", \"taikai -☆\")] // it's converted by package, let's skip this checking.\r\n    public void TestToRomaji(string text, string expected)\r\n    {\r\n        string actual = JpStringUtils.ToRomaji(text);\r\n        Assert.That(actual, Is.EqualTo(expected));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Utils/RectangleFUtilsTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing System.Linq;\r\nusing System.Text.RegularExpressions;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Graphics.Primitives;\r\nusing osu.Game.Rulesets.Karaoke.Extensions;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Utils;\r\n\r\npublic class RectangleFUtilsTest\r\n{\r\n    [TestCase(new[] { \"[5,5,10,10]\" }, \"[5,5,10,10]\")]\r\n    [TestCase(new[] { \"[5,5,10,10]\", \"[5,5,10,10]\" }, \"[5,5,10,10]\")]\r\n    [TestCase(new[] { \"[0,0,0,0]\", \"[5,5,10,10]\" }, \"[0,0,15,15]\")]\r\n    [TestCase(new[] { \"[0,0,0,0]\", \"[5,0,0,0]\", \"[0,5,0,0]\" }, \"[0,0,5,5]\")]\r\n    [TestCase(new[] { \"\" }, \"\")]\r\n    [TestCase(new string[] { }, \"\")]\r\n    public void TestUnion(string[] positions, string expectedRectangle)\r\n    {\r\n        var objects = positions.Select(convertTestCaseToValue).ToArray();\r\n\r\n        var expected = convertTestCaseToValue(expectedRectangle);\r\n        var actual = RectangleFUtils.Union(objects);\r\n        Assert.That(actual, Is.EqualTo(expected));\r\n    }\r\n\r\n    private RectangleF convertTestCaseToValue(string str)\r\n    {\r\n        if (string.IsNullOrEmpty(str))\r\n            return new RectangleF();\r\n\r\n        var regex = new Regex(\"(?<x>[-0-9]+),(?<y>[-0-9]+),(?<width>[-0-9]+),(?<height>[-0-9]+)]\");\r\n        var result = regex.Match(str);\r\n        if (!result.Success)\r\n            throw new ArgumentException(null, nameof(str));\r\n\r\n        float x = result.GetGroupValue<float>(\"x\");\r\n        float y = result.GetGroupValue<float>(\"y\");\r\n        float width = result.GetGroupValue<float>(\"width\");\r\n        float height = result.GetGroupValue<float>(\"height\");\r\n        return new RectangleF(x, y, width, height);\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Utils/TextIndexUtilsTest.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing NUnit.Framework;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Utils;\r\n\r\n[TestFixture]\r\npublic class TextIndexUtilsTest\r\n{\r\n    [TestCase(0, TextIndex.IndexState.Start, 0)]\r\n    [TestCase(0, TextIndex.IndexState.End, 1)]\r\n    [TestCase(-1, TextIndex.IndexState.Start, -1)] // In utils not checking is index out of range\r\n    [TestCase(-1, TextIndex.IndexState.End, 0)]\r\n    public void TestToGapIndex(int index, TextIndex.IndexState state, int expected)\r\n    {\r\n        var textIndex = new TextIndex(index, state);\r\n\r\n        int actual = TextIndexUtils.ToGapIndex(textIndex);\r\n        Assert.That(actual, Is.EqualTo(expected));\r\n    }\r\n\r\n    [TestCase(0, TextIndex.IndexState.Start, 0)]\r\n    [TestCase(0, TextIndex.IndexState.End, 0)]\r\n    [TestCase(-1, TextIndex.IndexState.Start, -1)]\r\n    [TestCase(-1, TextIndex.IndexState.End, -1)]\r\n    public void TestToCharIndex(int index, TextIndex.IndexState state, int expected)\r\n    {\r\n        var textIndex = new TextIndex(index, state);\r\n\r\n        int actual = TextIndexUtils.ToCharIndex(textIndex);\r\n        Assert.That(actual, Is.EqualTo(expected));\r\n    }\r\n\r\n    [TestCase(0, false, 0, TextIndex.IndexState.Start)]\r\n    [TestCase(1, true, 0, TextIndex.IndexState.End)]\r\n    [TestCase(0, true, -1, TextIndex.IndexState.End)] // In utils not checking is index out of range\r\n    public void TestFromStringIndex(int textIndex, bool end, int expectedIndex, TextIndex.IndexState expectedState)\r\n    {\r\n        var expected = new TextIndex(expectedIndex, expectedState);\r\n        var actual = TextIndexUtils.FromStringIndex(textIndex, end);\r\n        Assert.That(actual, Is.EqualTo(expected));\r\n    }\r\n\r\n    [TestCase(TextIndex.IndexState.Start, TextIndex.IndexState.End)]\r\n    [TestCase(TextIndex.IndexState.End, TextIndex.IndexState.Start)]\r\n    public void TestReverseState(TextIndex.IndexState state, TextIndex.IndexState expected)\r\n    {\r\n        var actual = TextIndexUtils.ReverseState(state);\r\n        Assert.That(actual, Is.EqualTo(expected));\r\n    }\r\n\r\n    [TestCase(1, TextIndex.IndexState.End, 1, TextIndex.IndexState.Start)]\r\n    [TestCase(1, TextIndex.IndexState.Start, 0, TextIndex.IndexState.End)]\r\n    [TestCase(0, TextIndex.IndexState.Start, -1, TextIndex.IndexState.End)] // didn't care about negative value.\r\n    [TestCase(-1, TextIndex.IndexState.End, -1, TextIndex.IndexState.Start)] // didn't care about negative value.\r\n    public void TestGetPreviousIndex(int index, TextIndex.IndexState state, int expectedIndex, TextIndex.IndexState expectedState)\r\n    {\r\n        var textIndex = new TextIndex(index, state);\r\n\r\n        var expected = new TextIndex(expectedIndex, expectedState);\r\n        var actual = TextIndexUtils.GetPreviousIndex(textIndex);\r\n        Assert.That(actual, Is.EqualTo(expected));\r\n    }\r\n\r\n    [TestCase(0, TextIndex.IndexState.Start, 0, TextIndex.IndexState.End)]\r\n    [TestCase(0, TextIndex.IndexState.End, 1, TextIndex.IndexState.Start)]\r\n    [TestCase(-1, TextIndex.IndexState.Start, -1, TextIndex.IndexState.End)] // didn't care about negative value.\r\n    [TestCase(-1, TextIndex.IndexState.End, 0, TextIndex.IndexState.Start)] // didn't care about negative value.\r\n    public void TestGetNextIndex(int index, TextIndex.IndexState state, int expectedIndex, TextIndex.IndexState expectedState)\r\n    {\r\n        var textIndex = new TextIndex(index, state);\r\n\r\n        var expected = new TextIndex(expectedIndex, expectedState);\r\n        var actual = TextIndexUtils.GetNextIndex(textIndex);\r\n        Assert.That(actual, Is.EqualTo(expected));\r\n    }\r\n\r\n    [TestCase(0, TextIndex.IndexState.Start, 1, 1, TextIndex.IndexState.Start)]\r\n    [TestCase(0, TextIndex.IndexState.End, 1, 1, TextIndex.IndexState.End)]\r\n    [TestCase(0, TextIndex.IndexState.Start, -1, -1, TextIndex.IndexState.Start)]\r\n    [TestCase(0, TextIndex.IndexState.End, -1, -1, TextIndex.IndexState.End)]\r\n    public void TestShiftingIndex(int index, TextIndex.IndexState state, int offset, int expectedIndex, TextIndex.IndexState expectedState)\r\n    {\r\n        var textIndex = new TextIndex(index, state);\r\n\r\n        var expected = new TextIndex(expectedIndex, expectedState);\r\n        var actual = TextIndexUtils.ShiftingIndex(textIndex, offset);\r\n        Assert.That(actual, Is.EqualTo(expected));\r\n    }\r\n\r\n    [TestCase(0, TextIndex.IndexState.Start, \"karaoke\", false)]\r\n    [TestCase(0, TextIndex.IndexState.End, \"karaoke\", false)]\r\n    [TestCase(-1, TextIndex.IndexState.Start, \"karaoke\", true)]\r\n    [TestCase(-1, TextIndex.IndexState.End, \"karaoke\", true)]\r\n    [TestCase(0, TextIndex.IndexState.Start, \"\", true)] // should be counted as out of range if lyric is empty\r\n    [TestCase(0, TextIndex.IndexState.End, \"\", true)]\r\n    public void TestOutOfRange(int index, TextIndex.IndexState state, string lyric, bool expected)\r\n    {\r\n        var textIndex = new TextIndex(index, state);\r\n\r\n        bool actual = TextIndexUtils.OutOfRange(textIndex, lyric);\r\n        Assert.That(actual, Is.EqualTo(expected));\r\n    }\r\n\r\n    [TestCase(TextIndex.IndexState.Start, 1, -1, 1)]\r\n    [TestCase(TextIndex.IndexState.End, 1, -1, -1)]\r\n    [TestCase(TextIndex.IndexState.Start, \"1\", \"-1\", \"1\")]\r\n    [TestCase(TextIndex.IndexState.End, \"1\", \"-1\", \"-1\")]\r\n    public void TestGetValueByState(TextIndex.IndexState state, object startValue, object endValue, object expected)\r\n    {\r\n        var textIndex = new TextIndex(0, state);\r\n\r\n        object valueByTextIndex = TextIndexUtils.GetValueByState(textIndex, startValue, endValue);\r\n        Assert.That(valueByTextIndex, Is.EqualTo(expected));\r\n\r\n        object valueByState = TextIndexUtils.GetValueByState(state, startValue, endValue);\r\n        Assert.That(valueByState, Is.EqualTo(expected));\r\n\r\n        object valueByTextIndexWithFunction = TextIndexUtils.GetValueByState(textIndex, () => startValue, () => endValue);\r\n        Assert.That(valueByTextIndexWithFunction, Is.EqualTo(expected));\r\n\r\n        object valueByStateWithFunction = TextIndexUtils.GetValueByState(state, () => startValue, () => endValue);\r\n        Assert.That(valueByStateWithFunction, Is.EqualTo(expected));\r\n    }\r\n\r\n    [TestCase(0, TextIndex.IndexState.Start, \"0\")]\r\n    [TestCase(0, TextIndex.IndexState.End, \"0(end)\")]\r\n    [TestCase(-1, TextIndex.IndexState.Start, \"-1\")]\r\n    [TestCase(-1, TextIndex.IndexState.End, \"-1(end)\")]\r\n    public void TestPositionFormattedString(int index, TextIndex.IndexState state, string expected)\r\n    {\r\n        var textIndex = new TextIndex(index, state);\r\n\r\n        string actual = TextIndexUtils.PositionFormattedString(textIndex);\r\n        Assert.That(actual, Is.EqualTo(expected));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Utils/TypeUtilsTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing NUnit.Framework;\r\nusing osu.Framework.Graphics.Sprites;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Utils;\r\n\r\npublic class TypeUtilsTest\r\n{\r\n    [Test]\r\n    public void TestChangeTypeToSameType()\r\n    {\r\n        // test string\r\n        Assert.That(TypeUtils.ChangeType<string>(\"123\"), Is.EqualTo(\"123\"));\r\n\r\n        // test number\r\n        Assert.That(TypeUtils.ChangeType<int>(456), Is.EqualTo(456));\r\n\r\n        // test another number\r\n        Assert.That(TypeUtils.ChangeType<float>(789f), Is.EqualTo(789f));\r\n\r\n        // test struct\r\n        Assert.That(TypeUtils.ChangeType<FontUsage>(new FontUsage(\"123\")), Is.EqualTo(new FontUsage(\"123\")));\r\n\r\n        // test class, should use same instance.\r\n        var testClass = new TestClass();\r\n        Assert.That(TypeUtils.ChangeType<TestClass>(testClass), Is.EqualTo(testClass));\r\n    }\r\n\r\n    [Test]\r\n    public void TestChangeTypeToDifferentType()\r\n    {\r\n        // test convert to number\r\n        Assert.That(TypeUtils.ChangeType<double>(123), Is.EqualTo(Convert.ToDouble(123)));\r\n\r\n        // test convert to string\r\n        Assert.That(TypeUtils.ChangeType<string>(123), Is.EqualTo(Convert.ToString(123)));\r\n\r\n        // test convert to nullable\r\n        Assert.That(TypeUtils.ChangeType<double?>(123d), Is.EqualTo(123));\r\n        Assert.That(TypeUtils.ChangeType<double?>(null), Is.EqualTo(default(double?)));\r\n    }\r\n\r\n    private class TestClass;\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/Utils/VersionUtilsTest.cs",
    "content": "// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing NUnit.Framework;\r\nusing osu.Game.Rulesets.Karaoke.Utils;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests.Utils;\r\n\r\npublic class VersionUtilsTest\r\n{\r\n    [Test]\r\n    public void TestGetVersion()\r\n    {\r\n        var expected = new Version(1, 0, 0, 0);\r\n        var actual = VersionUtils.GetVersion();\r\n        Assert.That(actual, Is.Not.Null);\r\n        Assert.That(actual.Major, Is.EqualTo(expected.Major));\r\n        Assert.That(actual.Minor, Is.EqualTo(expected.Minor));\r\n        Assert.That(actual.Build, Is.EqualTo(expected.Build));\r\n        Assert.That(actual.Revision, Is.EqualTo(expected.Revision));\r\n    }\r\n\r\n    [Test]\r\n    public void TestMajorVersionName()\r\n    {\r\n        const string expected = \"UwU\";\r\n        string actual = VersionUtils.MajorVersionName;\r\n        Assert.That(actual, Is.EqualTo(expected));\r\n    }\r\n\r\n    [Test]\r\n    public void TestIsDeployedBuild()\r\n    {\r\n        // should not be deploy build if not build by github action.\r\n        const bool expected = false;\r\n        bool actual = VersionUtils.IsDeployedBuild;\r\n        Assert.That(actual, Is.EqualTo(expected));\r\n    }\r\n\r\n    [Test]\r\n    public void TestGetDisplayVersion()\r\n    {\r\n        const string expected = \"1.0.0-UwU\";\r\n        string actual = VersionUtils.DisplayVersion;\r\n        Assert.That(actual, Is.EqualTo(expected));\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/VisualTestRunner.cs",
    "content": "﻿// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nusing System;\r\nusing osu.Framework;\r\n\r\nnamespace osu.Game.Rulesets.Karaoke.Tests;\r\n\r\npublic static class VisualTestRunner\r\n{\r\n    [STAThread]\r\n    public static int Main(string[] args)\r\n    {\r\n        using var host = Host.GetSuitableDesktopHost(\"karaoke-visual-test-runner\");\r\n        host.Run(new KaraokeTestBrowser());\r\n\r\n        return 0;\r\n    }\r\n}\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.Tests/osu.Game.Rulesets.Karaoke.Tests.csproj",
    "content": "﻿<Project Sdk=\"Microsoft.NET.Sdk\">\r\n  <PropertyGroup>\r\n    <StartupObject>osu.Game.Rulesets.Karaoke.Tests.VisualTestRunner</StartupObject>\r\n  </PropertyGroup>\r\n  <ItemGroup>\r\n    <EmbeddedResource Remove=\"Resources\\TestResources.cs\" />\r\n  </ItemGroup>\r\n  <ItemGroup Label=\"Service\">\r\n    <Service Include=\"{82a7f48d-3b50-4b1e-b82e-3ada8210c358}\" />\r\n  </ItemGroup>\r\n  <PropertyGroup>\r\n    <GenerateProgramFile>false</GenerateProgramFile>\r\n  </PropertyGroup>\r\n  <ItemGroup Label=\"Package References\">\r\n    <PackageReference Include=\"Appveyor.TestLogger\" Version=\"2.0.0\" />\r\n    <PackageReference Include=\"Microsoft.NET.Test.Sdk\" Version=\"18.0.1\" />\r\n    <PackageReference Include=\"NUnit\" Version=\"4.5.1\" />\r\n    <PackageReference Include=\"NUnit3TestAdapter\" Version=\"6.2.0\" />\r\n    <PackageReference Update=\"Microsoft.EntityFrameworkCore.Sqlite\" Version=\"2.1.4\" />\r\n  </ItemGroup>\r\n  <ItemGroup>\r\n    <EmbeddedResource Remove=\"Resources\\TestResources.cs\" />\r\n  </ItemGroup>\r\n  <ItemGroup>\r\n    <ProjectReference Include=\"..\\osu.Game.Rulesets.Karaoke\\osu.Game.Rulesets.Karaoke.csproj\" />\r\n  </ItemGroup>\r\n  <PropertyGroup Label=\"Project\">\r\n    <OutputType>WinExe</OutputType>\r\n    <RootNamespace>osu.Game.Rulesets.Karaoke.Tests</RootNamespace>\r\n    <TargetFramework>net8.0</TargetFramework>\r\n  </PropertyGroup>\r\n</Project>\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.sln",
    "content": "﻿\r\nMicrosoft Visual Studio Solution File, Format Version 12.00\r\n# Visual Studio Version 16\r\nVisualStudioVersion = 16.4.2.00\r\nMinimumVisualStudioVersion = 10.0.40219.1\r\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"osu.Game.Rulesets.Karaoke\", \"osu.Game.Rulesets.Karaoke\\osu.Game.Rulesets.Karaoke.csproj\", \"{5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}\"\r\nEndProject\r\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"osu.Game.Rulesets.Karaoke.Tests\", \"osu.Game.Rulesets.Karaoke.Tests\\osu.Game.Rulesets.Karaoke.Tests.csproj\", \"{B4577C85-CB83-462A-BCE3-22FFEB16311D}\"\r\nEndProject\r\nProject(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\") = \"Solution Items\", \"Solution Items\", \"{10DF8F12-50FD-45D8-8A38-17BA764BF54D}\"\r\n\tProjectSection(SolutionItems) = preProject\r\n\t\t.editorconfig = .editorconfig\r\n\t\t.globalconfig = .globalconfig\r\n\t\tDirectory.Build.props = Directory.Build.props\r\n\t\tCodeAnalysis\\osu.ruleset = CodeAnalysis\\osu.ruleset\r\n\t\tosu.Game.Rulesets.Karaoke.sln.DotSettings = osu.Game.Rulesets.Karaoke.sln.DotSettings\r\n\tEndProjectSection\r\nEndProject\r\nProject(\"{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}\") = \"osu.Game.Rulesets.Karaoke.Architectures\", \"osu.Game.Rulesets.Karaoke.Architectures\\osu.Game.Rulesets.Karaoke.Architectures.csproj\", \"{2A14C732-3B88-453F-B953-4204581DED27}\"\r\nEndProject\r\nGlobal\r\n\tGlobalSection(SolutionConfigurationPlatforms) = preSolution\r\n\t\tDebug|Any CPU = Debug|Any CPU\r\n\t\tRelease|Any CPU = Release|Any CPU\r\n\t\tVisualTests|Any CPU = VisualTests|Any CPU\r\n\tEndGlobalSection\r\n\tGlobalSection(ProjectConfigurationPlatforms) = postSolution\r\n\t\t{5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\r\n\t\t{5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Debug|Any CPU.Build.0 = Debug|Any CPU\r\n\t\t{5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Release|Any CPU.ActiveCfg = Release|Any CPU\r\n\t\t{5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Release|Any CPU.Build.0 = Release|Any CPU\r\n\t\t{5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.VisualTests|Any CPU.ActiveCfg = Release|Any CPU\r\n\t\t{5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.VisualTests|Any CPU.Build.0 = Release|Any CPU\r\n\t\t{B4577C85-CB83-462A-BCE3-22FFEB16311D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\r\n\t\t{B4577C85-CB83-462A-BCE3-22FFEB16311D}.Debug|Any CPU.Build.0 = Debug|Any CPU\r\n\t\t{B4577C85-CB83-462A-BCE3-22FFEB16311D}.Release|Any CPU.ActiveCfg = Release|Any CPU\r\n\t\t{B4577C85-CB83-462A-BCE3-22FFEB16311D}.Release|Any CPU.Build.0 = Release|Any CPU\r\n\t\t{B4577C85-CB83-462A-BCE3-22FFEB16311D}.VisualTests|Any CPU.ActiveCfg = Debug|Any CPU\r\n\t\t{B4577C85-CB83-462A-BCE3-22FFEB16311D}.VisualTests|Any CPU.Build.0 = Debug|Any CPU\r\n\t\t{2A14C732-3B88-453F-B953-4204581DED27}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\r\n\t\t{2A14C732-3B88-453F-B953-4204581DED27}.Debug|Any CPU.Build.0 = Debug|Any CPU\r\n\t\t{2A14C732-3B88-453F-B953-4204581DED27}.Release|Any CPU.ActiveCfg = Release|Any CPU\r\n\t\t{2A14C732-3B88-453F-B953-4204581DED27}.Release|Any CPU.Build.0 = Release|Any CPU\r\n\t\t{2A14C732-3B88-453F-B953-4204581DED27}.VisualTests|Any CPU.ActiveCfg = Debug|Any CPU\r\n\t\t{2A14C732-3B88-453F-B953-4204581DED27}.VisualTests|Any CPU.Build.0 = Debug|Any CPU\r\n\tEndGlobalSection\r\n\tGlobalSection(SolutionProperties) = preSolution\r\n\t\tHideSolutionNode = FALSE\r\n\tEndGlobalSection\r\n\tGlobalSection(NestedProjects) = preSolution\r\n\tEndGlobalSection\r\n\tGlobalSection(ExtensibilityGlobals) = postSolution\r\n\t\tSolutionGuid = {671B0BEC-2403-45B0-9357-2C97CC517668}\r\n\tEndGlobalSection\r\n\tGlobalSection(MonoDevelopProperties) = preSolution\r\n\t\tPolicies = $0\r\n\t\t$0.TextStylePolicy = $1\r\n\t\t$1.EolMarker = Windows\r\n\t\t$1.inheritsSet = VisualStudio\r\n\t\t$1.inheritsScope = text/plain\r\n\t\t$1.scope = text/x-csharp\r\n\t\t$0.CSharpFormattingPolicy = $2\r\n\t\t$2.IndentSwitchSection = True\r\n\t\t$2.NewLinesForBracesInProperties = True\r\n\t\t$2.NewLinesForBracesInAccessors = True\r\n\t\t$2.NewLinesForBracesInAnonymousMethods = True\r\n\t\t$2.NewLinesForBracesInControlBlocks = True\r\n\t\t$2.NewLinesForBracesInAnonymousTypes = True\r\n\t\t$2.NewLinesForBracesInObjectCollectionArrayInitializers = True\r\n\t\t$2.NewLinesForBracesInLambdaExpressionBody = True\r\n\t\t$2.NewLineForElse = True\r\n\t\t$2.NewLineForCatch = True\r\n\t\t$2.NewLineForFinally = True\r\n\t\t$2.NewLineForMembersInObjectInit = True\r\n\t\t$2.NewLineForMembersInAnonymousTypes = True\r\n\t\t$2.NewLineForClausesInQuery = True\r\n\t\t$2.SpacingAfterMethodDeclarationName = False\r\n\t\t$2.SpaceAfterMethodCallName = False\r\n\t\t$2.SpaceBeforeOpenSquareBracket = False\r\n\t\t$2.inheritsSet = Mono\r\n\t\t$2.inheritsScope = text/x-csharp\r\n\t\t$2.scope = text/x-csharp\r\n\tEndGlobalSection\r\n\tGlobalSection(MonoDevelopProperties) = preSolution\r\n\t\tPolicies = $0\r\n\t\t$0.TextStylePolicy = $1\r\n\t\t$1.EolMarker = Windows\r\n\t\t$1.inheritsSet = VisualStudio\r\n\t\t$1.inheritsScope = text/plain\r\n\t\t$1.scope = text/x-csharp\r\n\t\t$0.CSharpFormattingPolicy = $2\r\n\t\t$2.IndentSwitchSection = True\r\n\t\t$2.NewLinesForBracesInProperties = True\r\n\t\t$2.NewLinesForBracesInAccessors = True\r\n\t\t$2.NewLinesForBracesInAnonymousMethods = True\r\n\t\t$2.NewLinesForBracesInControlBlocks = True\r\n\t\t$2.NewLinesForBracesInAnonymousTypes = True\r\n\t\t$2.NewLinesForBracesInObjectCollectionArrayInitializers = True\r\n\t\t$2.NewLinesForBracesInLambdaExpressionBody = True\r\n\t\t$2.NewLineForElse = True\r\n\t\t$2.NewLineForCatch = True\r\n\t\t$2.NewLineForFinally = True\r\n\t\t$2.NewLineForMembersInObjectInit = True\r\n\t\t$2.NewLineForMembersInAnonymousTypes = True\r\n\t\t$2.NewLineForClausesInQuery = True\r\n\t\t$2.SpacingAfterMethodDeclarationName = False\r\n\t\t$2.SpaceAfterMethodCallName = False\r\n\t\t$2.SpaceBeforeOpenSquareBracket = False\r\n\t\t$2.inheritsSet = Mono\r\n\t\t$2.inheritsScope = text/x-csharp\r\n\t\t$2.scope = text/x-csharp\r\n\tEndGlobalSection\r\nEndGlobal\r\n"
  },
  {
    "path": "osu.Game.Rulesets.Karaoke.sln.DotSettings",
    "content": "<wpf:ResourceDictionary xml:space=\"preserve\" xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\" xmlns:s=\"clr-namespace:System;assembly=mscorlib\" xmlns:ss=\"urn:shemas-jetbrains-com:settings-storage-xaml\" xmlns:wpf=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\">\n\t<s:Boolean x:Key=\"/Default/CodeInspection/ExcludedFiles/FileMasksToSkip/=_002A_002Efnt/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/CodeInspection/ExcludedFiles/FileMasksToSkip/=_002A_002Emp3/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/CodeInspection/ExcludedFiles/FileMasksToSkip/=_002A_002Epng/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/CodeInspection/ExcludedFiles/FileMasksToSkip/=_002A_002Ewav/@EntryIndexedValue\">True</s:Boolean>\n\t<s:String x:Key=\"/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=2A66DD92_002DADB1_002D4994_002D89E2_002DC94E04ACDA0D_002Fd_003AMigrations/@EntryIndexedValue\">ExplicitlyExcluded</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=D9A367C9_002D4C1A_002D489F_002D9B05_002DA0CEA2B53B58/@EntryIndexedValue\">ExplicitlyExcluded</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/GeneratedCode/GeneratedFileMasks/=g_005F_002A_002Ecs/@EntryIndexedValue\">g_*.cs</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/AnalysisEnabled/@EntryValue\">SOLUTION</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeAccessorOwnerBody/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeDefaultValueWhenTypeEvident/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeDefaultValueWhenTypeNotEvident/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeMissingParentheses/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeConstructorOrDestructorBody/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeLocalFunctionBody/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeMethodOrOperatorBody/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeModifiersOrder/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeNamespaceBody/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeObjectCreationWhenTypeEvident/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeObjectCreationWhenTypeNotEvident/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeRedundantParentheses/@EntryIndexedValue\">WARNING</s:String>\n\t<s:Boolean x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeRedundantParentheses/@EntryIndexRemoved\">True</s:Boolean>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeTrailingCommaInMultilineLists/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeTrailingCommaInSinglelineLists/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeTypeMemberModifiers/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeTypeModifiers/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=AssignedValueIsNeverUsed/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=AssignmentIsFullyDiscarded/@EntryIndexedValue\">DO_NOT_SHOW</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=AssignNullToNotNullAttribute/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=AsyncVoidMethod/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=AutoPropertyCanBeMadeGetOnly_002EGlobal/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=AutoPropertyCanBeMadeGetOnly_002ELocal/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=BadAttributeBracketsSpaces/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=BadBracesSpaces/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=BadChildStatementIndent/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=BadColonSpaces/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=BadCommaSpaces/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=BadControlBracesIndent/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=BadControlBracesLineBreaks/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=BadDeclarationBracesIndent/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=BadDeclarationBracesLineBreaks/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=BadEmptyBracesLineBreaks/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=BadExpressionBracesIndent/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=BadExpressionBracesLineBreaks/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=BadGenericBracketsSpaces/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=BadIndent/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=BadLinqLineBreaks/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=BadListLineBreaks/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=BadMemberAccessSpaces/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=BadNamespaceBracesIndent/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=BadParensLineBreaks/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=BadParensSpaces/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=BadPreprocessorIndent/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=BadSemicolonSpaces/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=BadSpacesAfterKeyword/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=BadSquareBracketsSpaces/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=BadSwitchBracesIndent/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=BadSymbolSpaces/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=CanBeReplacedWithTryCastAndCheckForNull/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=CheckForReferenceEqualityInstead_002E1/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=CheckForReferenceEqualityInstead_002E2/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ClassNeverInstantiated_002EGlobal/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ClassNeverInstantiated_002ELocal/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ClassWithVirtualMembersNeverInherited_002EGlobal/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=CollectionNeverQueried_002EGlobal/@EntryIndexedValue\">SUGGESTION</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=CollectionNeverQueried_002ELocal/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=CommentTypo/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=CompareOfFloatsByEqualityOperator/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ConvertClosureToMethodGroup/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ConvertConditionalTernaryExpressionToSwitchExpression/@EntryIndexedValue\">DO_NOT_SHOW</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ConvertConstructorToMemberInitializers/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ConvertIfDoToWhile/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ConvertIfStatementToConditionalTernaryExpression/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ConvertIfStatementToNullCoalescingAssignment/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ConvertIfStatementToNullCoalescingExpression/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ConvertIfStatementToSwitchExpression/@EntryIndexedValue\">DO_NOT_SHOW</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ConvertIfToOrExpression/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ConvertNullableToShortForm/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ConvertPropertyToExpressionBody/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ConvertSwitchStatementToSwitchExpression/@EntryIndexedValue\">DO_NOT_SHOW</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ConvertToAutoProperty/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ConvertToAutoPropertyWhenPossible/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ConvertToAutoPropertyWithPrivateSetter/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ConvertToConstant_002ELocal/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ConvertToLambdaExpression/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ConvertToLocalFunction/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ConvertToPrimaryConstructor/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ConvertToStaticClass/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ConvertToUsingDeclaration/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ConvertTypeCheckPatternToNullCheck/@EntryIndexedValue\">DO_NOT_SHOW</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=DoubleNegationOperator/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=EmptyGeneralCatchClause/@EntryIndexedValue\">DO_NOT_SHOW</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=EnforceDoWhileStatementBraces/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=EnforceFixedStatementBraces/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=EnforceForeachStatementBraces/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=EnforceForStatementBraces/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=EnforceIfStatementBraces/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=EnforceLockStatementBraces/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=EnforceUsingStatementBraces/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=EnforceWhileStatementBraces/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=EventNeverSubscribedTo_002EGlobal/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=EventNeverSubscribedTo_002ELocal/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=FieldCanBeMadeReadOnly_002EGlobal/@EntryIndexedValue\">DO_NOT_SHOW</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=FieldCanBeMadeReadOnly_002ELocal/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ForCanBeConvertedToForeach/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ForStatementConditionIsTrue/@EntryIndexedValue\">DO_NOT_SHOW</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=HeapView_002ECanAvoidClosure/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=IdentifierTypo/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ImpureMethodCallOnReadonlyValueField/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=InconsistentNaming/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=IncorrectBlankLinesNearBraces/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=InheritdocConsiderUsage/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=InlineOutVariableDeclaration/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=InvertIf/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=InvokeAsExtensionMethod/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=JoinDeclarationAndInitializer/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=JoinNullCheckWithUsage/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=MemberCanBeMadeStatic_002ELocal/@EntryIndexedValue\">DO_NOT_SHOW</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=MemberCanBePrivate_002EGlobal/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=MemberCanBePrivate_002ELocal/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=MemberCanBeProtected_002EGlobal/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=MergeCastWithTypeCheck/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=MergeConditionalExpression/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=MergeIntoNegatedPattern/@EntryIndexedValue\">DO_NOT_SHOW</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=MergeIntoPattern/@EntryIndexedValue\">DO_NOT_SHOW</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=MergeSequentialChecks/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=MethodHasAsyncOverload/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=MethodHasAsyncOverloadWithCancellation/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=MethodSupportsCancellation/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=MissingBlankLines/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=MissingIndent/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=MissingLinebreak/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=MissingSpace/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=MoreSpecificForeachVariableTypeAvailable/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=MoveVariableDeclarationInsideLoopCondition/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=MultipleSpaces/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=MultipleStatementsOnOneLine/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=MultipleTypeMembersOnOneLine/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=NestedStringInterpolation/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=NotAccessedField_002EGlobal/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=NotOverriddenInSpecificCulture/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=OutdentIsOffPrevLevel/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=OutParameterValueIsAlwaysDiscarded_002EGlobal/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=OutParameterValueIsAlwaysDiscarded_002ELocal/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ParameterHidesMember/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ParameterOnlyUsedForPreconditionCheck_002EGlobal/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ParameterOnlyUsedForPreconditionCheck_002ELocal/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=PartialMethodWithSinglePart/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=PartialTypeWithSinglePart/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=PatternAlwaysOfType/@EntryIndexedValue\">DO_NOT_SHOW</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=PossibleInterfaceMemberAmbiguity/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=PossibleMultipleEnumeration/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=PrivateVariableCanBeMadeReadonly/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=PropertyCanBeMadeInitOnly_002EGlobal/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=PropertyCanBeMadeInitOnly_002ELocal/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=PublicConstructorInAbstractClass/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantArgumentDefaultValue/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantArrayCreationExpression/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantAttributeParentheses/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantAttributeUsageProperty/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantBlankLines/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantCaseLabel/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantCommaInAttributeList/@EntryIndexedValue\">DO_NOT_SHOW</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantCommaInEnumDeclaration/@EntryIndexedValue\">DO_NOT_SHOW</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantCommaInInitializer/@EntryIndexedValue\">DO_NOT_SHOW</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantDiscardDesignation/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantEmptyObjectCreationArgumentList/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantExplicitParamsArrayCreation/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantImmediateDelegateInvocation/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantLambdaSignatureParentheses/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantReadonlyModifier/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantTypeSpecificationInDefaultExpression/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantLinebreak/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantSpace/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantUsingDirective/@EntryIndexedValue\">ERROR</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantStringInterpolation/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantVerbatimPrefix/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RedundantVerbatimStringPrefix/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RemoveRedundantOrStatement_002EFalse/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RemoveRedundantOrStatement_002ETrue/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RemoveToList_002E1/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=RemoveToList_002E2/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ReplaceWithFirstOrDefault_002E1/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ReplaceWithFirstOrDefault_002E2/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ReplaceWithFirstOrDefault_002E3/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ReplaceWithFirstOrDefault_002E4/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ReplaceWithLastOrDefault_002E1/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ReplaceWithLastOrDefault_002E2/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ReplaceWithLastOrDefault_002E3/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ReplaceWithLastOrDefault_002E4/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ReplaceWithOfType_002E1/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ReplaceWithOfType_002E2/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ReplaceWithOfType_002E3/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ReplaceWithOfType_002EAny_002E1/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ReplaceWithOfType_002EAny_002E2/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ReplaceWithOfType_002ECount_002E1/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ReplaceWithOfType_002ECount_002E2/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ReplaceWithOfType_002EFirst_002E1/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ReplaceWithOfType_002EFirst_002E2/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ReplaceWithOfType_002EFirstOrDefault_002E1/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ReplaceWithOfType_002EFirstOrDefault_002E2/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ReplaceWithOfType_002ELast_002E1/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ReplaceWithOfType_002ELast_002E2/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ReplaceWithOfType_002ELastOrDefault_002E1/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ReplaceWithOfType_002ELastOrDefault_002E2/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ReplaceWithOfType_002ELongCount/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ReplaceWithOfType_002ESingle_002E1/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ReplaceWithOfType_002ESingle_002E2/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ReplaceWithOfType_002ESingleOrDefault_002E1/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ReplaceWithOfType_002ESingleOrDefault_002E2/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ReplaceWithOfType_002EWhere/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ReplaceWithSimpleAssignment_002EFalse/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ReplaceWithSimpleAssignment_002ETrue/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ReplaceWithSingleAssignment_002EFalse/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ReplaceWithSingleAssignment_002ETrue/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ReplaceWithSingleCallToAny/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ReplaceWithSingleCallToCount/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ReplaceWithSingleCallToFirst/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ReplaceWithSingleCallToFirstOrDefault/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ReplaceWithSingleCallToLast/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ReplaceWithSingleCallToLastOrDefault/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ReplaceWithSingleCallToSingle/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ReplaceWithSingleCallToSingleOrDefault/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ReplaceWithSingleOrDefault_002E1/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ReplaceWithSingleOrDefault_002E2/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ReplaceWithSingleOrDefault_002E3/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=ReplaceWithSingleOrDefault_002E4/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=SeparateControlTransferStatement/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=SimplifyLinqExpressionUseMinByAndMaxBy/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=StringEndsWithIsCultureSpecific/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=StringLiteralTypo/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=StringStartsWithIsCultureSpecific/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=StructCanBeMadeReadOnly/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=SuggestVarOrType_005FBuiltInTypes/@EntryIndexedValue\">SUGGESTION</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=SuggestVarOrType_005FSimpleTypes/@EntryIndexedValue\">DO_NOT_SHOW</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=SuspiciousTypeConversion_002EGlobal/@EntryIndexedValue\"></s:String>\n\t<s:Boolean x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=SuspiciousTypeConversion_002EGlobal/@EntryIndexRemoved\">True</s:Boolean>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=SwitchStatementMissingSomeCases/@EntryIndexedValue\">DO_NOT_SHOW</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=TabsAndSpacesMismatch/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=TabsAreDisallowed/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=TabsOutsideIndent/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=TooWideLocalVariableScope/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=TryCastAlwaysSucceeds/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=UnassignedField_002EGlobal/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=UnnecessaryWhitespace/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=UnusedAutoPropertyAccessor_002EGlobal/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=UnusedMemberHierarchy_002EGlobal/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=UnusedMemberInSuper_002EGlobal/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=UnusedMember_002EGlobal/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=UnusedMember_002ELocal/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=UnusedMethodReturnValue_002EGlobal/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=UnusedMethodReturnValue_002ELocal/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=UnusedParameter_002EGlobal/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=UnusedParameter_002ELocal/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=UnusedType_002EGlobal/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=UseAwaitUsing/@EntryIndexedValue\">DO_NOT_SHOW</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=UseCollectionCountProperty/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=UseCollectionExpression/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=UseConfigureAwaitFalseForAsyncDisposable/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=UseFormatSpecifierInFormatString/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=UseFormatSpecifierInInterpolation/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=UseIndexFromEndExpression/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=UseNameofExpression/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=UseNameOfInsteadOfTypeOf/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=UseNegatedPatternMatching/@EntryIndexedValue\"></s:String>\n\t<s:Boolean x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=UseNegatedPatternMatching/@EntryIndexRemoved\">True</s:Boolean>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=UseNullPropagation/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=UseObjectOrCollectionInitializer/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=UsePatternMatching/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=UseStringInterpolation/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=UseUtf8StringLiteral/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=VariableCanBeMadeConst/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=VirtualMemberCallInConstructor/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=VirtualMemberNeverOverridden_002EGlobal/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=VirtualMemberNeverOverridden_002ELocal/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeInspection/Highlighting/InspectionSeverities/=WrongIndentSize/@EntryIndexedValue\">WARNING</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/CodeCleanup/Profiles/=Code_0020Cleanup_0020_0028peppy_0029/@EntryIndexedValue\">&lt;?xml version=\"1.0\" encoding=\"utf-16\"?&gt;&lt;Profile name=\"Code Cleanup (peppy)\"&gt;&lt;CSArrangeThisQualifier&gt;True&lt;/CSArrangeThisQualifier&gt;&lt;CSUseVar&gt;&lt;BehavourStyle&gt;CAN_CHANGE_TO_EXPLICIT&lt;/BehavourStyle&gt;&lt;LocalVariableStyle&gt;ALWAYS_EXPLICIT&lt;/LocalVariableStyle&gt;&lt;ForeachVariableStyle&gt;ALWAYS_EXPLICIT&lt;/ForeachVariableStyle&gt;&lt;/CSUseVar&gt;&lt;CSOptimizeUsings&gt;&lt;OptimizeUsings&gt;True&lt;/OptimizeUsings&gt;&lt;EmbraceInRegion&gt;False&lt;/EmbraceInRegion&gt;&lt;RegionName&gt;&lt;/RegionName&gt;&lt;/CSOptimizeUsings&gt;&lt;CSShortenReferences&gt;True&lt;/CSShortenReferences&gt;&lt;CSReformatCode&gt;True&lt;/CSReformatCode&gt;&lt;CSUpdateFileHeader&gt;True&lt;/CSUpdateFileHeader&gt;&lt;CSCodeStyleAttributes ArrangeTypeAccessModifier=\"False\" ArrangeTypeMemberAccessModifier=\"False\" SortModifiers=\"True\" RemoveRedundantParentheses=\"True\" AddMissingParentheses=\"False\" ArrangeBraces=\"False\" ArrangeAttributes=\"False\" ArrangeArgumentsStyle=\"False\" /&gt;&lt;XAMLCollapseEmptyTags&gt;False&lt;/XAMLCollapseEmptyTags&gt;&lt;CSFixBuiltinTypeReferences&gt;True&lt;/CSFixBuiltinTypeReferences&gt;&lt;CSArrangeQualifiers&gt;True&lt;/CSArrangeQualifiers&gt;&lt;/Profile&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/CodeCleanup/RecentlyUsedProfile/@EntryValue\">Code Cleanup (peppy)</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/BRACES_FOR_DOWHILE/@EntryValue\">RequiredForMultiline</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/BRACES_FOR_FIXED/@EntryValue\">RequiredForMultiline</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/BRACES_FOR_FOR/@EntryValue\">RequiredForMultiline</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/BRACES_FOR_FOREACH/@EntryValue\">RequiredForMultiline</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/BRACES_FOR_IFELSE/@EntryValue\">RequiredForMultiline</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/BRACES_FOR_LOCK/@EntryValue\">RequiredForMultiline</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/BRACES_FOR_USING/@EntryValue\">RequiredForMultiline</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/BRACES_FOR_WHILE/@EntryValue\">RequiredForMultiline</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/DEFAULT_INTERNAL_MODIFIER/@EntryValue\">Explicit</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/LOCAL_FUNCTION_BODY/@EntryValue\">ExpressionBody</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/METHOD_OR_OPERATOR_BODY/@EntryValue\">BlockBody</s:String>\n\t<s:Boolean x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/TRAILING_COMMA_IN_MULTILINE_LISTS/@EntryValue\">True</s:Boolean>\n\t<s:String x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/NAMESPACE_BODY/@EntryValue\">FileScoped</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/OBJECT_CREATION_WHEN_TYPE_EVIDENT/@EntryValue\">TargetTyped</s:String>\n\t<s:Boolean x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/USE_HEURISTICS_FOR_BODY_STYLE/@EntryValue\">True</s:Boolean>\n\t<s:String x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpFormat/ACCESSOR_DECLARATION_BRACES/@EntryValue\">NEXT_LINE</s:String>\n\t<s:Boolean x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpFormat/ALIGN_LINQ_QUERY/@EntryValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpFormat/ALIGN_MULTILINE_CALLS_CHAIN/@EntryValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpFormat/ALIGN_MULTILINE_EXTENDS_LIST/@EntryValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpFormat/ALIGN_MULTILINE_FOR_STMT/@EntryValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpFormat/ALIGN_MULTILINE_PARAMETER/@EntryValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpFormat/ALIGN_MULTIPLE_DECLARATION/@EntryValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpFormat/ALIGN_MULTLINE_TYPE_PARAMETER_CONSTRAINS/@EntryValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpFormat/ALIGN_MULTLINE_TYPE_PARAMETER_LIST/@EntryValue\">True</s:Boolean>\n\t<s:String x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpFormat/ANONYMOUS_METHOD_DECLARATION_BRACES/@EntryValue\">NEXT_LINE</s:String>\n\t<s:Int64 x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpFormat/BLANK_LINES_BEFORE_BLOCK_STATEMENTS/@EntryValue\">1</s:Int64>\n\t<s:Int64 x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpFormat/BLANK_LINES_BEFORE_CASE/@EntryValue\">1</s:Int64>\n\t<s:String x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpFormat/CASE_BLOCK_BRACES/@EntryValue\">NEXT_LINE</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpFormat/EMPTY_BLOCK_STYLE/@EntryValue\">MULTILINE</s:String>\n\t<s:Boolean x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpFormat/INDENT_NESTED_FOR_STMT/@EntryValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpFormat/INDENT_NESTED_FOREACH_STMT/@EntryValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpFormat/INDENT_NESTED_LOCK_STMT/@EntryValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpFormat/INDENT_NESTED_WHILE_STMT/@EntryValue\">True</s:Boolean>\n\t<s:String x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpFormat/INITIALIZER_BRACES/@EntryValue\">NEXT_LINE</s:String>\n\t<s:Int64 x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpFormat/KEEP_BLANK_LINES_IN_CODE/@EntryValue\">1</s:Int64>\n\t<s:Int64 x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpFormat/KEEP_BLANK_LINES_IN_DECLARATIONS/@EntryValue\">1</s:Int64>\n\t<s:Boolean x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpFormat/LINE_FEED_AT_FILE_END/@EntryValue\">True</s:Boolean>\n\t<s:String x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpFormat/OTHER_BRACES/@EntryValue\">NEXT_LINE</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_ACCESSORHOLDER_ATTRIBUTE_ON_SAME_LINE_EX/@EntryValue\">NEVER</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_ACCESSOR_ATTRIBUTE_ON_SAME_LINE_EX/@EntryValue\">NEVER</s:String>\n\t<s:Boolean x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_CATCH_ON_NEW_LINE/@EntryValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_CONSTRUCTOR_INITIALIZER_ON_SAME_LINE/@EntryValue\">False</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_ELSE_ON_NEW_LINE/@EntryValue\">True</s:Boolean>\n\t<s:String x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_FIELD_ATTRIBUTE_ON_SAME_LINE_EX/@EntryValue\">NEVER</s:String>\n\t<s:Boolean x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_WHILE_ON_NEW_LINE/@EntryValue\">False</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpFormat/SPACE_AFTER_TYPECAST_PARENTHESES/@EntryValue\">False</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpFormat/SPACE_AROUND_MULTIPLICATIVE_OP/@EntryValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpFormat/SPACE_BEFORE_SIZEOF_PARENTHESES/@EntryValue\">False</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpFormat/SPACE_BEFORE_TYPEOF_PARENTHESES/@EntryValue\">False</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpFormat/SPACE_WITHIN_SINGLE_LINE_ARRAY_INITIALIZER_BRACES/@EntryValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpFormat/SPACE_WITHING_EMPTY_BRACES/@EntryValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpFormat/STICK_COMMENT/@EntryValue\">False</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_AFTER_DECLARATION_LPAR/@EntryValue\">False</s:Boolean>\n\t<s:String x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_ARRAY_INITIALIZER_STYLE/@EntryValue\">CHOP_IF_LONG</s:String>\n\t<s:Boolean x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_BEFORE_BINARY_OPSIGN/@EntryValue\">True</s:Boolean>\n\t<s:Int64 x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_LIMIT/@EntryValue\">200</s:Int64>\n\t<s:String x:Key=\"/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_OBJECT_AND_COLLECTION_INITIALIZER_STYLE/@EntryValue\">CHOP_IF_LONG</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/CSharpVarKeywordUsage/ForBuiltInTypes/@EntryValue\">UseExplicitType</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/CSharpVarKeywordUsage/ForOtherTypes/@EntryValue\">UseVarWhenEvident</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/CSharpVarKeywordUsage/ForSimpleTypes/@EntryValue\">UseVarWhenEvident</s:String>\n\t<s:Boolean x:Key=\"/Default/CodeStyle/EncapsulateField/MakeFieldPrivate/@EntryValue\">False</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/CodeStyle/EncapsulateField/UseAutoProperty/@EntryValue\">False</s:Boolean>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=AABB/@EntryIndexedValue\">AABB</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=API/@EntryIndexedValue\">API</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=ARGB/@EntryIndexedValue\">ARGB</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=BPM/@EntryIndexedValue\">BPM</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=EF/@EntryIndexedValue\">EF</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=FPS/@EntryIndexedValue\">FPS</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=GC/@EntryIndexedValue\">GC</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=GL/@EntryIndexedValue\">GL</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=GLSL/@EntryIndexedValue\">GLSL</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=HID/@EntryIndexedValue\">HID</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=HSL/@EntryIndexedValue\">HSL</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=HSPA/@EntryIndexedValue\">HSPA</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=HSV/@EntryIndexedValue\">HSV</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=HTML/@EntryIndexedValue\">HTML</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=HUD/@EntryIndexedValue\">HUD</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=ID/@EntryIndexedValue\">ID</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=IL/@EntryIndexedValue\">IL</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=IOS/@EntryIndexedValue\">IOS</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=IP/@EntryIndexedValue\">IP</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=IPC/@EntryIndexedValue\">IPC</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=JIT/@EntryIndexedValue\">JIT</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LTRB/@EntryIndexedValue\">LTRB</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=MD/@EntryIndexedValue\">MD5</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=NS/@EntryIndexedValue\">NS</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=OS/@EntryIndexedValue\">OS</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=PM/@EntryIndexedValue\">PM</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=RGB/@EntryIndexedValue\">RGB</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=RGBA/@EntryIndexedValue\">RGBA</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=RNG/@EntryIndexedValue\">RNG</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=SDL/@EntryIndexedValue\">SDL</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=SHA/@EntryIndexedValue\">SHA</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=SRGB/@EntryIndexedValue\">SRGB</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=TK/@EntryIndexedValue\">TK</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=SS/@EntryIndexedValue\">SS</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=PP/@EntryIndexedValue\">PP</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=GMT/@EntryIndexedValue\">GMT</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=QAT/@EntryIndexedValue\">QAT</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=BNG/@EntryIndexedValue\">BNG</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=UI/@EntryIndexedValue\">UI</s:String>\n\t<s:Boolean x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/ApplyAutoDetectedRules/@EntryValue\">False</s:Boolean>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=EnumMember/@EntryIndexedValue\">HINT</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/CSharpFileLayoutPatterns/Pattern/@EntryValue\">&lt;?xml version=\"1.0\" encoding=\"utf-16\"?&gt;&#xD;\n&lt;Patterns xmlns=\"urn:schemas-jetbrains-com:member-reordering-patterns\"&gt;&#xD;\n\t&lt;TypePattern DisplayName=\"COM interfaces or structs\"&gt;&#xD;\n\t&lt;TypePattern.Match&gt;&#xD;\n\t\t&lt;Or&gt;&#xD;\n\t\t&lt;And&gt;&#xD;\n\t\t\t&lt;Kind Is=\"Interface\" /&gt;&#xD;\n\t\t\t&lt;Or&gt;&#xD;\n\t\t\t&lt;HasAttribute Name=\"System.Runtime.InteropServices.InterfaceTypeAttribute\" /&gt;&#xD;\n\t\t\t&lt;HasAttribute Name=\"System.Runtime.InteropServices.ComImport\" /&gt;&#xD;\n\t\t\t&lt;/Or&gt;&#xD;\n\t\t&lt;/And&gt;&#xD;\n\t\t&lt;Kind Is=\"Struct\" /&gt;&#xD;\n\t\t&lt;/Or&gt;&#xD;\n\t&lt;/TypePattern.Match&gt;&#xD;\n\t&lt;/TypePattern&gt;&#xD;\n\t&lt;TypePattern DisplayName=\"NUnit Test Fixtures\" RemoveRegions=\"All\"&gt;&#xD;\n\t&lt;TypePattern.Match&gt;&#xD;\n\t\t&lt;And&gt;&#xD;\n\t\t&lt;Kind Is=\"Class\" /&gt;&#xD;\n\t\t&lt;HasAttribute Name=\"NUnit.Framework.TestFixtureAttribute\" Inherited=\"True\" /&gt;&#xD;\n\t\t&lt;/And&gt;&#xD;\n\t&lt;/TypePattern.Match&gt;&#xD;\n\t&lt;Entry DisplayName=\"Setup/Teardown Methods\"&gt;&#xD;\n\t\t&lt;Entry.Match&gt;&#xD;\n\t\t&lt;And&gt;&#xD;\n\t\t\t&lt;Kind Is=\"Method\" /&gt;&#xD;\n\t\t\t&lt;Or&gt;&#xD;\n\t\t\t&lt;HasAttribute Name=\"NUnit.Framework.SetUpAttribute\" Inherited=\"True\" /&gt;&#xD;\n\t\t\t&lt;HasAttribute Name=\"NUnit.Framework.TearDownAttribute\" Inherited=\"True\" /&gt;&#xD;\n\t\t\t&lt;HasAttribute Name=\"NUnit.Framework.FixtureSetUpAttribute\" Inherited=\"True\" /&gt;&#xD;\n\t\t\t&lt;HasAttribute Name=\"NUnit.Framework.FixtureTearDownAttribute\" Inherited=\"True\" /&gt;&#xD;\n\t\t\t&lt;/Or&gt;&#xD;\n\t\t&lt;/And&gt;&#xD;\n\t\t&lt;/Entry.Match&gt;&#xD;\n\t&lt;/Entry&gt;&#xD;\n\t&lt;Entry DisplayName=\"All other members\" /&gt;&#xD;\n\t&lt;Entry Priority=\"100\" DisplayName=\"Test Methods\"&gt;&#xD;\n\t\t&lt;Entry.Match&gt;&#xD;\n\t\t&lt;And&gt;&#xD;\n\t\t\t&lt;Kind Is=\"Method\" /&gt;&#xD;\n\t\t\t&lt;HasAttribute Name=\"NUnit.Framework.TestAttribute\" /&gt;&#xD;\n\t\t&lt;/And&gt;&#xD;\n\t\t&lt;/Entry.Match&gt;&#xD;\n\t\t&lt;Entry.SortBy&gt;&#xD;\n\t\t&lt;Name /&gt;&#xD;\n\t\t&lt;/Entry.SortBy&gt;&#xD;\n\t&lt;/Entry&gt;&#xD;\n\t&lt;/TypePattern&gt;&#xD;\n\t&lt;TypePattern DisplayName=\"Default Pattern\"&gt;&#xD;\n\t&lt;Group DisplayName=\"Fields/Properties\"&gt;&#xD;\n\t\t&lt;Group DisplayName=\"Public Fields\"&gt;&#xD;\n\t\t&lt;Entry DisplayName=\"Constant Fields\"&gt;&#xD;\n\t\t\t&lt;Entry.Match&gt;&#xD;\n\t\t\t&lt;And&gt;&#xD;\n\t\t\t\t&lt;Access Is=\"Public\" /&gt;&#xD;\n\t\t\t\t&lt;Or&gt;&#xD;\n\t\t\t\t&lt;Kind Is=\"Constant\" /&gt;&#xD;\n\t\t\t\t&lt;Readonly /&gt;&#xD;\n\t\t\t\t&lt;And&gt;&#xD;\n\t\t\t\t\t&lt;Static /&gt;&#xD;\n\t\t\t\t\t&lt;Readonly /&gt;&#xD;\n\t\t\t\t&lt;/And&gt;&#xD;\n\t\t\t\t&lt;/Or&gt;&#xD;\n\t\t\t&lt;/And&gt;&#xD;\n\t\t\t&lt;/Entry.Match&gt;&#xD;\n\t\t&lt;/Entry&gt;&#xD;\n\t\t&lt;Entry DisplayName=\"Static Fields\"&gt;&#xD;\n\t\t\t&lt;Entry.Match&gt;&#xD;\n\t\t\t&lt;And&gt;&#xD;\n\t\t\t\t&lt;Access Is=\"Public\" /&gt;&#xD;\n\t\t\t\t&lt;Static /&gt;&#xD;\n\t\t\t\t&lt;Not&gt;&#xD;\n\t\t\t\t&lt;Readonly /&gt;&#xD;\n\t\t\t\t&lt;/Not&gt;&#xD;\n\t\t\t\t&lt;Kind Is=\"Field\" /&gt;&#xD;\n\t\t\t&lt;/And&gt;&#xD;\n\t\t\t&lt;/Entry.Match&gt;&#xD;\n\t\t&lt;/Entry&gt;&#xD;\n\t\t&lt;Entry DisplayName=\"Normal Fields\"&gt;&#xD;\n\t\t\t&lt;Entry.Match&gt;&#xD;\n\t\t\t&lt;And&gt;&#xD;\n\t\t\t\t&lt;Access Is=\"Public\" /&gt;&#xD;\n\t\t\t\t&lt;Not&gt;&#xD;\n\t\t\t\t&lt;Or&gt;&#xD;\n\t\t\t\t\t&lt;Static /&gt;&#xD;\n\t\t\t\t\t&lt;Readonly /&gt;&#xD;\n\t\t\t\t&lt;/Or&gt;&#xD;\n\t\t\t\t&lt;/Not&gt;&#xD;\n\t\t\t\t&lt;Kind Is=\"Field\" /&gt;&#xD;\n\t\t\t&lt;/And&gt;&#xD;\n\t\t\t&lt;/Entry.Match&gt;&#xD;\n\t\t&lt;/Entry&gt;&#xD;\n\t\t&lt;/Group&gt;&#xD;\n\t\t&lt;Entry DisplayName=\"Public Properties\"&gt;&#xD;\n\t\t&lt;Entry.Match&gt;&#xD;\n\t\t\t&lt;And&gt;&#xD;\n\t\t\t&lt;Access Is=\"Public\" /&gt;&#xD;\n\t\t\t&lt;Kind Is=\"Property\" /&gt;&#xD;\n\t\t\t&lt;/And&gt;&#xD;\n\t\t&lt;/Entry.Match&gt;&#xD;\n\t\t&lt;/Entry&gt;&#xD;\n\t\t&lt;Group DisplayName=\"Internal Fields\"&gt;&#xD;\n\t\t&lt;Entry DisplayName=\"Constant Fields\"&gt;&#xD;\n\t\t\t&lt;Entry.Match&gt;&#xD;\n\t\t\t&lt;And&gt;&#xD;\n\t\t\t\t&lt;Access Is=\"Internal\" /&gt;&#xD;\n\t\t\t\t&lt;Or&gt;&#xD;\n\t\t\t\t&lt;Kind Is=\"Constant\" /&gt;&#xD;\n\t\t\t\t&lt;Readonly /&gt;&#xD;\n\t\t\t\t&lt;And&gt;&#xD;\n\t\t\t\t\t&lt;Static /&gt;&#xD;\n\t\t\t\t\t&lt;Readonly /&gt;&#xD;\n\t\t\t\t&lt;/And&gt;&#xD;\n\t\t\t\t&lt;/Or&gt;&#xD;\n\t\t\t&lt;/And&gt;&#xD;\n\t\t\t&lt;/Entry.Match&gt;&#xD;\n\t\t&lt;/Entry&gt;&#xD;\n\t\t&lt;Entry DisplayName=\"Static Fields\"&gt;&#xD;\n\t\t\t&lt;Entry.Match&gt;&#xD;\n\t\t\t&lt;And&gt;&#xD;\n\t\t\t\t&lt;Access Is=\"Internal\" /&gt;&#xD;\n\t\t\t\t&lt;Static /&gt;&#xD;\n\t\t\t\t&lt;Not&gt;&#xD;\n\t\t\t\t&lt;Readonly /&gt;&#xD;\n\t\t\t\t&lt;/Not&gt;&#xD;\n\t\t\t\t&lt;Kind Is=\"Field\" /&gt;&#xD;\n\t\t\t&lt;/And&gt;&#xD;\n\t\t\t&lt;/Entry.Match&gt;&#xD;\n\t\t&lt;/Entry&gt;&#xD;\n\t\t&lt;Entry DisplayName=\"Normal Fields\"&gt;&#xD;\n\t\t\t&lt;Entry.Match&gt;&#xD;\n\t\t\t&lt;And&gt;&#xD;\n\t\t\t\t&lt;Access Is=\"Internal\" /&gt;&#xD;\n\t\t\t\t&lt;Not&gt;&#xD;\n\t\t\t\t&lt;Or&gt;&#xD;\n\t\t\t\t\t&lt;Static /&gt;&#xD;\n\t\t\t\t\t&lt;Readonly /&gt;&#xD;\n\t\t\t\t&lt;/Or&gt;&#xD;\n\t\t\t\t&lt;/Not&gt;&#xD;\n\t\t\t\t&lt;Kind Is=\"Field\" /&gt;&#xD;\n\t\t\t&lt;/And&gt;&#xD;\n\t\t\t&lt;/Entry.Match&gt;&#xD;\n\t\t&lt;/Entry&gt;&#xD;\n\t\t&lt;/Group&gt;&#xD;\n\t\t&lt;Entry DisplayName=\"Internal Properties\"&gt;&#xD;\n\t\t&lt;Entry.Match&gt;&#xD;\n\t\t\t&lt;And&gt;&#xD;\n\t\t\t&lt;Access Is=\"Internal\" /&gt;&#xD;\n\t\t\t&lt;Kind Is=\"Property\" /&gt;&#xD;\n\t\t\t&lt;/And&gt;&#xD;\n\t\t&lt;/Entry.Match&gt;&#xD;\n\t\t&lt;/Entry&gt;&#xD;\n\t\t&lt;Group DisplayName=\"Protected Fields\"&gt;&#xD;\n\t\t&lt;Entry DisplayName=\"Constant Fields\"&gt;&#xD;\n\t\t\t&lt;Entry.Match&gt;&#xD;\n\t\t\t&lt;And&gt;&#xD;\n\t\t\t\t&lt;Access Is=\"Protected\" /&gt;&#xD;\n\t\t\t\t&lt;Or&gt;&#xD;\n\t\t\t\t&lt;Kind Is=\"Constant\" /&gt;&#xD;\n\t\t\t\t&lt;Readonly /&gt;&#xD;\n\t\t\t\t&lt;And&gt;&#xD;\n\t\t\t\t\t&lt;Static /&gt;&#xD;\n\t\t\t\t\t&lt;Readonly /&gt;&#xD;\n\t\t\t\t&lt;/And&gt;&#xD;\n\t\t\t\t&lt;/Or&gt;&#xD;\n\t\t\t&lt;/And&gt;&#xD;\n\t\t\t&lt;/Entry.Match&gt;&#xD;\n\t\t&lt;/Entry&gt;&#xD;\n\t\t&lt;Entry DisplayName=\"Static Fields\"&gt;&#xD;\n\t\t\t&lt;Entry.Match&gt;&#xD;\n\t\t\t&lt;And&gt;&#xD;\n\t\t\t\t&lt;Access Is=\"Protected\" /&gt;&#xD;\n\t\t\t\t&lt;Static /&gt;&#xD;\n\t\t\t\t&lt;Not&gt;&#xD;\n\t\t\t\t&lt;Readonly /&gt;&#xD;\n\t\t\t\t&lt;/Not&gt;&#xD;\n\t\t\t\t&lt;Kind Is=\"Field\" /&gt;&#xD;\n\t\t\t&lt;/And&gt;&#xD;\n\t\t\t&lt;/Entry.Match&gt;&#xD;\n\t\t&lt;/Entry&gt;&#xD;\n\t\t&lt;Entry DisplayName=\"Normal Fields\"&gt;&#xD;\n\t\t\t&lt;Entry.Match&gt;&#xD;\n\t\t\t&lt;And&gt;&#xD;\n\t\t\t\t&lt;Access Is=\"Protected\" /&gt;&#xD;\n\t\t\t\t&lt;Not&gt;&#xD;\n\t\t\t\t&lt;Or&gt;&#xD;\n\t\t\t\t\t&lt;Static /&gt;&#xD;\n\t\t\t\t\t&lt;Readonly /&gt;&#xD;\n\t\t\t\t&lt;/Or&gt;&#xD;\n\t\t\t\t&lt;/Not&gt;&#xD;\n\t\t\t\t&lt;Kind Is=\"Field\" /&gt;&#xD;\n\t\t\t&lt;/And&gt;&#xD;\n\t\t\t&lt;/Entry.Match&gt;&#xD;\n\t\t&lt;/Entry&gt;&#xD;\n\t\t&lt;/Group&gt;&#xD;\n\t\t&lt;Entry DisplayName=\"Protected Properties\"&gt;&#xD;\n\t\t&lt;Entry.Match&gt;&#xD;\n\t\t\t&lt;And&gt;&#xD;\n\t\t\t&lt;Access Is=\"Protected\" /&gt;&#xD;\n\t\t\t&lt;Kind Is=\"Property\" /&gt;&#xD;\n\t\t\t&lt;/And&gt;&#xD;\n\t\t&lt;/Entry.Match&gt;&#xD;\n\t\t&lt;/Entry&gt;&#xD;\n\t\t&lt;Group DisplayName=\"Private Fields\"&gt;&#xD;\n\t\t&lt;Entry DisplayName=\"Constant Fields\"&gt;&#xD;\n\t\t\t&lt;Entry.Match&gt;&#xD;\n\t\t\t&lt;And&gt;&#xD;\n\t\t\t\t&lt;Access Is=\"Private\" /&gt;&#xD;\n\t\t\t\t&lt;Or&gt;&#xD;\n\t\t\t\t&lt;Kind Is=\"Constant\" /&gt;&#xD;\n\t\t\t\t&lt;Readonly /&gt;&#xD;\n\t\t\t\t&lt;And&gt;&#xD;\n\t\t\t\t\t&lt;Static /&gt;&#xD;\n\t\t\t\t\t&lt;Readonly /&gt;&#xD;\n\t\t\t\t&lt;/And&gt;&#xD;\n\t\t\t\t&lt;/Or&gt;&#xD;\n\t\t\t&lt;/And&gt;&#xD;\n\t\t\t&lt;/Entry.Match&gt;&#xD;\n\t\t&lt;/Entry&gt;&#xD;\n\t\t&lt;Entry DisplayName=\"Static Fields\"&gt;&#xD;\n\t\t\t&lt;Entry.Match&gt;&#xD;\n\t\t\t&lt;And&gt;&#xD;\n\t\t\t\t&lt;Access Is=\"Private\" /&gt;&#xD;\n\t\t\t\t&lt;Static /&gt;&#xD;\n\t\t\t\t&lt;Not&gt;&#xD;\n\t\t\t\t&lt;Readonly /&gt;&#xD;\n\t\t\t\t&lt;/Not&gt;&#xD;\n\t\t\t\t&lt;Kind Is=\"Field\" /&gt;&#xD;\n\t\t\t&lt;/And&gt;&#xD;\n\t\t\t&lt;/Entry.Match&gt;&#xD;\n\t\t&lt;/Entry&gt;&#xD;\n\t\t&lt;Entry DisplayName=\"Normal Fields\"&gt;&#xD;\n\t\t\t&lt;Entry.Match&gt;&#xD;\n\t\t\t&lt;And&gt;&#xD;\n\t\t\t\t&lt;Access Is=\"Private\" /&gt;&#xD;\n\t\t\t\t&lt;Not&gt;&#xD;\n\t\t\t\t&lt;Or&gt;&#xD;\n\t\t\t\t\t&lt;Static /&gt;&#xD;\n\t\t\t\t\t&lt;Readonly /&gt;&#xD;\n\t\t\t\t&lt;/Or&gt;&#xD;\n\t\t\t\t&lt;/Not&gt;&#xD;\n\t\t\t\t&lt;Kind Is=\"Field\" /&gt;&#xD;\n\t\t\t&lt;/And&gt;&#xD;\n\t\t\t&lt;/Entry.Match&gt;&#xD;\n\t\t&lt;/Entry&gt;&#xD;\n\t\t&lt;/Group&gt;&#xD;\n\t\t&lt;Entry DisplayName=\"Private Properties\"&gt;&#xD;\n\t\t&lt;Entry.Match&gt;&#xD;\n\t\t\t&lt;And&gt;&#xD;\n\t\t\t&lt;Access Is=\"Private\" /&gt;&#xD;\n\t\t\t&lt;Kind Is=\"Property\" /&gt;&#xD;\n\t\t\t&lt;/And&gt;&#xD;\n\t\t&lt;/Entry.Match&gt;&#xD;\n\t\t&lt;/Entry&gt;&#xD;\n\t&lt;/Group&gt;&#xD;\n\t&lt;Group DisplayName=\"Constructor/Destructor\"&gt;&#xD;\n\t\t&lt;Entry DisplayName=\"Ctor\"&gt;&#xD;\n\t\t&lt;Entry.Match&gt;&#xD;\n\t\t\t&lt;Kind Is=\"Constructor\" /&gt;&#xD;\n\t\t&lt;/Entry.Match&gt;&#xD;\n\t\t&lt;/Entry&gt;&#xD;\n\t\t&lt;Region Name=\"Disposal\"&gt;&#xD;\n\t\t&lt;Entry DisplayName=\"Dtor\"&gt;&#xD;\n\t\t\t&lt;Entry.Match&gt;&#xD;\n\t\t\t&lt;Kind Is=\"Destructor\" /&gt;&#xD;\n\t\t\t&lt;/Entry.Match&gt;&#xD;\n\t\t&lt;/Entry&gt;&#xD;\n\t\t&lt;Entry DisplayName=\"Dispose()\"&gt;&#xD;\n\t\t\t&lt;Entry.Match&gt;&#xD;\n\t\t\t&lt;And&gt;&#xD;\n\t\t\t\t&lt;Access Is=\"Public\" /&gt;&#xD;\n\t\t\t\t&lt;Kind Is=\"Method\" /&gt;&#xD;\n\t\t\t\t&lt;Name Is=\"Dispose\" /&gt;&#xD;\n\t\t\t&lt;/And&gt;&#xD;\n\t\t\t&lt;/Entry.Match&gt;&#xD;\n\t\t&lt;/Entry&gt;&#xD;\n\t\t&lt;Entry DisplayName=\"Dispose(true)\"&gt;&#xD;\n\t\t\t&lt;Entry.Match&gt;&#xD;\n\t\t\t&lt;And&gt;&#xD;\n\t\t\t\t&lt;Access Is=\"Protected\" /&gt;&#xD;\n\t\t\t\t&lt;Or&gt;&#xD;\n\t\t\t\t&lt;Virtual /&gt;&#xD;\n\t\t\t\t&lt;Override /&gt;&#xD;\n\t\t\t\t&lt;/Or&gt;&#xD;\n\t\t\t\t&lt;Kind Is=\"Method\" /&gt;&#xD;\n\t\t\t\t&lt;Name Is=\"Dispose\" /&gt;&#xD;\n\t\t\t&lt;/And&gt;&#xD;\n\t\t\t&lt;/Entry.Match&gt;&#xD;\n\t\t&lt;/Entry&gt;&#xD;\n\t\t&lt;/Region&gt;&#xD;\n\t&lt;/Group&gt;&#xD;\n\t&lt;Group DisplayName=\"Methods\"&gt;&#xD;\n\t\t&lt;Group DisplayName=\"Public\"&gt;&#xD;\n\t\t&lt;Entry DisplayName=\"Static Methods\"&gt;&#xD;\n\t\t\t&lt;Entry.Match&gt;&#xD;\n\t\t\t&lt;And&gt;&#xD;\n\t\t\t\t&lt;Access Is=\"Public\" /&gt;&#xD;\n\t\t\t\t&lt;Static /&gt;&#xD;\n\t\t\t\t&lt;Kind Is=\"Method\" /&gt;&#xD;\n\t\t\t&lt;/And&gt;&#xD;\n\t\t\t&lt;/Entry.Match&gt;&#xD;\n\t\t&lt;/Entry&gt;&#xD;\n\t\t&lt;Entry DisplayName=\"Methods\"&gt;&#xD;\n\t\t\t&lt;Entry.Match&gt;&#xD;\n\t\t\t&lt;And&gt;&#xD;\n\t\t\t\t&lt;Access Is=\"Public\" /&gt;&#xD;\n\t\t\t\t&lt;Not&gt;&#xD;\n\t\t\t\t&lt;Static /&gt;&#xD;\n\t\t\t\t&lt;/Not&gt;&#xD;\n\t\t\t\t&lt;Kind Is=\"Method\" /&gt;&#xD;\n\t\t\t&lt;/And&gt;&#xD;\n\t\t\t&lt;/Entry.Match&gt;&#xD;\n\t\t&lt;/Entry&gt;&#xD;\n\t\t&lt;/Group&gt;&#xD;\n\t\t&lt;Group DisplayName=\"Internal\"&gt;&#xD;\n\t\t&lt;Entry DisplayName=\"Static Methods\"&gt;&#xD;\n\t\t\t&lt;Entry.Match&gt;&#xD;\n\t\t\t&lt;And&gt;&#xD;\n\t\t\t\t&lt;Access Is=\"Internal\" /&gt;&#xD;\n\t\t\t\t&lt;Static /&gt;&#xD;\n\t\t\t\t&lt;Kind Is=\"Method\" /&gt;&#xD;\n\t\t\t&lt;/And&gt;&#xD;\n\t\t\t&lt;/Entry.Match&gt;&#xD;\n\t\t&lt;/Entry&gt;&#xD;\n\t\t&lt;Entry DisplayName=\"Methods\"&gt;&#xD;\n\t\t\t&lt;Entry.Match&gt;&#xD;\n\t\t\t&lt;And&gt;&#xD;\n\t\t\t\t&lt;Access Is=\"Internal\" /&gt;&#xD;\n\t\t\t\t&lt;Not&gt;&#xD;\n\t\t\t\t&lt;Static /&gt;&#xD;\n\t\t\t\t&lt;/Not&gt;&#xD;\n\t\t\t\t&lt;Kind Is=\"Method\" /&gt;&#xD;\n\t\t\t&lt;/And&gt;&#xD;\n\t\t\t&lt;/Entry.Match&gt;&#xD;\n\t\t&lt;/Entry&gt;&#xD;\n\t\t&lt;/Group&gt;&#xD;\n\t\t&lt;Group DisplayName=\"Protected\"&gt;&#xD;\n\t\t&lt;Entry DisplayName=\"Static Methods\"&gt;&#xD;\n\t\t\t&lt;Entry.Match&gt;&#xD;\n\t\t\t&lt;And&gt;&#xD;\n\t\t\t\t&lt;Access Is=\"Protected\" /&gt;&#xD;\n\t\t\t\t&lt;Static /&gt;&#xD;\n\t\t\t\t&lt;Kind Is=\"Method\" /&gt;&#xD;\n\t\t\t&lt;/And&gt;&#xD;\n\t\t\t&lt;/Entry.Match&gt;&#xD;\n\t\t&lt;/Entry&gt;&#xD;\n\t\t&lt;Entry DisplayName=\"Methods\"&gt;&#xD;\n\t\t\t&lt;Entry.Match&gt;&#xD;\n\t\t\t&lt;And&gt;&#xD;\n\t\t\t\t&lt;Access Is=\"Protected\" /&gt;&#xD;\n\t\t\t\t&lt;Not&gt;&#xD;\n\t\t\t\t&lt;Static /&gt;&#xD;\n\t\t\t\t&lt;/Not&gt;&#xD;\n\t\t\t\t&lt;Kind Is=\"Method\" /&gt;&#xD;\n\t\t\t&lt;/And&gt;&#xD;\n\t\t\t&lt;/Entry.Match&gt;&#xD;\n\t\t&lt;/Entry&gt;&#xD;\n\t\t&lt;/Group&gt;&#xD;\n\t\t&lt;Group DisplayName=\"Private\"&gt;&#xD;\n\t\t&lt;Entry DisplayName=\"Static Methods\"&gt;&#xD;\n\t\t\t&lt;Entry.Match&gt;&#xD;\n\t\t\t&lt;And&gt;&#xD;\n\t\t\t\t&lt;Access Is=\"Private\" /&gt;&#xD;\n\t\t\t\t&lt;Static /&gt;&#xD;\n\t\t\t\t&lt;Kind Is=\"Method\" /&gt;&#xD;\n\t\t\t&lt;/And&gt;&#xD;\n\t\t\t&lt;/Entry.Match&gt;&#xD;\n\t\t&lt;/Entry&gt;&#xD;\n\t\t&lt;Entry DisplayName=\"Methods\"&gt;&#xD;\n\t\t\t&lt;Entry.Match&gt;&#xD;\n\t\t\t&lt;And&gt;&#xD;\n\t\t\t\t&lt;Access Is=\"Private\" /&gt;&#xD;\n\t\t\t\t&lt;Not&gt;&#xD;\n\t\t\t\t&lt;Static /&gt;&#xD;\n\t\t\t\t&lt;/Not&gt;&#xD;\n\t\t\t\t&lt;Kind Is=\"Method\" /&gt;&#xD;\n\t\t\t&lt;/And&gt;&#xD;\n\t\t\t&lt;/Entry.Match&gt;&#xD;\n\t\t&lt;/Entry&gt;&#xD;\n\t\t&lt;/Group&gt;&#xD;\n\t&lt;/Group&gt;&#xD;\n\t&lt;/TypePattern&gt;&#xD;\n&lt;/Patterns&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/FileHeader/FileHeaderText/@EntryValue\">Copyright (c) andy840119 &lt;andy840119@gmail.com&gt;. Licensed under the GPL Licence.&#xD;\nSee the LICENCE file in the repository root for full licence text.&#xD;\n</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=Constants/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"AA_BB\" /&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=EnumMember/@EntryIndexedValue\">&lt;Policy Inspect=\"False\" Prefix=\"\" Suffix=\"\" Style=\"AaBb\" /&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=LocalConstants/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"aa_bb\" /&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=LocalFunctions/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"aaBb\" /&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateConstants/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"aa_bb\" /&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateInstanceFields/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"aaBb\"&gt;&lt;ExtraRule Prefix=\"_\" Suffix=\"\" Style=\"aaBb\" /&gt;&lt;/Policy&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateStaticFields/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"aaBb\" /&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateStaticReadonly/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"aa_bb\" /&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=StaticReadonly/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"AA_BB\" /&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=TypeParameters/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"AaBb\" /&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/UserRules/=15b5b1f1_002D457c_002D4ca6_002Db278_002D5615aedc07d3/@EntryIndexedValue\">&lt;Policy&gt;&lt;Descriptor Staticness=\"Static\" AccessRightKinds=\"Private\" Description=\"Static readonly fields (private)\"&gt;&lt;ElementKinds&gt;&lt;Kind Name=\"READONLY_FIELD\" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"aa_bb\" /&gt;&lt;/Policy&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/UserRules/=236f7aa5_002D7b06_002D43ca_002Dbf2a_002D9b31bfcff09a/@EntryIndexedValue\">&lt;Policy&gt;&lt;Descriptor Staticness=\"Any\" AccessRightKinds=\"Private\" Description=\"Constant fields (private)\"&gt;&lt;ElementKinds&gt;&lt;Kind Name=\"CONSTANT_FIELD\" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"aa_bb\" /&gt;&lt;/Policy&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/UserRules/=2c62818f_002D621b_002D4425_002Dadc9_002D78611099bfcb/@EntryIndexedValue\">&lt;Policy&gt;&lt;Descriptor Staticness=\"Any\" AccessRightKinds=\"Any\" Description=\"Type parameters\"&gt;&lt;ElementKinds&gt;&lt;Kind Name=\"TYPE_PARAMETER\" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"AaBb\" /&gt;&lt;/Policy&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/UserRules/=4a98fdf6_002D7d98_002D4f5a_002Dafeb_002Dea44ad98c70c/@EntryIndexedValue\">&lt;Policy&gt;&lt;Descriptor Staticness=\"Instance\" AccessRightKinds=\"Private\" Description=\"Instance fields (private)\"&gt;&lt;ElementKinds&gt;&lt;Kind Name=\"FIELD\" /&gt;&lt;Kind Name=\"READONLY_FIELD\" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"aaBb\"&gt;&lt;ExtraRule Prefix=\"_\" Suffix=\"\" Style=\"aaBb\" /&gt;&lt;/Policy&gt;&lt;/Policy&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/UserRules/=669e5282_002Dfb4b_002D4e90_002D91e7_002D07d269d04b60/@EntryIndexedValue\">&lt;Policy&gt;&lt;Descriptor Staticness=\"Any\" AccessRightKinds=\"Protected, ProtectedInternal, Internal, Public, PrivateProtected\" Description=\"Constant fields (not private)\"&gt;&lt;ElementKinds&gt;&lt;Kind Name=\"CONSTANT_FIELD\" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"AA_BB\" /&gt;&lt;/Policy&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/UserRules/=76f79b1e_002Dece7_002D4df2_002Da322_002D1bd7fea25eb7/@EntryIndexedValue\">&lt;Policy&gt;&lt;Descriptor Staticness=\"Any\" AccessRightKinds=\"Any\" Description=\"Local functions\"&gt;&lt;ElementKinds&gt;&lt;Kind Name=\"LOCAL_FUNCTION\" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"aaBb\" /&gt;&lt;/Policy&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/UserRules/=8b8504e3_002Df0be_002D4c14_002D9103_002Dc732f2bddc15/@EntryIndexedValue\">&lt;Policy&gt;&lt;Descriptor Staticness=\"Any\" AccessRightKinds=\"Any\" Description=\"Enum members\"&gt;&lt;ElementKinds&gt;&lt;Kind Name=\"ENUM_MEMBER\" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect=\"False\" Prefix=\"\" Suffix=\"\" Style=\"AaBb\" /&gt;&lt;/Policy&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/UserRules/=9d1af99b_002Dbefe_002D48a4_002D9eb3_002D661384e29869/@EntryIndexedValue\">&lt;Policy&gt;&lt;Descriptor Staticness=\"Static, Instance\" AccessRightKinds=\"Private\" Description=\"private methods\"&gt;&lt;ElementKinds&gt;&lt;Kind Name=\"ASYNC_METHOD\" /&gt;&lt;Kind Name=\"METHOD\" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"aaBb\" /&gt;&lt;/Policy&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/UserRules/=9ffbe43b_002Dc610_002D411b_002D9839_002D1416a146d9b0/@EntryIndexedValue\">&lt;Policy&gt;&lt;Descriptor Staticness=\"Static, Instance\" AccessRightKinds=\"Protected, ProtectedInternal, Internal, Public\" Description=\"internal/protected/public methods\"&gt;&lt;ElementKinds&gt;&lt;Kind Name=\"ASYNC_METHOD\" /&gt;&lt;Kind Name=\"METHOD\" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"AaBb\" /&gt;&lt;/Policy&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/UserRules/=a4c2df6c_002Db202_002D48d5_002Db077_002De678cb548c25/@EntryIndexedValue\">&lt;Policy&gt;&lt;Descriptor Staticness=\"Static, Instance\" AccessRightKinds=\"Private\" Description=\"private properties\"&gt;&lt;ElementKinds&gt;&lt;Kind Name=\"PROPERTY\" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"aaBb\" /&gt;&lt;/Policy&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/UserRules/=a4f433b8_002Dabcd_002D4e55_002Da08f_002D82e78cef0f0c/@EntryIndexedValue\">&lt;Policy&gt;&lt;Descriptor Staticness=\"Any\" AccessRightKinds=\"Any\" Description=\"Local constants\"&gt;&lt;ElementKinds&gt;&lt;Kind Name=\"LOCAL_CONSTANT\" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"aa_bb\" /&gt;&lt;/Policy&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/UserRules/=c873eafb_002Dd57f_002D481d_002D8c93_002D77f6863c2f88/@EntryIndexedValue\">&lt;Policy&gt;&lt;Descriptor Staticness=\"Static\" AccessRightKinds=\"Protected, ProtectedInternal, Internal, Public, PrivateProtected\" Description=\"Static readonly fields (not private)\"&gt;&lt;ElementKinds&gt;&lt;Kind Name=\"READONLY_FIELD\" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"AA_BB\" /&gt;&lt;/Policy&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/UserRules/=f9fce829_002De6f4_002D4cb2_002D80f1_002D5497c44f51df/@EntryIndexedValue\">&lt;Policy&gt;&lt;Descriptor Staticness=\"Static\" AccessRightKinds=\"Private\" Description=\"Static fields (private)\"&gt;&lt;ElementKinds&gt;&lt;Kind Name=\"FIELD\" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"aaBb\" /&gt;&lt;/Policy&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/CSharpNaming/UserRules/=fd562728_002Dc23d_002D417f_002Da19f_002D9d854247fbea/@EntryIndexedValue\">&lt;Policy&gt;&lt;Descriptor Staticness=\"Static, Instance\" AccessRightKinds=\"Protected, ProtectedInternal, Internal, Public\" Description=\"internal/protected/public properties\"&gt;&lt;ElementKinds&gt;&lt;Kind Name=\"PROPERTY\" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"AaBb\" /&gt;&lt;/Policy&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FBLOCK_005FSCOPE_005FCONSTANT/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"aaBb\" /&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FBLOCK_005FSCOPE_005FFUNCTION/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"aaBb\" /&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FBLOCK_005FSCOPE_005FVARIABLE/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"aaBb\" /&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FCLASS/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"aaBb\" /&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FCONSTRUCTOR/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"AaBb\" /&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FFUNCTION/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"aaBb\" /&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FGLOBAL_005FVARIABLE/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"AaBb\" /&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FLABEL/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"aaBb\" /&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FLOCAL_005FCONSTRUCTOR/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"AaBb\" /&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FLOCAL_005FVARIABLE/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"aaBb\" /&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FOBJECT_005FPROPERTY_005FOF_005FFUNCTION/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"aaBb\" /&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=JS_005FPARAMETER/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"aaBb\" /&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FCLASS/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"AaBb\" /&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FENUM/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"AaBb\" /&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FENUM_005FMEMBER/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"AaBb\" /&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FINTERFACE/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"I\" Suffix=\"\" Style=\"AaBb\" /&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FMIXED_005FENUM/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"AaBb\" /&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FMODULE/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"AaBb\" /&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FMODULE_005FEXPORTED/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"aaBb\" /&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FMODULE_005FLOCAL/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"aaBb\" /&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FPRIVATE_005FMEMBER_005FACCESSOR/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"aaBb\" /&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FPRIVATE_005FSTATIC_005FTYPE_005FFIELD/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"aaBb\" /&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FPRIVATE_005FTYPE_005FFIELD/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"aaBb\" /&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FPRIVATE_005FTYPE_005FMETHOD/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"aaBb\" /&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FPROTECTED_005FMEMBER_005FACCESSOR/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"aaBb\" /&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FPROTECTED_005FSTATIC_005FTYPE_005FFIELD/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"aaBb\" /&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FPROTECTED_005FTYPE_005FFIELD/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"aaBb\" /&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FPROTECTED_005FTYPE_005FMETHOD/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"aaBb\" /&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FPUBLIC_005FMEMBER_005FACCESSOR/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"aaBb\" /&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FPUBLIC_005FSTATIC_005FTYPE_005FFIELD/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"aaBb\" /&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FPUBLIC_005FTYPE_005FFIELD/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"aaBb\" /&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FPUBLIC_005FTYPE_005FMETHOD/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"aaBb\" /&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FTYPE_005FALIAS/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"AaBb\" /&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/JavaScriptNaming/UserRules/=TS_005FTYPE_005FPARAMETER/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"T\" Suffix=\"\" Style=\"AaBb\" /&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/WebNaming/UserRules/=ASP_005FFIELD/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"AaBb\" /&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/WebNaming/UserRules/=ASP_005FHTML_005FCONTROL/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"AaBb\" /&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/WebNaming/UserRules/=ASP_005FTAG_005FNAME/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"AaBb\" /&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/WebNaming/UserRules/=ASP_005FTAG_005FPREFIX/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"AaBb\" /&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/XamlNaming/UserRules/=NAMESPACE_005FALIAS/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"aaBb\" /&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/XamlNaming/UserRules/=XAML_005FFIELD/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"AaBb\" /&gt;</s:String>\n\t<s:String x:Key=\"/Default/CodeStyle/Naming/XamlNaming/UserRules/=XAML_005FRESOURCE/@EntryIndexedValue\">&lt;Policy Inspect=\"True\" Prefix=\"\" Suffix=\"\" Style=\"AaBb\" /&gt;</s:String>\n\t<s:String x:Key=\"/Default/CustomTools/CustomToolsData/@EntryValue\"></s:String>\n\t<s:Boolean x:Key=\"/Default/Environment/AutoImport2/=CSHARP/BlackLists/=Microsoft_002EExtensions_002ELogging_002E_002A/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/Environment/AutoImport2/=CSHARP/BlackLists/=Microsoft_002EToolkit_002EHighPerformance_002EBox_002A/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/Environment/AutoImport2/=CSHARP/BlackLists/=OpenTabletDriver_002EPlugin_002EDependencyInjection_002E_002A/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/Environment/AutoImport2/=CSHARP/BlackLists/=Realms_002ELogging_002ELogger/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/Environment/AutoImport2/=CSHARP/BlackLists/=System_002EComponentModel_002EComponent/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/Environment/AutoImport2/=CSHARP/BlackLists/=System_002EComponentModel_002EContainer/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/Environment/AutoImport2/=CSHARP/BlackLists/=System_002ENumerics_002E_002A/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/Environment/AutoImport2/=CSHARP/BlackLists/=System_002ESecurity_002ECryptography_002ERSA/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/Environment/AutoImport2/=CSHARP/BlackLists/=TagLib_002EMpeg4_002EBox/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EFeature_002EServices_002ECodeCleanup_002EFileHeader_002EFileHeaderSettingsMigrate/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EFeature_002EServices_002EDaemon_002ESettings_002EMigration_002ESwaWarningsModeSettingsMigrate/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpAttributeForSingleLineMethodUpgrade/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpKeepExistingMigration/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpRenamePlacementToArrangementMigration/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpUseContinuousIndentInsideBracesMigration/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EAddAccessorOwnerDeclarationBracesMigration/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EAlwaysTreatStructAsNotReorderableMigration/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002ECSharpPlaceAttributeOnSameLineMigration/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateThisQualifierSettings/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EPredefinedNamingRulesToUserRulesUpgrade/@EntryIndexedValue\">True</s:Boolean>\n\t<s:String x:Key=\"/Default/Environment/UnitTesting/NUnitProvider/SetCurrentDirectoryTo/@EntryValue\">TestFolder</s:String>\n\t<s:Boolean x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=28A2A5FC43E07C488A4BC7430879479E/@KeyIndexDefined\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=28A2A5FC43E07C488A4BC7430879479E/Applicability/=Live/@EntryIndexedValue\">True</s:Boolean>\n\t<s:String x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=28A2A5FC43E07C488A4BC7430879479E/Description/@EntryValue\">o!f – Object Initializer: Anchor&amp;Origin</s:String>\n\t<s:Boolean x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=28A2A5FC43E07C488A4BC7430879479E/Field/=anchor/@KeyIndexDefined\">True</s:Boolean>\n\t<s:String x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=28A2A5FC43E07C488A4BC7430879479E/Field/=anchor/Expression/@EntryValue\">constant(\"Centre\")</s:String>\n\t<s:Int64 x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=28A2A5FC43E07C488A4BC7430879479E/Field/=anchor/Order/@EntryValue\">0</s:Int64>\n\t<s:Boolean x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=28A2A5FC43E07C488A4BC7430879479E/Reformat/@EntryValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=28A2A5FC43E07C488A4BC7430879479E/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined\">True</s:Boolean>\n\t<s:String x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=28A2A5FC43E07C488A4BC7430879479E/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue\">2.0</s:String>\n\t<s:String x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=28A2A5FC43E07C488A4BC7430879479E/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue\">InCSharpFile</s:String>\n\t<s:String x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=28A2A5FC43E07C488A4BC7430879479E/Shortcut/@EntryValue\">ofao</s:String>\n\t<s:Boolean x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=28A2A5FC43E07C488A4BC7430879479E/ShortenQualifiedReferences/@EntryValue\">True</s:Boolean>\n\t<s:String x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=28A2A5FC43E07C488A4BC7430879479E/Text/@EntryValue\">Anchor = Anchor.$anchor$,\nOrigin = Anchor.$anchor$,</s:String>\n\t<s:Boolean x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=2A3ECBA387AF6D468F6ABDA35DED325A/@KeyIndexDefined\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=2A3ECBA387AF6D468F6ABDA35DED325A/Applicability/=Live/@EntryIndexedValue\">True</s:Boolean>\n\t<s:String x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=2A3ECBA387AF6D468F6ABDA35DED325A/Description/@EntryValue\">o!f – InternalChildren = []</s:String>\n\t<s:Boolean x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=2A3ECBA387AF6D468F6ABDA35DED325A/Reformat/@EntryValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=2A3ECBA387AF6D468F6ABDA35DED325A/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined\">True</s:Boolean>\n\t<s:String x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=2A3ECBA387AF6D468F6ABDA35DED325A/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue\">2.0</s:String>\n\t<s:String x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=2A3ECBA387AF6D468F6ABDA35DED325A/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue\">InCSharpFile</s:String>\n\t<s:String x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=2A3ECBA387AF6D468F6ABDA35DED325A/Shortcut/@EntryValue\">ofic</s:String>\n\t<s:Boolean x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=2A3ECBA387AF6D468F6ABDA35DED325A/ShortenQualifiedReferences/@EntryValue\">True</s:Boolean>\n\t<s:String x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=2A3ECBA387AF6D468F6ABDA35DED325A/Text/@EntryValue\">InternalChildren = new Drawable[]\n{\n\t$END$\n};</s:String>\n\t<s:Boolean x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=62B70E4DCA5E284A9E383E16C13789C1/@KeyIndexDefined\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=62B70E4DCA5E284A9E383E16C13789C1/Applicability/=Live/@EntryIndexedValue\">True</s:Boolean>\n\t<s:String x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=62B70E4DCA5E284A9E383E16C13789C1/Description/@EntryValue\">o!f – new GridContainer { .. }</s:String>\n\t<s:Boolean x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=62B70E4DCA5E284A9E383E16C13789C1/Reformat/@EntryValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=62B70E4DCA5E284A9E383E16C13789C1/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined\">True</s:Boolean>\n\t<s:String x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=62B70E4DCA5E284A9E383E16C13789C1/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue\">2.0</s:String>\n\t<s:String x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=62B70E4DCA5E284A9E383E16C13789C1/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue\">InCSharpFile</s:String>\n\t<s:String x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=62B70E4DCA5E284A9E383E16C13789C1/Shortcut/@EntryValue\">ofgc</s:String>\n\t<s:Boolean x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=62B70E4DCA5E284A9E383E16C13789C1/ShortenQualifiedReferences/@EntryValue\">True</s:Boolean>\n\t<s:String x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=62B70E4DCA5E284A9E383E16C13789C1/Text/@EntryValue\">new GridContainer\n{\n\tRelativeSizeAxes = Axes.Both,\n\tContent = new[]\n\t{\n\t\tnew Drawable[] { $END$ },\n\t\tnew Drawable[] { }\n\t}\n};</s:String>\n\t<s:Boolean x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=72BD3C3DCA42C84DA1E71F1D05A903C4/@KeyIndexDefined\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=72BD3C3DCA42C84DA1E71F1D05A903C4/Applicability/=Live/@EntryIndexedValue\">True</s:Boolean>\n\t<s:String x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=72BD3C3DCA42C84DA1E71F1D05A903C4/Description/@EntryValue\">o!f – new FillFlowContainer { .. }</s:String>\n\t<s:Boolean x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=72BD3C3DCA42C84DA1E71F1D05A903C4/Reformat/@EntryValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=72BD3C3DCA42C84DA1E71F1D05A903C4/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined\">True</s:Boolean>\n\t<s:String x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=72BD3C3DCA42C84DA1E71F1D05A903C4/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue\">2.0</s:String>\n\t<s:String x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=72BD3C3DCA42C84DA1E71F1D05A903C4/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue\">InCSharpFile</s:String>\n\t<s:String x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=72BD3C3DCA42C84DA1E71F1D05A903C4/Shortcut/@EntryValue\">offf</s:String>\n\t<s:Boolean x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=72BD3C3DCA42C84DA1E71F1D05A903C4/ShortenQualifiedReferences/@EntryValue\">True</s:Boolean>\n\t<s:String x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=72BD3C3DCA42C84DA1E71F1D05A903C4/Text/@EntryValue\">new FillFlowContainer\n{\n\tRelativeSizeAxes = Axes.Both,\n\tDirection = FillDirection.Vertical,\n\tChildren = new Drawable[]\n\t{\n\t\t$END$\n\t}\n},</s:String>\n\t<s:Boolean x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=750A3C67E083484FAEEA0ED2382181CC/@KeyIndexDefined\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=750A3C67E083484FAEEA0ED2382181CC/Applicability/=Live/@EntryIndexedValue\">True</s:Boolean>\n\t<s:String x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=750A3C67E083484FAEEA0ED2382181CC/Description/@EntryValue\">o!f – new Container { .. }</s:String>\n\t<s:Boolean x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=750A3C67E083484FAEEA0ED2382181CC/Reformat/@EntryValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=750A3C67E083484FAEEA0ED2382181CC/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined\">True</s:Boolean>\n\t<s:String x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=750A3C67E083484FAEEA0ED2382181CC/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue\">2.0</s:String>\n\t<s:String x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=750A3C67E083484FAEEA0ED2382181CC/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue\">InCSharpFile</s:String>\n\t<s:String x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=750A3C67E083484FAEEA0ED2382181CC/Shortcut/@EntryValue\">ofcont</s:String>\n\t<s:Boolean x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=750A3C67E083484FAEEA0ED2382181CC/ShortenQualifiedReferences/@EntryValue\">True</s:Boolean>\n\t<s:String x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=750A3C67E083484FAEEA0ED2382181CC/Text/@EntryValue\">new Container\n{\n\tRelativeSizeAxes = Axes.Both,\n\tChildren = new Drawable[]\n\t{\n\t\t$END$\n\t}\n},</s:String>\n\t<s:Boolean x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=78D9C2B1742FD449BD69CD18437E0C07/@KeyIndexDefined\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=78D9C2B1742FD449BD69CD18437E0C07/Applicability/=Live/@EntryIndexedValue\">True</s:Boolean>\n\t<s:String x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=78D9C2B1742FD449BD69CD18437E0C07/Description/@EntryValue\">o!f – BackgroundDependencyLoader load()</s:String>\n\t<s:Boolean x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=78D9C2B1742FD449BD69CD18437E0C07/Reformat/@EntryValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=78D9C2B1742FD449BD69CD18437E0C07/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined\">True</s:Boolean>\n\t<s:String x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=78D9C2B1742FD449BD69CD18437E0C07/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue\">2.0</s:String>\n\t<s:String x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=78D9C2B1742FD449BD69CD18437E0C07/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue\">InCSharpFile</s:String>\n\t<s:String x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=78D9C2B1742FD449BD69CD18437E0C07/Shortcut/@EntryValue\">ofbdl</s:String>\n\t<s:Boolean x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=78D9C2B1742FD449BD69CD18437E0C07/ShortenQualifiedReferences/@EntryValue\">True</s:Boolean>\n\t<s:String x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=78D9C2B1742FD449BD69CD18437E0C07/Text/@EntryValue\">[BackgroundDependencyLoader]\nprivate void load()\n{\n\t$END$\n}</s:String>\n\t<s:Boolean x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=CC879477D8841A4CBD724C2DCD249435/@KeyIndexDefined\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=CC879477D8841A4CBD724C2DCD249435/Applicability/=Live/@EntryIndexedValue\">True</s:Boolean>\n\t<s:String x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=CC879477D8841A4CBD724C2DCD249435/Description/@EntryValue\">o!f – new Box { .. }</s:String>\n\t<s:Boolean x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=CC879477D8841A4CBD724C2DCD249435/Reformat/@EntryValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=CC879477D8841A4CBD724C2DCD249435/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined\">True</s:Boolean>\n\t<s:String x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=CC879477D8841A4CBD724C2DCD249435/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue\">2.0</s:String>\n\t<s:String x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=CC879477D8841A4CBD724C2DCD249435/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue\">InCSharpFile</s:String>\n\t<s:String x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=CC879477D8841A4CBD724C2DCD249435/Shortcut/@EntryValue\">ofbox</s:String>\n\t<s:Boolean x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=CC879477D8841A4CBD724C2DCD249435/ShortenQualifiedReferences/@EntryValue\">True</s:Boolean>\n\t<s:String x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=CC879477D8841A4CBD724C2DCD249435/Text/@EntryValue\">new Box\n{\n\tColour = Color4.Black,\n\tRelativeSizeAxes = Axes.Both,\n},</s:String>\n\t<s:Boolean x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=F5B3CB743153774F99FB9FCA0FC744EE/@KeyIndexDefined\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=F5B3CB743153774F99FB9FCA0FC744EE/Applicability/=Live/@EntryIndexedValue\">True</s:Boolean>\n\t<s:String x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=F5B3CB743153774F99FB9FCA0FC744EE/Description/@EntryValue\">o!f – Children = []</s:String>\n\t<s:Boolean x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=F5B3CB743153774F99FB9FCA0FC744EE/Reformat/@EntryValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=F5B3CB743153774F99FB9FCA0FC744EE/Scope/=C3001E7C0DA78E4487072B7E050D86C5/@KeyIndexDefined\">True</s:Boolean>\n\t<s:String x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=F5B3CB743153774F99FB9FCA0FC744EE/Scope/=C3001E7C0DA78E4487072B7E050D86C5/CustomProperties/=minimumLanguageVersion/@EntryIndexedValue\">2.0</s:String>\n\t<s:String x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=F5B3CB743153774F99FB9FCA0FC744EE/Scope/=C3001E7C0DA78E4487072B7E050D86C5/Type/@EntryValue\">InCSharpFile</s:String>\n\t<s:String x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=F5B3CB743153774F99FB9FCA0FC744EE/Shortcut/@EntryValue\">ofc</s:String>\n\t<s:Boolean x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=F5B3CB743153774F99FB9FCA0FC744EE/ShortenQualifiedReferences/@EntryValue\">True</s:Boolean>\n\t<s:String x:Key=\"/Default/PatternsAndTemplates/LiveTemplates/Template/=F5B3CB743153774F99FB9FCA0FC744EE/Text/@EntryValue\">Children = new Drawable[]\n{\n\t$END$\n};</s:String>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=antiflow/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Beatmap/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=beatmaps/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Beatmapset/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=beatmap_0027s/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=bindable/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=bindables/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Catmull/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Clickthrough/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Closedness/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=crosshair/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Daycore/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Deserialise/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=deserialised/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Dimmable/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Disableable/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Drawables/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Failable/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=favourited/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Feuille/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=FFFFFF/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Gamefield/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=gameplay/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Geki/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=gomi/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=haku/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=hanabi/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Hant/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Hatsune/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Hashable/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Hitnormal/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=hitobject/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=hitobjects/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Hitsound/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Hitsounded/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Hitsounds/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Judgeable/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=kagayaku/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=kake/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=kanji/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=kareta/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Katu/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=kaze/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=keymods/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Kiai/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Kudosu/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Lazer/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Leaderboard/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Leaderboards/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=letterboxing/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=localisable/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=macbook/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Metadatas/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Migratable/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Miku/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Moji/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Naka/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Nightcore/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Noto/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Omni/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Overlined/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=pasokonn/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Pausable/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Pippidon/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Playfield/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=playfields/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Poolable/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Preclicked/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=pupu/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=purgeable/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Rearrangeable/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=refetch/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=refetched/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Refilter/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Reinstantiation/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=resampler/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Romaji/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=romajies/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Romajis/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=romanisable/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Romanisation/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Romanised/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=ruleset/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=rulesets/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=ruleset_0027s/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Scorable/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=seeya/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=sekaini/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Skinnable/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Snappable/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Soleily/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Spinnable/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=splitted/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Strongable/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=taikai/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=taikaii/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Taiko/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=undim/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=undownloadable/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Unescaping/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Unhover/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Unhovered/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Unplayed/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Unproxy/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Unranked/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=upppppdate/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=vocaloid/@EntryIndexedValue\">True</s:Boolean>\n\t<s:Boolean x:Key=\"/Default/UserDictionary/Words/=Zoomable/@EntryIndexedValue\">True</s:Boolean>\n</wpf:ResourceDictionary>\n"
  },
  {
    "path": "osu.licenseheader",
    "content": "﻿extensions: .cs\r\n// Copyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\n// See the LICENCE file in the repository root for full licence text.\r\n\r\nextensions:  .xml .config .xsd\r\n<!--\r\nCopyright (c) andy840119 <andy840119@gmail.com>. Licensed under the GPL Licence.\r\nSee the LICENCE file in the repository root for full licence text.\r\n-->"
  }
]