Numerous chart editor fixes.

This commit is contained in:
EliteMasterEric
2023-02-28 21:06:09 -05:00
parent 20e6c7a2be
commit fe92d00a04
10 changed files with 575 additions and 367 deletions
@@ -1,18 +1,16 @@
package funkin.ui.debug.charting;
import haxe.io.Path;
import flixel.FlxSprite;
import flixel.util.FlxTimer;
import funkin.input.Cursor;
import funkin.play.character.BaseCharacter;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.play.song.Song;
import funkin.play.song.SongData.SongDataParser;
import funkin.play.song.SongData.SongPlayableChar;
import funkin.play.song.SongData.SongTimeChange;
import haxe.ui.core.Component;
import haxe.io.Path;
import haxe.ui.components.Button;
import haxe.ui.components.DropDown;
import haxe.ui.components.Image;
import haxe.ui.components.Label;
import haxe.ui.components.Link;
import haxe.ui.components.NumberStepper;
@@ -23,8 +21,10 @@ import haxe.ui.containers.dialogs.Dialogs;
import haxe.ui.containers.properties.PropertyGrid;
import haxe.ui.containers.properties.PropertyGroup;
import haxe.ui.containers.VBox;
import haxe.ui.events.MouseEvent;
import haxe.ui.core.Component;
import haxe.ui.events.UIEvent;
import haxe.ui.notifications.NotificationManager;
import haxe.ui.notifications.NotificationType;
using Lambda;
@@ -43,7 +43,9 @@ class ChartEditorDialogHandler
static final CHART_EDITOR_DIALOG_USER_GUIDE_LAYOUT:String = Paths.ui('chart-editor/dialogs/user-guide');
/**
*
* 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 inline function openAboutDialog(state:ChartEditorState):Dialog
{
@@ -52,72 +54,70 @@ class ChartEditorDialogHandler
/**
* Builds and opens a dialog letting the user create a new chart, open a recent chart, or load from a template.
* @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 openWelcomeDialog(state:ChartEditorState, closable:Bool = true):Dialog
{
var dialog:Dialog = openDialog(state, CHART_EDITOR_DIALOG_WELCOME_LAYOUT, true, closable);
// TODO: Add callbacks to the dialog buttons
// Add handlers to the "Create From Song" section.
var linkCreateBasic:Link = dialog.findComponent('splashCreateFromSongBasic', Link);
linkCreateBasic.onClick = (_event) -> {
linkCreateBasic.onClick = function(_event) {
// Hide the welcome dialog
dialog.hideDialog(DialogButton.CANCEL);
// Create song wizard
var uploadInstDialog = openUploadInstDialog(state, false);
uploadInstDialog.onDialogClosed = (_event) -> {
//
// Create Song Wizard
//
// Step 1. Upload Instrumental
var uploadInstDialog:Dialog = openUploadInstDialog(state, false);
uploadInstDialog.onDialogClosed = function(_event) {
state.isHaxeUIDialogOpen = false;
if (_event.button == DialogButton.APPLY)
{
var songMetadataDialog = openSongMetadataDialog(state);
songMetadataDialog.onDialogClosed = (_event) -> {
// Step 2. Song Metadata
var songMetadataDialog:Dialog = openSongMetadataDialog(state);
songMetadataDialog.onDialogClosed = function(_event) {
state.isHaxeUIDialogOpen = false;
if (_event.button == DialogButton.APPLY)
{
var uploadVocalsDialog = openUploadVocalsDialog(state, false);
// Step 3. Upload Vocals
// NOTE: Uploading vocals is optional, so we don't need to check if the user cancelled the wizard.
openUploadVocalsDialog(state, false); // var uploadVocalsDialog:Dialog
}
else
{
// User cancelled the wizard! Back to the welcome dialog.
openWelcomeDialog(state);
}
};
}
else
{
// User cancelled the wizard! Back to the welcome dialog.
openWelcomeDialog(state);
}
};
}
// TODO: Get the list of songs and insert them as links into the "Create From Song" section.
/*
var linkTemplateDadBattle:Link = dialog.findComponent('splashTemplateDadBattle', Link);
linkTemplateDadBattle.onClick = (_event) ->
{
dialog.hideDialog(DialogButton.CANCEL);
// Load song from template
state.loadSongAsTemplate('dadbattle');
}
var linkTemplateBopeebo:Link = dialog.findComponent('splashTemplateBopeebo', Link);
linkTemplateBopeebo.onClick = (_event) ->
{
dialog.hideDialog(DialogButton.CANCEL);
// Load song from template
state.loadSongAsTemplate('bopeebo');
}
*/
var splashTemplateContainer:VBox = dialog.findComponent('splashTemplateContainer', VBox);
var songList:Array<String> = SongDataParser.listSongIds();
for (targetSongId in songList)
{
var songData = SongDataParser.fetchSong(targetSongId);
var songData:Song = SongDataParser.fetchSong(targetSongId);
if (songData == null) continue;
var songName = songData.getDifficulty().songName;
var songName:String = songData.getDifficulty().songName;
var linkTemplateSong:Link = new Link();
linkTemplateSong.text = songName;
linkTemplateSong.onClick = (_event) -> {
linkTemplateSong.onClick = function(_event) {
dialog.hideDialog(DialogButton.CANCEL);
// Load song from template
@@ -130,42 +130,99 @@ class ChartEditorDialogHandler
return dialog;
}
/**
* 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.
*/
public static function openUploadInstDialog(state:ChartEditorState, ?closable:Bool = true):Dialog
{
var dialog:Dialog = openDialog(state, CHART_EDITOR_DIALOG_UPLOAD_INST_LAYOUT, true, closable);
var buttonCancel:Button = dialog.findComponent('dialogCancel', Button);
buttonCancel.onClick = function(_event) {
dialog.hideDialog(DialogButton.CANCEL);
}
var instrumentalBox:Box = dialog.findComponent('instrumentalBox', Box);
instrumentalBox.onMouseOver = (_event) -> {
instrumentalBox.onMouseOver = function(_event) {
instrumentalBox.swapClass('upload-bg', 'upload-bg-hover');
Cursor.cursorMode = Pointer;
}
instrumentalBox.onMouseOut = (_event) -> {
instrumentalBox.onMouseOut = function(_event) {
instrumentalBox.swapClass('upload-bg-hover', 'upload-bg');
Cursor.cursorMode = Default;
}
var onDropFile:String->Void;
instrumentalBox.onClick = (_event) -> {
Dialogs.openBinaryFile("Open Instrumental", [
{label: "Audio File (.ogg)", extension: "ogg"}], function(selectedFile) {
instrumentalBox.onClick = function(_event) {
Dialogs.openBinaryFile('Open Instrumental', [
{label: 'Audio File (.ogg)', extension: 'ogg'}], function(selectedFile:SelectedFileInfo) {
if (selectedFile != null)
{
trace('Selected file: ' + selectedFile);
state.loadInstrumentalFromBytes(selectedFile.bytes);
dialog.hideDialog(DialogButton.APPLY);
removeDropHandler(onDropFile);
if (state.loadInstrumentalFromBytes(selectedFile.bytes))
{
trace('Selected file: ' + selectedFile.fullPath);
NotificationManager.instance.addNotification(
{
title: 'Success',
body: 'Loaded instrumental track (${selectedFile.name})',
type: NotificationType.Success,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
});
dialog.hideDialog(DialogButton.APPLY);
removeDropHandler(onDropFile);
}
else
{
trace('Failed to load instrumental (${selectedFile.fullPath})');
NotificationManager.instance.addNotification(
{
title: 'Failure',
body: 'Failed to load instrumental track (${selectedFile.name})',
type: NotificationType.Error,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
});
}
}
});
}
onDropFile = (path:String) -> {
trace('Dropped file: ' + path);
state.loadInstrumentalFromPath(path);
dialog.hideDialog(DialogButton.APPLY);
removeDropHandler(onDropFile);
onDropFile = function(pathStr:String) {
var path:Path = new Path(pathStr);
trace('Dropped file (${path})');
if (state.loadInstrumentalFromPath(path))
{
// Tell the user the load was successful.
NotificationManager.instance.addNotification(
{
title: 'Success',
body: 'Loaded instrumental track (${path.file}.${path.ext})',
type: NotificationType.Success,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
});
dialog.hideDialog(DialogButton.APPLY);
removeDropHandler(onDropFile);
}
else
{
// Tell the user the load was successful.
NotificationManager.instance.addNotification(
{
title: 'Failure',
body: 'Failed to load instrumental track (${path.file}.${path.ext})',
type: NotificationType.Error,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
});
}
};
addDropHandler(instrumentalBox, onDropFile);
@@ -213,19 +270,14 @@ class ChartEditorDialogHandler
{
// a VERY short timer to wait for the mouse position to update
new FlxTimer().start(0.01, function(_) {
trace("mouseX: " + FlxG.mouse.screenX + ", mouseY: " + FlxG.mouse.screenY);
for (handler in dropHandlers)
{
if (handler.component.hitTest(FlxG.mouse.screenX, FlxG.mouse.screenY))
{
trace('File dropped on component! ' + handler.component.id);
handler.handler(path);
return;
}
}
trace('File dropped on nothing!' + path);
});
}
@@ -238,6 +290,12 @@ class ChartEditorDialogHandler
{
var dialog:Dialog = openDialog(state, CHART_EDITOR_DIALOG_SONG_METADATA_LAYOUT, true, false);
var buttonCancel:Button = dialog.findComponent('dialogCancel', Button);
buttonCancel.onClick = function(_event) {
dialog.hideDialog(DialogButton.CANCEL);
}
var dialogSongName:TextField = dialog.findComponent('dialogSongName', TextField);
dialogSongName.onChange = function(event:UIEvent) {
var valid:Bool = event.target.text != null && event.target.text != '';
@@ -272,25 +330,23 @@ class ChartEditorDialogHandler
var dialogStage:DropDown = dialog.findComponent('dialogStage', DropDown);
dialogStage.onChange = function(event:UIEvent) {
var valid = event.data != null && event.data.id != null;
if (event.data.id == null) return;
if (event.data == null && event.data.id == null) return;
state.currentSongMetadata.playData.stage = event.data.id;
};
state.currentSongMetadata.playData.stage = null;
var dialogNoteSkin:DropDown = dialog.findComponent('dialogNoteSkin', DropDown);
dialogNoteSkin.onChange = (event:UIEvent) -> {
dialogNoteSkin.onChange = function(event:UIEvent) {
if (event.data.id == null) return;
state.currentSongMetadata.playData.noteSkin = event.data.id;
};
state.currentSongMetadata.playData.noteSkin = null;
var dialogBPM:NumberStepper = dialog.findComponent('dialogBPM', NumberStepper);
dialogBPM.onChange = (event:UIEvent) -> {
dialogBPM.onChange = function(event:UIEvent) {
if (event.value == null || event.value <= 0) return;
var timeChanges = state.currentSongMetadata.timeChanges;
var timeChanges:Array<SongTimeChange> = state.currentSongMetadata.timeChanges;
if (timeChanges == null || timeChanges.length == 0)
{
timeChanges = [new SongTimeChange(-1, 0, event.value, 4, 4, [4, 4, 4, 4])];
@@ -307,11 +363,9 @@ class ChartEditorDialogHandler
var dialogCharGrid:PropertyGrid = dialog.findComponent('dialogCharGrid', PropertyGrid);
var dialogCharAdd:Button = dialog.findComponent('dialogCharAdd', Button);
dialogCharAdd.onClick = (_event) -> {
dialogCharAdd.onClick = function(event:UIEvent) {
var charGroup:PropertyGroup;
charGroup = buildCharGroup(state, null, () -> {
dialogCharGrid.removeComponent(charGroup);
});
charGroup = buildCharGroup(state, null, () -> dialogCharGrid.removeComponent(charGroup));
dialogCharGrid.addComponent(charGroup);
};
@@ -321,18 +375,16 @@ class ChartEditorDialogHandler
dialogCharGrid.addComponent(buildCharGroup(state, 'bf', null));
var dialogContinue:Button = dialog.findComponent('dialogContinue', Button);
dialogContinue.onClick = (_event) -> {
dialog.hideDialog(DialogButton.APPLY);
};
dialogContinue.onClick = (_event) -> dialog.hideDialog(DialogButton.APPLY);
return dialog;
}
static function buildCharGroup(state:ChartEditorState, ?key:String = null, removeFunc:Void->Void):PropertyGroup
static function buildCharGroup(state:ChartEditorState, key:String = null, removeFunc:Void->Void):PropertyGroup
{
var groupKey = key;
var groupKey:String = key;
var getCharData = () -> {
var getCharData:Void->SongPlayableChar = function() {
if (groupKey == null) groupKey = 'newChar${state.currentSongMetadata.playData.playableChars.keys().count()}';
var result = state.currentSongMetadata.playData.playableChars.get(groupKey);
@@ -344,24 +396,24 @@ class ChartEditorDialogHandler
return result;
}
var moveCharGroup = (target:String) -> {
var moveCharGroup:String->Void = function(target:String) {
var charData = getCharData();
state.currentSongMetadata.playData.playableChars.remove(groupKey);
state.currentSongMetadata.playData.playableChars.set(target, charData);
groupKey = target;
}
var removeGroup = () -> {
var removeGroup:Void->Void = function() {
state.currentSongMetadata.playData.playableChars.remove(groupKey);
removeFunc();
}
var charData = getCharData();
var charData:SongPlayableChar = getCharData();
var charGroup:PropertyGroup = cast state.buildComponent(CHART_EDITOR_DIALOG_SONG_METADATA_CHARGROUP_LAYOUT);
var charGroupPlayer:DropDown = charGroup.findComponent('charGroupPlayer', DropDown);
charGroupPlayer.onChange = (event:UIEvent) -> {
charGroupPlayer.onChange = function(event:UIEvent) {
charGroup.text = event.data.text;
moveCharGroup(event.data.id);
};
@@ -373,19 +425,19 @@ class ChartEditorDialogHandler
}
var charGroupOpponent:DropDown = charGroup.findComponent('charGroupOpponent', DropDown);
charGroupOpponent.onChange = (event:UIEvent) -> {
charGroupOpponent.onChange = function(event:UIEvent) {
charData.opponent = event.data.id;
};
charGroupOpponent.value = getCharData().opponent;
var charGroupGirlfriend:DropDown = charGroup.findComponent('charGroupGirlfriend', DropDown);
charGroupGirlfriend.onChange = (event:UIEvent) -> {
charGroupGirlfriend.onChange = function(event:UIEvent) {
charData.girlfriend = event.data.id;
};
charGroupGirlfriend.value = getCharData().girlfriend;
var charGroupRemove:Button = charGroup.findComponent('charGroupRemove', Button);
charGroupRemove.onClick = (_event:MouseEvent) -> {
charGroupRemove.onClick = function(event:UIEvent) {
removeGroup();
};
@@ -394,20 +446,31 @@ class ChartEditorDialogHandler
return charGroup;
}
/**
* 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 charIdsForVocals = [];
var charIdsForVocals:Array<String> = [];
for (charKey in state.currentSongMetadata.playData.playableChars.keys())
{
var charData = state.currentSongMetadata.playData.playableChars.get(charKey);
var charData:SongPlayableChar = state.currentSongMetadata.playData.playableChars.get(charKey);
charIdsForVocals.push(charKey);
if (charData.opponent != null) charIdsForVocals.push(charData.opponent);
}
var dialog:Dialog = openDialog(state, CHART_EDITOR_DIALOG_UPLOAD_VOCALS_LAYOUT, true, closable);
var dialogContainer = dialog.findComponent('vocalContainer');
var dialogContainer:Component = dialog.findComponent('vocalContainer');
var buttonCancel:Button = dialog.findComponent('dialogCancel', Button);
buttonCancel.onClick = function(_event) {
dialog.hideDialog(DialogButton.CANCEL);
}
var dialogNoVocals:Button = dialog.findComponent('dialogNoVocals', Button);
dialogNoVocals.onClick = function(_event) {
@@ -421,20 +484,42 @@ class ChartEditorDialogHandler
var charMetadata:BaseCharacter = CharacterDataParser.fetchCharacter(charKey);
var charName:String = charMetadata.characterName;
var vocalsEntry = state.buildComponent(CHART_EDITOR_DIALOG_UPLOAD_VOCALS_ENTRY_LAYOUT);
var vocalsEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_UPLOAD_VOCALS_ENTRY_LAYOUT);
var vocalsEntryLabel:Label = vocalsEntry.findComponent('vocalsEntryLabel', Label);
vocalsEntryLabel.text = 'Click to browse for a vocal track for $charName.';
vocalsEntryLabel.text = 'Drag and drop vocals for $charName here, or click to browse.';
var onDropFile:String->Void = function(fullPath:String) {
trace('Selected file: $fullPath');
var directory:String = Path.directory(fullPath);
var filename:String = Path.withoutDirectory(directory);
var onDropFile:String->Void = function(pathStr:String) {
trace('Selected file: $pathStr');
var path:Path = new Path(pathStr);
vocalsEntryLabel.text = 'Vocals for $charName (click to browse)\n${filename}';
state.loadVocalsFromPath(fullPath, charKey);
dialogNoVocals.hidden = true;
removeDropHandler(onDropFile);
if (state.loadVocalsFromPath(path, charKey))
{
// Tell the user the load was successful.
NotificationManager.instance.addNotification(
{
title: 'Success',
body: 'Loaded vocal track for $charName (${path.file}.${path.ext})',
type: NotificationType.Success,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
});
vocalsEntryLabel.text = 'Vocals for $charName (drag and drop, or click to browse)\nSelected file: ${path.file}.${path.ext}';
dialogNoVocals.hidden = true;
removeDropHandler(onDropFile);
}
else
{
// Vocals failed to load.
NotificationManager.instance.addNotification(
{
title: 'Failure',
body: 'Failed to load vocal track (${path.file}.${path.ext})',
type: NotificationType.Error,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
});
vocalsEntryLabel.text = 'Drag and drop vocals for $charName here, or click to browse.';
}
};
vocalsEntry.onClick = function(_event) {
@@ -449,11 +534,10 @@ class ChartEditorDialogHandler
removeDropHandler(onDropFile);
}
});
// onDropFile
addDropHandler(vocalsEntry, onDropFile);
}
// onDropFile
addDropHandler(vocalsEntry, onDropFile);
dialogContainer.addComponent(vocalsEntry);
}
@@ -463,14 +547,14 @@ class ChartEditorDialogHandler
dialog.hideDialog(DialogButton.APPLY);
};
// TODO: Redo the logic for file drop handler to be more robust.
// We need to distinguish which component the mouse is over when the file is dropped.
return dialog;
}
/**
* Builds and opens a dialog displaying the user guide, providing guidance and help on how to use the chart editor.
*
* @param state The current chart editor state.
* @return The dialog that was opened.
*/
public static inline function openUserGuideDialog(state:ChartEditorState):Dialog
{
@@ -490,7 +574,7 @@ class ChartEditorDialogHandler
dialog.showDialog(modal);
state.isHaxeUIDialogOpen = true;
dialog.onDialogClosed = (_event) -> {
dialog.onDialogClosed = function(event:UIEvent) {
state.isHaxeUIDialogOpen = false;
};