From fe92d00a04704b3af48476850dc7ebc2a4825a73 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Tue, 28 Feb 2023 21:06:09 -0500 Subject: [PATCH] Numerous chart editor fixes. --- checkstyle.json | 16 +- hmm.json | 4 +- source/funkin/input/TurboKeyHandler.hx | 116 +++++ source/funkin/modding/module/Module.hx | 3 +- .../play/character/AnimateAtlasCharacter.hx | 33 +- source/funkin/play/song/Song.hx | 4 +- source/funkin/play/stage/Bopper.hx | 3 +- .../charting/ChartEditorDialogHandler.hx | 286 ++++++++---- .../ui/debug/charting/ChartEditorState.hx | 439 +++++++++--------- source/funkin/ui/haxeui/HaxeUIState.hx | 38 +- 10 files changed, 575 insertions(+), 367 deletions(-) create mode 100644 source/funkin/input/TurboKeyHandler.hx diff --git a/checkstyle.json b/checkstyle.json index d9200ea12..445f82b42 100644 --- a/checkstyle.json +++ b/checkstyle.json @@ -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" }, { diff --git a/hmm.json b/hmm.json index ca2bfcfbe..914d9ef58 100644 --- a/hmm.json +++ b/hmm.json @@ -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" }, { diff --git a/source/funkin/input/TurboKeyHandler.hx b/source/funkin/input/TurboKeyHandler.hx new file mode 100644 index 000000000..9181f836b --- /dev/null +++ b/source/funkin/input/TurboKeyHandler.hx @@ -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; + var delay:Float; + var interval:Float; + + var allPressedTime:Float = 0; + + function new(keys:Array, 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, ?delay:Float = DEFAULT_DELAY, + ?interval:Float = DEFAULT_INTERVAL):TurboKeyHandler + { + return new TurboKeyHandler(inputKeys, delay, interval); + } +} diff --git a/source/funkin/modding/module/Module.hx b/source/funkin/modding/module/Module.hx index 0d76c2c14..d1a54b619 100644 --- a/source/funkin/modding/module/Module.hx +++ b/source/funkin/modding/module/Module.hx @@ -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'; diff --git a/source/funkin/play/character/AnimateAtlasCharacter.hx b/source/funkin/play/character/AnimateAtlasCharacter.hx index 339662704..483e1b1b8 100644 --- a/source/funkin/play/character/AnimateAtlasCharacter.hx +++ b/source/funkin/play/character/AnimateAtlasCharacter.hx @@ -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 diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx index 5cd4d1589..853846414 100644 --- a/source/funkin/play/song/Song.hx +++ b/source/funkin/play/song/Song.hx @@ -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]; diff --git a/source/funkin/play/stage/Bopper.hx b/source/funkin/play/stage/Bopper.hx index 1c03cb7bc..09aa910e0 100644 --- a/source/funkin/play/stage/Bopper.hx +++ b/source/funkin/play/stage/Bopper.hx @@ -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 = [0, 0]; diff --git a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx index cecbbfb64..4240773e4 100644 --- a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx +++ b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx @@ -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 = 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 = 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 = []; 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; }; diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index a1bda9fcd..63ff44be2 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -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 = []; /** - * 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):Array { - 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(); 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 = song.getRawMetadata(); + var songName:String = rawSongMetadata[0].songName; this.songMetadata = new Map(); @@ -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->Void = (paths:Array) -> { + var onSave:Array->Void = function(paths:Array) { trace('Successfully exported files.'); }; - var onCancel:Void->Void = () -> { + var onCancel:Void->Void = function() { trace('Export cancelled.'); }; diff --git a/source/funkin/ui/haxeui/HaxeUIState.hx b/source/funkin/ui/haxeui/HaxeUIState.hx index e23768131..6d432b68c 100644 --- a/source/funkin/ui/haxeui/HaxeUIState.hx +++ b/source/funkin/ui/haxeui/HaxeUIState.hx @@ -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;