Funkin/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx

1647 lines
68 KiB
Haxe
Raw Normal View History

package funkin.ui.debug.charting.handlers;
import flixel.util.FlxTimer;
import funkin.data.song.importer.FNFLegacyData;
import funkin.data.song.importer.FNFLegacyImporter;
import funkin.data.song.SongData.SongCharacterData;
import funkin.data.song.SongData.SongChartData;
import funkin.data.song.SongData.SongMetadata;
import funkin.data.song.SongData.SongTimeChange;
import funkin.data.song.SongRegistry;
2022-11-25 20:48:05 -05:00
import funkin.input.Cursor;
import funkin.play.character.BaseCharacter;
import funkin.play.character.CharacterData;
import funkin.play.character.CharacterData.CharacterDataParser;
2023-02-28 21:06:09 -05:00
import funkin.play.song.Song;
import funkin.play.stage.StageData;
import funkin.ui.debug.charting.util.ChartEditorDropdowns;
import funkin.util.Constants;
2023-06-08 16:48:34 -04:00
import funkin.util.FileUtil;
import funkin.util.SerializerUtil;
import funkin.util.SortUtil;
import funkin.util.VersionUtil;
2023-02-28 21:06:09 -05:00
import haxe.io.Path;
2022-12-01 17:32:10 -05:00
import haxe.ui.components.Button;
import haxe.ui.components.DropDown;
import haxe.ui.components.Label;
2022-11-25 20:48:05 -05:00
import haxe.ui.components.Link;
2022-12-01 17:32:10 -05:00
import haxe.ui.components.NumberStepper;
import haxe.ui.components.Slider;
2022-12-01 17:32:10 -05:00
import haxe.ui.components.TextField;
import haxe.ui.containers.Box;
2022-11-24 19:09:47 -05:00
import haxe.ui.containers.dialogs.Dialog;
import haxe.ui.containers.dialogs.Dialog.DialogButton;
2022-12-01 17:32:10 -05:00
import haxe.ui.containers.dialogs.Dialogs;
import haxe.ui.containers.Form;
2022-11-24 19:09:47 -05:00
import haxe.ui.containers.VBox;
2023-02-28 21:06:09 -05:00
import haxe.ui.core.Component;
2022-12-01 17:32:10 -05:00
import haxe.ui.events.UIEvent;
import thx.semver.Version;
2022-12-01 17:32:10 -05:00
using Lambda;
2022-11-24 19:09:47 -05:00
2023-02-28 13:17:28 -05:00
/**
* Handles dialogs for the new Chart Editor.
*/
@:nullSafety
@:access(funkin.ui.debug.charting.ChartEditorState)
class ChartEditorDialogHandler
{
// Paths to HaxeUI layout files for each dialog.
2023-02-28 13:17:28 -05:00
static final CHART_EDITOR_DIALOG_ABOUT_LAYOUT:String = Paths.ui('chart-editor/dialogs/about');
static final CHART_EDITOR_DIALOG_WELCOME_LAYOUT:String = Paths.ui('chart-editor/dialogs/welcome');
static final CHART_EDITOR_DIALOG_UPLOAD_CHART_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-chart');
2023-02-28 13:17:28 -05:00
static final CHART_EDITOR_DIALOG_UPLOAD_INST_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-inst');
static final CHART_EDITOR_DIALOG_SONG_METADATA_LAYOUT:String = Paths.ui('chart-editor/dialogs/song-metadata');
static final CHART_EDITOR_DIALOG_UPLOAD_VOCALS_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-vocals');
static final CHART_EDITOR_DIALOG_UPLOAD_VOCALS_ENTRY_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-vocals-entry');
static final CHART_EDITOR_DIALOG_OPEN_CHART_PARTS_LAYOUT:String = Paths.ui('chart-editor/dialogs/open-chart-parts');
static final CHART_EDITOR_DIALOG_OPEN_CHART_PARTS_ENTRY_LAYOUT:String = Paths.ui('chart-editor/dialogs/open-chart-parts-entry');
2023-06-08 16:48:34 -04:00
static final CHART_EDITOR_DIALOG_IMPORT_CHART_LAYOUT:String = Paths.ui('chart-editor/dialogs/import-chart');
2023-02-28 13:17:28 -05:00
static final CHART_EDITOR_DIALOG_USER_GUIDE_LAYOUT:String = Paths.ui('chart-editor/dialogs/user-guide');
static final CHART_EDITOR_DIALOG_ADD_VARIATION_LAYOUT:String = Paths.ui('chart-editor/dialogs/add-variation');
static final CHART_EDITOR_DIALOG_ADD_DIFFICULTY_LAYOUT:String = Paths.ui('chart-editor/dialogs/add-difficulty');
/**
2023-02-28 21:06:09 -05:00
* Builds and opens a dialog giving brief credits for the chart editor.
* @param state The current chart editor state.
* @return The dialog that was opened.
*/
public static function openAboutDialog(state:ChartEditorState):Null<Dialog>
{
return openDialog(state, CHART_EDITOR_DIALOG_ABOUT_LAYOUT, true, true);
}
/**
* Builds and opens a dialog letting the user create a new chart, open a recent chart, or load from a template.
2023-02-28 21:06:09 -05:00
* @param state The current chart editor state.
* @param closable Whether the dialog can be closed by the user.
* @return The dialog that was opened.
*/
2023-08-31 18:47:23 -04:00
public static function openWelcomeDialog(state:ChartEditorState, closable:Bool = true):Null<Dialog>
{
2023-08-31 18:47:23 -04:00
var dialog:Null<Dialog> = openDialog(state, CHART_EDITOR_DIALOG_WELCOME_LAYOUT, true, closable);
if (dialog == null) throw 'Could not locate Welcome dialog';
2023-10-22 15:43:39 -04:00
state.isHaxeUIDialogOpen = true;
dialog.onDialogClosed = function(_event) {
2023-10-22 15:43:39 -04:00
state.isHaxeUIDialogOpen = false;
// Called when the Welcome dialog is closed while it is closable.
state.stopWelcomeMusic();
}
2023-10-22 15:43:39 -04:00
#if sys
var splashRecentContainer:Null<VBox> = dialog.findComponent('splashRecentContainer', VBox);
if (splashRecentContainer == null) throw 'Could not locate splashRecentContainer in Welcome dialog';
for (chartPath in state.previousWorkingFilePaths)
{
2023-10-23 12:22:29 -04:00
if (chartPath == null) continue;
var linkRecentChart:Link = new Link();
2023-10-22 15:43:39 -04:00
linkRecentChart.text = chartPath;
linkRecentChart.onClick = function(_event) {
dialog.hideDialog(DialogButton.CANCEL);
state.stopWelcomeMusic();
// Load chart from file
2023-10-24 15:50:02 -04:00
var result:Null<Array<String>> = ChartEditorImportExportHandler.loadFromFNFCPath(state, chartPath);
if (result != null)
{
if (result.length == 0)
{
// No warnings.
state.success('Loaded Chart', 'Loaded chart (${chartPath.toString()})');
}
else
{
// One or more warnings.
state.warning('Loaded Chart', 'Loaded chart (${chartPath.toString()})\n${result.join("\n")}');
}
2023-10-24 15:50:02 -04:00
}
else
{
state.error('Failed to Load Chart', 'Failed to load chart (${chartPath.toString()})');
2023-10-24 15:50:02 -04:00
}
2023-10-22 15:43:39 -04:00
}
if (!FileUtil.doesFileExist(chartPath))
{
trace('Previously loaded chart file (${chartPath}) does not exist, disabling link...');
linkRecentChart.disabled = true;
}
splashRecentContainer.addComponent(linkRecentChart);
}
#else
var splashRecentContainer:Null<VBox> = dialog.findComponent('splashRecentContainer', VBox);
if (splashRecentContainer == null) throw 'Could not locate splashRecentContainer in Welcome dialog';
var webLoadLabel:Label = new Label();
webLoadLabel.text = 'Click the button below to load a chart file (.fnfc) from your computer.';
splashRecentContainer.add(webLoadLabel);
#end
// Create New Song "Easy/Normal/Hard"
var linkCreateBasic:Null<Link> = dialog.findComponent('splashCreateFromSongBasicOnly', Link);
if (linkCreateBasic == null) throw 'Could not locate splashCreateFromSongBasicOnly link in Welcome dialog';
2023-02-28 21:06:09 -05:00
linkCreateBasic.onClick = function(_event) {
// Hide the welcome dialog
dialog.hideDialog(DialogButton.CANCEL);
2023-09-29 22:29:32 -04:00
state.stopWelcomeMusic();
2023-02-28 21:06:09 -05:00
//
// Create Song Wizard
//
openCreateSongWizardBasicOnly(state, false);
}
// Create New Song "Erect/Nightmare"
var linkCreateErect:Null<Link> = dialog.findComponent('splashCreateFromSongErectOnly', Link);
if (linkCreateErect == null) throw 'Could not locate splashCreateFromSongErectOnly link in Welcome dialog';
linkCreateErect.onClick = function(_event) {
// Hide the welcome dialog
dialog.hideDialog(DialogButton.CANCEL);
//
// Create Song Wizard
//
openCreateSongWizardErectOnly(state, false);
}
// Create New Song "Easy/Normal/Hard/Erect/Nightmare"
var linkCreateErect:Null<Link> = dialog.findComponent('splashCreateFromSongBasicErect', Link);
if (linkCreateErect == null) throw 'Could not locate splashCreateFromSongBasicErect link in Welcome dialog';
linkCreateErect.onClick = function(_event) {
// Hide the welcome dialog
dialog.hideDialog(DialogButton.CANCEL);
//
// Create Song Wizard
//
openCreateSongWizardBasicErect(state, false);
2023-06-08 16:48:34 -04:00
}
2023-02-28 21:06:09 -05:00
2023-08-31 18:47:23 -04:00
var linkImportChartLegacy:Null<Link> = dialog.findComponent('splashImportChartLegacy', Link);
if (linkImportChartLegacy == null) throw 'Could not locate splashImportChartLegacy link in Welcome dialog';
2023-06-08 16:48:34 -04:00
linkImportChartLegacy.onClick = function(_event) {
// Hide the welcome dialog
dialog.hideDialog(DialogButton.CANCEL);
2023-09-29 22:29:32 -04:00
state.stopWelcomeMusic();
2023-06-08 16:48:34 -04:00
// Open the "Import Chart" dialog
openImportChartWizard(state, 'legacy', false);
};
2023-08-31 18:47:23 -04:00
var buttonBrowse:Null<Button> = dialog.findComponent('splashBrowse', Button);
if (buttonBrowse == null) throw 'Could not locate splashBrowse button in Welcome dialog';
2023-06-08 16:48:34 -04:00
buttonBrowse.onClick = function(_event) {
// Hide the welcome dialog
dialog.hideDialog(DialogButton.CANCEL);
2023-09-29 22:29:32 -04:00
state.stopWelcomeMusic();
2023-06-08 16:48:34 -04:00
// Open the "Open Chart" dialog
openBrowseFNFC(state, false);
}
2023-08-31 18:47:23 -04:00
var splashTemplateContainer:Null<VBox> = dialog.findComponent('splashTemplateContainer', VBox);
if (splashTemplateContainer == null) throw 'Could not locate splashTemplateContainer in Welcome dialog';
var songList:Array<String> = SongRegistry.instance.listEntryIds();
songList.sort(SortUtil.alphabetically);
for (targetSongId in songList)
{
var songData:Null<Song> = SongRegistry.instance.fetchEntry(targetSongId);
if (songData == null) continue;
var songName:Null<String> = songData.getDifficulty('normal')?.songName;
if (songName == null) songName = songData.getDifficulty()?.songName;
if (songName == null) // Still null?
{
trace('[WARN] Could not fetch song name for ${targetSongId}');
2023-08-31 18:47:23 -04:00
continue;
}
var linkTemplateSong:Link = new Link();
linkTemplateSong.text = songName;
2023-02-28 21:06:09 -05:00
linkTemplateSong.onClick = function(_event) {
dialog.hideDialog(DialogButton.CANCEL);
2023-09-29 22:29:32 -04:00
state.stopWelcomeMusic();
// Load song from template
state.loadSongAsTemplate(targetSongId);
}
splashTemplateContainer.addComponent(linkTemplateSong);
}
2023-09-29 22:29:32 -04:00
state.fadeInWelcomeMusic();
return dialog;
}
2023-11-21 13:31:02 -05:00
/**
* [Description]
* @param state
* @return Null<Dialog>
*/
public static function openBackupAvailableDialog(state:ChartEditorState):Null<Dialog>
{
var dialog:Null<Dialog> = openDialog(state, CHART_EDITOR_DIALOG_BACKUP_AVAILABLE_LAYOUT, true, true);
if (dialog == null) throw 'Could not locate Backup Available dialog';
state.isHaxeUIDialogOpen = true;
var buttonCancel:Null<Button> = dialog.findComponent('dialogCancel', Button);
if (buttonCancel == null) throw 'Could not locate dialogCancel button in Backup Available dialog';
buttonCancel.onClick = function(_event) {
dialog.hideDialog(DialogButton.CANCEL);
}
var buttonGoToFolder:Null<Button> = dialog.findComponent('buttonGoToFolder', Button);
if (buttonGoToFolder == null) throw 'Could not locate buttonGoToFolder button in Backup Available dialog';
buttonGoToFolder.onClick = function(_event) {
dialog.hideDialog(DialogButton.CANCEL);
state.openBackupFolder();
}
var buttonOpenBackup:Null<Button> = dialog.findComponent('buttonOpenBackup', Button);
if (buttonOpenBackup == null) throw 'Could not locate buttonOpenBackup button in Backup Available dialog';
buttonOpenBackup.onClick = function(_event) {
dialog.hideDialog(DialogButton.CANCEL);
state.openBackup();
}
}
public static function openBrowseFNFC(state:ChartEditorState, closable:Bool):Null<Dialog>
{
var dialog:Null<Dialog> = openDialog(state, CHART_EDITOR_DIALOG_UPLOAD_CHART_LAYOUT, true, closable);
if (dialog == null) throw 'Could not locate Upload Chart dialog';
dialog.onDialogClosed = function(_event) {
2023-10-22 15:43:39 -04:00
state.isHaxeUIDialogOpen = false;
if (_event.button == DialogButton.APPLY)
{
// Simply let the dialog close.
}
else
{
// User cancelled the wizard! Back to the welcome dialog.
openWelcomeDialog(state);
}
};
var buttonCancel:Null<Button> = dialog.findComponent('dialogCancel', Button);
if (buttonCancel == null) throw 'Could not locate dialogCancel button in Upload Chart dialog';
2023-10-22 15:43:39 -04:00
state.isHaxeUIDialogOpen = true;
buttonCancel.onClick = function(_event) {
dialog.hideDialog(DialogButton.CANCEL);
}
var chartBox:Null<Box> = dialog.findComponent('chartBox', Box);
if (chartBox == null) throw 'Could not locate chartBox in Upload Chart dialog';
chartBox.onMouseOver = function(_event) {
chartBox.swapClass('upload-bg', 'upload-bg-hover');
Cursor.cursorMode = Pointer;
}
chartBox.onMouseOut = function(_event) {
chartBox.swapClass('upload-bg-hover', 'upload-bg');
Cursor.cursorMode = Default;
}
var onDropFile:String->Void;
chartBox.onClick = function(_event) {
Dialogs.openBinaryFile('Open Chart', [
{label: 'Friday Night Funkin\' Chart (.fnfc)', extension: 'fnfc'}], function(selectedFile:SelectedFileInfo) {
if (selectedFile != null && selectedFile.bytes != null)
{
try
{
2023-10-24 15:50:02 -04:00
var result:Null<Array<String>> = ChartEditorImportExportHandler.loadFromFNFC(state, selectedFile.bytes);
if (result != null)
{
if (result.length == 0)
{
state.success('Loaded Chart', 'Loaded chart (${selectedFile.name})');
}
else
{
state.warning('Loaded Chart', 'Loaded chart (${selectedFile.name})\n${result.join("\n")}');
}
if (selectedFile.fullPath != null) state.currentWorkingFilePath = selectedFile.fullPath;
dialog.hideDialog(DialogButton.APPLY);
removeDropHandler(onDropFile);
}
}
catch (err)
{
state.error('Failed to Load Chart', 'Failed to load chart (${selectedFile.name}): ${err}');
}
}
});
}
onDropFile = function(pathStr:String) {
var path:Path = new Path(pathStr);
trace('Dropped file (${path})');
try
{
2023-10-24 15:50:02 -04:00
var result:Null<Array<String>> = ChartEditorImportExportHandler.loadFromFNFCPath(state, path.toString());
if (result != null)
{
if (result.length == 0)
{
state.success('Loaded Chart', 'Loaded chart (${path.file}.${path.ext})');
}
else
{
state.warning('Loaded Chart', 'Loaded chart (${path.file}.${path.ext})\n${result.join("\n")}');
}
dialog.hideDialog(DialogButton.APPLY);
removeDropHandler(onDropFile);
}
2023-10-24 15:50:02 -04:00
else
{
state.error('Failed to Load Chart', 'Failed to load chart (${path.file}.${path.ext})');
2023-10-24 15:50:02 -04:00
}
}
catch (err)
{
state.error('Failed to Load Chart', 'Failed to load chart (${path.file}.${path.ext})');
}
};
addDropHandler(chartBox, onDropFile);
return dialog;
}
2023-06-08 16:48:34 -04:00
/**
* Open the wizard for opening an existing chart from individual files.
* @param state
* @param closable
*/
public static function openBrowseWizard(state:ChartEditorState, closable:Bool):Void
{
// Open the "Open Chart" wizard
// Step 1. Open Chart
var openChartDialog:Dialog = openChartDialog(state);
openChartDialog.onDialogClosed = function(_event) {
state.isHaxeUIDialogOpen = false;
if (_event.button == DialogButton.APPLY)
{
// Step 2. Upload instrumental
var uploadInstDialog:Dialog = openUploadInstDialog(state, closable);
uploadInstDialog.onDialogClosed = function(_event) {
state.isHaxeUIDialogOpen = false;
if (_event.button == DialogButton.APPLY)
{
// Step 3. Upload Vocals
// NOTE: Uploading vocals is optional, so we don't need to check if the user cancelled the wizard.
var uploadVocalsDialog:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog
uploadVocalsDialog.onDialogClosed = function(_event) {
state.isHaxeUIDialogOpen = false;
2023-10-24 15:50:02 -04:00
state.currentWorkingFilePath = null; // Built from parts, so no .fnfc to save to.
state.switchToCurrentInstrumental();
2023-06-08 16:48:34 -04:00
state.postLoadInstrumental();
}
}
else
{
// User cancelled the wizard! Back to the welcome dialog.
openWelcomeDialog(state);
}
};
}
else
{
// User cancelled the wizard! Back to the welcome dialog.
openWelcomeDialog(state);
}
};
}
public static function openImportChartWizard(state:ChartEditorState, format:String, closable:Bool):Void
{
// Open the "Open Chart" wizard
// Step 1. Open Chart
2023-08-31 18:47:23 -04:00
var openChartDialog:Null<Dialog> = openImportChartDialog(state, format);
if (openChartDialog == null) throw 'Could not locate Import Chart dialog';
2023-06-08 16:48:34 -04:00
openChartDialog.onDialogClosed = function(_event) {
state.isHaxeUIDialogOpen = false;
if (_event.button == DialogButton.APPLY)
{
// Step 2. Upload instrumental
var uploadInstDialog:Dialog = openUploadInstDialog(state, closable);
uploadInstDialog.onDialogClosed = function(_event) {
state.isHaxeUIDialogOpen = false;
if (_event.button == DialogButton.APPLY)
{
// Step 3. Upload Vocals
// NOTE: Uploading vocals is optional, so we don't need to check if the user cancelled the wizard.
var uploadVocalsDialog:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog
uploadVocalsDialog.onDialogClosed = function(_event) {
state.isHaxeUIDialogOpen = false;
2023-10-24 15:50:02 -04:00
state.currentWorkingFilePath = null; // New file, so no path.
state.switchToCurrentInstrumental();
2023-06-08 16:48:34 -04:00
state.postLoadInstrumental();
}
}
else
{
// User cancelled the wizard! Back to the welcome dialog.
openWelcomeDialog(state);
}
};
}
else
{
// User cancelled the wizard! Back to the welcome dialog.
openWelcomeDialog(state);
}
};
}
public static function openCreateSongWizardBasicOnly(state:ChartEditorState, closable:Bool):Void
2023-06-08 16:48:34 -04:00
{
// Step 1. Song Metadata
var songMetadataDialog:Dialog = openSongMetadataDialog(state, false, Constants.DEFAULT_VARIATION);
songMetadataDialog.onDialogClosed = function(_event) {
2023-06-08 16:48:34 -04:00
state.isHaxeUIDialogOpen = false;
if (_event.button == DialogButton.APPLY)
{
// Step 2. Upload Instrumental
var uploadInstDialog:Dialog = openUploadInstDialog(state, closable);
uploadInstDialog.onDialogClosed = function(_event) {
2023-06-08 16:48:34 -04:00
state.isHaxeUIDialogOpen = false;
if (_event.button == DialogButton.APPLY)
{
// Step 3. Upload Vocals
// NOTE: Uploading vocals is optional, so we don't need to check if the user cancelled the wizard.
var uploadVocalsDialog:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog
uploadVocalsDialog.onDialogClosed = function(_event) {
state.isHaxeUIDialogOpen = false;
2023-10-23 12:22:29 -04:00
state.currentWorkingFilePath = null; // New file, so no path.
state.switchToCurrentInstrumental();
state.postLoadInstrumental();
}
2023-06-08 16:48:34 -04:00
}
else
{
// User cancelled the wizard at Step 2! Back to the welcome dialog.
2023-06-08 16:48:34 -04:00
openWelcomeDialog(state);
}
};
}
else
{
// User cancelled the wizard at Step 1! Back to the welcome dialog.
openWelcomeDialog(state);
}
};
}
public static function openCreateSongWizardErectOnly(state:ChartEditorState, closable:Bool):Void
{
// Step 1. Song Metadata
var songMetadataDialog:Dialog = openSongMetadataDialog(state, true, Constants.DEFAULT_VARIATION);
songMetadataDialog.onDialogClosed = function(_event) {
state.isHaxeUIDialogOpen = false;
if (_event.button == DialogButton.APPLY)
{
// Step 2. Upload Instrumental
var uploadInstDialog:Dialog = openUploadInstDialog(state, closable);
uploadInstDialog.onDialogClosed = function(_event) {
state.isHaxeUIDialogOpen = false;
if (_event.button == DialogButton.APPLY)
{
// Step 3. Upload Vocals
// NOTE: Uploading vocals is optional, so we don't need to check if the user cancelled the wizard.
var uploadVocalsDialog:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog
uploadVocalsDialog.onDialogClosed = function(_event) {
state.isHaxeUIDialogOpen = false;
state.currentWorkingFilePath = null; // New file, so no path.
state.switchToCurrentInstrumental();
state.postLoadInstrumental();
}
}
else
{
// User cancelled the wizard at Step 2! Back to the welcome dialog.
openWelcomeDialog(state);
}
};
}
else
{
// User cancelled the wizard at Step 1! Back to the welcome dialog.
openWelcomeDialog(state);
}
};
}
public static function openCreateSongWizardBasicErect(state:ChartEditorState, closable:Bool):Void
{
// Step 1. Song Metadata
var songMetadataDialog:Dialog = openSongMetadataDialog(state, false, Constants.DEFAULT_VARIATION);
songMetadataDialog.onDialogClosed = function(_event) {
state.isHaxeUIDialogOpen = false;
if (_event.button == DialogButton.APPLY)
{
// Step 2. Upload Instrumental
var uploadInstDialog:Dialog = openUploadInstDialog(state, closable);
uploadInstDialog.onDialogClosed = function(_event) {
state.isHaxeUIDialogOpen = false;
if (_event.button == DialogButton.APPLY)
{
// Step 3. Upload Vocals
// NOTE: Uploading vocals is optional, so we don't need to check if the user cancelled the wizard.
var uploadVocalsDialog:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog
uploadVocalsDialog.onDialogClosed = function(_event) {
state.switchToCurrentInstrumental();
// Step 4. Song Metadata (Erect)
var songMetadataDialogErect:Dialog = openSongMetadataDialog(state, true, 'erect');
songMetadataDialogErect.onDialogClosed = function(_event) {
state.isHaxeUIDialogOpen = false;
if (_event.button == DialogButton.APPLY)
{
// Switch to the Erect variation so uploading the instrumental applies properly.
state.selectedVariation = 'erect';
// Step 5. Upload Instrumental (Erect)
var uploadInstDialogErect:Dialog = openUploadInstDialog(state, closable);
uploadInstDialogErect.onDialogClosed = function(_event) {
state.isHaxeUIDialogOpen = false;
if (_event.button == DialogButton.APPLY)
{
// Step 6. Upload Vocals (Erect)
// NOTE: Uploading vocals is optional, so we don't need to check if the user cancelled the wizard.
var uploadVocalsDialogErect:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog
uploadVocalsDialogErect.onDialogClosed = function(_event) {
state.isHaxeUIDialogOpen = false;
2023-10-23 12:22:29 -04:00
state.currentWorkingFilePath = null; // New file, so no path.
state.switchToCurrentInstrumental();
state.postLoadInstrumental();
}
}
else
{
// User cancelled the wizard at Step 5! Back to the welcome dialog.
openWelcomeDialog(state);
}
};
}
else
{
// User cancelled the wizard at Step 4! Back to the welcome dialog.
openWelcomeDialog(state);
}
}
}
}
else
{
// User cancelled the wizard at Step 2! Back to the welcome dialog.
openWelcomeDialog(state);
}
};
}
else
{
// User cancelled the wizard at Step 1! Back to the welcome dialog.
2023-06-08 16:48:34 -04:00
openWelcomeDialog(state);
}
};
}
2023-02-28 21:06:09 -05:00
/**
* Builds and opens a dialog where the user uploads an instrumental for the current song.
* @param state The current chart editor state.
* @param closable Whether the dialog can be closed by the user.
* @return The dialog that was opened.
*/
Unit Tests: Coverage Reporting and Github Actions Integration (#131) * Initial test suite * Fix some build warnings * Implemented working unit tests with coverage * Reduced some warnings * Fix a mac-specific issue * Add 2 additional unit test classes. * Multiple new unit tests * Some fixins * Remove auto-generated file * WIP on hiding ignored tests * Added list of debug hotkeys * Remove old website * Remove empty file * Add more unit tests * Fix bug where arrows would nudge BF * Fix bug where ctrl/alt would flash capsules * Fixed bug where bf-old easter egg broke * Remove duplicate lines * More test-related stuff * Some code cleanup * Add mocking and a test assets folder * More TESTS! * Update Hmm... * Update artist on Monster * More minor fixes to individual functions * 1.38% unit test coverage! * Even more tests? :O * More unit test work * Rework migration for BaseRegistry * gameover fix * Fix an issue with Lime * Fix issues with version parsing on data files * 100 total unit tests! * Added even MORE unit tests! * Additional test tweaks :3 * Fixed tests on windows by updating libraries. * A bunch of smaller syntax tweaks. * New crash handler catches and logs critical errors! * Chart editor now has null safety enabled. * Null safety on all tests * New Level data test * Generate proper code coverage reports! * Disable null safety on ChartEditorState for unit testing * Update openfl to use latest fixes for crash reporting * Added unit test to Github Workflow * Updated unit tests to compile with null safety enabled by inlining assertions. * Added coverage gutters as a recommended extension * Impreovements to tests involving exceptions * Disable a few incomplete tests. * Add scripts for building unit coverage reports on linux --------- Co-authored-by: Cameron Taylor <cameron.taylor.ninja@gmail.com>
2023-08-30 18:31:59 -04:00
@:haxe.warning("-WVarInit") // Hide the warning about the onDropFile handler.
public static function openUploadInstDialog(state:ChartEditorState, closable:Bool = true):Dialog
{
2023-08-31 18:47:23 -04:00
var dialog:Null<Dialog> = openDialog(state, CHART_EDITOR_DIALOG_UPLOAD_INST_LAYOUT, true, closable);
if (dialog == null) throw 'Could not locate Upload Instrumental dialog';
2023-08-31 18:47:23 -04:00
var buttonCancel:Null<Button> = dialog.findComponent('dialogCancel', Button);
if (buttonCancel == null) throw 'Could not locate dialogCancel button in Upload Instrumental dialog';
2023-02-28 21:06:09 -05:00
buttonCancel.onClick = function(_event) {
dialog.hideDialog(DialogButton.CANCEL);
}
2023-08-31 18:47:23 -04:00
var instrumentalBox:Null<Box> = dialog.findComponent('instrumentalBox', Box);
if (instrumentalBox == null) throw 'Could not locate instrumentalBox in Upload Instrumental dialog';
2023-02-28 21:06:09 -05:00
instrumentalBox.onMouseOver = function(_event) {
instrumentalBox.swapClass('upload-bg', 'upload-bg-hover');
Cursor.cursorMode = Pointer;
}
2023-02-28 21:06:09 -05:00
instrumentalBox.onMouseOut = function(_event) {
instrumentalBox.swapClass('upload-bg-hover', 'upload-bg');
Cursor.cursorMode = Default;
}
var instId:String = state.currentInstrumentalId;
var onDropFile:String->Void;
2023-02-28 21:06:09 -05:00
instrumentalBox.onClick = function(_event) {
Dialogs.openBinaryFile('Open Instrumental', [
{label: 'Audio File (.ogg)', extension: 'ogg'}], function(selectedFile:SelectedFileInfo) {
2023-08-31 18:47:23 -04:00
if (selectedFile != null && selectedFile.bytes != null)
2023-02-28 13:17:28 -05:00
{
if (state.loadInstFromBytes(selectedFile.bytes, instId))
2023-02-28 21:06:09 -05:00
{
state.success('Loaded Instrumental', 'Loaded instrumental track (${selectedFile.name}) for variation (${state.selectedVariation})');
2023-02-28 21:06:09 -05:00
2023-10-18 01:02:10 -04:00
state.switchToCurrentInstrumental();
2023-02-28 21:06:09 -05:00
dialog.hideDialog(DialogButton.APPLY);
removeDropHandler(onDropFile);
}
else
{
state.error('Failed to Load Instrumental', 'Failed to load instrumental track (${selectedFile.name}) for variation (${state.selectedVariation})');
2023-02-28 21:06:09 -05:00
}
2023-02-28 13:17:28 -05:00
}
});
}
2023-02-28 21:06:09 -05:00
onDropFile = function(pathStr:String) {
var path:Path = new Path(pathStr);
trace('Dropped file (${path})');
if (state.loadInstFromPath(path, instId))
2023-02-28 21:06:09 -05:00
{
// Tell the user the load was successful.
state.success('Loaded Instrumental', 'Loaded instrumental track (${path.file}.${path.ext}) for variation (${state.selectedVariation})');
2023-02-28 21:06:09 -05:00
2023-10-18 01:02:10 -04:00
state.switchToCurrentInstrumental();
2023-02-28 21:06:09 -05:00
dialog.hideDialog(DialogButton.APPLY);
removeDropHandler(onDropFile);
}
else
{
2023-08-31 18:47:23 -04:00
var message:String = if (!ChartEditorState.SUPPORTED_MUSIC_FORMATS.contains(path.ext ?? ''))
2023-06-08 16:48:34 -04:00
{
'File format (${path.ext}) not supported for instrumental track (${path.file}.${path.ext})';
}
else
{
'Failed to load instrumental track (${path.file}.${path.ext}) for variation (${state.selectedVariation})';
2023-06-08 16:48:34 -04:00
}
2023-02-28 21:06:09 -05:00
// Tell the user the load was successful.
state.error('Failed to Load Instrumental', message);
2023-02-28 21:06:09 -05:00
}
};
2023-02-28 13:17:28 -05:00
addDropHandler(instrumentalBox, onDropFile);
return dialog;
}
2023-02-28 13:17:28 -05:00
/**
* Opens the dialog in the wizard where the user can set song metadata like name and artist and BPM.
* @param state The ChartEditorState instance.
* @return The dialog to open.
*/
@:haxe.warning("-WVarInit")
public static function openSongMetadataDialog(state:ChartEditorState, erect:Bool, targetVariation:String):Dialog
{
2023-08-31 18:47:23 -04:00
var dialog:Null<Dialog> = openDialog(state, CHART_EDITOR_DIALOG_SONG_METADATA_LAYOUT, true, false);
if (dialog == null) throw 'Could not locate Song Metadata dialog';
2023-02-28 21:06:09 -05:00
if (targetVariation != Constants.DEFAULT_VARIATION)
{
2023-10-15 00:22:25 -04:00
dialog.title = 'New Chart - Provide Song Metadata (${targetVariation.toTitleCase()})';
}
2023-08-31 18:47:23 -04:00
var buttonCancel:Null<Button> = dialog.findComponent('dialogCancel', Button);
if (buttonCancel == null) throw 'Could not locate dialogCancel button in Song Metadata dialog';
2023-10-22 15:43:39 -04:00
state.isHaxeUIDialogOpen = true;
2023-02-28 21:06:09 -05:00
buttonCancel.onClick = function(_event) {
2023-10-22 15:43:39 -04:00
state.isHaxeUIDialogOpen = false;
2023-02-28 21:06:09 -05:00
dialog.hideDialog(DialogButton.CANCEL);
}
var newSongMetadata:SongMetadata = new SongMetadata('', '', Constants.DEFAULT_VARIATION);
newSongMetadata.variation = targetVariation;
newSongMetadata.playData.difficulties = (erect) ? ['erect', 'nightmare'] : ['easy', 'normal', 'hard'];
2023-10-18 01:02:10 -04:00
var inputSongName:Null<TextField> = dialog.findComponent('inputSongName', TextField);
if (inputSongName == null) throw 'Could not locate inputSongName TextField in Song Metadata dialog';
inputSongName.onChange = function(event:UIEvent) {
2023-02-28 13:17:28 -05:00
var valid:Bool = event.target.text != null && event.target.text != '';
if (valid)
{
inputSongName.removeClass('invalid-value');
newSongMetadata.songName = event.target.text;
}
else
{
newSongMetadata.songName = "";
}
};
inputSongName.text = "";
var inputSongArtist:Null<TextField> = dialog.findComponent('inputSongArtist', TextField);
if (inputSongArtist == null) throw 'Could not locate inputSongArtist TextField in Song Metadata dialog';
inputSongArtist.onChange = function(event:UIEvent) {
2023-02-28 13:17:28 -05:00
var valid:Bool = event.target.text != null && event.target.text != '';
if (valid)
{
inputSongArtist.removeClass('invalid-value');
newSongMetadata.artist = event.target.text;
}
else
{
newSongMetadata.artist = "";
}
};
inputSongArtist.text = "";
var inputStage:Null<DropDown> = dialog.findComponent('inputStage', DropDown);
if (inputStage == null) throw 'Could not locate inputStage DropDown in Song Metadata dialog';
inputStage.onChange = function(event:UIEvent) {
2023-02-28 21:06:09 -05:00
if (event.data == null && event.data.id == null) return;
newSongMetadata.playData.stage = event.data.id;
};
var startingValueStage = ChartEditorDropdowns.populateDropdownWithStages(inputStage, newSongMetadata.playData.stage);
inputStage.value = startingValueStage;
var inputNoteStyle:Null<DropDown> = dialog.findComponent('inputNoteStyle', DropDown);
if (inputNoteStyle == null) throw 'Could not locate inputNoteStyle DropDown in Song Metadata dialog';
inputNoteStyle.onChange = function(event:UIEvent) {
if (event.data.id == null) return;
newSongMetadata.playData.noteStyle = event.data.id;
};
var startingValueNoteStyle = ChartEditorDropdowns.populateDropdownWithNoteStyles(inputNoteStyle, newSongMetadata.playData.noteStyle);
inputNoteStyle.value = startingValueNoteStyle;
var inputCharacterPlayer:Null<DropDown> = dialog.findComponent('inputCharacterPlayer', DropDown);
if (inputCharacterPlayer == null) throw 'ChartEditorToolboxHandler.buildToolboxMetadataLayout() - Could not find inputCharacterPlayer component.';
inputCharacterPlayer.onChange = function(event:UIEvent) {
if (event.data?.id == null) return;
newSongMetadata.playData.characters.player = event.data.id;
};
var startingValuePlayer = ChartEditorDropdowns.populateDropdownWithCharacters(inputCharacterPlayer, CharacterType.BF,
newSongMetadata.playData.characters.player);
inputCharacterPlayer.value = startingValuePlayer;
var inputCharacterOpponent:Null<DropDown> = dialog.findComponent('inputCharacterOpponent', DropDown);
if (inputCharacterOpponent == null) throw 'ChartEditorToolboxHandler.buildToolboxMetadataLayout() - Could not find inputCharacterOpponent component.';
inputCharacterOpponent.onChange = function(event:UIEvent) {
if (event.data?.id == null) return;
newSongMetadata.playData.characters.opponent = event.data.id;
};
var startingValueOpponent = ChartEditorDropdowns.populateDropdownWithCharacters(inputCharacterOpponent, CharacterType.DAD,
newSongMetadata.playData.characters.opponent);
inputCharacterOpponent.value = startingValueOpponent;
var inputCharacterGirlfriend:Null<DropDown> = dialog.findComponent('inputCharacterGirlfriend', DropDown);
if (inputCharacterGirlfriend == null) throw 'ChartEditorToolboxHandler.buildToolboxMetadataLayout() - Could not find inputCharacterGirlfriend component.';
inputCharacterGirlfriend.onChange = function(event:UIEvent) {
if (event.data?.id == null) return;
newSongMetadata.playData.characters.girlfriend = event.data.id == "none" ? "" : event.data.id;
};
var startingValueGirlfriend = ChartEditorDropdowns.populateDropdownWithCharacters(inputCharacterGirlfriend, CharacterType.GF,
newSongMetadata.playData.characters.girlfriend);
inputCharacterGirlfriend.value = startingValueGirlfriend;
2023-08-31 18:47:23 -04:00
var dialogBPM:Null<NumberStepper> = dialog.findComponent('dialogBPM', NumberStepper);
if (dialogBPM == null) throw 'Could not locate dialogBPM NumberStepper in Song Metadata dialog';
2023-02-28 21:06:09 -05:00
dialogBPM.onChange = function(event:UIEvent) {
if (event.value == null || event.value <= 0) return;
var timeChanges:Array<SongTimeChange> = newSongMetadata.timeChanges;
if (timeChanges == null || timeChanges.length == 0)
{
timeChanges = [new SongTimeChange(0, event.value)];
}
else
{
timeChanges[0].bpm = event.value;
}
newSongMetadata.timeChanges = timeChanges;
};
2023-08-31 18:47:23 -04:00
var dialogContinue:Null<Button> = dialog.findComponent('dialogContinue', Button);
if (dialogContinue == null) throw 'Could not locate dialogContinue button in Song Metadata dialog';
dialogContinue.onClick = (_event) -> {
if (targetVariation == Constants.DEFAULT_VARIATION) state.songMetadata.clear();
state.songMetadata.set(targetVariation, newSongMetadata);
Conductor.mapTimeChanges(state.currentSongMetadata.timeChanges);
state.difficultySelectDirty = true;
dialog.hideDialog(DialogButton.APPLY);
}
return dialog;
}
2023-02-28 21:06:09 -05:00
/**
* Builds and opens a dialog where the user uploads vocals for the current song.
* @param state The current chart editor state.
* @param closable Whether the dialog can be closed by the user.
* @return The dialog that was opened.
*/
public static function openUploadVocalsDialog(state:ChartEditorState, closable:Bool = true):Dialog
{
var instId:String = state.currentInstrumentalId;
2023-02-28 21:06:09 -05:00
var charIdsForVocals:Array<String> = [];
var charData:SongCharacterData = state.currentSongMetadata.playData.characters;
var hasClearedVocals:Bool = false;
charIdsForVocals.push(charData.player);
charIdsForVocals.push(charData.opponent);
2023-08-31 18:47:23 -04:00
var dialog:Null<Dialog> = openDialog(state, CHART_EDITOR_DIALOG_UPLOAD_VOCALS_LAYOUT, true, closable);
if (dialog == null) throw 'Could not locate Upload Vocals dialog';
2023-08-31 18:47:23 -04:00
var dialogContainer:Null<Component> = dialog.findComponent('vocalContainer');
if (dialogContainer == null) throw 'Could not locate vocalContainer in Upload Vocals dialog';
2023-02-28 21:06:09 -05:00
2023-08-31 18:47:23 -04:00
var buttonCancel:Null<Button> = dialog.findComponent('dialogCancel', Button);
if (buttonCancel == null) throw 'Could not locate dialogCancel button in Upload Vocals dialog';
2023-02-28 21:06:09 -05:00
buttonCancel.onClick = function(_event) {
dialog.hideDialog(DialogButton.CANCEL);
}
2023-08-31 18:47:23 -04:00
var dialogNoVocals:Null<Button> = dialog.findComponent('dialogNoVocals', Button);
if (dialogNoVocals == null) throw 'Could not locate dialogNoVocals button in Upload Vocals dialog';
2023-02-28 13:17:28 -05:00
dialogNoVocals.onClick = function(_event) {
// Dismiss
state.wipeVocalData();
2023-02-28 13:17:28 -05:00
dialog.hideDialog(DialogButton.APPLY);
};
for (charKey in charIdsForVocals)
{
trace('Adding vocal upload for character ${charKey}');
2023-08-31 18:47:23 -04:00
var charMetadata:Null<CharacterData> = CharacterDataParser.fetchCharacterData(charKey);
var charName:String = charMetadata != null ? charMetadata.name : charKey;
2023-02-28 21:06:09 -05:00
var vocalsEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_UPLOAD_VOCALS_ENTRY_LAYOUT);
2023-08-31 18:47:23 -04:00
var vocalsEntryLabel:Null<Label> = vocalsEntry.findComponent('vocalsEntryLabel', Label);
if (vocalsEntryLabel == null) throw 'Could not locate vocalsEntryLabel in Upload Vocals dialog';
#if FILE_DROP_SUPPORTED
2023-02-28 21:06:09 -05:00
vocalsEntryLabel.text = 'Drag and drop vocals for $charName here, or click to browse.';
#else
vocalsEntryLabel.text = 'Click to browse for vocals for $charName.';
#end
2023-02-28 21:06:09 -05:00
var onDropFile:String->Void = function(pathStr:String) {
trace('Selected file: $pathStr');
var path:Path = new Path(pathStr);
2023-02-28 13:17:28 -05:00
if (!hasClearedVocals)
{
hasClearedVocals = true;
state.stopExistingVocals();
}
if (state.loadVocalsFromPath(path, charKey, instId))
2023-02-28 21:06:09 -05:00
{
// Tell the user the load was successful.
state.success('Loaded Vocals', 'Loaded vocals for $charName (${path.file}.${path.ext}), variation ${state.selectedVariation}');
#if FILE_DROP_SUPPORTED
vocalsEntryLabel.text = 'Voices for $charName (drag and drop, or click to browse)\nSelected file: ${path.file}.${path.ext}';
#else
vocalsEntryLabel.text = 'Voices for $charName (click to browse)\n${path.file}.${path.ext}';
#end
2023-02-28 21:06:09 -05:00
dialogNoVocals.hidden = true;
removeDropHandler(onDropFile);
}
else
{
trace('Failed to load vocal track (${path.file}.${path.ext})');
2023-06-08 16:48:34 -04:00
state.error('Failed to Load Vocals', 'Failed to load vocal track (${path.file}.${path.ext}) for variation (${state.selectedVariation})');
2023-02-28 21:06:09 -05:00
#if FILE_DROP_SUPPORTED
2023-02-28 21:06:09 -05:00
vocalsEntryLabel.text = 'Drag and drop vocals for $charName here, or click to browse.';
#else
vocalsEntryLabel.text = 'Click to browse for vocals for $charName.';
#end
2023-02-28 21:06:09 -05:00
}
2023-02-28 13:17:28 -05:00
};
vocalsEntry.onClick = function(_event) {
Dialogs.openBinaryFile('Open $charName Vocals', [
2023-02-28 13:17:28 -05:00
{label: 'Audio File (.ogg)', extension: 'ogg'}], function(selectedFile) {
2023-08-31 18:47:23 -04:00
if (selectedFile != null && selectedFile.bytes != null)
2023-02-28 13:17:28 -05:00
{
trace('Selected file: ' + selectedFile.name);
if (!hasClearedVocals)
{
hasClearedVocals = true;
state.stopExistingVocals();
}
if (state.loadVocalsFromBytes(selectedFile.bytes, charKey, instId))
{
// Tell the user the load was successful.
state.success('Loaded Vocals', 'Loaded vocals for $charName (${selectedFile.name}), variation ${state.selectedVariation}');
#if FILE_DROP_SUPPORTED
vocalsEntryLabel.text = 'Voices for $charName (drag and drop, or click to browse)\nSelected file: ${selectedFile.name}';
#else
vocalsEntryLabel.text = 'Voices for $charName (click to browse)\n${selectedFile.name}';
#end
dialogNoVocals.hidden = true;
}
else
{
trace('Failed to load vocal track (${selectedFile.fullPath})');
state.error('Failed to Load Vocals', 'Failed to load vocal track (${selectedFile.name}) for variation (${state.selectedVariation})');
#if FILE_DROP_SUPPORTED
vocalsEntryLabel.text = 'Drag and drop vocals for $charName here, or click to browse.';
#else
vocalsEntryLabel.text = 'Click to browse for vocals for $charName.';
#end
}
2023-02-28 13:17:28 -05:00
}
});
}
2023-02-28 21:06:09 -05:00
// onDropFile
#if FILE_DROP_SUPPORTED
2023-02-28 21:06:09 -05:00
addDropHandler(vocalsEntry, onDropFile);
#end
dialogContainer.addComponent(vocalsEntry);
}
2023-08-31 18:47:23 -04:00
var dialogContinue:Null<Button> = dialog.findComponent('dialogContinue', Button);
if (dialogContinue == null) throw 'Could not locate dialogContinue button in Upload Vocals dialog';
2023-02-28 13:17:28 -05:00
dialogContinue.onClick = function(_event) {
// Dismiss
dialog.hideDialog(DialogButton.APPLY);
};
return dialog;
}
2023-06-08 16:48:34 -04:00
/**
* Builds and opens a dialog where the user upload the JSON files for a song.
* @param state The current chart editor state.
* @param closable Whether the dialog can be closed by the user.
* @return The dialog that was opened.
*/
@:haxe.warning('-WVarInit')
public static function openChartDialog(state:ChartEditorState, closable:Bool = true):Dialog
2023-06-08 16:48:34 -04:00
{
var dialog:Null<Dialog> = openDialog(state, CHART_EDITOR_DIALOG_OPEN_CHART_PARTS_LAYOUT, true, closable);
2023-08-31 18:47:23 -04:00
if (dialog == null) throw 'Could not locate Open Chart dialog';
2023-06-08 16:48:34 -04:00
2023-08-31 18:47:23 -04:00
var buttonCancel:Null<Button> = dialog.findComponent('dialogCancel', Button);
if (buttonCancel == null) throw 'Could not locate dialogCancel button in Open Chart dialog';
2023-06-08 16:48:34 -04:00
buttonCancel.onClick = function(_event) {
dialog.hideDialog(DialogButton.CANCEL);
}
2023-08-31 18:47:23 -04:00
var chartContainerA:Null<Component> = dialog.findComponent('chartContainerA');
if (chartContainerA == null) throw 'Could not locate chartContainerA in Open Chart dialog';
var chartContainerB:Null<Component> = dialog.findComponent('chartContainerB');
if (chartContainerB == null) throw 'Could not locate chartContainerB in Open Chart dialog';
2023-06-08 16:48:34 -04:00
var songMetadata:Map<String, SongMetadata> = [];
var songChartData:Map<String, SongChartData> = [];
2023-08-31 18:47:23 -04:00
var buttonContinue:Null<Button> = dialog.findComponent('dialogContinue', Button);
if (buttonContinue == null) throw 'Could not locate dialogContinue button in Open Chart dialog';
2023-06-08 16:48:34 -04:00
buttonContinue.onClick = function(_event) {
state.loadSong(songMetadata, songChartData);
2023-06-08 16:48:34 -04:00
dialog.hideDialog(DialogButton.APPLY);
}
var onDropFileMetadataVariation:String->Label->String->Void;
var onClickMetadataVariation:String->Label->UIEvent->Void;
var onDropFileChartDataVariation:String->Label->String->Void;
var onClickChartDataVariation:String->Label->UIEvent->Void;
var constructVariationEntries:Array<String>->Void = function(variations:Array<String>) {
// Clear the chart container.
while (chartContainerB.getComponentAt(0) != null)
{
chartContainerB.removeComponent(chartContainerB.getComponentAt(0));
}
// Build an entry for -chart.json.
var songDefaultChartDataEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_OPEN_CHART_PARTS_ENTRY_LAYOUT);
2023-08-31 18:47:23 -04:00
var songDefaultChartDataEntryLabel:Null<Label> = songDefaultChartDataEntry.findComponent('chartEntryLabel', Label);
if (songDefaultChartDataEntryLabel == null) throw 'Could not locate chartEntryLabel in Open Chart dialog';
#if FILE_DROP_SUPPORTED
2023-06-08 16:48:34 -04:00
songDefaultChartDataEntryLabel.text = 'Drag and drop <song>-chart.json file, or click to browse.';
#else
songDefaultChartDataEntryLabel.text = 'Click to browse for <song>-chart.json file.';
#end
2023-06-08 16:48:34 -04:00
songDefaultChartDataEntry.onClick = onClickChartDataVariation.bind(Constants.DEFAULT_VARIATION).bind(songDefaultChartDataEntryLabel);
addDropHandler(songDefaultChartDataEntry, onDropFileChartDataVariation.bind(Constants.DEFAULT_VARIATION).bind(songDefaultChartDataEntryLabel));
chartContainerB.addComponent(songDefaultChartDataEntry);
for (variation in variations)
{
// Build entries for -metadata-<variation>.json.
var songVariationMetadataEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_OPEN_CHART_PARTS_ENTRY_LAYOUT);
2023-08-31 18:47:23 -04:00
var songVariationMetadataEntryLabel:Null<Label> = songVariationMetadataEntry.findComponent('chartEntryLabel', Label);
if (songVariationMetadataEntryLabel == null) throw 'Could not locate chartEntryLabel in Open Chart dialog';
#if FILE_DROP_SUPPORTED
2023-06-08 16:48:34 -04:00
songVariationMetadataEntryLabel.text = 'Drag and drop <song>-metadata-${variation}.json file, or click to browse.';
#else
songVariationMetadataEntryLabel.text = 'Click to browse for <song>-metadata-${variation}.json file.';
#end
2023-06-08 16:48:34 -04:00
songVariationMetadataEntry.onMouseOver = function(_event) {
songVariationMetadataEntry.swapClass('upload-bg', 'upload-bg-hover');
Cursor.cursorMode = Pointer;
}
songVariationMetadataEntry.onMouseOut = function(_event) {
songVariationMetadataEntry.swapClass('upload-bg-hover', 'upload-bg');
Cursor.cursorMode = Default;
}
2023-06-08 16:48:34 -04:00
songVariationMetadataEntry.onClick = onClickMetadataVariation.bind(variation).bind(songVariationMetadataEntryLabel);
#if FILE_DROP_SUPPORTED
2023-06-08 16:48:34 -04:00
addDropHandler(songVariationMetadataEntry, onDropFileMetadataVariation.bind(variation).bind(songVariationMetadataEntryLabel));
#end
2023-06-08 16:48:34 -04:00
chartContainerB.addComponent(songVariationMetadataEntry);
// Build entries for -chart-<variation>.json.
var songVariationChartDataEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_OPEN_CHART_PARTS_ENTRY_LAYOUT);
2023-08-31 18:47:23 -04:00
var songVariationChartDataEntryLabel:Null<Label> = songVariationChartDataEntry.findComponent('chartEntryLabel', Label);
if (songVariationChartDataEntryLabel == null) throw 'Could not locate chartEntryLabel in Open Chart dialog';
#if FILE_DROP_SUPPORTED
2023-06-08 16:48:34 -04:00
songVariationChartDataEntryLabel.text = 'Drag and drop <song>-chart-${variation}.json file, or click to browse.';
#else
songVariationChartDataEntryLabel.text = 'Click to browse for <song>-chart-${variation}.json file.';
#end
2023-06-08 16:48:34 -04:00
songVariationChartDataEntry.onMouseOver = function(_event) {
songVariationChartDataEntry.swapClass('upload-bg', 'upload-bg-hover');
Cursor.cursorMode = Pointer;
}
songVariationChartDataEntry.onMouseOut = function(_event) {
songVariationChartDataEntry.swapClass('upload-bg-hover', 'upload-bg');
Cursor.cursorMode = Default;
}
2023-06-08 16:48:34 -04:00
songVariationChartDataEntry.onClick = onClickChartDataVariation.bind(variation).bind(songVariationChartDataEntryLabel);
#if FILE_DROP_SUPPORTED
2023-06-08 16:48:34 -04:00
addDropHandler(songVariationChartDataEntry, onDropFileChartDataVariation.bind(variation).bind(songVariationChartDataEntryLabel));
#end
2023-06-08 16:48:34 -04:00
chartContainerB.addComponent(songVariationChartDataEntry);
}
}
onDropFileMetadataVariation = function(variation:String, label:Label, pathStr:String) {
var path:Path = new Path(pathStr);
trace('Dropped JSON file (${path})');
var songMetadataTxt:String = FileUtil.readStringFromPath(path.toString());
var songMetadataVersion:Null<Version> = VersionUtil.getVersionFromJSON(songMetadataTxt);
if (songMetadataVersion == null)
{
// Tell the user the load was not successful.
state.error('Failure', 'Could not parse metadata file version (${path.file}.${path.ext})');
return;
}
var songMetadataVariation:Null<SongMetadata> = SongRegistry.instance.parseEntryMetadataRawWithMigration(songMetadataTxt, path.toString(),
songMetadataVersion);
2023-06-08 16:48:34 -04:00
if (songMetadataVariation == null)
{
// Tell the user the load was not successful.
state.error('Failure', 'Could not load metadata file (${path.file}.${path.ext})');
return;
}
2023-06-08 16:48:34 -04:00
songMetadata.set(variation, songMetadataVariation);
// Tell the user the load was successful.
state.success('Loaded Metadata', 'Loaded metadata file (${path.file}.${path.ext})');
2023-06-08 16:48:34 -04:00
#if FILE_DROP_SUPPORTED
2023-06-08 16:48:34 -04:00
label.text = 'Metadata file (drag and drop, or click to browse)\nSelected file: ${path.file}.${path.ext}';
#else
label.text = 'Metadata file (click to browse)\n${path.file}.${path.ext}';
#end
2023-06-08 16:48:34 -04:00
if (variation == Constants.DEFAULT_VARIATION) constructVariationEntries(songMetadataVariation.playData.songVariations);
};
onClickMetadataVariation = function(variation:String, label:Label, _event:UIEvent) {
Dialogs.openBinaryFile('Open Chart ($variation) Metadata', [
{label: 'JSON File (.json)', extension: 'json'}], function(selectedFile) {
2023-08-31 18:47:23 -04:00
if (selectedFile != null && selectedFile.bytes != null)
2023-06-08 16:48:34 -04:00
{
trace('Selected file: ' + selectedFile.name);
var songMetadataTxt:String = selectedFile.bytes.toString();
var songMetadataVersion:Null<Version> = VersionUtil.getVersionFromJSON(songMetadataTxt);
if (songMetadataVersion == null)
{
// Tell the user the load was not successful.
state.error('Failure', 'Could not parse metadata file version (${selectedFile.name})');
return;
}
var songMetadataVariation:Null<SongMetadata> = SongRegistry.instance.parseEntryMetadataRawWithMigration(songMetadataTxt, selectedFile.name,
songMetadataVersion);
2023-06-08 16:48:34 -04:00
if (songMetadataVariation != null)
{
songMetadata.set(variation, songMetadataVariation);
2023-06-08 16:48:34 -04:00
// Tell the user the load was successful.
state.success('Loaded Metadata', 'Loaded metadata file (${selectedFile.name})');
2023-06-08 16:48:34 -04:00
#if FILE_DROP_SUPPORTED
label.text = 'Metadata file (drag and drop, or click to browse)\nSelected file: ${selectedFile.name}';
#else
label.text = 'Metadata file (click to browse)\n${selectedFile.name}';
#end
2023-06-08 16:48:34 -04:00
if (variation == Constants.DEFAULT_VARIATION) constructVariationEntries(songMetadataVariation.playData.songVariations);
}
else
{
// Tell the user the load was unsuccessful.
state.error('Failure', 'Failed to load metadata file (${selectedFile.name})');
}
2023-06-08 16:48:34 -04:00
}
});
}
onDropFileChartDataVariation = function(variation:String, label:Label, pathStr:String) {
var path:Path = new Path(pathStr);
trace('Dropped JSON file (${path})');
var songChartDataTxt:String = FileUtil.readStringFromPath(path.toString());
2023-06-08 16:48:34 -04:00
var songChartDataVersion:Null<Version> = VersionUtil.getVersionFromJSON(songChartDataTxt);
if (songChartDataVersion == null)
{
// Tell the user the load was not successful.
state.error('Failure', 'Could not parse chart data file version (${path.file}.${path.ext})');
return;
}
2023-06-08 16:48:34 -04:00
var songChartDataVariation:Null<SongChartData> = SongRegistry.instance.parseEntryChartDataRawWithMigration(songChartDataTxt, path.toString(),
songChartDataVersion);
2023-06-08 16:48:34 -04:00
if (songChartDataVariation != null)
{
songChartData.set(variation, songChartDataVariation);
state.notePreviewDirty = true;
state.notePreviewViewportBoundsDirty = true;
state.noteDisplayDirty = true;
// Tell the user the load was successful.
state.success('Loaded Chart Data', 'Loaded chart data file (${path.file}.${path.ext})');
#if FILE_DROP_SUPPORTED
label.text = 'Chart data file (drag and drop, or click to browse)\nSelected file: ${path.file}.${path.ext}';
#else
label.text = 'Chart data file (click to browse)\n${path.file}.${path.ext}';
#end
}
else
{
// Tell the user the load was unsuccessful.
state.error('Failure', 'Failed to load chart data file (${path.file}.${path.ext})');
}
2023-06-08 16:48:34 -04:00
};
onClickChartDataVariation = function(variation:String, label:Label, _event:UIEvent) {
Dialogs.openBinaryFile('Open Chart ($variation) Metadata', [
{label: 'JSON File (.json)', extension: 'json'}], function(selectedFile) {
2023-08-31 18:47:23 -04:00
if (selectedFile != null && selectedFile.bytes != null)
2023-06-08 16:48:34 -04:00
{
trace('Selected file: ' + selectedFile.name);
var songChartDataTxt:String = selectedFile.bytes.toString();
2023-06-08 16:48:34 -04:00
var songChartDataVersion:Null<Version> = VersionUtil.getVersionFromJSON(songChartDataTxt);
if (songChartDataVersion == null)
{
// Tell the user the load was not successful.
state.error('Failure', 'Could not parse chart data file version (${selectedFile.name})');
return;
}
2023-06-08 16:48:34 -04:00
var songChartDataVariation:Null<SongChartData> = SongRegistry.instance.parseEntryChartDataRawWithMigration(songChartDataTxt, selectedFile.name,
songChartDataVersion);
2023-06-08 16:48:34 -04:00
if (songChartDataVariation != null)
{
songChartData.set(variation, songChartDataVariation);
state.notePreviewDirty = true;
state.notePreviewViewportBoundsDirty = true;
state.noteDisplayDirty = true;
// Tell the user the load was successful.
state.success('Loaded Chart Data', 'Loaded chart data file (${selectedFile.name})');
#if FILE_DROP_SUPPORTED
label.text = 'Chart data file (drag and drop, or click to browse)\nSelected file: ${selectedFile.name}';
#else
label.text = 'Chart data file (click to browse)\n${selectedFile.name}';
#end
}
2023-06-08 16:48:34 -04:00
}
});
}
var metadataEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_OPEN_CHART_PARTS_ENTRY_LAYOUT);
2023-08-31 18:47:23 -04:00
var metadataEntryLabel:Null<Label> = metadataEntry.findComponent('chartEntryLabel', Label);
if (metadataEntryLabel == null) throw 'Could not locate chartEntryLabel in Open Chart dialog';
#if FILE_DROP_SUPPORTED
2023-06-08 16:48:34 -04:00
metadataEntryLabel.text = 'Drag and drop <song>-metadata.json file, or click to browse.';
#else
metadataEntryLabel.text = 'Click to browse for <song>-metadata.json file.';
#end
2023-06-08 16:48:34 -04:00
metadataEntry.onClick = onClickMetadataVariation.bind(Constants.DEFAULT_VARIATION).bind(metadataEntryLabel);
addDropHandler(metadataEntry, onDropFileMetadataVariation.bind(Constants.DEFAULT_VARIATION).bind(metadataEntryLabel));
metadataEntry.onMouseOver = function(_event) {
metadataEntry.swapClass('upload-bg', 'upload-bg-hover');
Cursor.cursorMode = Pointer;
}
metadataEntry.onMouseOut = function(_event) {
metadataEntry.swapClass('upload-bg-hover', 'upload-bg');
Cursor.cursorMode = Default;
}
2023-06-08 16:48:34 -04:00
chartContainerA.addComponent(metadataEntry);
return dialog;
}
/**
* Builds and opens a dialog where the user can import a chart from an existing file format.
* @param state The current chart editor state.
* @param format The format to import from.
* @param closable
* @return Dialog
*/
2023-08-31 18:47:23 -04:00
public static function openImportChartDialog(state:ChartEditorState, format:String, closable:Bool = true):Null<Dialog>
2023-06-08 16:48:34 -04:00
{
2023-08-31 18:47:23 -04:00
var dialog:Null<Dialog> = openDialog(state, CHART_EDITOR_DIALOG_IMPORT_CHART_LAYOUT, true, closable);
if (dialog == null) return null;
2023-06-08 16:48:34 -04:00
var prettyFormat:String = switch (format)
{
case 'legacy': 'FNF Legacy';
default: 'Unknown';
}
var fileFilter = switch (format)
{
case 'legacy': {label: 'JSON Data File (.json)', extension: 'json'};
default: null;
}
dialog.title = 'Import Chart - ${prettyFormat}';
2023-08-31 18:47:23 -04:00
var buttonCancel:Null<Button> = dialog.findComponent('dialogCancel', Button);
if (buttonCancel == null) throw 'Could not locate dialogCancel button in Import Chart dialog';
2023-06-08 16:48:34 -04:00
2023-10-22 15:43:39 -04:00
state.isHaxeUIDialogOpen = true;
2023-06-08 16:48:34 -04:00
buttonCancel.onClick = function(_event) {
2023-10-22 15:43:39 -04:00
state.isHaxeUIDialogOpen = false;
2023-06-08 16:48:34 -04:00
dialog.hideDialog(DialogButton.CANCEL);
}
2023-08-31 18:47:23 -04:00
var importBox:Null<Box> = dialog.findComponent('importBox', Box);
if (importBox == null) throw 'Could not locate importBox in Import Chart dialog';
2023-06-08 16:48:34 -04:00
importBox.onMouseOver = function(_event) {
importBox.swapClass('upload-bg', 'upload-bg-hover');
Cursor.cursorMode = Pointer;
}
importBox.onMouseOut = function(_event) {
importBox.swapClass('upload-bg-hover', 'upload-bg');
Cursor.cursorMode = Default;
}
var onDropFile:String->Void;
importBox.onClick = function(_event) {
2023-08-31 18:47:23 -04:00
Dialogs.openBinaryFile('Import Chart - ${prettyFormat}', fileFilter != null ? [fileFilter] : [], function(selectedFile:SelectedFileInfo) {
if (selectedFile != null && selectedFile.bytes != null)
2023-06-08 16:48:34 -04:00
{
trace('Selected file: ' + selectedFile.fullPath);
var selectedFileTxt:String = selectedFile.bytes.toString();
var fnfLegacyData:Null<FNFLegacyData> = FNFLegacyImporter.parseLegacyDataRaw(selectedFileTxt, selectedFile.fullPath);
2023-06-08 16:48:34 -04:00
if (fnfLegacyData == null)
{
state.error('Failure', 'Failed to parse FNF chart file (${selectedFile.name})');
return;
}
var songMetadata:SongMetadata = FNFLegacyImporter.migrateMetadata(fnfLegacyData);
var songChartData:SongChartData = FNFLegacyImporter.migrateChartData(fnfLegacyData);
state.loadSong([Constants.DEFAULT_VARIATION => songMetadata], [Constants.DEFAULT_VARIATION => songChartData]);
2023-06-08 16:48:34 -04:00
dialog.hideDialog(DialogButton.APPLY);
state.success('Success', 'Loaded chart file (${selectedFile.name})');
2023-06-08 16:48:34 -04:00
}
});
}
onDropFile = function(pathStr:String) {
var path:Path = new Path(pathStr);
var selectedFileText:String = FileUtil.readStringFromPath(path.toString());
var selectedFileData:FNFLegacyData = FNFLegacyImporter.parseLegacyDataRaw(selectedFileText, path.toString());
var songMetadata:SongMetadata = FNFLegacyImporter.migrateMetadata(selectedFileData);
var songChartData:SongChartData = FNFLegacyImporter.migrateChartData(selectedFileData);
2023-06-08 16:48:34 -04:00
state.loadSong([Constants.DEFAULT_VARIATION => songMetadata], [Constants.DEFAULT_VARIATION => songChartData]);
2023-06-08 16:48:34 -04:00
dialog.hideDialog(DialogButton.APPLY);
state.success('Success', 'Loaded chart file (${path.file}.${path.ext})');
2023-06-08 16:48:34 -04:00
};
addDropHandler(importBox, onDropFile);
return dialog;
}
/**
* Builds and opens a dialog displaying the user guide, providing guidance and help on how to use the chart editor.
2023-06-08 16:30:45 -04:00
*
2023-02-28 21:06:09 -05:00
* @param state The current chart editor state.
* @return The dialog that was opened.
*/
public static function openUserGuideDialog(state:ChartEditorState):Null<Dialog>
{
return openDialog(state, CHART_EDITOR_DIALOG_USER_GUIDE_LAYOUT, true, true);
}
/**
* Builds and opens a dialog where the user can add a new variation for a song.
* @param state The current chart editor state.
* @param closable Whether the dialog can be closed by the user.
* @return The dialog that was opened.
*/
public static function openAddVariationDialog(state:ChartEditorState, closable:Bool = true):Dialog
{
var dialog:Null<Dialog> = openDialog(state, CHART_EDITOR_DIALOG_ADD_VARIATION_LAYOUT, true, false);
if (dialog == null) throw 'Could not locate Add Variation dialog';
var variationForm:Null<Form> = dialog.findComponent('variationForm', Form);
if (variationForm == null) throw 'Could not locate variationForm Form in Add Variation dialog';
var buttonCancel:Null<Button> = dialog.findComponent('dialogCancel', Button);
if (buttonCancel == null) throw 'Could not locate dialogCancel button in Add Variation dialog';
buttonCancel.onClick = function(_event) {
dialog.hideDialog(DialogButton.CANCEL);
}
var buttonAdd:Null<Button> = dialog.findComponent('dialogAdd', Button);
if (buttonAdd == null) throw 'Could not locate dialogAdd button in Add Variation dialog';
buttonAdd.onClick = function(_event) {
// This performs validation before the onSubmit callback is called.
variationForm.submit();
}
var dialogSongName:Null<TextField> = dialog.findComponent('dialogSongName', TextField);
if (dialogSongName == null) throw 'Could not locate dialogSongName TextField in Add Variation dialog';
dialogSongName.value = state.currentSongMetadata.songName;
var dialogSongArtist:Null<TextField> = dialog.findComponent('dialogSongArtist', TextField);
if (dialogSongArtist == null) throw 'Could not locate dialogSongArtist TextField in Add Variation dialog';
dialogSongArtist.value = state.currentSongMetadata.artist;
var dialogStage:Null<DropDown> = dialog.findComponent('dialogStage', DropDown);
if (dialogStage == null) throw 'Could not locate dialogStage DropDown in Add Variation dialog';
var startingValueStage = ChartEditorDropdowns.populateDropdownWithStages(dialogStage, state.currentSongMetadata.playData.stage);
dialogStage.value = startingValueStage;
var dialogNoteStyle:Null<DropDown> = dialog.findComponent('dialogNoteStyle', DropDown);
if (dialogNoteStyle == null) throw 'Could not locate dialogNoteStyle DropDown in Add Variation dialog';
dialogNoteStyle.value = state.currentSongMetadata.playData.noteStyle;
var dialogCharacterPlayer:Null<DropDown> = dialog.findComponent('dialogCharacterPlayer', DropDown);
if (dialogCharacterPlayer == null) throw 'Could not locate dialogCharacterPlayer DropDown in Add Variation dialog';
dialogCharacterPlayer.value = ChartEditorDropdowns.populateDropdownWithCharacters(dialogCharacterPlayer, CharacterType.BF,
state.currentSongMetadata.playData.characters.player);
var dialogCharacterOpponent:Null<DropDown> = dialog.findComponent('dialogCharacterOpponent', DropDown);
if (dialogCharacterOpponent == null) throw 'Could not locate dialogCharacterOpponent DropDown in Add Variation dialog';
dialogCharacterOpponent.value = ChartEditorDropdowns.populateDropdownWithCharacters(dialogCharacterOpponent, CharacterType.DAD,
state.currentSongMetadata.playData.characters.opponent);
var dialogCharacterGirlfriend:Null<DropDown> = dialog.findComponent('dialogCharacterGirlfriend', DropDown);
if (dialogCharacterGirlfriend == null) throw 'Could not locate dialogCharacterGirlfriend DropDown in Add Variation dialog';
dialogCharacterGirlfriend.value = ChartEditorDropdowns.populateDropdownWithCharacters(dialogCharacterGirlfriend, CharacterType.GF,
state.currentSongMetadata.playData.characters.girlfriend);
var dialogBPM:Null<NumberStepper> = dialog.findComponent('dialogBPM', NumberStepper);
if (dialogBPM == null) throw 'Could not locate dialogBPM NumberStepper in Add Variation dialog';
dialogBPM.value = state.currentSongMetadata.timeChanges[0].bpm;
// If all validators succeeded, this callback is called.
2023-10-22 15:43:39 -04:00
state.isHaxeUIDialogOpen = true;
variationForm.onSubmit = function(_event) {
2023-10-22 15:43:39 -04:00
state.isHaxeUIDialogOpen = false;
trace('Add Variation dialog submitted, validation succeeded!');
var dialogVariationName:Null<TextField> = dialog.findComponent('dialogVariationName', TextField);
if (dialogVariationName == null) throw 'Could not locate dialogVariationName TextField in Add Variation dialog';
var pendingVariation:SongMetadata = new SongMetadata(dialogSongName.text, dialogSongArtist.text, dialogVariationName.text.toLowerCase());
pendingVariation.playData.stage = dialogStage.value.id;
pendingVariation.playData.noteStyle = dialogNoteStyle.value;
pendingVariation.timeChanges[0].bpm = dialogBPM.value;
state.songMetadata.set(pendingVariation.variation, pendingVariation);
state.difficultySelectDirty = true; // Force the Difficulty toolbox to update.
state.success('Add Variation', 'Added new variation "${pendingVariation.variation}"');
dialog.hideDialog(DialogButton.APPLY);
}
return dialog;
}
/**
* Builds and opens a dialog where the user can add a new difficulty for a song.
* @param state The current chart editor state.
* @param closable Whether the dialog can be closed by the user.
* @return The dialog that was opened.
*/
public static function openAddDifficultyDialog(state:ChartEditorState, closable:Bool = true):Dialog
{
var dialog:Null<Dialog> = openDialog(state, CHART_EDITOR_DIALOG_ADD_DIFFICULTY_LAYOUT, true, false);
if (dialog == null) throw 'Could not locate Add Difficulty dialog';
var difficultyForm:Null<Form> = dialog.findComponent('difficultyForm', Form);
if (difficultyForm == null) throw 'Could not locate difficultyForm Form in Add Difficulty dialog';
var buttonCancel:Null<Button> = dialog.findComponent('dialogCancel', Button);
if (buttonCancel == null) throw 'Could not locate dialogCancel button in Add Difficulty dialog';
buttonCancel.onClick = function(_event) {
dialog.hideDialog(DialogButton.CANCEL);
}
var buttonAdd:Null<Button> = dialog.findComponent('dialogAdd', Button);
if (buttonAdd == null) throw 'Could not locate dialogAdd button in Add Difficulty dialog';
buttonAdd.onClick = function(_event) {
// This performs validation before the onSubmit callback is called.
difficultyForm.submit();
}
var dialogVariation:Null<DropDown> = dialog.findComponent('dialogVariation', DropDown);
if (dialogVariation == null) throw 'Could not locate dialogVariation DropDown in Add Variation dialog';
dialogVariation.value = ChartEditorDropdowns.populateDropdownWithVariations(dialogVariation, state, true);
var labelScrollSpeed:Null<Label> = dialog.findComponent('labelScrollSpeed', Label);
if (labelScrollSpeed == null) throw 'Could not find labelScrollSpeed component.';
var inputScrollSpeed:Null<Slider> = dialog.findComponent('inputScrollSpeed', Slider);
if (inputScrollSpeed == null) throw 'Could not find inputScrollSpeed component.';
inputScrollSpeed.onChange = function(event:UIEvent) {
labelScrollSpeed.text = 'Scroll Speed: ${inputScrollSpeed.value}x';
};
inputScrollSpeed.value = state.currentSongChartScrollSpeed;
labelScrollSpeed.text = 'Scroll Speed: ${inputScrollSpeed.value}x';
difficultyForm.onSubmit = function(_event) {
trace('Add Difficulty dialog submitted, validation succeeded!');
var dialogDifficultyName:Null<TextField> = dialog.findComponent('dialogDifficultyName', TextField);
if (dialogDifficultyName == null) throw 'Could not locate dialogDifficultyName TextField in Add Difficulty dialog';
state.createDifficulty(dialogVariation.value.id, dialogDifficultyName.text.toLowerCase(), inputScrollSpeed.value ?? 1.0);
state.success('Add Difficulty', 'Added new difficulty "${dialogDifficultyName.text.toLowerCase()}"');
dialog.hideDialog(DialogButton.APPLY);
}
return dialog;
}
/**
* Builds and opens a dialog from a given layout path.
* @param modal Makes the background uninteractable while the dialog is open.
* @param closable Hides the close button on the dialog, preventing it from being closed unless the user interacts with the dialog.
*/
static function openDialog(state:ChartEditorState, key:String, modal:Bool = true, closable:Bool = true):Null<Dialog>
{
var dialog:Null<Dialog> = cast state.buildComponent(key);
if (dialog == null) return null;
dialog.destroyOnClose = true;
dialog.closable = closable;
dialog.showDialog(modal);
state.isHaxeUIDialogOpen = true;
dialog.onDialogClosed = function(event:UIEvent) {
state.isHaxeUIDialogOpen = false;
};
dialog.zIndex = 1000;
return dialog;
}
// ==========
// DROP HANDLERS
// ==========
static var dropHandlers:Array<
{
component:Component,
handler:(String->Void)
}> = [];
/**
* Add a callback for when a file is dropped on a component.
*
* On OS X you cant drop on the application window, but rather only the app icon
* (either in the dock while running or the icon on the hard drive) so this must be disabled
* and UI updated appropriately.
* @param component
* @param handler
*/
static function addDropHandler(component:Component, handler:String->Void):Void
{
#if desktop
if (!FlxG.stage.window.onDropFile.has(onDropFile)) FlxG.stage.window.onDropFile.add(onDropFile);
dropHandlers.push(
{
component: component,
handler: handler
});
#else
trace('addDropHandler not implemented for this platform');
#end
}
static function removeDropHandler(handler:String->Void):Void
{
#if desktop
FlxG.stage.window.onDropFile.remove(handler);
#end
}
static function clearDropHandlers():Void
{
#if desktop
dropHandlers = [];
FlxG.stage.window.onDropFile.remove(onDropFile);
#end
}
static function onDropFile(path:String):Void
{
// a VERY short timer to wait for the mouse position to update
new FlxTimer().start(0.01, function(_) {
for (handler in dropHandlers)
{
if (handler.component.hitTest(FlxG.mouse.screenX, FlxG.mouse.screenY))
{
handler.handler(path);
return;
}
}
});
}
}