Merge pull request #20 from FunkinCrew/chart-editor-fixes-with-dave

Chart editor fixes with dave
This commit is contained in:
Cameron Taylor 2023-03-08 13:24:39 -05:00 committed by GitHub
commit bf7a42d143
17 changed files with 742 additions and 490 deletions

View file

@ -156,9 +156,13 @@
<haxeflag name="--macro" value="include('funkin')" />
<!-- Ensure all UI components are available at runtime. -->
<haxeflag name="--macro" value="include('haxe.ui.backend.flixel.components')" />
<haxeflag name="--macro" value="include('haxe.ui.containers.dialogs')" />
<haxeflag name="--macro" value="include('haxe.ui.containers.menus')" />
<haxeflag name="--macro" value="include('haxe.ui.containers.properties')" />
<haxeflag name="--macro" value="include('haxe.ui.core')" />
<haxeflag name="--macro" value="include('haxe.ui.components')" />
<haxeflag name="--macro" value="include('haxe.ui.containers')" />
<haxeflag name="--macro" value="include('haxe.ui.containers.menus')" />
<!--
Ensure additional class packages are available at runtime (some only really used by scripts).

View file

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

3
haxe_libraries/README.md Normal file
View file

@ -0,0 +1,3 @@
# haxe_libraries
Used by Lix

View file

@ -42,14 +42,14 @@
"name": "haxeui-core",
"type": "git",
"dir": null,
"ref": "e5cf78d",
"ref": "59157d2",
"url": "https://github.com/haxeui/haxeui-core/"
},
{
"name": "haxeui-flixel",
"type": "git",
"dir": null,
"ref": "f03bb6d",
"ref": "d353389",
"url": "https://github.com/haxeui/haxeui-flixel"
},
{
@ -68,7 +68,7 @@
"name": "hxcodec",
"type": "git",
"dir": null,
"ref": "2c9a89a",
"ref": "d74c2aa",
"url": "https://github.com/polybiusproxy/hxCodec"
},
{

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

@ -169,6 +169,7 @@ class PolymodHandler
// `polymod.*`
for (cls in ClassMacro.listClassesInPackage('polymod'))
{
if (cls == null) continue;
var className = Type.getClassName(cls);
Polymod.blacklistImport(className);
}

View file

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

View file

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

View file

@ -60,7 +60,7 @@ class VanillaCutscenes
#if html5
// Video displays OVER the FlxState.
vid = new FlxVideo(path);
vid.finishCallback = finishCutscene;
vid.finishCallback = finishCutscene.bind(0.5);
#else
// Video displays OVER the FlxState.
// vid = new FlxVideoSprite(0, 0);

View file

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

View file

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

View file

@ -1,16 +1,16 @@
package funkin.ui.debug.charting;
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.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;
@ -18,28 +18,34 @@ import haxe.ui.components.TextField;
import haxe.ui.containers.Box;
import haxe.ui.containers.dialogs.Dialog;
import haxe.ui.containers.dialogs.Dialogs;
import haxe.ui.containers.properties.Property;
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;
/**
* Handles dialogs for the new Chart Editor.
*/
class ChartEditorDialogHandler
{
static final CHART_EDITOR_DIALOG_ABOUT_LAYOUT = Paths.ui('chart-editor/dialogs/about');
static final CHART_EDITOR_DIALOG_WELCOME_LAYOUT = Paths.ui('chart-editor/dialogs/welcome');
static final CHART_EDITOR_DIALOG_UPLOAD_INST_LAYOUT = Paths.ui('chart-editor/dialogs/upload-inst');
static final CHART_EDITOR_DIALOG_SONG_METADATA_LAYOUT = Paths.ui('chart-editor/dialogs/song-metadata');
static final CHART_EDITOR_DIALOG_SONG_METADATA_CHARGROUP_LAYOUT = Paths.ui('chart-editor/dialogs/song-metadata-chargroup');
static final CHART_EDITOR_DIALOG_UPLOAD_VOCALS_LAYOUT = Paths.ui('chart-editor/dialogs/upload-vocals');
static final CHART_EDITOR_DIALOG_UPLOAD_VOCALS_ENTRY_LAYOUT = Paths.ui('chart-editor/dialogs/upload-vocals-entry');
static final CHART_EDITOR_DIALOG_USER_GUIDE_LAYOUT = Paths.ui('chart-editor/dialogs/user-guide');
static final CHART_EDITOR_DIALOG_ABOUT_LAYOUT:String = Paths.ui('chart-editor/dialogs/about');
static final CHART_EDITOR_DIALOG_WELCOME_LAYOUT:String = Paths.ui('chart-editor/dialogs/welcome');
static final CHART_EDITOR_DIALOG_UPLOAD_INST_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-inst');
static final CHART_EDITOR_DIALOG_SONG_METADATA_LAYOUT:String = Paths.ui('chart-editor/dialogs/song-metadata');
static final CHART_EDITOR_DIALOG_SONG_METADATA_CHARGROUP_LAYOUT:String = Paths.ui('chart-editor/dialogs/song-metadata-chargroup');
static final CHART_EDITOR_DIALOG_UPLOAD_VOCALS_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-vocals');
static final CHART_EDITOR_DIALOG_UPLOAD_VOCALS_ENTRY_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-vocals-entry');
static final CHART_EDITOR_DIALOG_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
{
@ -48,105 +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
// Switch the graphic for frames.
var bfSpritePlaceholder:Image = dialog.findComponent('bfSprite', Image);
// TODO: Replace this bullshit with a custom HaxeUI component that loads the sprite from the game's assets.
if (bfSpritePlaceholder != null)
{
var bfSprite:FlxSprite = new FlxSprite(0, 0);
bfSprite.visible = false;
var frames = Paths.getSparrowAtlas(bfSpritePlaceholder.resource);
bfSprite.frames = frames;
bfSprite.animation.addByPrefix('idle', 'Boyfriend DJ0', 24, true);
bfSprite.animation.play('idle');
bfSpritePlaceholder.rootComponent.add(bfSprite);
bfSpritePlaceholder.visible = false;
new FlxTimer().start(0.10, (_timer:FlxTimer) ->
{
bfSprite.x = bfSpritePlaceholder.screenLeft;
bfSprite.y = bfSpritePlaceholder.screenTop;
bfSprite.setGraphicSize(Std.int(bfSpritePlaceholder.width), Std.int(bfSpritePlaceholder.height));
bfSprite.visible = true;
});
}
// 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);
// Step 3. Upload Vocals
// NOTE: Uploading vocals is optional, so we don't need to check if the user cancelled the wizard.
openUploadVocalsDialog(state, false); // var uploadVocalsDialog:Dialog
}
else
{
// User cancelled the wizard! Back to the welcome dialog.
openWelcomeDialog(state);
}
};
}
else
{
// User cancelled the wizard! Back to the welcome dialog.
openWelcomeDialog(state);
}
};
}
// TODO: Get the list of songs and insert them as links into the "Create From Song" section.
/*
var linkTemplateDadBattle:Link = dialog.findComponent('splashTemplateDadBattle', Link);
linkTemplateDadBattle.onClick = (_event) ->
{
dialog.hideDialog(DialogButton.CANCEL);
// Load song from template
state.loadSongAsTemplate('dadbattle');
}
var linkTemplateBopeebo:Link = dialog.findComponent('splashTemplateBopeebo', Link);
linkTemplateBopeebo.onClick = (_event) ->
{
dialog.hideDialog(DialogButton.CANCEL);
// Load song from template
state.loadSongAsTemplate('bopeebo');
}
*/
var splashTemplateContainer:VBox = dialog.findComponent('splashTemplateContainer', VBox);
var songList:Array<String> = SongDataParser.listSongIds();
for (targetSongId in songList)
{
var songData = SongDataParser.fetchSong(targetSongId);
var songData:Song = SongDataParser.fetchSong(targetSongId);
if (songData == null) continue;
var songName = songData.getDifficulty().songName;
var songName:String = songData.getDifficulty().songName;
var linkTemplateSong:Link = new Link();
linkTemplateSong.text = songName;
linkTemplateSong.onClick = (_event) ->
{
linkTemplateSong.onClick = function(_event) {
dialog.hideDialog(DialogButton.CANCEL);
// Load song from template
@ -159,78 +130,175 @@ 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)
{
if (selectedFile != null)
{
trace('Selected file: ' + selectedFile);
state.loadInstrumentalFromBytes(selectedFile.bytes);
dialog.hideDialog(DialogButton.APPLY);
removeDropHandler(onDropFile);
}
instrumentalBox.onClick = function(_event) {
Dialogs.openBinaryFile('Open Instrumental', [
{label: 'Audio File (.ogg)', extension: 'ogg'}], function(selectedFile:SelectedFileInfo) {
if (selectedFile != null)
{
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(onDropFile);
addDropHandler(instrumentalBox, onDropFile);
return dialog;
}
static function addDropHandler(handler:String->Void)
static var dropHandlers:Array<
{
component:Component,
handler:(String->Void)
}> = [];
static function addDropHandler(component:Component, handler:String->Void):Void
{
#if desktop
FlxG.stage.window.onDropFile.add(handler);
if (!FlxG.stage.window.onDropFile.has(onDropFile)) FlxG.stage.window.onDropFile.add(onDropFile);
dropHandlers.push(
{
component: component,
handler: handler
});
#else
trace('addDropHandler not implemented for this platform');
#end
}
static function removeDropHandler(handler:String->Void)
static function removeDropHandler(handler:String->Void):Void
{
#if desktop
FlxG.stage.window.onDropFile.remove(handler);
#end
}
static function clearDropHandlers():Void
{
#if desktop
dropHandlers = [];
FlxG.stage.window.onDropFile.remove(onDropFile);
#end
}
static function onDropFile(path:String):Void
{
// a VERY short timer to wait for the mouse position to update
new FlxTimer().start(0.01, function(_) {
for (handler in dropHandlers)
{
if (handler.component.hitTest(FlxG.mouse.screenX, FlxG.mouse.screenY))
{
handler.handler(path);
return;
}
}
});
}
/**
* Opens the dialog in the wizard where the user can set song metadata like name and artist and BPM.
* @param state The ChartEditorState instance.
* @return The dialog to open.
*/
public static function openSongMetadataDialog(state:ChartEditorState):Dialog
{
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 = (event:UIEvent) ->
{
var valid = event.target.text != null && event.target.text != "";
dialogSongName.onChange = function(event:UIEvent) {
var valid:Bool = event.target.text != null && event.target.text != '';
if (valid)
{
@ -245,9 +313,8 @@ class ChartEditorDialogHandler
state.currentSongMetadata.songName = null;
var dialogSongArtist:TextField = dialog.findComponent('dialogSongArtist', TextField);
dialogSongArtist.onChange = (event:UIEvent) ->
{
var valid = event.target.text != null && event.target.text != "";
dialogSongArtist.onChange = function(event:UIEvent) {
var valid:Bool = event.target.text != null && event.target.text != '';
if (valid)
{
@ -262,29 +329,24 @@ class ChartEditorDialogHandler
state.currentSongMetadata.artist = null;
var dialogStage:DropDown = dialog.findComponent('dialogStage', DropDown);
dialogStage.onChange = (event:UIEvent) ->
{
var valid = event.data != null && event.data.id != null;
if (event.data.id == null) return;
dialogStage.onChange = function(event:UIEvent) {
if (event.data == null && event.data.id == null) return;
state.currentSongMetadata.playData.stage = event.data.id;
};
state.currentSongMetadata.playData.stage = null;
var dialogNoteSkin:DropDown = dialog.findComponent('dialogNoteSkin', DropDown);
dialogNoteSkin.onChange = (event:UIEvent) ->
{
dialogNoteSkin.onChange = function(event:UIEvent) {
if (event.data.id == null) return;
state.currentSongMetadata.playData.noteSkin = event.data.id;
};
state.currentSongMetadata.playData.noteSkin = null;
var dialogBPM:NumberStepper = dialog.findComponent('dialogBPM', NumberStepper);
dialogBPM.onChange = (event:UIEvent) ->
{
dialogBPM.onChange = function(event:UIEvent) {
if (event.value == null || event.value <= 0) return;
var timeChanges = state.currentSongMetadata.timeChanges;
var timeChanges:Array<SongTimeChange> = state.currentSongMetadata.timeChanges;
if (timeChanges == null || timeChanges.length == 0)
{
timeChanges = [new SongTimeChange(-1, 0, event.value, 4, 4, [4, 4, 4, 4])];
@ -301,13 +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);
};
@ -317,20 +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);
@ -342,27 +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);
};
@ -374,22 +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();
};
@ -398,22 +446,37 @@ class ChartEditorDialogHandler
return charGroup;
}
/**
* Builds and opens a dialog where the user uploads vocals for the current song.
* @param state The current chart editor state.
* @param closable Whether the dialog can be closed by the user.
* @return The dialog that was opened.
*/
public static function openUploadVocalsDialog(state:ChartEditorState, ?closable:Bool = true):Dialog
{
var charIdsForVocals = [];
var charIdsForVocals:Array<String> = [];
for (charKey in state.currentSongMetadata.playData.playableChars.keys())
{
var charData = state.currentSongMetadata.playData.playableChars.get(charKey);
var charData:SongPlayableChar = state.currentSongMetadata.playData.playableChars.get(charKey);
charIdsForVocals.push(charKey);
if (charData.opponent != null) charIdsForVocals.push(charData.opponent);
}
var dialog:Dialog = openDialog(state, CHART_EDITOR_DIALOG_UPLOAD_VOCALS_LAYOUT, true, closable);
var dialogContainer = dialog.findComponent('vocalContainer');
var dialogContainer:Component = dialog.findComponent('vocalContainer');
var onDropFile:String->Void;
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) {
// Dismiss
dialog.hideDialog(DialogButton.APPLY);
};
for (charKey in charIdsForVocals)
{
@ -421,49 +484,77 @@ 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.';
vocalsEntry.onClick = (_event) ->
{
Dialogs.openBinaryFile('Open $charName Vocals', [
{label: "Audio File (.ogg)", extension: "ogg"}], function(selectedFile)
var onDropFile:String->Void = function(pathStr:String) {
trace('Selected file: $pathStr');
var path:Path = new Path(pathStr);
if (state.loadVocalsFromPath(path, charKey))
{
if (selectedFile != null)
{
trace('Selected file: ' + selectedFile.name + "~" + selectedFile.fullPath);
vocalsEntryLabel.text = 'Vocals for $charName (click to browse)\n${selectedFile.name}';
state.loadVocalsFromBytes(selectedFile.bytes);
removeDropHandler(onDropFile);
}
// 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) {
Dialogs.openBinaryFile('Open $charName Vocals', [
{label: 'Audio File (.ogg)', extension: 'ogg'}], function(selectedFile) {
if (selectedFile != null)
{
trace('Selected file: ' + selectedFile.name);
vocalsEntryLabel.text = 'Vocals for $charName (click to browse)\n${selectedFile.name}';
state.loadVocalsFromBytes(selectedFile.bytes, charKey);
dialogNoVocals.hidden = true;
removeDropHandler(onDropFile);
}
});
}
// onDropFile
addDropHandler(vocalsEntry, onDropFile);
dialogContainer.addComponent(vocalsEntry);
}
var dialogContinue:Button = dialog.findComponent('dialogContinue', Button);
dialogContinue.onClick = (_event) ->
{
dialogContinue.onClick = function(_event) {
// Dismiss
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.
onDropFile = (path:String) ->
{
trace('Dropped file: ' + path);
};
addDropHandler(onDropFile);
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
{
@ -483,8 +574,7 @@ class ChartEditorDialogHandler
dialog.showDialog(modal);
state.isHaxeUIDialogOpen = true;
dialog.onDialogClosed = (_event) ->
{
dialog.onDialogClosed = function(event:UIEvent) {
state.isHaxeUIDialogOpen = false;
};

View file

@ -1,7 +1,6 @@
package funkin.ui.debug.charting;
import flixel.FlxObject;
import flixel.FlxBasic;
import flixel.FlxSprite;
import flixel.graphics.frames.FlxFramesCollection;
import flixel.graphics.frames.FlxTileFrames;
@ -14,6 +13,14 @@ import funkin.play.song.SongData.SongNoteData;
*/
class ChartEditorNoteSprite extends FlxSprite
{
/**
* The list of available note skin to validate against.
*/
public static final NOTE_STYLES:Array<String> = ['Normal', 'Pixel'];
/**
* The ChartEditorState this note belongs to.
*/
public var parentState:ChartEditorState;
/**
@ -22,6 +29,11 @@ class ChartEditorNoteSprite extends FlxSprite
*/
public var noteData(default, set):SongNoteData;
/**
* The name of the note style currently in use.
*/
public var noteStyle(get, null):String;
/**
* This note is the previous sprite in a sustain chain.
*/
@ -222,14 +234,20 @@ class ChartEditorNoteSprite extends FlxSprite
return this.childNoteSprite;
}
public function playNoteAnimation()
function get_noteStyle():String
{
// Fall back to 'Normal' if it's not a valid note style.
return if (NOTE_STYLES.contains(this.parentState.currentSongNoteSkin)) this.parentState.currentSongNoteSkin else 'Normal';
}
public function playNoteAnimation():Void
{
// Decide whether to display a note or a sustain.
var baseAnimationName:String = 'tap';
if (this.parentNoteSprite != null) baseAnimationName = (this.childNoteSprite != null) ? 'hold' : 'holdEnd';
// Play the appropriate animation for the type, direction, and skin.
var animationName = '${baseAnimationName}${this.noteData.getDirectionName()}${this.parentState.currentSongNoteSkin}';
var animationName:String = '${baseAnimationName}${this.noteData.getDirectionName()}${this.noteStyle}';
this.animation.play(animationName);

File diff suppressed because it is too large Load diff

View file

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

View file

@ -5,6 +5,10 @@ import lime.utils.Bytes;
import lime.ui.FileDialog;
import openfl.net.FileFilter;
import haxe.io.Path;
#if html5
import openfl.net.FileReference;
import openfl.events.Event;
#end
/**
* Utilities for reading and writing files on various platforms.
@ -141,8 +145,6 @@ class FileUtil
fileDialog.open(filter, defaultPath, dialogTitle);
return true;
#elseif html5
var filter = convertTypeFilter(typeFilter);
var onFileLoaded = function(event) {
var loadedFileRef:FileReference = event.target;
trace('Loaded file: ' + loadedFileRef.name);
@ -157,8 +159,9 @@ class FileUtil
}
var fileRef = new FileReference();
file.addEventListener(Event.SELECT, onFileSelected);
file.open(filter, defaultPath, dialogTitle);
fileRef.addEventListener(Event.SELECT, onFileSelected);
fileRef.browse(typeFilter);
return true;
#else
onCancel();
return false;
@ -169,7 +172,6 @@ class FileUtil
* Browses for a single file location, then writes the provided `haxe.io.Bytes` data and calls `onSave(path)` when done.
* Works great on desktop and HTML5.
*
* @param typeFilter TODO What does this do?
* @return Whether the file dialog was opened successfully.
*/
public static function saveFile(data:Bytes, ?onSave:String->Void, ?onCancel:Void->Void, ?defaultFileName:String, ?dialogTitle:String):Bool
@ -191,6 +193,7 @@ class FileUtil
if (onCancel != null) fileDialog.onCancel.add(onCancel);
fileDialog.save(data, filter, defaultFileName, dialogTitle);
return true;
#else
onCancel();
return false;
@ -374,7 +377,11 @@ class FileUtil
*/
public static function appendStringToPath(path:String, data:String)
{
#if sys
sys.io.File.append(path, false).writeString(data);
#else
throw 'Direct file writing by path not supported on this platform.';
#end
}
/**
@ -410,7 +417,7 @@ class FileUtil
{
path = Sys.getEnv(envName);
if (path == "") path = null;
if (path == '') path = null;
if (path != null) break;
}

View file

@ -7,14 +7,13 @@
This needs to be done HERE and not via the `include` macro because `Toolkit.init()`
reads this to build the component registry.
-->
<class package="haxe.ui.core" loadAll="true" />
<class package="haxe.ui.backend.flixel.components" loadAll="true" />
<class package="haxe.ui.components" loadAll="true" />
<class package="haxe.ui.containers" loadAll="true" />
<class package="haxe.ui.containers.menus" loadAll="true" />
<class package="haxe.ui.containers.dialogs" loadAll="true" />
<class package="haxe.ui.containers.menus" loadAll="true" />
<class package="haxe.ui.containers.properties" loadAll="true" />
<class package="haxe.ui.containers" loadAll="true" />
<class package="haxe.ui.core" loadAll="true" />
<!-- Custom components. -->
<class package="funkin.ui.haxeui.components" loadAll="true" />