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

View file

@ -193,7 +193,8 @@
{
"props": {
"max": 1000,
"ignoreEmptyLines": true
"ignoreEmptyLines": true,
"severity": "IGNORE"
},
"type": "FileLength"
},
@ -232,7 +233,7 @@
},
{
"props": {
"ignoreReturnAssignments": false,
"ignoreReturnAssignments": true,
"severity": "WARNING"
},
"type": "InnerAssignment"
@ -392,12 +393,13 @@
},
{
"props": {
"oldFunctionTypePolicy": "none",
"unaryOpPolicy": "none",
"intervalOpPolicy": "none",
"newFunctionTypePolicy": "around",
"ternaryOpPolicy": "around",
"unaryOpPolicy": "none",
"oldFunctionTypePolicy": "around",
"boolOpPolicy": "around",
"intervalOpPolicy": "none",
"assignOpPolicy": "around",
"bitwiseOpPolicy": "around",
"arithmeticOpPolicy": "around",
@ -623,7 +625,9 @@
"type": "UnusedImport"
},
{
"props": {},
"props": {
"severity": "WARNING"
},
"type": "UnusedLocalVar"
},
{

View file

@ -42,14 +42,14 @@
"name": "haxeui-core",
"type": "git",
"dir": null,
"ref": "e5cf78d",
"ref": "59157d2",
"url": "https://github.com/haxeui/haxeui-core/"
},
{
"name": "haxeui-flixel",
"type": "git",
"dir": null,
"ref": "f03bb6d",
"ref": "d353389",
"url": "https://github.com/haxeui/haxeui-flixel"
},
{

View file

@ -0,0 +1,116 @@
package funkin.input;
import flixel.input.keyboard.FlxKey;
import flixel.FlxBasic;
/**
* Handles repeating behavior when holding down a key or key combination.
*
* When the `keys` are pressed, `activated` will be true for the first frame,
* then wait `delay` seconds before becoming true for one frame every `interval` seconds.
*
* Example: Pressing Ctrl+Z will undo, while holding Ctrl+Z will start to undo repeatedly.
*/
class TurboKeyHandler extends FlxBasic
{
/**
* Default delay before repeating.
*/
static inline final DEFAULT_DELAY:Float = 0.4;
/**
* Default interval between repeats.
*/
static inline final DEFAULT_INTERVAL:Float = 0.1;
/**
* Whether all of the keys for this handler are pressed.
*/
public var allPressed(get, null):Bool;
/**
* Whether all of the keys for this handler are activated,
* and the handler is ready to repeat.
*/
public var activated(default, null):Bool = false;
var keys:Array<FlxKey>;
var delay:Float;
var interval:Float;
var allPressedTime:Float = 0;
function new(keys:Array<FlxKey>, delay:Float = DEFAULT_DELAY, interval:Float = DEFAULT_INTERVAL)
{
super();
this.keys = keys;
this.delay = delay;
this.interval = interval;
}
function get_allPressed():Bool
{
if (keys == null || keys.length == 0) return false;
if (keys.length == 1) return FlxG.keys.anyPressed(keys);
// Check if ANY keys are unpressed
for (key in keys)
{
if (!FlxG.keys.anyPressed([key])) return false;
}
return true;
}
public override function update(elapsed:Float):Void
{
super.update(elapsed);
if (allPressed)
{
if (allPressedTime == 0)
{
activated = true;
}
else if (allPressedTime >= (delay + interval))
{
activated = true;
allPressedTime -= interval;
}
else
{
activated = false;
}
allPressedTime += elapsed;
}
else
{
allPressedTime = 0;
activated = false;
}
}
/**
* Builds a TurboKeyHandler that monitors from a single key.
* @param inputKey The key to monitor.
* @param delay How long to wait before repeating.
* @param repeatDelay How long to wait between repeats.
* @return A TurboKeyHandler
*/
public static overload inline extern function build(inputKey:FlxKey, ?delay:Float = DEFAULT_DELAY, ?interval:Float = DEFAULT_INTERVAL):TurboKeyHandler
{
return new TurboKeyHandler([inputKey], delay, interval);
}
/**
* Builds a TurboKeyHandler that monitors a key combination.
* @param inputKeys The combination of keys to monitor.
* @param delay How long to wait before repeating.
* @param repeatDelay How long to wait between repeats.
* @return A TurboKeyHandler
*/
public static overload inline extern function build(inputKeys:Array<FlxKey>, ?delay:Float = DEFAULT_DELAY,
?interval:Float = DEFAULT_INTERVAL):TurboKeyHandler
{
return new TurboKeyHandler(inputKeys, delay, interval);
}
}

View file

@ -17,8 +17,7 @@ class Module implements IPlayStateScriptedClass implements IStateChangingScripte
function set_active(value:Bool):Bool
{
this.active = value;
return value;
return this.active = value;
}
public var moduleId(default, null):String = 'UNKNOWN';

View file

@ -436,22 +436,19 @@ class AnimateAtlasCharacter extends BaseCharacter
if (!exists || x == value) return x; // early return (no need to transform)
transformChildren(xTransform, value - x); // offset
x = value;
return x;
return x = value;
}
override function set_y(value:Float):Float
{
if (exists && y != value) transformChildren(yTransform, value - y); // offset
y = value;
return y;
return y = value;
}
override function set_angle(value:Float):Float
{
if (exists && angle != value) transformChildren(angleTransform, value - angle); // offset
angle = value;
return angle;
return angle = value;
}
override function set_alpha(value:Float):Float
@ -462,43 +459,37 @@ class AnimateAtlasCharacter extends BaseCharacter
{
transformChildren(directAlphaTransform, value);
}
alpha = value;
return alpha;
return alpha = value;
}
override function set_facing(value:Int):Int
{
if (exists && facing != value) transformChildren(facingTransform, value);
facing = value;
return facing;
return facing = value;
}
override function set_flipX(value:Bool):Bool
{
if (exists && flipX != value) transformChildren(flipXTransform, value);
flipX = value;
return flipX;
return flipX = value;
}
override function set_flipY(value:Bool):Bool
{
if (exists && flipY != value) transformChildren(flipYTransform, value);
flipY = value;
return flipY;
return flipY = value;
}
override function set_moves(value:Bool):Bool
{
if (exists && moves != value) transformChildren(movesTransform, value);
moves = value;
return moves;
return moves = value;
}
override function set_immovable(value:Bool):Bool
{
if (exists && immovable != value) transformChildren(immovableTransform, value);
immovable = value;
return immovable;
return immovable = value;
}
override function set_solid(value:Bool):Bool
@ -510,15 +501,13 @@ class AnimateAtlasCharacter extends BaseCharacter
override function set_color(value:Int):Int
{
if (exists && color != value) transformChildren(gColorTransform, value);
color = value;
return color;
return color = value;
}
override function set_blend(value:BlendMode):BlendMode
{
if (exists && blend != value) transformChildren(blendTransform, value);
blend = value;
return blend;
return blend = value;
}
override function set_clipRect(rect:FlxRect):FlxRect

View file

@ -126,8 +126,10 @@ class Song // implements IPlayStateScriptedClass
/**
* Retrieve the metadata for a specific difficulty, including the chart if it is loaded.
* @param diffId The difficulty ID, such as `easy` or `hard`.
* @return The difficulty data.
*/
public inline function getDifficulty(?diffId:String):SongDifficulty
public inline function getDifficulty(diffId:String = null):SongDifficulty
{
if (diffId == null) diffId = difficulties.keys().array()[0];

View file

@ -70,8 +70,7 @@ class Bopper extends StageProp implements IPlayStateScriptedClass
this.x += xDiff;
this.y += yDiff;
globalOffsets = value;
return value;
return globalOffsets = value;
}
var animOffsets(default, set):Array<Float> = [0, 0];

View file

@ -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;
};

View file

@ -1,5 +1,8 @@
package funkin.ui.debug.charting;
import funkin.ui.debug.charting.ChartEditorCommand;
import flixel.input.keyboard.FlxKey;
import funkin.input.TurboKeyHandler;
import haxe.ui.notifications.NotificationType;
import haxe.ui.notifications.NotificationManager;
import haxe.DynamicAccess;
@ -18,7 +21,6 @@ import funkin.audio.visualize.PolygonSpectogram;
import funkin.audio.VocalGroup;
import funkin.input.Cursor;
import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEventDispatcher;
import funkin.play.HealthIcon;
import funkin.play.song.Song;
import funkin.play.song.SongData.SongChartData;
@ -27,8 +29,6 @@ import funkin.play.song.SongData.SongEventData;
import funkin.play.song.SongData.SongMetadata;
import funkin.play.song.SongData.SongNoteData;
import funkin.play.song.SongDataUtils;
import funkin.play.song.SongSerializer;
import funkin.ui.debug.charting.ChartEditorCommand;
import funkin.ui.debug.charting.ChartEditorThemeHandler.ChartEditorTheme;
import funkin.ui.debug.charting.ChartEditorToolboxHandler.ChartEditorToolMode;
import funkin.ui.haxeui.components.CharacterPlayer;
@ -37,23 +37,16 @@ import funkin.util.Constants;
import funkin.util.FileUtil;
import funkin.util.DateUtil;
import funkin.util.SerializerUtil;
import haxe.ui.components.Button;
import haxe.ui.components.CheckBox;
import haxe.ui.components.Label;
import haxe.ui.components.Slider;
import haxe.ui.containers.dialogs.Dialog;
import haxe.ui.containers.dialogs.MessageBox;
import haxe.ui.containers.menus.MenuCheckBox;
import haxe.ui.containers.menus.MenuItem;
import haxe.ui.containers.SideBar;
import haxe.ui.containers.TreeView;
import haxe.ui.containers.TreeViewNode;
import haxe.ui.core.Component;
import haxe.ui.core.Screen;
import haxe.ui.events.DragEvent;
import haxe.ui.events.MouseEvent;
import haxe.ui.events.UIEvent;
import lime.media.AudioBuffer;
import funkin.util.WindowUtil;
import openfl.display.BitmapData;
import openfl.geom.Rectangle;
@ -523,14 +516,34 @@ class ChartEditorState extends HaxeUIState
var redoHistory:Array<ChartEditorCommand> = [];
/**
* Variable used to track how long the user has been holding the undo keybind.
* Handler used to track how long the user has been holding the undo keybind.
*/
var undoHeldTime:Float = 0.0;
var undoKeyHandler:TurboKeyHandler = TurboKeyHandler.build([FlxKey.CONTROL, FlxKey.Z]);
/**
* Variable used to track how long the user has been holding the redo keybind.
*/
var redoHeldTime:Float = 0.0;
var redoKeyHandler:TurboKeyHandler = TurboKeyHandler.build([FlxKey.CONTROL, FlxKey.Y]);
/**
* Variable used to track how long the user has been holding the up keybind.
*/
var upKeyHandler:TurboKeyHandler = TurboKeyHandler.build(FlxKey.UP);
/**
* Variable used to track how long the user has been holding the down keybind.
*/
var downKeyHandler:TurboKeyHandler = TurboKeyHandler.build(FlxKey.DOWN);
/**
* Variable used to track how long the user has been holding the page-up keybind.
*/
var pageUpKeyHandler:TurboKeyHandler = TurboKeyHandler.build(FlxKey.PAGEUP);
/**
* Variable used to track how long the user has been holding the page-down keybind.
*/
var pageDownKeyHandler:TurboKeyHandler = TurboKeyHandler.build(FlxKey.PAGEDOWN);
/**
* Whether the undo/redo histories have changed since the last time the UI was updated.
@ -728,8 +741,7 @@ class ChartEditorState extends HaxeUIState
function set_currentSongChartEventData(value:Array<SongEventData>):Array<SongEventData>
{
currentSongChartData.events = value;
return value;
return currentSongChartData.events = value;
}
public var currentSongNoteSkin(get, set):String;
@ -911,7 +923,7 @@ class ChartEditorState extends HaxeUIState
super(CHART_EDITOR_LAYOUT);
}
override function create()
override function create():Void
{
// Get rid of any music from the previous state.
FlxG.sound.music.stop();
@ -931,18 +943,14 @@ class ChartEditorState extends HaxeUIState
// Setup the onClick listeners for the UI after it's been created.
setupUIListeners();
setupTurboKeyHandlers();
setupAutoSave();
// TODO: We should be loading the music later when the user requests it.
// loadDefaultMusic();
// TODO: Change to false.
var canCloseInitialDialog = true;
ChartEditorDialogHandler.openWelcomeDialog(this, canCloseInitialDialog);
ChartEditorDialogHandler.openWelcomeDialog(this, false);
}
function buildDefaultSongData()
function buildDefaultSongData():Void
{
selectedVariation = Constants.DEFAULT_VARIATION;
selectedDifficulty = Constants.DEFAULT_DIFFICULTY;
@ -959,7 +967,7 @@ class ChartEditorState extends HaxeUIState
/**
* Builds and displays the background sprite.
*/
function buildBackground()
function buildBackground():Void
{
menuBG = new FlxSprite().loadGraphic(Paths.image('menuDesat'));
add(menuBG);
@ -973,7 +981,7 @@ class ChartEditorState extends HaxeUIState
/**
* Builds and displays the chart editor grid, including the playhead and cursor.
*/
function buildGrid()
function buildGrid():Void
{
gridTiledSprite = new FlxTiledSprite(gridBitmap, gridBitmap.width, 1000, false, true);
gridTiledSprite.x = FlxG.width / 2 - GRID_SIZE * STRUMLINE_SIZE; // Center the grid.
@ -1032,7 +1040,7 @@ class ChartEditorState extends HaxeUIState
add(healthIconBF);
}
function buildSelectionBox()
function buildSelectionBox():Void
{
selectionBoxSprite.scrollFactor.set(0, 0);
add(selectionBoxSprite);
@ -1040,7 +1048,7 @@ class ChartEditorState extends HaxeUIState
setSelectionBoxBounds();
}
function setSelectionBoxBounds(?bounds:FlxRect = null)
function setSelectionBoxBounds(?bounds:FlxRect = null):Void
{
if (bounds == null)
{
@ -1058,7 +1066,7 @@ class ChartEditorState extends HaxeUIState
}
}
function buildSpectrogram(target:FlxSound)
function buildSpectrogram(target:FlxSound):Void
{
gridSpectrogram = new PolygonSpectogram(target, SPECTROGRAM_COLOR, FlxG.height / 2, Math.floor(FlxG.height / 2));
// Halfway through the grid.
@ -1075,7 +1083,7 @@ class ChartEditorState extends HaxeUIState
/**
* Builds the group that will hold all the notes.
*/
function buildNoteGroup()
function buildNoteGroup():Void
{
renderedNotes = new FlxTypedSpriteGroup<ChartEditorNoteSprite>();
renderedNotes.setPosition(gridTiledSprite.x, gridTiledSprite.y);
@ -1148,23 +1156,23 @@ class ChartEditorState extends HaxeUIState
{
// Add functionality to the playbar.
addUIClickListener('playbarPlay', (event:MouseEvent) -> toggleAudioPlayback());
addUIClickListener('playbarStart', (event:MouseEvent) -> playbarButtonPressed = 'playbarStart');
addUIClickListener('playbarBack', (event:MouseEvent) -> playbarButtonPressed = 'playbarBack');
addUIClickListener('playbarForward', (event:MouseEvent) -> playbarButtonPressed = 'playbarForward');
addUIClickListener('playbarEnd', (event:MouseEvent) -> playbarButtonPressed = 'playbarEnd');
addUIClickListener('playbarPlay', _ -> toggleAudioPlayback());
addUIClickListener('playbarStart', _ -> playbarButtonPressed = 'playbarStart');
addUIClickListener('playbarBack', _ -> playbarButtonPressed = 'playbarBack');
addUIClickListener('playbarForward', _ -> playbarButtonPressed = 'playbarForward');
addUIClickListener('playbarEnd', _ -> playbarButtonPressed = 'playbarEnd');
// Add functionality to the menu items.
addUIClickListener('menubarItemNewChart', (event:MouseEvent) -> ChartEditorDialogHandler.openWelcomeDialog(this, true));
addUIClickListener('menubarItemSaveChartAs', (event:MouseEvent) -> exportAllSongData());
addUIClickListener('menubarItemLoadInst', (event:MouseEvent) -> ChartEditorDialogHandler.openUploadInstDialog(this, true));
addUIClickListener('menubarItemNewChart', _ -> ChartEditorDialogHandler.openWelcomeDialog(this, true));
addUIClickListener('menubarItemSaveChartAs', _ -> exportAllSongData());
addUIClickListener('menubarItemLoadInst', _ -> ChartEditorDialogHandler.openUploadInstDialog(this, true));
addUIClickListener('menubarItemUndo', (event:MouseEvent) -> undoLastCommand());
addUIClickListener('menubarItemUndo', _ -> undoLastCommand());
addUIClickListener('menubarItemRedo', (event:MouseEvent) -> redoLastCommand());
addUIClickListener('menubarItemRedo', _ -> redoLastCommand());
addUIClickListener('menubarItemCopy', (event:MouseEvent) -> {
addUIClickListener('menubarItemCopy', function(_) {
// Doesn't use a command because it's not undoable.
SongDataUtils.writeItemsToClipboard(
{
@ -1173,15 +1181,11 @@ class ChartEditorState extends HaxeUIState
});
});
addUIClickListener('menubarItemCut', (event:MouseEvent) -> {
performCommand(new CutItemsCommand(currentNoteSelection, currentEventSelection));
});
addUIClickListener('menubarItemCut', _ -> performCommand(new CutItemsCommand(currentNoteSelection, currentEventSelection)));
addUIClickListener('menubarItemPaste', (event:MouseEvent) -> {
performCommand(new PasteItemsCommand(scrollPositionInMs + playheadPositionInMs));
});
addUIClickListener('menubarItemPaste', _ -> performCommand(new PasteItemsCommand(scrollPositionInMs + playheadPositionInMs)));
addUIClickListener('menubarItemDelete', (event:MouseEvent) -> {
addUIClickListener('menubarItemDelete', function(_) {
if (currentNoteSelection.length > 0 && currentEventSelection.length > 0)
{
performCommand(new RemoveItemsCommand(currentNoteSelection, currentEventSelection));
@ -1200,84 +1204,60 @@ class ChartEditorState extends HaxeUIState
}
});
addUIClickListener('menubarItemSelectAll', (event:MouseEvent) -> {
performCommand(new SelectAllItemsCommand(currentNoteSelection, currentEventSelection));
});
addUIClickListener('menubarItemSelectAll', _ -> performCommand(new SelectAllItemsCommand(currentNoteSelection, currentEventSelection)));
addUIClickListener('menubarItemSelectInverse', (event:MouseEvent) -> {
performCommand(new InvertSelectedItemsCommand(currentNoteSelection, currentEventSelection));
});
addUIClickListener('menubarItemSelectInverse', _ -> performCommand(new InvertSelectedItemsCommand(currentNoteSelection, currentEventSelection)));
addUIClickListener('menubarItemSelectNone', (event:MouseEvent) -> {
performCommand(new DeselectAllItemsCommand(currentNoteSelection, currentEventSelection));
});
addUIClickListener('menubarItemSelectNone', _ -> performCommand(new DeselectAllItemsCommand(currentNoteSelection, currentEventSelection)));
addUIClickListener('menubarItemSelectRegion', (event:MouseEvent) ->
{
// TODO: Implement this.
});
// TODO: Implement these.
// addUIClickListener('menubarItemSelectRegion', _ -> doSomething());
// addUIClickListener('menubarItemSelectBeforeCursor', _ -> doSomething());
// addUIClickListener('menubarItemSelectAfterCursor', _ -> doSomething());
addUIClickListener('menubarItemSelectBeforeCursor', (event:MouseEvent) ->
{
// TODO: Implement this.
});
addUIClickListener('menubarItemAbout', _ -> ChartEditorDialogHandler.openAboutDialog(this));
addUIClickListener('menubarItemSelectAfterCursor', (event:MouseEvent) ->
{
// TODO: Implement this.
});
addUIClickListener('menubarItemUserGuide', _ -> ChartEditorDialogHandler.openUserGuideDialog(this));
addUIClickListener('menubarItemAbout', (event:MouseEvent) -> ChartEditorDialogHandler.openAboutDialog(this));
addUIClickListener('menubarItemUserGuide', (event:MouseEvent) -> ChartEditorDialogHandler.openUserGuideDialog(this));
addUIChangeListener('menubarItemDownscroll', (event:UIEvent) -> {
isViewDownscroll = event.value;
});
addUIChangeListener('menubarItemDownscroll', event -> isViewDownscroll = event.value);
setUICheckboxSelected('menubarItemDownscroll', isViewDownscroll);
addUIChangeListener('menuBarItemThemeLight', (event:UIEvent) -> {
addUIChangeListener('menuBarItemThemeLight', function(event:UIEvent) {
if (event.target.value) currentTheme = ChartEditorTheme.Light;
});
setUICheckboxSelected('menuBarItemThemeLight', currentTheme == ChartEditorTheme.Light);
addUIChangeListener('menuBarItemThemeDark', (event:UIEvent) -> {
addUIChangeListener('menuBarItemThemeDark', function(event:UIEvent) {
if (event.target.value) currentTheme = ChartEditorTheme.Dark;
});
setUICheckboxSelected('menuBarItemThemeDark', currentTheme == ChartEditorTheme.Dark);
addUIChangeListener('menubarItemMetronomeEnabled', (event:UIEvent) -> {
shouldPlayMetronome = event.value;
});
addUIChangeListener('menubarItemMetronomeEnabled', event -> shouldPlayMetronome = event.value);
setUICheckboxSelected('menubarItemMetronomeEnabled', shouldPlayMetronome);
addUIChangeListener('menubarItemPlayerHitsounds', (event:UIEvent) -> {
hitsoundsEnabledPlayer = event.value;
});
addUIChangeListener('menubarItemPlayerHitsounds', event -> hitsoundsEnabledPlayer = event.value);
setUICheckboxSelected('menubarItemPlayerHitsounds', hitsoundsEnabledPlayer);
addUIChangeListener('menubarItemOpponentHitsounds', (event:UIEvent) -> {
hitsoundsEnabledOpponent = event.value;
});
addUIChangeListener('menubarItemOpponentHitsounds', event -> hitsoundsEnabledOpponent = event.value);
setUICheckboxSelected('menubarItemOpponentHitsounds', hitsoundsEnabledOpponent);
var instVolumeLabel:Label = findComponent('menubarLabelVolumeInstrumental', Label);
addUIChangeListener('menubarItemVolumeInstrumental', (event:UIEvent) -> {
addUIChangeListener('menubarItemVolumeInstrumental', function(event:UIEvent) {
var volume:Float = event.value / 100.0;
if (audioInstTrack != null) audioInstTrack.volume = volume;
instVolumeLabel.text = 'Instrumental - ${Std.int(event.value)}%';
});
var vocalsVolumeLabel:Label = findComponent('menubarLabelVolumeVocals', Label);
addUIChangeListener('menubarItemVolumeVocals', (event:UIEvent) -> {
addUIChangeListener('menubarItemVolumeVocals', function(event:UIEvent) {
var volume:Float = event.value / 100.0;
if (audioVocalTrackGroup != null) audioVocalTrackGroup.volume = volume;
vocalsVolumeLabel.text = 'Vocals - ${Std.int(event.value)}%';
});
var playbackSpeedLabel:Label = findComponent('menubarLabelPlaybackSpeed', Label);
addUIChangeListener('menubarItemPlaybackSpeed', (event:UIEvent) -> {
var pitch = event.value * 2.0 / 100.0;
addUIChangeListener('menubarItemPlaybackSpeed', function(event:UIEvent) {
var pitch:Float = event.value * 2.0 / 100.0;
#if FLX_PITCH
if (audioInstTrack != null) audioInstTrack.pitch = pitch;
if (audioVocalTrackGroup != null) audioVocalTrackGroup.pitch = pitch;
@ -1285,40 +1265,45 @@ class ChartEditorState extends HaxeUIState
playbackSpeedLabel.text = 'Playback Speed - ${Std.int(pitch * 100) / 100}x';
});
addUIChangeListener('menubarItemToggleToolboxTools', (event:UIEvent) -> {
ChartEditorToolboxHandler.setToolboxState(this, CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT, event.value);
});
// setUICheckboxSelected('menubarItemToggleToolboxTools', true);
addUIChangeListener('menubarItemToggleToolboxNotes', (event:UIEvent) -> {
ChartEditorToolboxHandler.setToolboxState(this, CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT, event.value);
});
addUIChangeListener('menubarItemToggleToolboxEvents', (event:UIEvent) -> {
ChartEditorToolboxHandler.setToolboxState(this, CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT, event.value);
});
addUIChangeListener('menubarItemToggleToolboxDifficulty', (event:UIEvent) -> {
ChartEditorToolboxHandler.setToolboxState(this, CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT, event.value);
});
addUIChangeListener('menubarItemToggleToolboxMetadata', (event:UIEvent) -> {
ChartEditorToolboxHandler.setToolboxState(this, CHART_EDITOR_TOOLBOX_METADATA_LAYOUT, event.value);
});
addUIChangeListener('menubarItemToggleToolboxCharacters', (event:UIEvent) -> {
ChartEditorToolboxHandler.setToolboxState(this, CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT, event.value);
});
addUIChangeListener('menubarItemToggleToolboxPlayerPreview', (event:UIEvent) -> {
ChartEditorToolboxHandler.setToolboxState(this, CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT, event.value);
});
addUIChangeListener('menubarItemToggleToolboxOpponentPreview', (event:UIEvent) -> {
ChartEditorToolboxHandler.setToolboxState(this, CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT, event.value);
});
addUIChangeListener('menubarItemToggleToolboxTools',
event -> ChartEditorToolboxHandler.setToolboxState(this, CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT, event.value));
addUIChangeListener('menubarItemToggleToolboxNotes',
event -> ChartEditorToolboxHandler.setToolboxState(this, CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT, event.value));
addUIChangeListener('menubarItemToggleToolboxEvents',
event -> ChartEditorToolboxHandler.setToolboxState(this, CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT, event.value));
addUIChangeListener('menubarItemToggleToolboxDifficulty',
event -> ChartEditorToolboxHandler.setToolboxState(this, CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT, event.value));
addUIChangeListener('menubarItemToggleToolboxMetadata',
event -> ChartEditorToolboxHandler.setToolboxState(this, CHART_EDITOR_TOOLBOX_METADATA_LAYOUT, event.value));
addUIChangeListener('menubarItemToggleToolboxCharacters',
event -> ChartEditorToolboxHandler.setToolboxState(this, CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT, event.value));
addUIChangeListener('menubarItemToggleToolboxPlayerPreview',
event -> ChartEditorToolboxHandler.setToolboxState(this, CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT, event.value));
addUIChangeListener('menubarItemToggleToolboxOpponentPreview',
event -> ChartEditorToolboxHandler.setToolboxState(this, CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT, event.value));
// TODO: Pass specific HaxeUI components to add context menus to them.
registerContextMenu(null, Paths.ui('chart-editor/context/test'));
}
/**
* Initialize TurboKeyHandlers and add them to the state (so `update()` is called)
* We can then probe `keyHandler.activated` to see if the key combo's action should be taken.
*/
function setupTurboKeyHandlers():Void
{
add(undoKeyHandler);
add(redoKeyHandler);
add(upKeyHandler);
add(downKeyHandler);
add(pageUpKeyHandler);
add(pageDownKeyHandler);
}
/**
* Setup timers and listerners to handle auto-save.
*/
function setupAutoSave()
function setupAutoSave():Void
{
WindowUtil.windowExit.add(onWindowClose);
saveDataDirty = false;
@ -1327,7 +1312,7 @@ class ChartEditorState extends HaxeUIState
/**
* Called after 5 minutes without saving.
*/
function autoSave()
function autoSave():Void
{
saveDataDirty = false;
@ -1466,42 +1451,49 @@ class ChartEditorState extends HaxeUIState
var shouldPause:Bool = false;
// Up Arrow = Scroll Up
if (FlxG.keys.justPressed.UP)
if (upKeyHandler.activated)
{
scrollAmount = -GRID_SIZE * 0.25 * 5;
scrollAmount = -GRID_SIZE * 0.25 * 5.0;
shouldPause = true;
}
// Down Arrow = Scroll Down
if (FlxG.keys.justPressed.DOWN)
if (downKeyHandler.activated)
{
scrollAmount = GRID_SIZE * 0.25 * 5;
scrollAmount = GRID_SIZE * 0.25 * 5.0;
shouldPause = true;
}
// PAGE UP = Jump Up 1 Measure
if (FlxG.keys.justPressed.PAGEUP)
if (pageUpKeyHandler.activated)
{
scrollAmount = -GRID_SIZE * 4 * Conductor.beatsPerMeasure;
shouldPause = true;
}
if (playbarButtonPressed == 'playbarBack')
{
playbarButtonPressed = '';
scrollAmount = -GRID_SIZE * 4 * Conductor.beatsPerMeasure;
shouldPause = true;
}
// PAGE DOWN = Jump Down 1 Measure
if (FlxG.keys.justPressed.PAGEDOWN)
if (pageDownKeyHandler.activated)
{
scrollAmount = GRID_SIZE * 4 * Conductor.beatsPerMeasure;
shouldPause = true;
}
if (playbarButtonPressed == 'playbarForward')
{
playbarButtonPressed = '';
scrollAmount = GRID_SIZE * 4 * Conductor.beatsPerMeasure;
shouldPause = true;
}
// Mouse Wheel = Scroll
if (FlxG.mouse.wheel != 0 && !FlxG.keys.pressed.CONTROL)
{
scrollAmount = -10 * FlxG.mouse.wheel;
shouldPause = true;
}
// Middle Mouse + Drag = Scroll but move the playhead the same amount.
@ -1532,6 +1524,7 @@ class ChartEditorState extends HaxeUIState
{
playheadAmount = scrollAmount;
scrollAmount = 0;
shouldPause = false;
}
// HOME = Scroll to Top
@ -1540,12 +1533,14 @@ class ChartEditorState extends HaxeUIState
// Scroll amount is the difference between the current position and the top.
scrollAmount = 0 - this.scrollPositionInPixels;
playheadAmount = 0 - this.playheadPositionInPixels;
shouldPause = true;
}
if (playbarButtonPressed == 'playbarStart')
{
playbarButtonPressed = '';
scrollAmount = 0 - this.scrollPositionInPixels;
playheadAmount = 0 - this.playheadPositionInPixels;
shouldPause = true;
}
// END = Scroll to Bottom
@ -1553,11 +1548,13 @@ class ChartEditorState extends HaxeUIState
{
// Scroll amount is the difference between the current position and the bottom.
scrollAmount = this.songLengthInPixels - this.scrollPositionInPixels;
shouldPause = true;
}
if (playbarButtonPressed == 'playbarEnd')
{
playbarButtonPressed = '';
scrollAmount = this.songLengthInPixels - this.scrollPositionInPixels;
shouldPause = true;
}
// Apply the scroll amount.
@ -1566,9 +1563,10 @@ class ChartEditorState extends HaxeUIState
// Resync the conductor and audio tracks.
if (scrollAmount != 0 || playheadAmount != 0) moveSongToScrollPosition();
if (shouldPause) stopAudioPlayback();
}
function handleZoom()
function handleZoom():Void
{
if (FlxG.keys.justPressed.MINUS)
{
@ -1591,7 +1589,7 @@ class ChartEditorState extends HaxeUIState
}
}
function handleSnap()
function handleSnap():Void
{
if (FlxG.keys.justPressed.LEFT)
{
@ -1607,7 +1605,7 @@ class ChartEditorState extends HaxeUIState
/**
* Handle display of the mouse cursor.
*/
function handleCursor()
function handleCursor():Void
{
// Note: If a menu is open in HaxeUI, don't handle cursor behavior.
var shouldHandleCursor = !isCursorOverHaxeUI || (selectionBoxStartPos != null);
@ -2330,7 +2328,7 @@ class ChartEditorState extends HaxeUIState
/**
* Handles display elements for the playbar at the bottom.
*/
function handlePlaybar()
function handlePlaybar():Void
{
// Make sure the playbar is never nudged out of the correct spot.
playbarHeadLayout.x = 4;
@ -2362,7 +2360,7 @@ class ChartEditorState extends HaxeUIState
/**
* Handle keybinds for File menu items.
*/
function handleFileKeybinds()
function handleFileKeybinds():Void
{
// CTRL + Q = Quit to Menu
if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.Q)
@ -2374,48 +2372,20 @@ class ChartEditorState extends HaxeUIState
/**
* Handle keybinds for edit menu items.
*/
function handleEditKeybinds()
function handleEditKeybinds():Void
{
// CTRL + Z = Undo
if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.Z)
if (undoKeyHandler.activated)
{
undoLastCommand();
}
if (FlxG.keys.pressed.CONTROL && FlxG.keys.pressed.Z && !FlxG.keys.pressed.Y)
{
undoHeldTime += FlxG.elapsed;
}
else
{
undoHeldTime = 0;
}
if (undoHeldTime > RAPID_UNDO_DELAY + RAPID_UNDO_INTERVAL)
{
undoLastCommand();
undoHeldTime -= RAPID_UNDO_INTERVAL;
}
// CTRL + Y = Redo
if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.Y)
if (redoKeyHandler.activated)
{
redoLastCommand();
}
if (FlxG.keys.pressed.CONTROL && FlxG.keys.pressed.Y && !FlxG.keys.pressed.Z)
{
redoHeldTime += FlxG.elapsed;
}
else
{
redoHeldTime = 0;
}
if (redoHeldTime > RAPID_UNDO_DELAY + RAPID_UNDO_INTERVAL)
{
redoLastCommand();
redoHeldTime -= RAPID_UNDO_INTERVAL;
}
// CTRL + C = Copy
if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.C)
{
@ -2485,25 +2455,25 @@ class ChartEditorState extends HaxeUIState
/**
* Handle keybinds for View menu items.
*/
function handleViewKeybinds() {}
function handleViewKeybinds():Void {}
/**
* Handle keybinds for Help menu items.
*/
function handleHelpKeybinds()
function handleHelpKeybinds():Void
{
// F1 = Open Help
if (FlxG.keys.justPressed.F1) ChartEditorDialogHandler.openUserGuideDialog(this);
}
function handleToolboxes()
function handleToolboxes():Void
{
handleDifficultyToolbox();
handlePlayerPreviewToolbox();
handleOpponentPreviewToolbox();
}
function handleDifficultyToolbox()
function handleDifficultyToolbox():Void
{
if (difficultySelectDirty)
{
@ -2552,7 +2522,7 @@ class ChartEditorState extends HaxeUIState
}
}
function handlePlayerPreviewToolbox()
function handlePlayerPreviewToolbox():Void
{
// Manage the Select Difficulty tree view.
var charPreviewToolbox = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT);
@ -2564,7 +2534,7 @@ class ChartEditorState extends HaxeUIState
currentPlayerCharacterPlayer = charPlayer;
}
function handleOpponentPreviewToolbox()
function handleOpponentPreviewToolbox():Void
{
// Manage the Select Difficulty tree view.
var charPreviewToolbox = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT);
@ -2576,7 +2546,7 @@ class ChartEditorState extends HaxeUIState
currentOpponentCharacterPlayer = charPlayer;
}
override function dispatchEvent(event:ScriptEvent)
override function dispatchEvent(event:ScriptEvent):Void
{
super.dispatchEvent(event);
@ -2660,9 +2630,9 @@ class ChartEditorState extends HaxeUIState
}
}
function addDifficulty(variation:String) {}
function addDifficulty(variation:String):Void {}
function addVariation(variationId:String)
function addVariation(variationId:String):Void
{
// Create a new variation with the specified ID.
songMetadata.set(variationId, currentSongMetadata.clone(variationId));
@ -2673,7 +2643,7 @@ class ChartEditorState extends HaxeUIState
/**
* Handle the player preview/gameplay test area on the left side.
*/
function handlePlayerDisplay() {}
function handlePlayerDisplay():Void {}
/**
* Handles the note preview/scroll area on the right side.
@ -2683,7 +2653,7 @@ class ChartEditorState extends HaxeUIState
* - Scrolling the note preview area down if the note preview is taller than the screen,
* and the viewport nears the end of the visible area.
*/
function handleNotePreview()
function handleNotePreview():Void
{
//
if (notePreviewDirty)
@ -2703,13 +2673,13 @@ class ChartEditorState extends HaxeUIState
* Perform a spot update on the note preview, by editing the note preview
* only where necessary. More efficient than a full update.
*/
function updateNotePreview(note:SongNoteData, ?deleteNote:Bool = false) {}
function updateNotePreview(note:SongNoteData, ?deleteNote:Bool = false):Void {}
/**
* Handles passive behavior of the menu bar, such as updating labels or enabled/disabled status.
* Does not handle onClick ACTIONS of the menubar.
*/
function handleMenubar()
function handleMenubar():Void
{
if (commandHistoryDirty)
{
@ -2765,7 +2735,7 @@ class ChartEditorState extends HaxeUIState
/**
* Handle syncronizing the conductor with the music playback.
*/
function handleMusicPlayback()
function handleMusicPlayback():Void
{
if (audioInstTrack != null && audioInstTrack.playing)
{
@ -2856,21 +2826,25 @@ class ChartEditorState extends HaxeUIState
}
}
function startAudioPlayback()
function startAudioPlayback():Void
{
if (audioInstTrack != null) audioInstTrack.play();
if (audioVocalTrackGroup != null) audioVocalTrackGroup.play();
if (audioVocalTrackGroup != null) audioVocalTrackGroup.play();
setComponentText('playbarPlay', '||');
}
function stopAudioPlayback()
function stopAudioPlayback():Void
{
if (audioInstTrack != null) audioInstTrack.pause();
if (audioVocalTrackGroup != null) audioVocalTrackGroup.pause();
if (audioVocalTrackGroup != null) audioVocalTrackGroup.pause();
setComponentText('playbarPlay', '>');
}
function toggleAudioPlayback()
function toggleAudioPlayback():Void
{
if (audioInstTrack == null) return;
@ -2884,7 +2858,7 @@ class ChartEditorState extends HaxeUIState
}
}
function handlePlayhead()
function handlePlayhead():Void
{
// Place notes at the playhead.
// TODO: Add the ability to switch modes.
@ -2973,52 +2947,64 @@ class ChartEditorState extends HaxeUIState
* Loads an instrumental from an absolute file path, replacing the current instrumental.
*
* @param path The absolute path to the audio file.
* @return Success or failure.
*/
public function loadInstrumentalFromPath(path:String):Void
public function loadInstrumentalFromPath(path:Path):Bool
{
#if sys
// Validate file extension.
var fileExtension:String = Path.extension(path);
if (!SUPPORTED_MUSIC_FORMATS.contains(fileExtension))
if (!SUPPORTED_MUSIC_FORMATS.contains(path.ext))
{
trace('[WARN] Unsupported file extension: $fileExtension');
return;
return false;
}
var fileBytes:haxe.io.Bytes = sys.io.File.getBytes(path);
loadInstrumentalFromBytes(fileBytes);
var fileBytes:haxe.io.Bytes = sys.io.File.getBytes(path.toString());
return loadInstrumentalFromBytes(fileBytes, '${path.file}.${path.ext}');
#else
trace("[WARN] This platform can't load audio from a file path, you'll need to fetch the bytes some other way.");
return false;
#end
}
/**
* Loads an instrumental from audio byte data, replacing the current instrumental.
* @param bytes The audio byte data.
* @param fileName The name of the file, if available. Used for notifications.
* @return Success or failure.
*/
public function loadInstrumentalFromBytes(bytes:haxe.io.Bytes):Void
public function loadInstrumentalFromBytes(bytes:haxe.io.Bytes, fileName:String = null):Bool
{
var openflSound = new openfl.media.Sound();
var openflSound:openfl.media.Sound = new openfl.media.Sound();
openflSound.loadCompressedDataFromByteArray(openfl.utils.ByteArray.fromBytes(bytes), bytes.length);
audioInstTrack = FlxG.sound.load(openflSound, 1.0, false);
audioInstTrack.autoDestroy = false;
audioInstTrack.pause();
// Tell the user the load was successful.
// TODO: Un-bork this.
// showNotification('Loaded instrumental track successfully.');
postLoadInstrumental();
return true;
}
public function loadInstrumentalFromAsset(path:String):Void
/**
* Loads an instrumental from an OpenFL asset, replacing the current instrumental.
* @param path The path to the asset. Use `Paths` to build this.
* @return Success or failure.
*/
public function loadInstrumentalFromAsset(path:String):Bool
{
var instTrack = FlxG.sound.load(path, 1.0, false);
audioInstTrack = instTrack;
var instTrack:FlxSound = FlxG.sound.load(path, 1.0, false);
if (instTrack != null)
{
audioInstTrack = instTrack;
postLoadInstrumental();
postLoadInstrumental();
return true;
}
return false;
}
function postLoadInstrumental()
function postLoadInstrumental():Void
{
// Prevent the time from skipping back to 0 when the song ends.
audioInstTrack.onComplete = function() {
@ -3042,42 +3028,47 @@ class ChartEditorState extends HaxeUIState
/**
* Loads a vocal track from an absolute file path.
* @param path The absolute path to the audio file.
* @param charKey The character to load the vocal track for.
*/
public function loadVocalsFromPath(path:String, ?charKey:String):Void
public function loadVocalsFromPath(path:Path, charKey:String = null):Bool
{
#if sys
var fileBytes:haxe.io.Bytes = sys.io.File.getBytes(path);
loadVocalsFromBytes(fileBytes, charKey);
var fileBytes:haxe.io.Bytes = sys.io.File.getBytes(path.toString());
return loadVocalsFromBytes(fileBytes, charKey);
#else
trace("[WARN] This platform can't load audio from a file path, you'll need to fetch the bytes some other way.");
return false;
#end
}
public function loadVocalsFromAsset(path:String, ?charKey:String):Void
public function loadVocalsFromAsset(path:String, charKey:String = null):Bool
{
var vocalTrack:FlxSound = FlxG.sound.load(path, 1.0, false);
audioVocalTrackGroup.add(vocalTrack);
if (vocalTrack != null)
{
audioVocalTrackGroup.add(vocalTrack);
return true;
}
return false;
}
/**
* Loads a vocal track from audio byte data.
*/
public function loadVocalsFromBytes(bytes:haxe.io.Bytes, ?charKey:String):Void
public function loadVocalsFromBytes(bytes:haxe.io.Bytes, charKey:String = null):Bool
{
var openflSound = new openfl.media.Sound();
openflSound.loadCompressedDataFromByteArray(openfl.utils.ByteArray.fromBytes(bytes), bytes.length);
var vocalTrack:FlxSound = FlxG.sound.load(openflSound, 1.0, false);
audioVocalTrackGroup.add(vocalTrack);
// Tell the user the load was successful.
// TODO: Un-bork this.
// showNotification('Loaded instrumental track successfully.');
return true;
}
/**
* Fetch's a song's existing chart and audio and loads it, replacing the current song.
*/
public function loadSongAsTemplate(songId:String)
public function loadSongAsTemplate(songId:String):Void
{
var song:Song = SongDataParser.fetchSong(songId);
@ -3089,6 +3080,7 @@ class ChartEditorState extends HaxeUIState
// Load the song metadata.
var rawSongMetadata:Array<SongMetadata> = song.getRawMetadata();
var songName:String = rawSongMetadata[0].songName;
this.songMetadata = new Map<String, SongMetadata>();
@ -3112,14 +3104,20 @@ class ChartEditorState extends HaxeUIState
loadInstrumentalFromAsset(Paths.inst(songId));
loadVocalsFromAsset(Paths.voices(songId));
// showNotification('Loaded song ${songId}.');
NotificationManager.instance.addNotification(
{
title: 'Success',
body: 'Loaded song ($songName)',
type: NotificationType.Success,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
});
}
/**
* When setting the scroll position, except when automatically scrolling during song playback,
* we need to update the conductor's current step time and the timestamp of the audio tracks.
*/
function moveSongToScrollPosition()
function moveSongToScrollPosition():Void
{
// Update the songPosition in the Conductor.
Conductor.update(scrollPositionInMs);
@ -3168,7 +3166,7 @@ class ChartEditorState extends HaxeUIState
return;
}
var command = undoHistory.pop();
var command:ChartEditorCommand = undoHistory.pop();
undoCommand(command);
}
@ -3183,11 +3181,11 @@ class ChartEditorState extends HaxeUIState
return;
}
var command = redoHistory.pop();
var command:ChartEditorCommand = redoHistory.pop();
performCommand(command, false);
}
function sortChartData()
function sortChartData():Void
{
currentSongChartNoteData.sort(function(a:SongNoteData, b:SongNoteData):Int {
return FlxSort.byValues(FlxSort.ASCENDING, a.time, b.time);
@ -3198,7 +3196,7 @@ class ChartEditorState extends HaxeUIState
});
}
function playMetronomeTick(?high:Bool = false)
function playMetronomeTick(?high:Bool = false):Void
{
playSound(Paths.sound('pianoStuff/piano-${high ? '001' : '008'}'));
}
@ -3217,7 +3215,7 @@ class ChartEditorState extends HaxeUIState
* Play a sound effect.
* Automatically cleans up after itself and recycles previous FlxSound instances if available, for performance.
*/
function playSound(path:String)
function playSound(path:String):Void
{
var snd:FlxSound = FlxG.sound.list.recycle(FlxSound);
snd.loadEmbedded(FlxG.sound.cache(path));
@ -3226,7 +3224,7 @@ class ChartEditorState extends HaxeUIState
snd.play();
}
override function destroy()
override function destroy():Void
{
super.destroy();
@ -3282,7 +3280,14 @@ class ChartEditorState extends HaxeUIState
if (force)
{
var targetPath:String = tmp ? Path.join([FileUtil.getTempDir(), 'chart-editor-exit-${DateUtil.generateTimestamp()}.zip']) : Path.join(['./backups/', 'chart-editor-exit-${DateUtil.generateTimestamp()}.zip']);
var targetPath:String = if (tmp)
{
Path.join([FileUtil.getTempDir(), 'chart-editor-exit-${DateUtil.generateTimestamp()}.zip']);
}
else
{
Path.join(['./backups/', 'chart-editor-exit-${DateUtil.generateTimestamp()}.zip']);
}
// We have to force write because the program will die before the save dialog is closed.
trace('Force exporting to $targetPath...');
@ -3291,11 +3296,11 @@ class ChartEditorState extends HaxeUIState
}
// Prompt and save.
var onSave:Array<String>->Void = (paths:Array<String>) -> {
var onSave:Array<String>->Void = function(paths:Array<String>) {
trace('Successfully exported files.');
};
var onCancel:Void->Void = () -> {
var onCancel:Void->Void = function() {
trace('Export cancelled.');
};

View file

@ -1,14 +1,12 @@
package funkin.ui.haxeui;
import haxe.ui.containers.menus.MenuCheckBox;
import haxe.ui.components.CheckBox;
import haxe.ui.events.DragEvent;
import haxe.ui.events.MouseEvent;
import haxe.ui.events.UIEvent;
import haxe.ui.RuntimeComponentBuilder;
import haxe.ui.containers.menus.MenuCheckBox;
import haxe.ui.core.Component;
import haxe.ui.core.Screen;
import haxe.ui.events.MouseEvent;
import haxe.ui.events.UIEvent;
import haxe.ui.RuntimeComponentBuilder;
import lime.app.Application;
class HaxeUIState extends MusicBeatState
@ -23,7 +21,7 @@ class HaxeUIState extends MusicBeatState
_componentKey = key;
}
override function create()
override function create():Void
{
super.create();
@ -31,7 +29,7 @@ class HaxeUIState extends MusicBeatState
if (component != null) add(component);
}
public function buildComponent(assetPath:String)
public function buildComponent(assetPath:String):Component
{
try
{
@ -81,15 +79,13 @@ class HaxeUIState extends MusicBeatState
{
if (target == null)
{
Screen.instance.registerEvent(MouseEvent.RIGHT_CLICK, function(e:MouseEvent)
{
Screen.instance.registerEvent(MouseEvent.RIGHT_CLICK, function(e:MouseEvent) {
showContextMenu(assetPath, e.screenX, e.screenY);
});
}
else
{
target.registerEvent(MouseEvent.RIGHT_CLICK, function(e:MouseEvent)
{
target.registerEvent(MouseEvent.RIGHT_CLICK, function(e:MouseEvent) {
showContextMenu(assetPath, e.screenX, e.screenY);
});
}
@ -98,7 +94,7 @@ class HaxeUIState extends MusicBeatState
/**
* Add an onClick listener to a HaxeUI menu bar item.
*/
function addUIClickListener(key:String, callback:MouseEvent->Void)
function addUIClickListener(key:String, callback:MouseEvent->Void):Void
{
var target:Component = findComponent(key);
if (target == null)
@ -112,10 +108,24 @@ class HaxeUIState extends MusicBeatState
}
}
function setComponentText(key:String, text:String):Void
{
var target:Component = findComponent(key);
if (target == null)
{
// Gracefully handle the case where the item can't be located.
trace('WARN: Could not locate menu item: $key');
}
else
{
target.text = text;
}
}
/**
* Add an onChange listener to a HaxeUI input component such as a slider or text field.
*/
function addUIChangeListener(key:String, callback:UIEvent->Void)
function addUIChangeListener(key:String, callback:UIEvent->Void):Void
{
var target:Component = findComponent(key);
if (target == null)
@ -179,7 +189,7 @@ class HaxeUIState extends MusicBeatState
return component.findComponent(criteria, type, recursive, searchType);
}
override function destroy()
override function destroy():Void
{
if (component != null) remove(component);
component = null;