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

View file

@ -42,14 +42,14 @@
"name": "haxeui-core", "name": "haxeui-core",
"type": "git", "type": "git",
"dir": null, "dir": null,
"ref": "e5cf78d", "ref": "59157d2",
"url": "https://github.com/haxeui/haxeui-core/" "url": "https://github.com/haxeui/haxeui-core/"
}, },
{ {
"name": "haxeui-flixel", "name": "haxeui-flixel",
"type": "git", "type": "git",
"dir": null, "dir": null,
"ref": "f03bb6d", "ref": "d353389",
"url": "https://github.com/haxeui/haxeui-flixel" "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 function set_active(value:Bool):Bool
{ {
this.active = value; return this.active = value;
return value;
} }
public var moduleId(default, null):String = 'UNKNOWN'; 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) if (!exists || x == value) return x; // early return (no need to transform)
transformChildren(xTransform, value - x); // offset transformChildren(xTransform, value - x); // offset
x = value; return x = value;
return x;
} }
override function set_y(value:Float):Float override function set_y(value:Float):Float
{ {
if (exists && y != value) transformChildren(yTransform, value - y); // offset if (exists && y != value) transformChildren(yTransform, value - y); // offset
y = value; return y = value;
return y;
} }
override function set_angle(value:Float):Float override function set_angle(value:Float):Float
{ {
if (exists && angle != value) transformChildren(angleTransform, value - angle); // offset if (exists && angle != value) transformChildren(angleTransform, value - angle); // offset
angle = value; return angle = value;
return angle;
} }
override function set_alpha(value:Float):Float override function set_alpha(value:Float):Float
@ -462,43 +459,37 @@ class AnimateAtlasCharacter extends BaseCharacter
{ {
transformChildren(directAlphaTransform, value); transformChildren(directAlphaTransform, value);
} }
alpha = value; return alpha = value;
return alpha;
} }
override function set_facing(value:Int):Int override function set_facing(value:Int):Int
{ {
if (exists && facing != value) transformChildren(facingTransform, value); if (exists && facing != value) transformChildren(facingTransform, value);
facing = value; return facing = value;
return facing;
} }
override function set_flipX(value:Bool):Bool override function set_flipX(value:Bool):Bool
{ {
if (exists && flipX != value) transformChildren(flipXTransform, value); if (exists && flipX != value) transformChildren(flipXTransform, value);
flipX = value; return flipX = value;
return flipX;
} }
override function set_flipY(value:Bool):Bool override function set_flipY(value:Bool):Bool
{ {
if (exists && flipY != value) transformChildren(flipYTransform, value); if (exists && flipY != value) transformChildren(flipYTransform, value);
flipY = value; return flipY = value;
return flipY;
} }
override function set_moves(value:Bool):Bool override function set_moves(value:Bool):Bool
{ {
if (exists && moves != value) transformChildren(movesTransform, value); if (exists && moves != value) transformChildren(movesTransform, value);
moves = value; return moves = value;
return moves;
} }
override function set_immovable(value:Bool):Bool override function set_immovable(value:Bool):Bool
{ {
if (exists && immovable != value) transformChildren(immovableTransform, value); if (exists && immovable != value) transformChildren(immovableTransform, value);
immovable = value; return immovable = value;
return immovable;
} }
override function set_solid(value:Bool):Bool override function set_solid(value:Bool):Bool
@ -510,15 +501,13 @@ class AnimateAtlasCharacter extends BaseCharacter
override function set_color(value:Int):Int override function set_color(value:Int):Int
{ {
if (exists && color != value) transformChildren(gColorTransform, value); if (exists && color != value) transformChildren(gColorTransform, value);
color = value; return color = value;
return color;
} }
override function set_blend(value:BlendMode):BlendMode override function set_blend(value:BlendMode):BlendMode
{ {
if (exists && blend != value) transformChildren(blendTransform, value); if (exists && blend != value) transformChildren(blendTransform, value);
blend = value; return blend = value;
return blend;
} }
override function set_clipRect(rect:FlxRect):FlxRect 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. * 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]; if (diffId == null) diffId = difficulties.keys().array()[0];

View file

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

View file

@ -1,18 +1,16 @@
package funkin.ui.debug.charting; package funkin.ui.debug.charting;
import haxe.io.Path;
import flixel.FlxSprite;
import flixel.util.FlxTimer; import flixel.util.FlxTimer;
import funkin.input.Cursor; import funkin.input.Cursor;
import funkin.play.character.BaseCharacter; import funkin.play.character.BaseCharacter;
import funkin.play.character.CharacterData.CharacterDataParser; import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.play.song.Song;
import funkin.play.song.SongData.SongDataParser; import funkin.play.song.SongData.SongDataParser;
import funkin.play.song.SongData.SongPlayableChar; import funkin.play.song.SongData.SongPlayableChar;
import funkin.play.song.SongData.SongTimeChange; import funkin.play.song.SongData.SongTimeChange;
import haxe.ui.core.Component; import haxe.io.Path;
import haxe.ui.components.Button; import haxe.ui.components.Button;
import haxe.ui.components.DropDown; import haxe.ui.components.DropDown;
import haxe.ui.components.Image;
import haxe.ui.components.Label; import haxe.ui.components.Label;
import haxe.ui.components.Link; import haxe.ui.components.Link;
import haxe.ui.components.NumberStepper; 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.PropertyGrid;
import haxe.ui.containers.properties.PropertyGroup; import haxe.ui.containers.properties.PropertyGroup;
import haxe.ui.containers.VBox; import haxe.ui.containers.VBox;
import haxe.ui.events.MouseEvent; import haxe.ui.core.Component;
import haxe.ui.events.UIEvent; import haxe.ui.events.UIEvent;
import haxe.ui.notifications.NotificationManager;
import haxe.ui.notifications.NotificationType;
using Lambda; using Lambda;
@ -43,7 +43,9 @@ class ChartEditorDialogHandler
static final CHART_EDITOR_DIALOG_USER_GUIDE_LAYOUT:String = Paths.ui('chart-editor/dialogs/user-guide'); 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 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. * 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 public static function openWelcomeDialog(state:ChartEditorState, closable:Bool = true):Dialog
{ {
var dialog:Dialog = openDialog(state, CHART_EDITOR_DIALOG_WELCOME_LAYOUT, true, closable); 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. // Add handlers to the "Create From Song" section.
var linkCreateBasic:Link = dialog.findComponent('splashCreateFromSongBasic', Link); var linkCreateBasic:Link = dialog.findComponent('splashCreateFromSongBasic', Link);
linkCreateBasic.onClick = (_event) -> { linkCreateBasic.onClick = function(_event) {
// Hide the welcome dialog
dialog.hideDialog(DialogButton.CANCEL); dialog.hideDialog(DialogButton.CANCEL);
// Create song wizard //
var uploadInstDialog = openUploadInstDialog(state, false); // Create Song Wizard
uploadInstDialog.onDialogClosed = (_event) -> { //
// Step 1. Upload Instrumental
var uploadInstDialog:Dialog = openUploadInstDialog(state, false);
uploadInstDialog.onDialogClosed = function(_event) {
state.isHaxeUIDialogOpen = false; state.isHaxeUIDialogOpen = false;
if (_event.button == DialogButton.APPLY) if (_event.button == DialogButton.APPLY)
{ {
var songMetadataDialog = openSongMetadataDialog(state); // Step 2. Song Metadata
songMetadataDialog.onDialogClosed = (_event) -> { var songMetadataDialog:Dialog = openSongMetadataDialog(state);
songMetadataDialog.onDialogClosed = function(_event) {
state.isHaxeUIDialogOpen = false; state.isHaxeUIDialogOpen = false;
if (_event.button == DialogButton.APPLY) 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 splashTemplateContainer:VBox = dialog.findComponent('splashTemplateContainer', VBox);
var songList:Array<String> = SongDataParser.listSongIds(); var songList:Array<String> = SongDataParser.listSongIds();
for (targetSongId in songList) for (targetSongId in songList)
{ {
var songData = SongDataParser.fetchSong(targetSongId); var songData:Song = SongDataParser.fetchSong(targetSongId);
if (songData == null) continue; if (songData == null) continue;
var songName = songData.getDifficulty().songName; var songName:String = songData.getDifficulty().songName;
var linkTemplateSong:Link = new Link(); var linkTemplateSong:Link = new Link();
linkTemplateSong.text = songName; linkTemplateSong.text = songName;
linkTemplateSong.onClick = (_event) -> { linkTemplateSong.onClick = function(_event) {
dialog.hideDialog(DialogButton.CANCEL); dialog.hideDialog(DialogButton.CANCEL);
// Load song from template // Load song from template
@ -130,42 +130,99 @@ class ChartEditorDialogHandler
return dialog; 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 public static function openUploadInstDialog(state:ChartEditorState, ?closable:Bool = true):Dialog
{ {
var dialog:Dialog = openDialog(state, CHART_EDITOR_DIALOG_UPLOAD_INST_LAYOUT, true, closable); 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); var instrumentalBox:Box = dialog.findComponent('instrumentalBox', Box);
instrumentalBox.onMouseOver = (_event) -> { instrumentalBox.onMouseOver = function(_event) {
instrumentalBox.swapClass('upload-bg', 'upload-bg-hover'); instrumentalBox.swapClass('upload-bg', 'upload-bg-hover');
Cursor.cursorMode = Pointer; Cursor.cursorMode = Pointer;
} }
instrumentalBox.onMouseOut = (_event) -> { instrumentalBox.onMouseOut = function(_event) {
instrumentalBox.swapClass('upload-bg-hover', 'upload-bg'); instrumentalBox.swapClass('upload-bg-hover', 'upload-bg');
Cursor.cursorMode = Default; Cursor.cursorMode = Default;
} }
var onDropFile:String->Void; var onDropFile:String->Void;
instrumentalBox.onClick = (_event) -> { instrumentalBox.onClick = function(_event) {
Dialogs.openBinaryFile("Open Instrumental", [ Dialogs.openBinaryFile('Open Instrumental', [
{label: "Audio File (.ogg)", extension: "ogg"}], function(selectedFile) { {label: 'Audio File (.ogg)', extension: 'ogg'}], function(selectedFile:SelectedFileInfo) {
if (selectedFile != null) if (selectedFile != null)
{ {
trace('Selected file: ' + selectedFile); if (state.loadInstrumentalFromBytes(selectedFile.bytes))
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); dialog.hideDialog(DialogButton.APPLY);
removeDropHandler(onDropFile); 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) -> { onDropFile = function(pathStr:String) {
trace('Dropped file: ' + path); var path:Path = new Path(pathStr);
state.loadInstrumentalFromPath(path); 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); dialog.hideDialog(DialogButton.APPLY);
removeDropHandler(onDropFile); 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); addDropHandler(instrumentalBox, onDropFile);
@ -213,19 +270,14 @@ class ChartEditorDialogHandler
{ {
// a VERY short timer to wait for the mouse position to update // a VERY short timer to wait for the mouse position to update
new FlxTimer().start(0.01, function(_) { new FlxTimer().start(0.01, function(_) {
trace("mouseX: " + FlxG.mouse.screenX + ", mouseY: " + FlxG.mouse.screenY);
for (handler in dropHandlers) for (handler in dropHandlers)
{ {
if (handler.component.hitTest(FlxG.mouse.screenX, FlxG.mouse.screenY)) if (handler.component.hitTest(FlxG.mouse.screenX, FlxG.mouse.screenY))
{ {
trace('File dropped on component! ' + handler.component.id);
handler.handler(path); handler.handler(path);
return; 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 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); var dialogSongName:TextField = dialog.findComponent('dialogSongName', TextField);
dialogSongName.onChange = function(event:UIEvent) { dialogSongName.onChange = function(event:UIEvent) {
var valid:Bool = event.target.text != null && event.target.text != ''; var valid:Bool = event.target.text != null && event.target.text != '';
@ -272,25 +330,23 @@ class ChartEditorDialogHandler
var dialogStage:DropDown = dialog.findComponent('dialogStage', DropDown); var dialogStage:DropDown = dialog.findComponent('dialogStage', DropDown);
dialogStage.onChange = function(event:UIEvent) { dialogStage.onChange = function(event:UIEvent) {
var valid = event.data != null && event.data.id != null; if (event.data == null && event.data.id == null) return;
if (event.data.id == null) return;
state.currentSongMetadata.playData.stage = event.data.id; state.currentSongMetadata.playData.stage = event.data.id;
}; };
state.currentSongMetadata.playData.stage = null; state.currentSongMetadata.playData.stage = null;
var dialogNoteSkin:DropDown = dialog.findComponent('dialogNoteSkin', DropDown); var dialogNoteSkin:DropDown = dialog.findComponent('dialogNoteSkin', DropDown);
dialogNoteSkin.onChange = (event:UIEvent) -> { dialogNoteSkin.onChange = function(event:UIEvent) {
if (event.data.id == null) return; if (event.data.id == null) return;
state.currentSongMetadata.playData.noteSkin = event.data.id; state.currentSongMetadata.playData.noteSkin = event.data.id;
}; };
state.currentSongMetadata.playData.noteSkin = null; state.currentSongMetadata.playData.noteSkin = null;
var dialogBPM:NumberStepper = dialog.findComponent('dialogBPM', NumberStepper); var dialogBPM:NumberStepper = dialog.findComponent('dialogBPM', NumberStepper);
dialogBPM.onChange = (event:UIEvent) -> { dialogBPM.onChange = function(event:UIEvent) {
if (event.value == null || event.value <= 0) return; 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) if (timeChanges == null || timeChanges.length == 0)
{ {
timeChanges = [new SongTimeChange(-1, 0, event.value, 4, 4, [4, 4, 4, 4])]; 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 dialogCharGrid:PropertyGrid = dialog.findComponent('dialogCharGrid', PropertyGrid);
var dialogCharAdd:Button = dialog.findComponent('dialogCharAdd', Button); var dialogCharAdd:Button = dialog.findComponent('dialogCharAdd', Button);
dialogCharAdd.onClick = (_event) -> { dialogCharAdd.onClick = function(event:UIEvent) {
var charGroup:PropertyGroup; var charGroup:PropertyGroup;
charGroup = buildCharGroup(state, null, () -> { charGroup = buildCharGroup(state, null, () -> dialogCharGrid.removeComponent(charGroup));
dialogCharGrid.removeComponent(charGroup);
});
dialogCharGrid.addComponent(charGroup); dialogCharGrid.addComponent(charGroup);
}; };
@ -321,18 +375,16 @@ class ChartEditorDialogHandler
dialogCharGrid.addComponent(buildCharGroup(state, 'bf', null)); dialogCharGrid.addComponent(buildCharGroup(state, 'bf', null));
var dialogContinue:Button = dialog.findComponent('dialogContinue', Button); var dialogContinue:Button = dialog.findComponent('dialogContinue', Button);
dialogContinue.onClick = (_event) -> { dialogContinue.onClick = (_event) -> dialog.hideDialog(DialogButton.APPLY);
dialog.hideDialog(DialogButton.APPLY);
};
return dialog; 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()}'; if (groupKey == null) groupKey = 'newChar${state.currentSongMetadata.playData.playableChars.keys().count()}';
var result = state.currentSongMetadata.playData.playableChars.get(groupKey); var result = state.currentSongMetadata.playData.playableChars.get(groupKey);
@ -344,24 +396,24 @@ class ChartEditorDialogHandler
return result; return result;
} }
var moveCharGroup = (target:String) -> { var moveCharGroup:String->Void = function(target:String) {
var charData = getCharData(); var charData = getCharData();
state.currentSongMetadata.playData.playableChars.remove(groupKey); state.currentSongMetadata.playData.playableChars.remove(groupKey);
state.currentSongMetadata.playData.playableChars.set(target, charData); state.currentSongMetadata.playData.playableChars.set(target, charData);
groupKey = target; groupKey = target;
} }
var removeGroup = () -> { var removeGroup:Void->Void = function() {
state.currentSongMetadata.playData.playableChars.remove(groupKey); state.currentSongMetadata.playData.playableChars.remove(groupKey);
removeFunc(); removeFunc();
} }
var charData = getCharData(); var charData:SongPlayableChar = getCharData();
var charGroup:PropertyGroup = cast state.buildComponent(CHART_EDITOR_DIALOG_SONG_METADATA_CHARGROUP_LAYOUT); var charGroup:PropertyGroup = cast state.buildComponent(CHART_EDITOR_DIALOG_SONG_METADATA_CHARGROUP_LAYOUT);
var charGroupPlayer:DropDown = charGroup.findComponent('charGroupPlayer', DropDown); var charGroupPlayer:DropDown = charGroup.findComponent('charGroupPlayer', DropDown);
charGroupPlayer.onChange = (event:UIEvent) -> { charGroupPlayer.onChange = function(event:UIEvent) {
charGroup.text = event.data.text; charGroup.text = event.data.text;
moveCharGroup(event.data.id); moveCharGroup(event.data.id);
}; };
@ -373,19 +425,19 @@ class ChartEditorDialogHandler
} }
var charGroupOpponent:DropDown = charGroup.findComponent('charGroupOpponent', DropDown); var charGroupOpponent:DropDown = charGroup.findComponent('charGroupOpponent', DropDown);
charGroupOpponent.onChange = (event:UIEvent) -> { charGroupOpponent.onChange = function(event:UIEvent) {
charData.opponent = event.data.id; charData.opponent = event.data.id;
}; };
charGroupOpponent.value = getCharData().opponent; charGroupOpponent.value = getCharData().opponent;
var charGroupGirlfriend:DropDown = charGroup.findComponent('charGroupGirlfriend', DropDown); var charGroupGirlfriend:DropDown = charGroup.findComponent('charGroupGirlfriend', DropDown);
charGroupGirlfriend.onChange = (event:UIEvent) -> { charGroupGirlfriend.onChange = function(event:UIEvent) {
charData.girlfriend = event.data.id; charData.girlfriend = event.data.id;
}; };
charGroupGirlfriend.value = getCharData().girlfriend; charGroupGirlfriend.value = getCharData().girlfriend;
var charGroupRemove:Button = charGroup.findComponent('charGroupRemove', Button); var charGroupRemove:Button = charGroup.findComponent('charGroupRemove', Button);
charGroupRemove.onClick = (_event:MouseEvent) -> { charGroupRemove.onClick = function(event:UIEvent) {
removeGroup(); removeGroup();
}; };
@ -394,20 +446,31 @@ class ChartEditorDialogHandler
return charGroup; 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 public static function openUploadVocalsDialog(state:ChartEditorState, ?closable:Bool = true):Dialog
{ {
var charIdsForVocals = []; var charIdsForVocals:Array<String> = [];
for (charKey in state.currentSongMetadata.playData.playableChars.keys()) 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); charIdsForVocals.push(charKey);
if (charData.opponent != null) charIdsForVocals.push(charData.opponent); if (charData.opponent != null) charIdsForVocals.push(charData.opponent);
} }
var dialog:Dialog = openDialog(state, CHART_EDITOR_DIALOG_UPLOAD_VOCALS_LAYOUT, true, closable); 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); var dialogNoVocals:Button = dialog.findComponent('dialogNoVocals', Button);
dialogNoVocals.onClick = function(_event) { dialogNoVocals.onClick = function(_event) {
@ -421,20 +484,42 @@ class ChartEditorDialogHandler
var charMetadata:BaseCharacter = CharacterDataParser.fetchCharacter(charKey); var charMetadata:BaseCharacter = CharacterDataParser.fetchCharacter(charKey);
var charName:String = charMetadata.characterName; 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); 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) { var onDropFile:String->Void = function(pathStr:String) {
trace('Selected file: $fullPath'); trace('Selected file: $pathStr');
var directory:String = Path.directory(fullPath); var path:Path = new Path(pathStr);
var filename:String = Path.withoutDirectory(directory);
vocalsEntryLabel.text = 'Vocals for $charName (click to browse)\n${filename}'; if (state.loadVocalsFromPath(path, charKey))
state.loadVocalsFromPath(fullPath, 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; dialogNoVocals.hidden = true;
removeDropHandler(onDropFile); 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) { vocalsEntry.onClick = function(_event) {
@ -449,11 +534,10 @@ class ChartEditorDialogHandler
removeDropHandler(onDropFile); removeDropHandler(onDropFile);
} }
}); });
}
// onDropFile // onDropFile
addDropHandler(vocalsEntry, onDropFile); addDropHandler(vocalsEntry, onDropFile);
}
dialogContainer.addComponent(vocalsEntry); dialogContainer.addComponent(vocalsEntry);
} }
@ -463,14 +547,14 @@ class ChartEditorDialogHandler
dialog.hideDialog(DialogButton.APPLY); 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; return dialog;
} }
/** /**
* Builds and opens a dialog displaying the user guide, providing guidance and help on how to use the chart editor. * 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 public static inline function openUserGuideDialog(state:ChartEditorState):Dialog
{ {
@ -490,7 +574,7 @@ class ChartEditorDialogHandler
dialog.showDialog(modal); dialog.showDialog(modal);
state.isHaxeUIDialogOpen = true; state.isHaxeUIDialogOpen = true;
dialog.onDialogClosed = (_event) -> { dialog.onDialogClosed = function(event:UIEvent) {
state.isHaxeUIDialogOpen = false; state.isHaxeUIDialogOpen = false;
}; };

View file

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

View file

@ -1,14 +1,12 @@
package funkin.ui.haxeui; package funkin.ui.haxeui;
import haxe.ui.containers.menus.MenuCheckBox;
import haxe.ui.components.CheckBox; import haxe.ui.components.CheckBox;
import haxe.ui.events.DragEvent; import haxe.ui.containers.menus.MenuCheckBox;
import haxe.ui.events.MouseEvent;
import haxe.ui.events.UIEvent;
import haxe.ui.RuntimeComponentBuilder;
import haxe.ui.core.Component; import haxe.ui.core.Component;
import haxe.ui.core.Screen; import haxe.ui.core.Screen;
import haxe.ui.events.MouseEvent; import haxe.ui.events.MouseEvent;
import haxe.ui.events.UIEvent;
import haxe.ui.RuntimeComponentBuilder;
import lime.app.Application; import lime.app.Application;
class HaxeUIState extends MusicBeatState class HaxeUIState extends MusicBeatState
@ -23,7 +21,7 @@ class HaxeUIState extends MusicBeatState
_componentKey = key; _componentKey = key;
} }
override function create() override function create():Void
{ {
super.create(); super.create();
@ -31,7 +29,7 @@ class HaxeUIState extends MusicBeatState
if (component != null) add(component); if (component != null) add(component);
} }
public function buildComponent(assetPath:String) public function buildComponent(assetPath:String):Component
{ {
try try
{ {
@ -81,15 +79,13 @@ class HaxeUIState extends MusicBeatState
{ {
if (target == null) 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); showContextMenu(assetPath, e.screenX, e.screenY);
}); });
} }
else else
{ {
target.registerEvent(MouseEvent.RIGHT_CLICK, function(e:MouseEvent) target.registerEvent(MouseEvent.RIGHT_CLICK, function(e:MouseEvent) {
{
showContextMenu(assetPath, e.screenX, e.screenY); showContextMenu(assetPath, e.screenX, e.screenY);
}); });
} }
@ -98,7 +94,7 @@ class HaxeUIState extends MusicBeatState
/** /**
* Add an onClick listener to a HaxeUI menu bar item. * 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); var target:Component = findComponent(key);
if (target == null) 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. * 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); var target:Component = findComponent(key);
if (target == null) if (target == null)
@ -179,7 +189,7 @@ class HaxeUIState extends MusicBeatState
return component.findComponent(criteria, type, recursive, searchType); return component.findComponent(criteria, type, recursive, searchType);
} }
override function destroy() override function destroy():Void
{ {
if (component != null) remove(component); if (component != null) remove(component);
component = null; component = null;