Merge branch 'rewrite/master' into bugfix/chart-editor-notif-in-bg

This commit is contained in:
EliteMasterEric 2023-11-29 16:42:40 -05:00
commit eebb2e874f
25 changed files with 1150 additions and 727 deletions

View file

@ -13,9 +13,24 @@ jobs:
steps: steps:
- name: ensure git cli is installed - name: ensure git cli is installed
run: apt update && apt install sudo git -y run: apt update && apt install sudo git -y
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: print latest_commit with:
run: echo ${{ github.sha }} submodules: 'recursive'
fetch-depth: 0
token: ${{ secrets.GH_RO_PAT }}
- name: check whether submodules exist
run: |
git config --global --add safe.directory $GITHUB_WORKSPACE
# debug output
echo gh=${{ github.sha }}
echo head=$(git rev-parse HEAD)
echo art=$(git -C art rev-parse HEAD)
echo assets=$(git -C assets rev-parse HEAD)
# checks if HEAD commit hash in submodules is diff from current repo, and therefore exists
test $(git rev-parse HEAD) != $(git -C art rev-parse HEAD)
test $(git rev-parse HEAD) != $(git -C assets rev-parse HEAD)
- id: should_run - id: should_run
continue-on-error: true continue-on-error: true
name: check latest commit is less than a day name: check latest commit is less than a day
@ -33,9 +48,10 @@ jobs:
apt install sudo git curl unzip -y apt install sudo git curl unzip -y
echo $GITHUB_WORKSPACE echo $GITHUB_WORKSPACE
git config --global --add safe.directory $GITHUB_WORKSPACE git config --global --add safe.directory $GITHUB_WORKSPACE
- uses: actions/checkout@v3 - uses: actions/checkout@v4
with: with:
submodules: 'recursive' submodules: 'recursive'
fetch-depth: 0
token: ${{ secrets.GH_RO_PAT }} token: ${{ secrets.GH_RO_PAT }}
- uses: ./.github/actions/setup-haxeshit - uses: ./.github/actions/setup-haxeshit
- name: Build game - name: Build game
@ -56,9 +72,10 @@ jobs:
contents: write contents: write
actions: write actions: write
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
with: with:
submodules: 'recursive' submodules: 'recursive'
fetch-depth: 0
token: ${{ secrets.GH_RO_PAT }} token: ${{ secrets.GH_RO_PAT }}
- uses: ./.github/actions/setup-haxeshit - uses: ./.github/actions/setup-haxeshit
- name: Make HXCPP cache dir - name: Make HXCPP cache dir
@ -91,9 +108,10 @@ jobs:
# contents: write # contents: write
# actions: write # actions: write
# steps: # steps:
# - uses: actions/checkout@v3 # - uses: actions/checkout@v4
# with: # with:
# submodules: 'recursive' # submodules: 'recursive'
# fetch-depth: 0
# token: ${{ secrets.GH_RO_PAT }} # token: ${{ secrets.GH_RO_PAT }}
# - uses: ./.github/actions/setup-haxeshit # - uses: ./.github/actions/setup-haxeshit
# - name: Run unit tests # - name: Run unit tests

View file

@ -162,10 +162,13 @@
<icon path="art/iconOG.png" /> <icon path="art/iconOG.png" />
<haxedef name="CAN_OPEN_LINKS" unless="switch" /> <haxedef name="CAN_OPEN_LINKS" unless="switch" />
<haxedef name="CAN_CHEAT" if="switch debug" /> <haxedef name="CAN_CHEAT" if="switch debug" />
<!-- I don't --> <!-- I don't remember what this is for. -->
<haxedef name="haxeui_no_mouse_reset" /> <haxedef name="haxeui_no_mouse_reset" />
<!-- Clicking outside a dialog should deselect the current focused component. --> <!-- Clicking outside a dialog should deselect the current focused component. -->
<haxedef name="haxeui_focus_out_on_click" /> <haxedef name="haxeui_focus_out_on_click" />
<!-- Required to use haxe.ui.backend.flixel.UIState with build macros. -->
<haxedef name="haxeui_dont_impose_base_class" />
<!-- Skip the Intro --> <!-- Skip the Intro -->
<section if="debug"> <section if="debug">
<!-- Starts the game at the specified week, at the first song --> <!-- Starts the game at the specified week, at the first song -->

2
assets

@ -1 +1 @@
Subproject commit a88cfa4c2eb3d4b8fc0bb0f7770d6a8359755bb4 Subproject commit d2a2327bb1eb04a1aa07d7340201f42217829005

View file

@ -32,6 +32,10 @@ Example:
public function checkSyncError(?targetTime:Float):Float public function checkSyncError(?targetTime:Float):Float
``` ```
## Commenting Unused Code
Do not comment out sections of code that are unused. Keep these snippets elsewhere or remove them. Older chunks of code can be retrieved by referring to the older Git commits, and having large chunks of commented code makes files longer and more confusing to navigate.
## License Headers ## License Headers
Do not include headers specifying code license on individual files in the repo, since the main `LICENSE.md` file covers all of them. Do not include headers specifying code license on individual files in the repo, since the main `LICENSE.md` file covers all of them.

View file

@ -1,6 +1,12 @@
# Troubleshooting Common Issues # Troubleshooting Common Issues
- Weird macro error with a very tall call stack: Restart Visual Studio Code - Weird macro error with a very tall call stack: Restart Visual Studio Code
- NOTE: This is caused by Polymod somewhere, and seems to only occur when there is another compile error somewhere in the program. There is a bounty up for it.
- `Get Thread Context Failed`: Turn off other expensive applications while building - `Get Thread Context Failed`: Turn off other expensive applications while building
- `Type not found: T1`: This is thrown by `json2object`, make sure the data type of `@:default` is correct. - `Type not found: T1`: This is thrown by `json2object`, make sure the data type of `@:default` is correct.
- NOTE: `flixel.util.typeLimit.OneOfTwo` isn't supported. - NOTE: `flixel.util.typeLimit.OneOfTwo` isn't supported.
- `Class lists not properly generated. Try cleaning out your export folder, restarting your IDE, and rebuilding your project.`
- This is a bug specific to HTML5. Simply perform the steps listed (don't forget to restart the IDE too).

View file

@ -11,7 +11,7 @@
"name": "flixel", "name": "flixel",
"type": "git", "type": "git",
"dir": null, "dir": null,
"ref": "8437a86aa5dafdb3f5dcb91d212cb10a4ee6e53b", "ref": "da04cbda49a4c5eebe93fb61296dbaf4f0f1b556",
"url": "https://github.com/EliteMasterEric/flixel" "url": "https://github.com/EliteMasterEric/flixel"
}, },
{ {
@ -49,7 +49,7 @@
"name": "haxeui-core", "name": "haxeui-core",
"type": "git", "type": "git",
"dir": null, "dir": null,
"ref": "bfdb49886f256a8c37edfc4f46586727d68e2756", "ref": "91ed8d7867c52af5ea2a9513204057d69ab33c8e",
"url": "https://github.com/haxeui/haxeui-core" "url": "https://github.com/haxeui/haxeui-core"
}, },
{ {
@ -144,7 +144,7 @@
"name": "polymod", "name": "polymod",
"type": "git", "type": "git",
"dir": null, "dir": null,
"ref": "e8a07b81e3bc535238ad8649e38f5d43c46f1b65", "ref": "80d1d309803c1b111866524f9769325e3b8b0b1b",
"url": "https://github.com/larsiusprime/polymod" "url": "https://github.com/larsiusprime/polymod"
}, },
{ {

View file

@ -35,15 +35,15 @@ class Conductor
static var timeChanges:Array<SongTimeChange> = []; static var timeChanges:Array<SongTimeChange> = [];
/** /**
* The current time change. * The most recent time change for the current song position.
*/ */
static var currentTimeChange:SongTimeChange; public static var currentTimeChange(default, null):SongTimeChange;
/** /**
* The current position in the song in milliseconds. * The current position in the song in milliseconds.
* Updated every frame based on the audio position. * Update this every frame based on the audio position using `Conductor.update()`.
*/ */
public static var songPosition:Float = 0; public static var songPosition(default, null):Float = 0;
/** /**
* Beats per minute of the current song at the current time. * Beats per minute of the current song at the current time.

View file

@ -233,6 +233,7 @@ class PauseSubState extends MusicBeatSubState
if (PlayStatePlaylist.isStoryMode) if (PlayStatePlaylist.isStoryMode)
{ {
PlayStatePlaylist.reset();
openSubState(new funkin.ui.transition.StickerSubState(null, STORY)); openSubState(new funkin.ui.transition.StickerSubState(null, STORY));
} }
else else

View file

@ -1375,8 +1375,7 @@ class PlayState extends MusicBeatSubState
else else
{ {
// lolol // lolol
lime.app.Application.current.window.alert('Nice job, you ignoramus. $id isn\'t a real stage.\nI\'m falling back to the default so the game doesn\'t shit itself.', lime.app.Application.current.window.alert('Unable to load stage ${id}, is its data corrupted?.', 'Stage Error');
'Stage Error');
} }
} }

View file

@ -19,17 +19,18 @@ import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween; import flixel.tweens.FlxTween;
import flixel.tweens.misc.VarTween; import flixel.tweens.misc.VarTween;
import flixel.util.FlxColor; import flixel.util.FlxColor;
import funkin.ui.mainmenu.MainMenuState;
import flixel.util.FlxSort; import flixel.util.FlxSort;
import flixel.util.FlxTimer; import flixel.util.FlxTimer;
import funkin.audio.visualize.PolygonSpectogram; import funkin.audio.visualize.PolygonSpectogram;
import funkin.audio.VoicesGroup; import funkin.audio.VoicesGroup;
import funkin.data.notestyle.NoteStyleRegistry; import funkin.data.notestyle.NoteStyleRegistry;
import funkin.data.song.SongData.SongCharacterData;
import funkin.data.song.SongData.SongChartData; import funkin.data.song.SongData.SongChartData;
import funkin.data.song.SongData.SongEventData; import funkin.data.song.SongData.SongEventData;
import funkin.data.song.SongData.SongMetadata; import funkin.data.song.SongData.SongMetadata;
import funkin.data.song.SongData.SongNoteData; import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongDataUtils; import funkin.data.song.SongDataUtils;
import funkin.data.song.SongRegistry;
import funkin.input.Cursor; import funkin.input.Cursor;
import funkin.input.TurboKeyHandler; import funkin.input.TurboKeyHandler;
import funkin.modding.events.ScriptEvent; import funkin.modding.events.ScriptEvent;
@ -53,6 +54,7 @@ import funkin.save.Save;
import funkin.ui.debug.charting.commands.AddEventsCommand; import funkin.ui.debug.charting.commands.AddEventsCommand;
import funkin.ui.debug.charting.commands.AddNotesCommand; import funkin.ui.debug.charting.commands.AddNotesCommand;
import funkin.ui.debug.charting.commands.ChartEditorCommand; import funkin.ui.debug.charting.commands.ChartEditorCommand;
import funkin.ui.debug.charting.commands.ChartEditorCommand;
import funkin.ui.debug.charting.commands.CutItemsCommand; import funkin.ui.debug.charting.commands.CutItemsCommand;
import funkin.ui.debug.charting.commands.DeselectAllItemsCommand; import funkin.ui.debug.charting.commands.DeselectAllItemsCommand;
import funkin.ui.debug.charting.commands.DeselectItemsCommand; import funkin.ui.debug.charting.commands.DeselectItemsCommand;
@ -73,15 +75,21 @@ import funkin.ui.debug.charting.components.ChartEditorEventSprite;
import funkin.ui.debug.charting.components.ChartEditorHoldNoteSprite; import funkin.ui.debug.charting.components.ChartEditorHoldNoteSprite;
import funkin.ui.debug.charting.components.ChartEditorNotePreview; import funkin.ui.debug.charting.components.ChartEditorNotePreview;
import funkin.ui.debug.charting.components.ChartEditorNoteSprite; import funkin.ui.debug.charting.components.ChartEditorNoteSprite;
import funkin.ui.debug.charting.components.ChartEditorPlaybarHead;
import funkin.ui.debug.charting.components.ChartEditorSelectionSquareSprite; import funkin.ui.debug.charting.components.ChartEditorSelectionSquareSprite;
import funkin.ui.debug.charting.handlers.ChartEditorShortcutHandler;
import funkin.ui.haxeui.components.CharacterPlayer; import funkin.ui.haxeui.components.CharacterPlayer;
import funkin.ui.haxeui.HaxeUIState; import funkin.ui.haxeui.HaxeUIState;
import funkin.ui.mainmenu.MainMenuState;
import funkin.util.Constants; import funkin.util.Constants;
import funkin.util.FileUtil;
import funkin.util.SortUtil; import funkin.util.SortUtil;
import funkin.util.WindowUtil; import funkin.util.WindowUtil;
import haxe.DynamicAccess; import haxe.DynamicAccess;
import haxe.io.Bytes; import haxe.io.Bytes;
import haxe.io.Path; import haxe.io.Path;
import haxe.ui.backend.flixel.UIRuntimeState;
import haxe.ui.backend.flixel.UIState;
import haxe.ui.components.DropDown; import haxe.ui.components.DropDown;
import haxe.ui.components.Label; import haxe.ui.components.Label;
import haxe.ui.components.NumberStepper; import haxe.ui.components.NumberStepper;
@ -90,16 +98,18 @@ import haxe.ui.components.TextField;
import haxe.ui.containers.dialogs.CollapsibleDialog; import haxe.ui.containers.dialogs.CollapsibleDialog;
import haxe.ui.containers.Frame; import haxe.ui.containers.Frame;
import haxe.ui.containers.menus.Menu; import haxe.ui.containers.menus.Menu;
import haxe.ui.containers.menus.MenuBar;
import haxe.ui.containers.menus.MenuItem; import haxe.ui.containers.menus.MenuItem;
import haxe.ui.containers.TreeView; import haxe.ui.containers.TreeView;
import haxe.ui.containers.TreeViewNode; import haxe.ui.containers.TreeViewNode;
import haxe.ui.core.Component; import haxe.ui.core.Component;
import haxe.ui.core.Screen; import haxe.ui.core.Screen;
import haxe.ui.events.DragEvent; import haxe.ui.events.DragEvent;
import haxe.ui.events.MouseEvent;
import haxe.ui.events.UIEvent;
import haxe.ui.events.UIEvent; import haxe.ui.events.UIEvent;
import haxe.ui.focus.FocusManager; import haxe.ui.focus.FocusManager;
import openfl.display.BitmapData; import openfl.display.BitmapData;
import funkin.util.FileUtil;
using Lambda; using Lambda;
@ -107,24 +117,22 @@ using Lambda;
* A state dedicated to allowing the user to create and edit song charts. * A state dedicated to allowing the user to create and edit song charts.
* Built with HaxeUI for use by both developers and modders. * Built with HaxeUI for use by both developers and modders.
* *
* Some functionality is moved to other classes to help maintain my sanity. * Some functionality is split into handler classes to help maintain my sanity.
* *
* @author MasterEric * @author MasterEric
*/ */
@:nullSafety // @:nullSafety
class ChartEditorState extends HaxeUIState
@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/main-view.xml"))
class ChartEditorState extends UIState // UIState derives from MusicBeatState
{ {
/** /**
* CONSTANTS * CONSTANTS
*/ */
// ============================== // ==============================
// XML Layouts // Layouts
public static final CHART_EDITOR_LAYOUT:String = Paths.ui('chart-editor/main-view');
public static final CHART_EDITOR_NOTIFBAR_LAYOUT:String = Paths.ui('chart-editor/components/notifbar');
public static final CHART_EDITOR_PLAYBARHEAD_LAYOUT:String = Paths.ui('chart-editor/components/playbar-head');
public static final CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/notedata'); public static final CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/notedata');
public static final CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/eventdata'); public static final CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/eventdata');
public static final CHART_EDITOR_TOOLBOX_METADATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/metadata'); public static final CHART_EDITOR_TOOLBOX_METADATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/metadata');
public static final CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT:String = Paths.ui('chart-editor/toolbox/difficulty'); public static final CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT:String = Paths.ui('chart-editor/toolbox/difficulty');
@ -866,7 +874,8 @@ class ChartEditorState extends HaxeUIState
// ============================== // ==============================
/** /**
* The chill audio track that plays when you open the Chart Editor. * The chill audio track that plays in the chart editor.
* Plays when the main music is NOT being played.
*/ */
var welcomeMusic:FlxSound = new FlxSound(); var welcomeMusic:FlxSound = new FlxSound();
@ -1209,6 +1218,110 @@ class ChartEditorState extends HaxeUIState
return currentSongMetadata.playData.characters.instrumental = value; return currentSongMetadata.playData.characters.instrumental = value;
} }
/**
* HAXEUI COMPONENTS
*/
// ==============================
/**
* The layout containing the playbar.
* Constructed manually and added to the layout so we can control its position.
*/
var playbarHeadLayout:Null<ChartEditorPlaybarHead> = null;
// NOTE: All the components below are automatically assigned via HaxeUI macros.
/**
* The menubar at the top of the screen.
*/
// var menubar:MenuBar;
/**
* The `File -> New Chart` menu item.
*/
// var menubarItemNewChart:MenuItem;
/**
* The `File -> Open Chart` menu item.
*/
// var menubarItemOpenChart:MenuItem;
/**
* The `File -> Open Recent` menu.
*/
// var menubarOpenRecent:Menu;
/**
* The `File -> Save Chart` menu item.
*/
// var menubarItemSaveChart:MenuItem;
/**
* The `File -> Save Chart As` menu item.
*/
// var menubarItemSaveChartAs:MenuItem;
/**
* The `File -> Preferences` menu item.
*/
// var menubarItemPreferences:MenuItem;
/**
* The `File -> Exit` menu item.
*/
// var menubarItemExit:MenuItem;
/**
* The `Edit -> Undo` menu item.
*/
// var menubarItemUndo:MenuItem;
/**
* The `Edit -> Redo` menu item.
*/
// var menubarItemRedo:MenuItem;
/**
* The `Edit -> Cut` menu item.
*/
// var menubarItemCut:MenuItem;
/**
* The `Edit -> Copy` menu item.
*/
// var menubarItemCopy:MenuItem;
/**
* The `Edit -> Paste` menu item.
*/
// var menubarItemPaste:MenuItem;
/**
* The `Edit -> Paste Unsnapped` menu item.
*/
// var menubarItemPasteUnsnapped:MenuItem;
/**
* The `Edit -> Delete` menu item.
*/
// var menubarItemDelete:MenuItem;
/**
* The label by the playbar telling the song position.
*/
// var playbarSongPos:Label;
/**
* The label by the playbar telling the song time remaining.
*/
// var playbarSongRemaining:Label;
/**
* The label by the playbar telling the note snap.
*/
// var playbarNoteSnap:Label;
/**
* The button by the playbar to jump to the start of the song.
*/
// var playbarStart:Button;
/**
* The button by the playbar to jump backwards in the song.
*/
// var playbarBack:Button;
/**
* The button by the playbar to play or pause the song.
*/
// var playbarPlay:Button;
/**
* The button by the playbar to jump forwards in the song.
*/
// var playbarForward:Button;
/**
* The button by the playbar to jump to the end of the song.
*/
// var playbarEnd:Button;
/** /**
* RENDER OBJECTS * RENDER OBJECTS
*/ */
@ -1298,41 +1411,6 @@ class ChartEditorState extends HaxeUIState
*/ */
var menuBG:Null<FlxSprite> = null; var menuBG:Null<FlxSprite> = null;
/**
* The layout containing the playbar head slider.
*/
var playbarHeadLayout:Null<Component> = null;
/**
* The submenu in the menubar containing recently opened files.
*/
var menubarOpenRecent:Null<Menu> = null;
/**
* The item in the menubar to save the currently opened chart.
*/
var menubarItemSaveChart:Null<MenuItem> = null;
/**
* The playbar head slider.
*/
var playbarHead:Null<Slider> = null;
/**
* The label by the playbar telling the song position.
*/
var playbarSongPos:Null<Label> = null;
/**
* The label by the playbar telling the song time remaining.
*/
var playbarSongRemaining:Null<Label> = null;
/**
* The label by the playbar telling the note snap.
*/
var playbarNoteSnap:Null<Label> = null;
/** /**
* The sprite group containing the note graphics. * The sprite group containing the note graphics.
* Only displays a subset of the data from `currentSongChartNoteData`, * Only displays a subset of the data from `currentSongChartNoteData`,
@ -1432,9 +1510,7 @@ class ChartEditorState extends HaxeUIState
public function new(?params:ChartEditorParams) public function new(?params:ChartEditorParams)
{ {
// Load the HaxeUI XML file. super();
super(CHART_EDITOR_LAYOUT);
this.params = params; this.params = params;
} }
@ -1481,7 +1557,7 @@ class ChartEditorState extends HaxeUIState
// super.create() must be called first, the HaxeUI components get created here. // super.create() must be called first, the HaxeUI components get created here.
super.create(); super.create();
// Set the z-index of the HaxeUI. // Set the z-index of the HaxeUI.
this.component.zIndex = 100; this.root.zIndex = 100;
// Show the mouse cursor. // Show the mouse cursor.
Cursor.show(); Cursor.show();
@ -1562,6 +1638,7 @@ class ChartEditorState extends HaxeUIState
function setupWelcomeMusic() function setupWelcomeMusic()
{ {
this.welcomeMusic.loadEmbedded(Paths.music('chartEditorLoop/chartEditorLoop')); this.welcomeMusic.loadEmbedded(Paths.music('chartEditorLoop/chartEditorLoop'));
FlxG.sound.list.add(this.welcomeMusic);
this.welcomeMusic.looped = true; this.welcomeMusic.looped = true;
} }
@ -1633,8 +1710,6 @@ class ChartEditorState extends HaxeUIState
var menuItemRecentChart:MenuItem = new MenuItem(); var menuItemRecentChart:MenuItem = new MenuItem();
menuItemRecentChart.text = chartPath; menuItemRecentChart.text = chartPath;
menuItemRecentChart.onClick = function(_event) { menuItemRecentChart.onClick = function(_event) {
stopWelcomeMusic();
// Load chart from file // Load chart from file
var result:Null<Array<String>> = this.loadFromFNFCPath(chartPath); var result:Null<Array<String>> = this.loadFromFNFCPath(chartPath);
if (result != null) if (result != null)
@ -1671,14 +1746,20 @@ class ChartEditorState extends HaxeUIState
#end #end
} }
function fadeInWelcomeMusic():Void var bgMusicTimer:FlxTimer;
function fadeInWelcomeMusic(?extraWait:Float = 0, ?fadeInTime:Float = 5):Void
{ {
bgMusicTimer = new FlxTimer().start(extraWait, (_) -> {
this.welcomeMusic.volume = 0;
this.welcomeMusic.play(); this.welcomeMusic.play();
this.welcomeMusic.fadeIn(4, 0, 1.0); this.welcomeMusic.fadeIn(fadeInTime, 0, 1.0);
});
} }
function stopWelcomeMusic():Void function stopWelcomeMusic():Void
{ {
if (bgMusicTimer != null) bgMusicTimer.cancel();
// this.welcomeMusic.fadeOut(4, 0); // this.welcomeMusic.fadeOut(4, 0);
this.welcomeMusic.pause(); this.welcomeMusic.pause();
} }
@ -1926,25 +2007,20 @@ class ChartEditorState extends HaxeUIState
function buildAdditionalUI():Void function buildAdditionalUI():Void
{ {
playbarHeadLayout = buildComponent(CHART_EDITOR_PLAYBARHEAD_LAYOUT); playbarHeadLayout = new ChartEditorPlaybarHead();
if (playbarHeadLayout == null) throw 'ERROR: Failed to construct playbarHeadLayout! Check "${CHART_EDITOR_PLAYBARHEAD_LAYOUT}".';
playbarHeadLayout.zIndex = 110; playbarHeadLayout.zIndex = 110;
playbarHeadLayout.width = FlxG.width - 8; playbarHeadLayout.width = FlxG.width - 8;
playbarHeadLayout.height = 10; playbarHeadLayout.height = 10;
playbarHeadLayout.x = 4; playbarHeadLayout.x = 4;
playbarHeadLayout.y = FlxG.height - 48 - 8; playbarHeadLayout.y = FlxG.height - 48 - 8;
playbarHead = playbarHeadLayout.findComponent('playbarHead', Slider); playbarHeadLayout.playbarHead.allowFocus = false;
if (playbarHead == null) throw 'ERROR: Failed to fetch playbarHead from playbarHeadLayout! Check "${CHART_EDITOR_PLAYBARHEAD_LAYOUT}".'; playbarHeadLayout.playbarHead.width = FlxG.width;
playbarHead.allowFocus = false; playbarHeadLayout.playbarHead.height = 10;
playbarHead.width = FlxG.width; playbarHeadLayout.playbarHead.styleString = 'padding-left: 0px; padding-right: 0px; border-left: 0px; border-right: 0px;';
playbarHead.height = 10;
playbarHead.styleString = 'padding-left: 0px; padding-right: 0px; border-left: 0px; border-right: 0px;';
playbarHead.onDragStart = function(_:DragEvent) { playbarHeadLayout.playbarHead.onDragStart = function(_:DragEvent) {
playbarHeadDragging = true; playbarHeadDragging = true;
// If we were dragging the playhead while the song was playing, resume playing. // If we were dragging the playhead while the song was playing, resume playing.
@ -1959,10 +2035,10 @@ class ChartEditorState extends HaxeUIState
} }
} }
playbarHead.onDrag = function(_:DragEvent) { playbarHeadLayout.playbarHead.onDrag = function(_:DragEvent) {
if (playbarHeadDragging) if (playbarHeadDragging)
{ {
var value:Null<Float> = playbarHead?.value; var value:Null<Float> = playbarHeadLayout.playbarHead?.value;
// Set the song position to where the playhead was moved to. // Set the song position to where the playhead was moved to.
scrollPositionInPixels = songLengthInPixels * ((value ?? 0.0) / 100); scrollPositionInPixels = songLengthInPixels * ((value ?? 0.0) / 100);
@ -1971,7 +2047,7 @@ class ChartEditorState extends HaxeUIState
} }
} }
playbarHead.onDragEnd = function(_:DragEvent) { playbarHeadLayout.playbarHead.onDragEnd = function(_:DragEvent) {
playbarHeadDragging = false; playbarHeadDragging = false;
// If we were dragging the playhead while the song was playing, resume playing. // If we were dragging the playhead while the song was playing, resume playing.
@ -1985,14 +2061,6 @@ class ChartEditorState extends HaxeUIState
add(playbarHeadLayout); add(playbarHeadLayout);
menubarOpenRecent = findComponent('menubarOpenRecent', Menu);
if (menubarOpenRecent == null) throw "Could not find menubarOpenRecent!";
menubarItemSaveChart = findComponent('menubarItemSaveChart', MenuItem);
if (menubarItemSaveChart == null) throw "Could not find menubarItemSaveChart!";
var menubar = findComponent('menubar', MenuBar);
if (menubar == null) throw "Could not find menubar!";
if (!Preferences.debugDisplay) menubar.paddingLeft = null; if (!Preferences.debugDisplay) menubar.paddingLeft = null;
this.setupNotifications(); this.setupNotifications();
@ -2005,27 +2073,52 @@ class ChartEditorState extends HaxeUIState
{ {
// Add functionality to the playbar. // Add functionality to the playbar.
addUIClickListener('playbarPlay', _ -> toggleAudioPlayback()); playbarStart.onClick = _ -> playbarButtonPressed = 'playbarStart';
addUIClickListener('playbarStart', _ -> playbarButtonPressed = 'playbarStart'); playbarBack.onClick = _ -> playbarButtonPressed = 'playbarBack';
addUIClickListener('playbarBack', _ -> playbarButtonPressed = 'playbarBack'); playbarPlay.onClick = _ -> toggleAudioPlayback();
addUIClickListener('playbarForward', _ -> playbarButtonPressed = 'playbarForward'); playbarForward.onClick = _ -> playbarButtonPressed = 'playbarForward';
addUIClickListener('playbarEnd', _ -> playbarButtonPressed = 'playbarEnd'); playbarEnd.onClick = _ -> playbarButtonPressed = 'playbarEnd';
// Cycle note snap quant. // Cycle note snap quant.
addUIRightClickListener('playbarNoteSnap', function(_) { playbarNoteSnap.onRightClick = _ -> {
noteSnapQuantIndex--; noteSnapQuantIndex--;
if (noteSnapQuantIndex < 0) noteSnapQuantIndex = SNAP_QUANTS.length - 1; if (noteSnapQuantIndex < 0) noteSnapQuantIndex = SNAP_QUANTS.length - 1;
}); };
addUIClickListener('playbarNoteSnap', function(_) { playbarNoteSnap.onClick = _ -> {
if (FlxG.keys.pressed.SHIFT)
{
noteSnapQuantIndex = BASE_QUANT_INDEX;
}
else
{
noteSnapQuantIndex++; noteSnapQuantIndex++;
if (noteSnapQuantIndex >= SNAP_QUANTS.length) noteSnapQuantIndex = 0; if (noteSnapQuantIndex >= SNAP_QUANTS.length) noteSnapQuantIndex = 0;
}); }
};
playbarBPM.onClick = _ -> {
if (FlxG.keys.pressed.CONTROL)
{
this.setToolboxState(CHART_EDITOR_TOOLBOX_METADATA_LAYOUT, true);
}
else
{
Conductor.currentTimeChange.bpm += 1;
refreshMetadataToolbox();
}
}
playbarBPM.onRightClick = _ -> {
Conductor.currentTimeChange.bpm -= 1;
refreshMetadataToolbox();
}
// Add functionality to the menu items. // Add functionality to the menu items.
addUIClickListener('menubarItemNewChart', _ -> this.openWelcomeDialog(true)); // File
addUIClickListener('menubarItemOpenChart', _ -> this.openBrowseFNFC(true)); menubarItemNewChart.onClick = _ -> this.openWelcomeDialog(true);
addUIClickListener('menubarItemSaveChart', _ -> { menubarItemOpenChart.onClick = _ -> this.openBrowseFNFC(true);
menubarItemSaveChart.onClick = _ -> {
if (currentWorkingFilePath != null) if (currentWorkingFilePath != null)
{ {
this.exportAllSongData(true, currentWorkingFilePath); this.exportAllSongData(true, currentWorkingFilePath);
@ -2034,17 +2127,14 @@ class ChartEditorState extends HaxeUIState
{ {
this.exportAllSongData(false, null); this.exportAllSongData(false, null);
} }
}); };
addUIClickListener('menubarItemSaveChartAs', _ -> this.exportAllSongData(false, null)); menubarItemSaveChartAs.onClick = _ -> this.exportAllSongData(false, null);
addUIClickListener('menubarItemLoadInst', _ -> this.openUploadInstDialog(true)); menubarItemExit.onClick = _ -> quitChartEditor();
addUIClickListener('menubarItemImportChart', _ -> this.openImportChartDialog('legacy', true));
addUIClickListener('menubarItemExit', _ -> quitChartEditor());
addUIClickListener('menubarItemUndo', _ -> undoLastCommand()); // Edit
menubarItemUndo.onClick = _ -> undoLastCommand();
addUIClickListener('menubarItemRedo', _ -> redoLastCommand()); menubarItemRedo.onClick = _ -> redoLastCommand();
menubarItemCopy.onClick = function(_) {
addUIClickListener('menubarItemCopy', function(_) {
// Doesn't use a command because it's not undoable. // Doesn't use a command because it's not undoable.
// Calculate a single time offset for all the notes and events. // Calculate a single time offset for all the notes and events.
@ -2062,24 +2152,23 @@ class ChartEditorState extends HaxeUIState
notes: SongDataUtils.buildNoteClipboard(currentNoteSelection, timeOffset), notes: SongDataUtils.buildNoteClipboard(currentNoteSelection, timeOffset),
events: SongDataUtils.buildEventClipboard(currentEventSelection, timeOffset), events: SongDataUtils.buildEventClipboard(currentEventSelection, timeOffset),
}); });
}); };
menubarItemCut.onClick = _ -> performCommand(new CutItemsCommand(currentNoteSelection, currentEventSelection));
addUIClickListener('menubarItemCut', _ -> performCommand(new CutItemsCommand(currentNoteSelection, currentEventSelection))); menubarItemPaste.onClick = _ -> {
addUIClickListener('menubarItemPaste', _ -> {
var targetMs:Float = scrollPositionInMs + playheadPositionInMs; var targetMs:Float = scrollPositionInMs + playheadPositionInMs;
var targetStep:Float = Conductor.getTimeInSteps(targetMs); var targetStep:Float = Conductor.getTimeInSteps(targetMs);
var targetSnappedStep:Float = Math.floor(targetStep / noteSnapRatio) * noteSnapRatio; var targetSnappedStep:Float = Math.floor(targetStep / noteSnapRatio) * noteSnapRatio;
var targetSnappedMs:Float = Conductor.getStepTimeInMs(targetSnappedStep); var targetSnappedMs:Float = Conductor.getStepTimeInMs(targetSnappedStep);
performCommand(new PasteItemsCommand(targetSnappedMs)); performCommand(new PasteItemsCommand(targetSnappedMs));
}); };
addUIClickListener('menubarItemPasteUnsnapped', _ -> { menubarItemPasteUnsnapped.onClick = _ -> {
var targetMs:Float = scrollPositionInMs + playheadPositionInMs; var targetMs:Float = scrollPositionInMs + playheadPositionInMs;
performCommand(new PasteItemsCommand(targetMs)); performCommand(new PasteItemsCommand(targetMs));
}); };
addUIClickListener('menubarItemDelete', function(_) { menubarItemDelete.onClick = _ -> {
if (currentNoteSelection.length > 0 && currentEventSelection.length > 0) if (currentNoteSelection.length > 0 && currentEventSelection.length > 0)
{ {
performCommand(new RemoveItemsCommand(currentNoteSelection, currentEventSelection)); performCommand(new RemoveItemsCommand(currentNoteSelection, currentEventSelection));
@ -2096,108 +2185,97 @@ class ChartEditorState extends HaxeUIState
{ {
// Do nothing??? // Do nothing???
} }
}); };
addUIClickListener('menubarItemFlipNotes', _ -> performCommand(new FlipNotesCommand(currentNoteSelection))); menubarItemFlipNotes.onClick = _ -> performCommand(new FlipNotesCommand(currentNoteSelection));
addUIClickListener('menubarItemSelectAll', _ -> performCommand(new SelectAllItemsCommand(currentNoteSelection, currentEventSelection))); menubarItemSelectAll.onClick = _ -> performCommand(new SelectAllItemsCommand(currentNoteSelection, currentEventSelection));
addUIClickListener('menubarItemSelectInverse', _ -> performCommand(new InvertSelectedItemsCommand(currentNoteSelection, currentEventSelection))); menubarItemSelectInverse.onClick = _ -> performCommand(new InvertSelectedItemsCommand(currentNoteSelection, currentEventSelection));
addUIClickListener('menubarItemSelectNone', _ -> performCommand(new DeselectAllItemsCommand(currentNoteSelection, currentEventSelection))); menubarItemSelectNone.onClick = _ -> performCommand(new DeselectAllItemsCommand(currentNoteSelection, currentEventSelection));
addUIClickListener('menubarItemPlaytestFull', _ -> testSongInPlayState(false)); menubarItemPlaytestFull.onClick = _ -> testSongInPlayState(false);
addUIClickListener('menubarItemPlaytestMinimal', _ -> testSongInPlayState(true)); menubarItemPlaytestMinimal.onClick = _ -> testSongInPlayState(true);
addUIClickListener('menuBarItemNoteSnapDecrease', _ -> { menuBarItemNoteSnapDecrease.onClick = _ -> {
noteSnapQuantIndex--; noteSnapQuantIndex--;
if (noteSnapQuantIndex < 0) noteSnapQuantIndex = SNAP_QUANTS.length - 1; if (noteSnapQuantIndex < 0) noteSnapQuantIndex = SNAP_QUANTS.length - 1;
}); };
addUIClickListener('menuBarItemNoteSnapIncrease', _ -> { menuBarItemNoteSnapIncrease.onClick = _ -> {
noteSnapQuantIndex++; noteSnapQuantIndex++;
if (noteSnapQuantIndex >= SNAP_QUANTS.length) noteSnapQuantIndex = 0; if (noteSnapQuantIndex >= SNAP_QUANTS.length) noteSnapQuantIndex = 0;
}); };
addUIChangeListener('menuBarItemInputStyleNone', function(event:UIEvent) { menuBarItemInputStyleNone.onClick = function(event:UIEvent) {
currentLiveInputStyle = None; currentLiveInputStyle = None;
}); };
addUIChangeListener('menuBarItemInputStyleNumberKeys', function(event:UIEvent) { menuBarItemInputStyleNumberKeys.onClick = function(event:UIEvent) {
currentLiveInputStyle = NumberKeys; currentLiveInputStyle = NumberKeys;
}); };
addUIChangeListener('menuBarItemInputStyleWASD', function(event:UIEvent) { menuBarItemInputStyleWASD.onClick = function(event:UIEvent) {
currentLiveInputStyle = WASD; currentLiveInputStyle = WASD;
}); };
addUIClickListener('menubarItemAbout', _ -> this.openAboutDialog()); menubarItemAbout.onClick = _ -> this.openAboutDialog();
addUIClickListener('menubarItemWelcomeDialog', _ -> this.openWelcomeDialog(true)); menubarItemWelcomeDialog.onClick = _ -> this.openWelcomeDialog(true);
#if sys #if sys
addUIClickListener('menubarItemGoToBackupsFolder', _ -> this.openBackupsFolder()); menubarItemGoToBackupsFolder.onClick = _ -> this.openBackupsFolder();
#else #else
// Disable the menu item if we're not on a desktop platform. // Disable the menu item if we're not on a desktop platform.
var menubarItemGoToBackupsFolder = findComponent('menubarItemGoToBackupsFolder', MenuItem); var menubarItemGoToBackupsFolder = findComponent('menubarItemGoToBackupsFolder', MenuItem);
if (menubarItemGoToBackupsFolder != null) menubarItemGoToBackupsFolder.disabled = true; if (menubarItemGoToBackupsFolder != null) menubarItemGoToBackupsFolder.disabled = true;
#end #end
addUIClickListener('menubarItemUserGuide', _ -> this.openUserGuideDialog()); menubarItemUserGuide.onClick = _ -> this.openUserGuideDialog();
addUIChangeListener('menubarItemDownscroll', event -> isViewDownscroll = event.value); menubarItemDownscroll.onClick = event -> isViewDownscroll = event.value;
setUICheckboxSelected('menubarItemDownscroll', isViewDownscroll); menubarItemDownscroll.selected = isViewDownscroll;
addUIClickListener('menubarItemDifficultyUp', _ -> incrementDifficulty(1)); menubarItemDifficultyUp.onClick = _ -> incrementDifficulty(1);
addUIClickListener('menubarItemDifficultyDown', _ -> incrementDifficulty(-1)); menubarItemDifficultyDown.onClick = _ -> incrementDifficulty(-1);
addUIChangeListener('menubarItemPlaytestStartTime', event -> playtestStartTime = event.value); menubarItemPlaytestStartTime.onChange = event -> playtestStartTime = event.value;
setUICheckboxSelected('menubarItemPlaytestStartTime', playtestStartTime); menubarItemPlaytestStartTime.selected = playtestStartTime;
addUIChangeListener('menuBarItemThemeLight', function(event:UIEvent) { menuBarItemThemeLight.onChange = function(event:UIEvent) {
if (event.target.value) currentTheme = ChartEditorTheme.Light; if (event.target.value) currentTheme = ChartEditorTheme.Light;
}); };
setUICheckboxSelected('menuBarItemThemeLight', currentTheme == ChartEditorTheme.Light); menuBarItemThemeLight.selected = currentTheme == ChartEditorTheme.Light;
addUIChangeListener('menuBarItemThemeDark', function(event:UIEvent) { menuBarItemThemeDark.onChange = function(event:UIEvent) {
if (event.target.value) currentTheme = ChartEditorTheme.Dark; if (event.target.value) currentTheme = ChartEditorTheme.Dark;
}); };
setUICheckboxSelected('menuBarItemThemeDark', currentTheme == ChartEditorTheme.Dark); menuBarItemThemeDark.selected = currentTheme == ChartEditorTheme.Dark;
addUIClickListener('menubarItemPlayPause', _ -> toggleAudioPlayback()); menubarItemPlayPause.onClick = _ -> toggleAudioPlayback();
addUIClickListener('menubarItemLoadInstrumental', _ -> this.openUploadInstDialog(true)); menubarItemLoadInstrumental.onClick = _ -> this.openUploadInstDialog(true);
addUIClickListener('menubarItemLoadVocals', _ -> this.openUploadVocalsDialog(true)); menubarItemLoadVocals.onClick = _ -> this.openUploadVocalsDialog(true);
addUIChangeListener('menubarItemMetronomeEnabled', event -> isMetronomeEnabled = event.value); menubarItemMetronomeEnabled.onChange = event -> isMetronomeEnabled = event.value;
setUICheckboxSelected('menubarItemMetronomeEnabled', isMetronomeEnabled); menubarItemMetronomeEnabled.selected = isMetronomeEnabled;
addUIChangeListener('menubarItemPlayerHitsounds', event -> hitsoundsEnabledPlayer = event.value); menubarItemPlayerHitsounds.onChange = event -> hitsoundsEnabledPlayer = event.value;
setUICheckboxSelected('menubarItemPlayerHitsounds', hitsoundsEnabledPlayer); menubarItemPlayerHitsounds.selected = hitsoundsEnabledPlayer;
addUIChangeListener('menubarItemOpponentHitsounds', event -> hitsoundsEnabledOpponent = event.value); menubarItemOpponentHitsounds.onChange = event -> hitsoundsEnabledOpponent = event.value;
setUICheckboxSelected('menubarItemOpponentHitsounds', hitsoundsEnabledOpponent); menubarItemOpponentHitsounds.selected = hitsoundsEnabledOpponent;
var instVolumeLabel:Null<Label> = findComponent('menubarLabelVolumeInstrumental', Label); menubarItemVolumeInstrumental.onChange = event -> {
if (instVolumeLabel != null)
{
addUIChangeListener('menubarItemVolumeInstrumental', function(event:UIEvent) {
var volume:Float = (event?.value ?? 0) / 100.0; var volume:Float = (event?.value ?? 0) / 100.0;
if (audioInstTrack != null) audioInstTrack.volume = volume; if (audioInstTrack != null) audioInstTrack.volume = volume;
instVolumeLabel.text = 'Instrumental - ${Std.int(event.value)}%'; menubarLabelVolumeInstrumental.text = 'Instrumental - ${Std.int(event.value)}%';
}); };
}
var vocalsVolumeLabel:Null<Label> = findComponent('menubarLabelVolumeVocals', Label); menubarItemVolumeVocals.onChange = event -> {
if (vocalsVolumeLabel != null)
{
addUIChangeListener('menubarItemVolumeVocals', function(event:UIEvent) {
var volume:Float = (event?.value ?? 0) / 100.0; var volume:Float = (event?.value ?? 0) / 100.0;
if (audioVocalTrackGroup != null) audioVocalTrackGroup.volume = volume; if (audioVocalTrackGroup != null) audioVocalTrackGroup.volume = volume;
vocalsVolumeLabel.text = 'Voices - ${Std.int(event.value)}%'; menubarLabelVolumeVocals.text = 'Voices - ${Std.int(event.value)}%';
});
} }
var playbackSpeedLabel:Null<Label> = findComponent('menubarLabelPlaybackSpeed', Label); menubarItemPlaybackSpeed.onChange = event -> {
if (playbackSpeedLabel != null)
{
addUIChangeListener('menubarItemPlaybackSpeed', function(event:UIEvent) {
var pitch:Float = event.value * 2.0 / 100.0; var pitch:Float = event.value * 2.0 / 100.0;
pitch = Math.floor(pitch / 0.25) * 0.25; // Round to nearest 0.25. pitch = Math.floor(pitch / 0.25) * 0.25; // Round to nearest 0.25.
#if FLX_PITCH #if FLX_PITCH
@ -2205,19 +2283,22 @@ class ChartEditorState extends HaxeUIState
if (audioVocalTrackGroup != null) audioVocalTrackGroup.pitch = pitch; if (audioVocalTrackGroup != null) audioVocalTrackGroup.pitch = pitch;
#end #end
var pitchDisplay:Float = Std.int(pitch * 100) / 100; // Round to 2 decimal places. var pitchDisplay:Float = Std.int(pitch * 100) / 100; // Round to 2 decimal places.
playbackSpeedLabel.text = 'Playback Speed - ${pitchDisplay}x'; menubarLabelPlaybackSpeed.text = 'Playback Speed - ${pitchDisplay}x';
});
} }
addUIChangeListener('menubarItemToggleToolboxDifficulty', event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT, event.value)); playbarDifficulty.onClick = _ -> {
addUIChangeListener('menubarItemToggleToolboxMetadata', event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_METADATA_LAYOUT, event.value)); this.setToolboxState(CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT, true);
addUIChangeListener('menubarItemToggleToolboxNotes', event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT, event.value)); }
addUIChangeListener('menubarItemToggleToolboxEvents', event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT, event.value));
addUIChangeListener('menubarItemToggleToolboxPlayerPreview', event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT, event.value)); menubarItemToggleToolboxDifficulty.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT, event.value);
addUIChangeListener('menubarItemToggleToolboxOpponentPreview', event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT, event.value)); menubarItemToggleToolboxMetadata.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_METADATA_LAYOUT, event.value);
menubarItemToggleToolboxNotes.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT, event.value);
menubarItemToggleToolboxEvents.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT, event.value);
menubarItemToggleToolboxPlayerPreview.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT, event.value);
menubarItemToggleToolboxOpponentPreview.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT, event.value);
// TODO: Pass specific HaxeUI components to add context menus to them. // TODO: Pass specific HaxeUI components to add context menus to them.
registerContextMenu(null, Paths.ui('chart-editor/context/test')); // registerContextMenu(null, Paths.ui('chart-editor/context/test'));
} }
/** /**
@ -2293,6 +2374,8 @@ class ChartEditorState extends HaxeUIState
// TODO: Is there a way to open a folder and highlight a file in it? // TODO: Is there a way to open a folder and highlight a file in it?
var absoluteBackupsPath:String = Path.join([Sys.getCwd(), ChartEditorImportExportHandler.BACKUPS_PATH]); var absoluteBackupsPath:String = Path.join([Sys.getCwd(), ChartEditorImportExportHandler.BACKUPS_PATH]);
WindowUtil.openFolder(absoluteBackupsPath); WindowUtil.openFolder(absoluteBackupsPath);
#else
trace('No file system access, cannot open backups folder.');
#end #end
} }
@ -3894,7 +3977,6 @@ class ChartEditorState extends HaxeUIState
function handlePlaybar():Void function handlePlaybar():Void
{ {
if (playbarHeadLayout == null) throw "ERROR: Tried to handle playbar, but playbarHeadLayout is null!"; if (playbarHeadLayout == null) throw "ERROR: Tried to handle playbar, but playbarHeadLayout is null!";
if (playbarHead == null) throw "ERROR: Tried to handle playbar, but playbarHeadLayout is null!";
// Make sure the playbar is never nudged out of the correct spot. // Make sure the playbar is never nudged out of the correct spot.
playbarHeadLayout.x = 4; playbarHeadLayout.x = 4;
@ -3907,26 +3989,24 @@ class ChartEditorState extends HaxeUIState
if (!playbarHeadDragging) if (!playbarHeadDragging)
{ {
var songPosPercent:Float = songPos / songLengthInMs * 100; var songPosPercent:Float = songPos / songLengthInMs * 100;
if (playbarHead.value != songPosPercent) playbarHead.value = songPosPercent; if (playbarHeadLayout.playbarHead.value != songPosPercent) playbarHeadLayout.playbarHead.value = songPosPercent;
} }
var songPosSeconds:String = Std.string(Math.floor((songPos / 1000) % 60)).lpad('0', 2); var songPosSeconds:String = Std.string(Math.floor((songPos / 1000) % 60)).lpad('0', 2);
var songPosMinutes:String = Std.string(Math.floor((songPos / 1000) / 60)).lpad('0', 2); var songPosMinutes:String = Std.string(Math.floor((songPos / 1000) / 60)).lpad('0', 2);
var songPosString:String = '${songPosMinutes}:${songPosSeconds}'; var songPosString:String = '${songPosMinutes}:${songPosSeconds}';
if (playbarSongPos == null) playbarSongPos = findComponent('playbarSongPos', Label); if (playbarSongPos.value != songPosString) playbarSongPos.value = songPosString;
if (playbarSongPos != null && playbarSongPos.value != songPosString) playbarSongPos.value = songPosString;
var songRemainingSeconds:String = Std.string(Math.floor((songRemaining / 1000) % 60)).lpad('0', 2); var songRemainingSeconds:String = Std.string(Math.floor((songRemaining / 1000) % 60)).lpad('0', 2);
var songRemainingMinutes:String = Std.string(Math.floor((songRemaining / 1000) / 60)).lpad('0', 2); var songRemainingMinutes:String = Std.string(Math.floor((songRemaining / 1000) / 60)).lpad('0', 2);
var songRemainingString:String = '-${songRemainingMinutes}:${songRemainingSeconds}'; var songRemainingString:String = '-${songRemainingMinutes}:${songRemainingSeconds}';
if (playbarSongRemaining == null) playbarSongRemaining = findComponent('playbarSongRemaining', Label); if (playbarSongRemaining.value != songRemainingString) playbarSongRemaining.value = songRemainingString;
if (playbarSongRemaining != null
&& playbarSongRemaining.value != songRemainingString) playbarSongRemaining.value = songRemainingString;
if (playbarNoteSnap == null) playbarNoteSnap = findComponent('playbarNoteSnap', Label); playbarNoteSnap.text = '1/${noteSnapQuant}';
if (playbarNoteSnap != null) playbarNoteSnap.text = '1/${noteSnapQuant}'; playbarDifficulty.text = "Difficulty: " + selectedDifficulty.toTitleCase();
playbarBPM.text = "BPM: " + Conductor.currentTimeChange.bpm;
} }
function handlePlayhead():Void function handlePlayhead():Void
@ -4028,18 +4108,18 @@ class ChartEditorState extends HaxeUIState
function handleFileKeybinds():Void function handleFileKeybinds():Void
{ {
// CTRL + N = New Chart // CTRL + N = New Chart
if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.N) if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.N && !isHaxeUIDialogOpen)
{ {
this.openWelcomeDialog(true); this.openWelcomeDialog(true);
} }
// CTRL + O = Open Chart // CTRL + O = Open Chart
if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.O) if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.O && !isHaxeUIDialogOpen)
{ {
this.openBrowseFNFC(true); this.openBrowseFNFC(true);
} }
if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.S) if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.S && !isHaxeUIDialogOpen)
{ {
if (currentWorkingFilePath == null || FlxG.keys.pressed.SHIFT) if (currentWorkingFilePath == null || FlxG.keys.pressed.SHIFT)
{ {
@ -4319,6 +4399,7 @@ class ChartEditorState extends HaxeUIState
this.persistentUpdate = false; this.persistentUpdate = false;
this.persistentDraw = false; this.persistentDraw = false;
stopWelcomeMusic();
openSubState(targetState); openSubState(targetState);
} }
@ -4408,7 +4489,7 @@ class ChartEditorState extends HaxeUIState
FlxG.camera.focusOn(new FlxPoint(FlxG.width / 2, FlxG.height / 2)); FlxG.camera.focusOn(new FlxPoint(FlxG.width / 2, FlxG.height / 2));
FlxG.camera.zoom = 1.0; FlxG.camera.zoom = 1.0;
add(this.component); add(this.root);
} }
/** /**
@ -4424,7 +4505,7 @@ class ChartEditorState extends HaxeUIState
if (audioVocalTrackGroup != null) audioVocalTrackGroup.play(false, audioInstTrack.time); if (audioVocalTrackGroup != null) audioVocalTrackGroup.play(false, audioInstTrack.time);
} }
setComponentText('playbarPlay', '||'); playbarPlay.text = '||'; // Pause
} }
/** /**
@ -4657,12 +4738,9 @@ class ChartEditorState extends HaxeUIState
moveSongToScrollPosition(); moveSongToScrollPosition();
var instVolumeSlider:Null<Slider> = findComponent('menubarItemVolumeInstrumental', Slider);
var vocalVolumeSlider:Null<Slider> = findComponent('menubarItemVolumeVocals', Slider);
// Reapply the volume. // Reapply the volume.
var instTargetVolume:Float = instVolumeSlider?.value ?? 1.0; var instTargetVolume:Float = menubarItemVolumeInstrumental.value ?? 1.0;
var vocalTargetVolume:Float = vocalVolumeSlider?.value ?? 1.0; var vocalTargetVolume:Float = menubarItemVolumeVocals.value ?? 1.0;
if (audioInstTrack != null) if (audioInstTrack != null)
{ {
@ -4873,48 +4951,30 @@ class ChartEditorState extends HaxeUIState
commandHistoryDirty = false; commandHistoryDirty = false;
// Update the Undo and Redo buttons. // Update the Undo and Redo buttons.
var undoButton:Null<MenuItem> = findComponent('menubarItemUndo', MenuItem);
if (undoButton != null)
{
if (undoHistory.length == 0) if (undoHistory.length == 0)
{ {
// Disable the Undo button. // Disable the Undo button.
undoButton.disabled = true; menubarItemUndo.disabled = true;
undoButton.text = 'Undo'; menubarItemUndo.text = 'Undo';
} }
else else
{ {
// Change the label to the last command. // Change the label to the last command.
undoButton.disabled = false; menubarItemUndo.disabled = false;
undoButton.text = 'Undo ${undoHistory[undoHistory.length - 1].toString()}'; menubarItemUndo.text = 'Undo ${undoHistory[undoHistory.length - 1].toString()}';
}
}
else
{
trace('undoButton is null');
} }
var redoButton:Null<MenuItem> = findComponent('menubarItemRedo', MenuItem);
if (redoButton != null)
{
if (redoHistory.length == 0) if (redoHistory.length == 0)
{ {
// Disable the Redo button. // Disable the Redo button.
redoButton.disabled = true; menubarItemRedo.disabled = true;
redoButton.text = 'Redo'; menubarItemRedo.text = 'Redo';
} }
else else
{ {
// Change the label to the last command. // Change the label to the last command.
redoButton.disabled = false; menubarItemRedo.disabled = false;
redoButton.text = 'Redo ${redoHistory[redoHistory.length - 1].toString()}'; menubarItemRedo.text = 'Redo ${redoHistory[redoHistory.length - 1].toString()}';
}
}
else
{
trace('redoButton is null');
} }
} }
} }
@ -4967,7 +5027,7 @@ class ChartEditorState extends HaxeUIState
if (audioInstTrack != null) audioInstTrack.pause(); if (audioInstTrack != null) audioInstTrack.pause();
if (audioVocalTrackGroup != null) audioVocalTrackGroup.pause(); if (audioVocalTrackGroup != null) audioVocalTrackGroup.pause();
setComponentText('playbarPlay', '>'); playbarPlay.text = '>';
} }
function toggleAudioPlayback():Void function toggleAudioPlayback():Void
@ -4976,10 +5036,12 @@ class ChartEditorState extends HaxeUIState
if (audioInstTrack.playing) if (audioInstTrack.playing)
{ {
fadeInWelcomeMusic(7, 10);
stopAudioPlayback(); stopAudioPlayback();
} }
else else
{ {
stopWelcomeMusic();
startAudioPlayback(); startAudioPlayback();
} }
} }

View file

@ -0,0 +1,17 @@
package funkin.ui.debug.charting.components;
import haxe.ui.containers.Box;
import haxe.ui.components.HorizontalSlider;
/**
* The component which contains the playhead for the chart editor.
* This is in a separate component so it can be positioned independently.
*/
@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/components/playbar-head.xml"))
class ChartEditorPlaybarHead extends Box
{
// Auto-populated
// public var playbarHead:HorizontalSlider;
// Auto-populated.
// public function new() { }
}

View file

@ -0,0 +1,25 @@
package funkin.ui.debug.charting.dialogs;
import funkin.ui.debug.charting.dialogs.ChartEditorBaseDialog.DialogParams;
@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/dialogs/about.xml"))
class ChartEditorAboutDialog extends ChartEditorBaseDialog
{
public function new(state2:ChartEditorState, params2:DialogParams)
{
super(state2, params2);
}
public static function build(state:ChartEditorState, ?closable:Bool, ?modal:Bool):ChartEditorAboutDialog
{
var dialog = new ChartEditorAboutDialog(state,
{
closable: closable ?? true,
modal: modal ?? true
});
dialog.showDialog(modal ?? true);
return dialog;
}
}

View file

@ -0,0 +1,69 @@
package funkin.ui.debug.charting.dialogs;
import haxe.ui.containers.dialogs.Dialog;
import haxe.ui.containers.dialogs.Dialog.DialogEvent;
import haxe.ui.core.Component;
@:access(funkin.ui.debug.charting.ChartEditorState)
class ChartEditorBaseDialog extends Dialog
{
var state:ChartEditorState;
var params:DialogParams;
var locked:Bool = false;
public function new(state:ChartEditorState, params:DialogParams)
{
super();
this.state = state;
this.params = params;
this.destroyOnClose = true;
this.closable = params.closable ?? false;
this.onDialogClosed = event -> onClose(event);
}
/**
* Called when the dialog is closed.
* Override this to add custom behavior.
*/
public function onClose(event:DialogEvent):Void
{
state.isHaxeUIDialogOpen = false;
}
/**
* Locks this dialog from interaction.
* Use this when you want to prevent dialog interaction while another dialog is open.
*/
public function lock():Void
{
this.locked = true;
this.closable = false;
}
/**
* Unlocks the dialog for interaction.
*/
public function unlock():Void
{
this.locked = false;
this.closable = params.closable ?? false;
}
}
typedef DialogParams =
{
?closable:Bool,
?modal:Bool
};
typedef DialogDropTarget =
{
component:Component,
handler:String->Void
}

View file

@ -0,0 +1,196 @@
package funkin.ui.debug.charting.dialogs;
import funkin.input.Cursor;
import funkin.ui.debug.charting.dialogs.ChartEditorBaseDialog.DialogDropTarget;
import funkin.ui.debug.charting.dialogs.ChartEditorBaseDialog.DialogParams;
import funkin.util.FileUtil;
import haxe.io.Path;
import haxe.ui.containers.dialogs.Dialog.DialogButton;
import haxe.ui.containers.dialogs.Dialog.DialogEvent;
import haxe.ui.containers.dialogs.Dialogs;
import haxe.ui.notifications.NotificationManager;
import haxe.ui.notifications.NotificationType;
@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/dialogs/upload-chart.xml"))
class ChartEditorUploadChartDialog extends ChartEditorBaseDialog
{
var dropHandlers:Array<DialogDropTarget> = [];
public function new(state2:ChartEditorState, params2:DialogParams)
{
super(state2, params2);
this.dialogCancel.onClick = (_) -> this.hideDialog(DialogButton.CANCEL);
this.chartBox.onClick = (_) -> this.onClickChartBox();
this.chartBox.onMouseOver = function(_event) {
if (this.locked) return;
this.chartBox.swapClass('upload-bg', 'upload-bg-hover');
Cursor.cursorMode = Pointer;
}
this.chartBox.onMouseOut = function(_event) {
this.chartBox.swapClass('upload-bg-hover', 'upload-bg');
Cursor.cursorMode = Default;
}
dropHandlers.push({component: this.chartBox, handler: this.onDropFileChartBox});
}
public static function build(state:ChartEditorState, ?closable:Bool, ?modal:Bool):ChartEditorUploadChartDialog
{
var dialog = new ChartEditorUploadChartDialog(state,
{
closable: closable ?? false,
modal: modal ?? true
});
for (dropTarget in dialog.dropHandlers)
{
state.addDropHandler(dropTarget);
}
dialog.showDialog(modal ?? true);
return dialog;
}
public override function onClose(event:DialogEvent):Void
{
super.onClose(event);
if (event.button != DialogButton.APPLY && !this.closable)
{
// User cancelled the wizard! Back to the welcome dialog.
state.openWelcomeDialog(this.closable);
}
for (dropTarget in dropHandlers)
{
state.removeDropHandler(dropTarget);
}
}
public override function lock():Void
{
super.lock();
this.dialogCancel.disabled = true;
}
public override function unlock():Void
{
super.unlock();
this.dialogCancel.disabled = false;
}
/**
* Called when clicking the Upload Chart box.
*/
public function onClickChartBox():Void
{
if (this.locked) return;
this.lock();
FileUtil.browseForBinaryFile('Open Chart', [FileUtil.FILE_EXTENSION_INFO_FNFC], onSelectFile, onCancelBrowse);
}
/**
* Called when a file is selected by dropping a file onto the Upload Chart box.
*/
function onDropFileChartBox(pathStr:String):Void
{
var path:Path = new Path(pathStr);
trace('Dropped file (${path})');
try
{
var result:Null<Array<String>> = ChartEditorImportExportHandler.loadFromFNFCPath(state, path.toString());
if (result != null)
{
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Success',
body: result.length == 0 ? 'Loaded chart (${path.toString()})' : 'Loaded chart (${path.toString()})\n${result.join("\n")}',
type: result.length == 0 ? NotificationType.Success : NotificationType.Warning,
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
this.hideDialog(DialogButton.APPLY);
}
else
{
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Failure',
body: 'Failed to load chart (${path.toString()})',
type: NotificationType.Error,
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
}
}
catch (err)
{
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Failure',
body: 'Failed to load chart (${path.toString()}): ${err}',
type: NotificationType.Error,
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
}
}
/**
* Called when a file is selected by the dialog displayed when clicking the Upload Chart box.
*/
function onSelectFile(selectedFile:SelectedFileInfo):Void
{
this.unlock();
if (selectedFile != null && selectedFile.bytes != null)
{
try
{
var result:Null<Array<String>> = ChartEditorImportExportHandler.loadFromFNFC(state, selectedFile.bytes);
if (result != null)
{
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Success',
body: 'Loaded chart (${selectedFile.name})',
type: NotificationType.Success,
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
if (selectedFile.fullPath != null) state.currentWorkingFilePath = selectedFile.fullPath;
this.hideDialog(DialogButton.APPLY);
}
}
catch (err)
{
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Failure',
body: 'Failed to load chart (${selectedFile.name}): ${err}',
type: NotificationType.Error,
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
}
}
}
function onCancelBrowse():Void
{
this.unlock();
}
}

View file

@ -0,0 +1,262 @@
package funkin.ui.debug.charting.dialogs;
import funkin.data.song.SongRegistry;
import funkin.play.song.Song;
import funkin.ui.debug.charting.ChartEditorState;
import funkin.ui.debug.charting.dialogs.ChartEditorBaseDialog;
import funkin.ui.debug.charting.dialogs.ChartEditorBaseDialog.DialogParams;
import funkin.util.FileUtil;
import funkin.util.SortUtil;
import haxe.ui.components.Label;
import haxe.ui.components.Link;
import haxe.ui.containers.dialogs.Dialog.DialogButton;
import haxe.ui.containers.dialogs.Dialog.DialogEvent;
import haxe.ui.core.Component;
import haxe.ui.events.MouseEvent;
import haxe.ui.events.UIEvent;
import haxe.ui.notifications.NotificationManager;
import haxe.ui.notifications.NotificationType;
/**
* Builds and opens a dialog letting the user create a new chart, open a recent chart, or load from a template.
* Opens when the chart editor first opens.
*/
@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/dialogs/welcome.xml"))
@:access(funkin.ui.debug.charting.ChartEditorState)
class ChartEditorWelcomeDialog extends ChartEditorBaseDialog
{
/**
* @param closable Whether the dialog can be closed by the user.
* @param modal Whether the dialog is locked to the center of the screen (with a dark overlay behind it).
*/
public function new(state2:ChartEditorState, params2:DialogParams)
{
super(state2, params2);
this.splashBrowse.onClick = _ -> onClickButtonBrowse();
this.splashCreateFromSongBasicOnly.onClick = _ -> onClickLinkCreateBasicOnly();
this.splashCreateFromSongErectOnly.onClick = _ -> onClickLinkCreateErectOnly();
this.splashCreateFromSongBasicErect.onClick = _ -> onClickLinkCreateBasicErect();
this.splashImportChartLegacy.onClick = _ -> onClickLinkImportChartLegacy();
// Add items to the Recent Charts list
#if sys
for (chartPath in state.previousWorkingFilePaths)
{
if (chartPath == null) continue;
this.addRecentFilePath(state, chartPath);
}
#else
this.addHTML5RecentFileMessage();
#end
// Add items to the Load From Template list
this.buildTemplateSongList(state);
}
/**
* @param state The current state of the chart editor.
* @return A newly created `ChartEditorWelcomeDialog`.
*/
public static function build(state:ChartEditorState, ?closable:Bool, ?modal:Bool):ChartEditorWelcomeDialog
{
var dialog = new ChartEditorWelcomeDialog(state,
{
closable: closable ?? false,
modal: modal ?? true
});
dialog.showDialog(modal ?? true);
return dialog;
}
public override function onClose(event:DialogEvent):Void
{
super.onClose(event);
}
/**
* Add a file path to the "Open Recent" scroll box on the left.
* @param path
*/
public function addRecentFilePath(state:ChartEditorState, chartPath:String):Void
{
var linkRecentChart:Link = new Link();
var fileNamePattern:EReg = new EReg("([^/\\\\]+)$", "");
var fileName:String = fileNamePattern.match(chartPath) ? fileNamePattern.matched(1) : chartPath;
linkRecentChart.text = fileName;
linkRecentChart.tooltip = chartPath;
#if sys
var lastModified:String = "Last Modified: " + sys.FileSystem.stat(chartPath).mtime.toString();
linkRecentChart.tooltip += "\n" + lastModified;
#end
linkRecentChart.onClick = function(_event) {
this.hideDialog(DialogButton.CANCEL);
// Load chart from file
var result:Null<Array<String>> = ChartEditorImportExportHandler.loadFromFNFCPath(state, chartPath);
if (result != null)
{
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Success',
body: result.length == 0 ? 'Loaded chart (${chartPath.toString()})' : 'Loaded chart (${chartPath.toString()})\n${result.join("\n")}',
type: result.length == 0 ? NotificationType.Success : NotificationType.Warning,
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
}
else
{
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Failure',
body: 'Failed to load chart (${chartPath.toString()})',
type: NotificationType.Error,
expiryMs: Constants.NOTIFICATION_DISMISS_TIME
});
#end
}
}
if (!FileUtil.doesFileExist(chartPath))
{
trace('Previously loaded chart file (${chartPath}) does not exist, disabling link...');
linkRecentChart.disabled = true;
}
splashRecentContainer.addComponent(linkRecentChart);
}
/**
* Add a string message to the "Open Recent" scroll box on the left.
* Only displays on platforms which don't support direct file system access.
*/
public function addHTML5RecentFileMessage():Void
{
var webLoadLabel:Label = new Label();
webLoadLabel.text = 'Click the button below to load a chart file (.fnfc) from your computer.';
splashRecentContainer.addComponent(webLoadLabel);
}
/**
* Add all the links to the "Create From Template" scroll box on the right.
*/
public function buildTemplateSongList(state:ChartEditorState):Void
{
var songList:Array<String> = SongRegistry.instance.listEntryIds();
songList.sort(SortUtil.alphabetically);
for (targetSongId in songList)
{
var songData:Null<Song> = SongRegistry.instance.fetchEntry(targetSongId);
if (songData == null) continue;
var songName:Null<String> = songData.getDifficulty('normal')?.songName;
if (songName == null) songName = songData.getDifficulty()?.songName;
if (songName == null) // Still null?
{
trace('[WARN] Could not fetch song name for ${targetSongId}');
continue;
}
this.addTemplateSong(songName, targetSongId, (_) -> {
this.hideDialog(DialogButton.CANCEL);
// Load song from template
state.loadSongAsTemplate(targetSongId);
});
}
}
/**
* @param loadTemplateCb The callback to call when the user clicks the link. The callback should load the song ID from the template.
*/
public function addTemplateSong(songName:String, songId:String, onClickCb:(MouseEvent) -> Void):Void
{
var linkTemplateSong:Link = new Link();
linkTemplateSong.text = songName;
linkTemplateSong.onClick = onClickCb;
this.splashTemplateContainer.addComponent(linkTemplateSong);
}
/**
* Called when the user clicks the "Browse Chart" button in the dialog.
* Reassign this function to change the behavior.
*/
public function onClickButtonBrowse():Void
{
// Hide the welcome dialog
this.hideDialog(DialogButton.CANCEL);
// Open the "Open Chart" dialog
state.openBrowseFNFC(false);
}
/**
* Called when the user clicks the "Create From Template: Easy/Normal/Hard Only" link in the dialog.
* Reassign this function to change the behavior.
*/
public function onClickLinkCreateBasicOnly():Void
{
// Hide the welcome dialog
this.hideDialog(DialogButton.CANCEL);
//
// Create Song Wizard
//
state.openCreateSongWizardBasicOnly(false);
}
/**
* Called when the user clicks the "Create From Template: Erect/Nightmare Only" link in the dialog.
* Reassign this function to change the behavior.
*/
public function onClickLinkCreateErectOnly():Void
{
// Hide the welcome dialog
this.hideDialog(DialogButton.CANCEL);
//
// Create Song Wizard
//
state.openCreateSongWizardErectOnly(false);
}
/**
* Called when the user clicks the "Create From Template: Easy/Normal/Hard/Erect/Nightmare" link in the dialog.
* Reassign this function to change the behavior.
*/
public function onClickLinkCreateBasicErect():Void
{
// Hide the welcome dialog
this.hideDialog(DialogButton.CANCEL);
//
// Create Song Wizard
//
state.openCreateSongWizardBasicErect(false);
}
/**
* Called when the user clicks the "Import Chart: FNF Legacy" link in the dialog.
* Reassign this function to change the behavior.
*/
public function onClickLinkImportChartLegacy():Void
{
// Hide the welcome dialog
this.hideDialog(DialogButton.CANCEL);
// Open the "Import Chart" dialog
state.openImportChartWizard('legacy', false);
}
}

View file

@ -14,13 +14,17 @@ import funkin.play.character.CharacterData;
import funkin.play.character.CharacterData.CharacterDataParser; import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.play.song.Song; import funkin.play.song.Song;
import funkin.play.stage.StageData; import funkin.play.stage.StageData;
import funkin.ui.debug.charting.dialogs.ChartEditorAboutDialog;
import funkin.ui.debug.charting.dialogs.ChartEditorBaseDialog.DialogDropTarget;
import funkin.ui.debug.charting.dialogs.ChartEditorUploadChartDialog;
import funkin.ui.debug.charting.dialogs.ChartEditorWelcomeDialog;
import funkin.ui.debug.charting.util.ChartEditorDropdowns; import funkin.ui.debug.charting.util.ChartEditorDropdowns;
import funkin.util.Constants; import funkin.util.Constants;
import funkin.util.DateUtil;
import funkin.util.FileUtil; import funkin.util.FileUtil;
import funkin.util.SerializerUtil; import funkin.util.SerializerUtil;
import funkin.util.SortUtil; import funkin.util.SortUtil;
import funkin.util.VersionUtil; import funkin.util.VersionUtil;
import funkin.util.DateUtil;
import funkin.util.WindowUtil; import funkin.util.WindowUtil;
import haxe.io.Path; import haxe.io.Path;
import haxe.ui.components.Button; import haxe.ui.components.Button;
@ -38,6 +42,9 @@ import haxe.ui.containers.Form;
import haxe.ui.containers.VBox; import haxe.ui.containers.VBox;
import haxe.ui.core.Component; import haxe.ui.core.Component;
import haxe.ui.events.UIEvent; import haxe.ui.events.UIEvent;
import haxe.ui.notifications.NotificationManager;
import haxe.ui.notifications.NotificationType;
import haxe.ui.RuntimeComponentBuilder;
import thx.semver.Version; import thx.semver.Version;
using Lambda; using Lambda;
@ -50,8 +57,6 @@ using Lambda;
class ChartEditorDialogHandler class ChartEditorDialogHandler
{ {
// Paths to HaxeUI layout files for each dialog. // Paths to HaxeUI layout files for each dialog.
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_CHART_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-chart'); static final CHART_EDITOR_DIALOG_UPLOAD_CHART_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-chart');
static final CHART_EDITOR_DIALOG_UPLOAD_INST_LAYOUT:String = Paths.ui('chart-editor/dialogs/upload-inst'); 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_LAYOUT:String = Paths.ui('chart-editor/dialogs/song-metadata');
@ -72,7 +77,12 @@ class ChartEditorDialogHandler
*/ */
public static function openAboutDialog(state:ChartEditorState):Null<Dialog> public static function openAboutDialog(state:ChartEditorState):Null<Dialog>
{ {
return openDialog(state, CHART_EDITOR_DIALOG_ABOUT_LAYOUT, true, true); var dialog = ChartEditorAboutDialog.build(state);
dialog.zIndex = 1000;
state.isHaxeUIDialogOpen = true;
return dialog;
} }
/** /**
@ -83,177 +93,18 @@ class ChartEditorDialogHandler
*/ */
public static function openWelcomeDialog(state:ChartEditorState, closable:Bool = true):Null<Dialog> public static function openWelcomeDialog(state:ChartEditorState, closable:Bool = true):Null<Dialog>
{ {
var dialog:Null<Dialog> = openDialog(state, CHART_EDITOR_DIALOG_WELCOME_LAYOUT, true, closable); var dialog = ChartEditorWelcomeDialog.build(state, closable);
if (dialog == null) throw 'Could not locate Welcome dialog';
dialog.zIndex = 1000;
state.isHaxeUIDialogOpen = true; state.isHaxeUIDialogOpen = true;
dialog.onDialogClosed = function(event) {
state.isHaxeUIDialogOpen = false;
// Called when the Welcome dialog is closed while it is closable.
state.stopWelcomeMusic();
}
#if sys
var splashRecentContainer:Null<VBox> = dialog.findComponent('splashRecentContainer', VBox);
if (splashRecentContainer == null) throw 'Could not locate splashRecentContainer in Welcome dialog';
for (chartPath in state.previousWorkingFilePaths)
{
if (chartPath == null) continue;
var linkRecentChart:Link = new Link();
// regex to only use the filename, not the full path
// "dadbattle.fnc" insted of "c:/user/docs/funkin/dadbattle.fnc"
// hovering tooltip shows full path
var fileNamePattern:EReg = new EReg("([^/\\\\]+)$", "");
var fileName:String = fileNamePattern.match(chartPath) ? fileNamePattern.matched(1) : chartPath;
linkRecentChart.text = fileName;
linkRecentChart.tooltip = chartPath;
linkRecentChart.onClick = function(_) {
dialog.hideDialog(DialogButton.CANCEL);
state.stopWelcomeMusic();
// Load chart from file
var result:Null<Array<String>> = ChartEditorImportExportHandler.loadFromFNFCPath(state, chartPath);
if (result != null)
{
if (result.length == 0)
{
// No warnings.
state.success('Loaded Chart', 'Loaded chart (${chartPath.toString()})');
}
else
{
// One or more warnings.
state.warning('Loaded Chart', 'Loaded chart (${chartPath.toString()})\n${result.join("\n")}');
}
}
else
{
state.error('Failed to Load Chart', 'Failed to load chart (${chartPath.toString()})');
}
}
if (!FileUtil.doesFileExist(chartPath))
{
trace('Previously loaded chart file (${chartPath}) does not exist, disabling link...');
linkRecentChart.disabled = true;
}
splashRecentContainer.addComponent(linkRecentChart);
}
#else
var splashRecentContainer:Null<VBox> = dialog.findComponent('splashRecentContainer', VBox);
if (splashRecentContainer == null) throw 'Could not locate splashRecentContainer in Welcome dialog';
var webLoadLabel:Label = new Label();
webLoadLabel.text = 'Click the button below to load a chart file (.fnfc) from your computer.';
splashRecentContainer.add(webLoadLabel);
#end
// Create New Song "Easy/Normal/Hard"
var linkCreateBasic:Null<Link> = dialog.findComponent('splashCreateFromSongBasicOnly', Link);
if (linkCreateBasic == null) throw 'Could not locate splashCreateFromSongBasicOnly link in Welcome dialog';
linkCreateBasic.onClick = function(_) {
// Hide the welcome dialog
dialog.hideDialog(DialogButton.CANCEL);
state.stopWelcomeMusic();
//
// Create Song Wizard
//
openCreateSongWizardBasicOnly(state, false);
}
// Create New Song "Erect/Nightmare"
var linkCreateErect:Null<Link> = dialog.findComponent('splashCreateFromSongErectOnly', Link);
if (linkCreateErect == null) throw 'Could not locate splashCreateFromSongErectOnly link in Welcome dialog';
linkCreateErect.onClick = function(_) {
// Hide the welcome dialog
dialog.hideDialog(DialogButton.CANCEL);
//
// Create Song Wizard
//
openCreateSongWizardErectOnly(state, false);
}
// Create New Song "Easy/Normal/Hard/Erect/Nightmare"
var linkCreateErect:Null<Link> = dialog.findComponent('splashCreateFromSongBasicErect', Link);
if (linkCreateErect == null) throw 'Could not locate splashCreateFromSongBasicErect link in Welcome dialog';
linkCreateErect.onClick = function(_) {
// Hide the welcome dialog
dialog.hideDialog(DialogButton.CANCEL);
//
// Create Song Wizard
//
openCreateSongWizardBasicErect(state, false);
}
var linkImportChartLegacy:Null<Link> = dialog.findComponent('splashImportChartLegacy', Link);
if (linkImportChartLegacy == null) throw 'Could not locate splashImportChartLegacy link in Welcome dialog';
linkImportChartLegacy.onClick = function(_) {
// Hide the welcome dialog
dialog.hideDialog(DialogButton.CANCEL);
state.stopWelcomeMusic();
// Open the "Import Chart" dialog
openImportChartWizard(state, 'legacy', false);
};
var buttonBrowse:Null<Button> = dialog.findComponent('splashBrowse', Button);
if (buttonBrowse == null) throw 'Could not locate splashBrowse button in Welcome dialog';
buttonBrowse.onClick = function(_) {
// Hide the welcome dialog
dialog.hideDialog(DialogButton.CANCEL);
state.stopWelcomeMusic();
// Open the "Open Chart" dialog
openBrowseFNFC(state, false);
}
var splashTemplateContainer:Null<VBox> = dialog.findComponent('splashTemplateContainer', VBox);
if (splashTemplateContainer == null) throw 'Could not locate splashTemplateContainer in Welcome dialog';
var songList:Array<String> = SongRegistry.instance.listEntryIds();
songList.sort(SortUtil.alphabetically);
for (targetSongId in songList)
{
var songData:Null<Song> = SongRegistry.instance.fetchEntry(targetSongId);
if (songData == null) continue;
var songName:Null<String> = songData.getDifficulty('normal')?.songName;
if (songName == null) songName = songData.getDifficulty()?.songName;
if (songName == null) // Still null?
{
trace('[WARN] Could not fetch song name for ${targetSongId}');
continue;
}
var linkTemplateSong:Link = new Link();
linkTemplateSong.text = songName;
linkTemplateSong.onClick = function(_) {
dialog.hideDialog(DialogButton.CANCEL);
state.stopWelcomeMusic();
// Load song from template
state.loadSongAsTemplate(targetSongId);
}
splashTemplateContainer.addComponent(linkTemplateSong);
}
state.fadeInWelcomeMusic(); state.fadeInWelcomeMusic();
return dialog; return dialog;
} }
/** /**
* [Description] * Builds and opens a dialog letting the user know a backup is available, and prompting them to load it.
* @param state
* @return Null<Dialog>
*/ */
public static function openBackupAvailableDialog(state:ChartEditorState, welcomeDialog:Null<Dialog>):Null<Dialog> public static function openBackupAvailableDialog(state:ChartEditorState, welcomeDialog:Null<Dialog>):Null<Dialog>
{ {
@ -333,112 +184,18 @@ class ChartEditorDialogHandler
return dialog; return dialog;
} }
/**
* Builds and opens a dialog letting the user browse for a chart file to open.
* @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 openBrowseFNFC(state:ChartEditorState, closable:Bool):Null<Dialog> public static function openBrowseFNFC(state:ChartEditorState, closable:Bool):Null<Dialog>
{ {
var dialog:Null<Dialog> = openDialog(state, CHART_EDITOR_DIALOG_UPLOAD_CHART_LAYOUT, true, closable); var dialog = ChartEditorUploadChartDialog.build(state, closable);
if (dialog == null) throw 'Could not locate Upload Chart dialog';
dialog.onDialogClosed = function(event) {
state.isHaxeUIDialogOpen = false;
if (event.button == DialogButton.APPLY)
{
// Simply let the dialog close.
}
else
{
// User cancelled the wizard! Back to the welcome dialog.
openWelcomeDialog(state);
}
};
var buttonCancel:Null<Button> = dialog.findComponent('dialogCancel', Button);
if (buttonCancel == null) throw 'Could not locate dialogCancel button in Upload Chart dialog';
dialog.zIndex = 1000;
state.isHaxeUIDialogOpen = true; state.isHaxeUIDialogOpen = true;
buttonCancel.onClick = function(_) {
dialog.hideDialog(DialogButton.CANCEL);
}
var chartBox:Null<Box> = dialog.findComponent('chartBox', Box);
if (chartBox == null) throw 'Could not locate chartBox in Upload Chart dialog';
chartBox.onMouseOver = function(_) {
chartBox.swapClass('upload-bg', 'upload-bg-hover');
Cursor.cursorMode = Pointer;
}
chartBox.onMouseOut = function(_) {
chartBox.swapClass('upload-bg-hover', 'upload-bg');
Cursor.cursorMode = Default;
}
var onDropFile:String->Void;
chartBox.onClick = function(_) {
Dialogs.openBinaryFile('Open Chart', [
{label: 'Friday Night Funkin\' Chart (.fnfc)', extension: 'fnfc'}], function(selectedFile:SelectedFileInfo) {
if (selectedFile != null && selectedFile.bytes != null)
{
try
{
var result:Null<Array<String>> = ChartEditorImportExportHandler.loadFromFNFC(state, selectedFile.bytes);
if (result != null)
{
if (result.length == 0)
{
state.success('Loaded Chart', 'Loaded chart (${selectedFile.name})');
}
else
{
state.warning('Loaded Chart', 'Loaded chart (${selectedFile.name})\n${result.join("\n")}');
}
if (selectedFile.fullPath != null) state.currentWorkingFilePath = selectedFile.fullPath;
dialog.hideDialog(DialogButton.APPLY);
removeDropHandler(onDropFile);
}
}
catch (err)
{
state.error('Failed to Load Chart', 'Failed to load chart (${selectedFile.name}): ${err}');
}
}
});
}
onDropFile = function(pathStr:String) {
var path:Path = new Path(pathStr);
trace('Dropped file (${path})');
try
{
var result:Null<Array<String>> = ChartEditorImportExportHandler.loadFromFNFCPath(state, path.toString());
if (result != null)
{
if (result.length == 0)
{
state.success('Loaded Chart', 'Loaded chart (${path.file}.${path.ext})');
}
else
{
state.warning('Loaded Chart', 'Loaded chart (${path.file}.${path.ext})\n${result.join("\n")}');
}
dialog.hideDialog(DialogButton.APPLY);
removeDropHandler(onDropFile);
}
else
{
state.error('Failed to Load Chart', 'Failed to load chart (${path.file}.${path.ext})');
}
}
catch (err)
{
state.error('Failed to Load Chart', 'Failed to load chart (${path.file}.${path.ext})');
}
};
addDropHandler(chartBox, onDropFile);
return dialog; return dialog;
} }
@ -476,14 +233,14 @@ class ChartEditorDialogHandler
else else
{ {
// User cancelled the wizard! Back to the welcome dialog. // User cancelled the wizard! Back to the welcome dialog.
openWelcomeDialog(state); state.openWelcomeDialog(closable);
} }
}; };
} }
else else
{ {
// User cancelled the wizard! Back to the welcome dialog. // User cancelled the wizard! Back to the welcome dialog.
openWelcomeDialog(state); state.openWelcomeDialog(closable);
} }
}; };
} }
@ -517,14 +274,14 @@ class ChartEditorDialogHandler
else else
{ {
// User cancelled the wizard! Back to the welcome dialog. // User cancelled the wizard! Back to the welcome dialog.
openWelcomeDialog(state); state.openWelcomeDialog(closable);
} }
}; };
} }
else else
{ {
// User cancelled the wizard! Back to the welcome dialog. // User cancelled the wizard! Back to the welcome dialog.
openWelcomeDialog(state); state.openWelcomeDialog(closable);
} }
}; };
} }
@ -556,14 +313,14 @@ class ChartEditorDialogHandler
else else
{ {
// User cancelled the wizard at Step 2! Back to the welcome dialog. // User cancelled the wizard at Step 2! Back to the welcome dialog.
openWelcomeDialog(state); state.openWelcomeDialog(closable);
} }
}; };
} }
else else
{ {
// User cancelled the wizard at Step 1! Back to the welcome dialog. // User cancelled the wizard at Step 1! Back to the welcome dialog.
openWelcomeDialog(state); state.openWelcomeDialog(closable);
} }
}; };
} }
@ -595,14 +352,14 @@ class ChartEditorDialogHandler
else else
{ {
// User cancelled the wizard at Step 2! Back to the welcome dialog. // User cancelled the wizard at Step 2! Back to the welcome dialog.
openWelcomeDialog(state); state.openWelcomeDialog(closable);
} }
}; };
} }
else else
{ {
// User cancelled the wizard at Step 1! Back to the welcome dialog. // User cancelled the wizard at Step 1! Back to the welcome dialog.
openWelcomeDialog(state); state.openWelcomeDialog(closable);
} }
}; };
} }
@ -654,14 +411,14 @@ class ChartEditorDialogHandler
else else
{ {
// User cancelled the wizard at Step 5! Back to the welcome dialog. // User cancelled the wizard at Step 5! Back to the welcome dialog.
openWelcomeDialog(state); state.openWelcomeDialog(closable);
} }
}; };
} }
else else
{ {
// User cancelled the wizard at Step 4! Back to the welcome dialog. // User cancelled the wizard at Step 4! Back to the welcome dialog.
openWelcomeDialog(state); state.openWelcomeDialog(closable);
} }
} }
} }
@ -669,14 +426,14 @@ class ChartEditorDialogHandler
else else
{ {
// User cancelled the wizard at Step 2! Back to the welcome dialog. // User cancelled the wizard at Step 2! Back to the welcome dialog.
openWelcomeDialog(state); state.openWelcomeDialog(closable);
} }
}; };
} }
else else
{ {
// User cancelled the wizard at Step 1! Back to the welcome dialog. // User cancelled the wizard at Step 1! Back to the welcome dialog.
openWelcomeDialog(state); state.openWelcomeDialog(closable);
} }
}; };
} }
@ -715,7 +472,7 @@ class ChartEditorDialogHandler
var instId:String = state.currentInstrumentalId; var instId:String = state.currentInstrumentalId;
var onDropFile:String->Void; var dropHandler:DialogDropTarget = {component: instrumentalBox, handler: null};
instrumentalBox.onClick = function(_) { instrumentalBox.onClick = function(_) {
Dialogs.openBinaryFile('Open Instrumental', [ Dialogs.openBinaryFile('Open Instrumental', [
@ -728,7 +485,7 @@ class ChartEditorDialogHandler
state.switchToCurrentInstrumental(); state.switchToCurrentInstrumental();
dialog.hideDialog(DialogButton.APPLY); dialog.hideDialog(DialogButton.APPLY);
removeDropHandler(onDropFile); state.removeDropHandler(dropHandler);
} }
else else
{ {
@ -738,7 +495,7 @@ class ChartEditorDialogHandler
}); });
} }
onDropFile = function(pathStr:String) { var onDropFile:String->Void = function(pathStr:String) {
var path:Path = new Path(pathStr); var path:Path = new Path(pathStr);
trace('Dropped file (${path})'); trace('Dropped file (${path})');
if (state.loadInstFromPath(path, instId)) if (state.loadInstFromPath(path, instId))
@ -748,7 +505,7 @@ class ChartEditorDialogHandler
state.switchToCurrentInstrumental(); state.switchToCurrentInstrumental();
dialog.hideDialog(DialogButton.APPLY); dialog.hideDialog(DialogButton.APPLY);
removeDropHandler(onDropFile); state.removeDropHandler(dropHandler);
} }
else else
{ {
@ -766,7 +523,9 @@ class ChartEditorDialogHandler
} }
}; };
addDropHandler(instrumentalBox, onDropFile); dropHandler.handler = onDropFile;
state.addDropHandler(dropHandler);
return dialog; return dialog;
} }
@ -968,7 +727,7 @@ class ChartEditorDialogHandler
var charMetadata:Null<CharacterData> = CharacterDataParser.fetchCharacterData(charKey); var charMetadata:Null<CharacterData> = CharacterDataParser.fetchCharacterData(charKey);
var charName:String = charMetadata != null ? charMetadata.name : charKey; var charName:String = charMetadata != null ? charMetadata.name : charKey;
var vocalsEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_UPLOAD_VOCALS_ENTRY_LAYOUT); var vocalsEntry:Component = RuntimeComponentBuilder.fromAsset(CHART_EDITOR_DIALOG_UPLOAD_VOCALS_ENTRY_LAYOUT);
var vocalsEntryLabel:Null<Label> = vocalsEntry.findComponent('vocalsEntryLabel', Label); var vocalsEntryLabel:Null<Label> = vocalsEntry.findComponent('vocalsEntryLabel', Label);
if (vocalsEntryLabel == null) throw 'Could not locate vocalsEntryLabel in Upload Vocals dialog'; if (vocalsEntryLabel == null) throw 'Could not locate vocalsEntryLabel in Upload Vocals dialog';
@ -978,6 +737,8 @@ class ChartEditorDialogHandler
vocalsEntryLabel.text = 'Click to browse for vocals for $charName.'; vocalsEntryLabel.text = 'Click to browse for vocals for $charName.';
#end #end
var dropHandler:DialogDropTarget = {component: vocalsEntry, handler: null};
var onDropFile:String->Void = function(pathStr:String) { var onDropFile:String->Void = function(pathStr:String) {
trace('Selected file: $pathStr'); trace('Selected file: $pathStr');
var path:Path = new Path(pathStr); var path:Path = new Path(pathStr);
@ -999,7 +760,7 @@ class ChartEditorDialogHandler
#end #end
dialogNoVocals.hidden = true; dialogNoVocals.hidden = true;
removeDropHandler(onDropFile); state.removeDropHandler(dropHandler);
} }
else else
{ {
@ -1015,7 +776,9 @@ class ChartEditorDialogHandler
} }
}; };
vocalsEntry.onClick = function(_) { dropHandler.handler = onDropFile;
vocalsEntry.onClick = function(_event) {
Dialogs.openBinaryFile('Open $charName Vocals', [ Dialogs.openBinaryFile('Open $charName Vocals', [
{label: 'Audio File (.ogg)', extension: 'ogg'}], function(selectedFile) { {label: 'Audio File (.ogg)', extension: 'ogg'}], function(selectedFile) {
if (selectedFile != null && selectedFile.bytes != null) if (selectedFile != null && selectedFile.bytes != null)
@ -1057,7 +820,7 @@ class ChartEditorDialogHandler
// onDropFile // onDropFile
#if FILE_DROP_SUPPORTED #if FILE_DROP_SUPPORTED
addDropHandler(vocalsEntry, onDropFile); addDropHandler(dropHandler);
#end #end
dialogContainer.addComponent(vocalsEntry); dialogContainer.addComponent(vocalsEntry);
} }
@ -1119,7 +882,7 @@ class ChartEditorDialogHandler
} }
// Build an entry for -chart.json. // Build an entry for -chart.json.
var songDefaultChartDataEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_OPEN_CHART_PARTS_ENTRY_LAYOUT); var songDefaultChartDataEntry:Component = RuntimeComponentBuilder.fromAsset(CHART_EDITOR_DIALOG_OPEN_CHART_PARTS_ENTRY_LAYOUT);
var songDefaultChartDataEntryLabel:Null<Label> = songDefaultChartDataEntry.findComponent('chartEntryLabel', Label); var songDefaultChartDataEntryLabel:Null<Label> = songDefaultChartDataEntry.findComponent('chartEntryLabel', Label);
if (songDefaultChartDataEntryLabel == null) throw 'Could not locate chartEntryLabel in Open Chart dialog'; if (songDefaultChartDataEntryLabel == null) throw 'Could not locate chartEntryLabel in Open Chart dialog';
#if FILE_DROP_SUPPORTED #if FILE_DROP_SUPPORTED
@ -1129,13 +892,17 @@ class ChartEditorDialogHandler
#end #end
songDefaultChartDataEntry.onClick = onClickChartDataVariation.bind(Constants.DEFAULT_VARIATION).bind(songDefaultChartDataEntryLabel); songDefaultChartDataEntry.onClick = onClickChartDataVariation.bind(Constants.DEFAULT_VARIATION).bind(songDefaultChartDataEntryLabel);
addDropHandler(songDefaultChartDataEntry, onDropFileChartDataVariation.bind(Constants.DEFAULT_VARIATION).bind(songDefaultChartDataEntryLabel)); state.addDropHandler(
{
component: songDefaultChartDataEntry,
handler: onDropFileChartDataVariation.bind(Constants.DEFAULT_VARIATION).bind(songDefaultChartDataEntryLabel)
});
chartContainerB.addComponent(songDefaultChartDataEntry); chartContainerB.addComponent(songDefaultChartDataEntry);
for (variation in variations) for (variation in variations)
{ {
// Build entries for -metadata-<variation>.json. // Build entries for -metadata-<variation>.json.
var songVariationMetadataEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_OPEN_CHART_PARTS_ENTRY_LAYOUT); var songVariationMetadataEntry:Component = RuntimeComponentBuilder.fromAsset(CHART_EDITOR_DIALOG_OPEN_CHART_PARTS_ENTRY_LAYOUT);
var songVariationMetadataEntryLabel:Null<Label> = songVariationMetadataEntry.findComponent('chartEntryLabel', Label); var songVariationMetadataEntryLabel:Null<Label> = songVariationMetadataEntry.findComponent('chartEntryLabel', Label);
if (songVariationMetadataEntryLabel == null) throw 'Could not locate chartEntryLabel in Open Chart dialog'; if (songVariationMetadataEntryLabel == null) throw 'Could not locate chartEntryLabel in Open Chart dialog';
#if FILE_DROP_SUPPORTED #if FILE_DROP_SUPPORTED
@ -1159,7 +926,7 @@ class ChartEditorDialogHandler
chartContainerB.addComponent(songVariationMetadataEntry); chartContainerB.addComponent(songVariationMetadataEntry);
// Build entries for -chart-<variation>.json. // Build entries for -chart-<variation>.json.
var songVariationChartDataEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_OPEN_CHART_PARTS_ENTRY_LAYOUT); var songVariationChartDataEntry:Component = RuntimeComponentBuilder.fromAsset(CHART_EDITOR_DIALOG_OPEN_CHART_PARTS_ENTRY_LAYOUT);
var songVariationChartDataEntryLabel:Null<Label> = songVariationChartDataEntry.findComponent('chartEntryLabel', Label); var songVariationChartDataEntryLabel:Null<Label> = songVariationChartDataEntry.findComponent('chartEntryLabel', Label);
if (songVariationChartDataEntryLabel == null) throw 'Could not locate chartEntryLabel in Open Chart dialog'; if (songVariationChartDataEntryLabel == null) throw 'Could not locate chartEntryLabel in Open Chart dialog';
#if FILE_DROP_SUPPORTED #if FILE_DROP_SUPPORTED
@ -1346,7 +1113,7 @@ class ChartEditorDialogHandler
}); });
} }
var metadataEntry:Component = state.buildComponent(CHART_EDITOR_DIALOG_OPEN_CHART_PARTS_ENTRY_LAYOUT); var metadataEntry:Component = RuntimeComponentBuilder.fromAsset(CHART_EDITOR_DIALOG_OPEN_CHART_PARTS_ENTRY_LAYOUT);
var metadataEntryLabel:Null<Label> = metadataEntry.findComponent('chartEntryLabel', Label); var metadataEntryLabel:Null<Label> = metadataEntry.findComponent('chartEntryLabel', Label);
if (metadataEntryLabel == null) throw 'Could not locate chartEntryLabel in Open Chart dialog'; if (metadataEntryLabel == null) throw 'Could not locate chartEntryLabel in Open Chart dialog';
@ -1357,8 +1124,8 @@ class ChartEditorDialogHandler
#end #end
metadataEntry.onClick = onClickMetadataVariation.bind(Constants.DEFAULT_VARIATION).bind(metadataEntryLabel); metadataEntry.onClick = onClickMetadataVariation.bind(Constants.DEFAULT_VARIATION).bind(metadataEntryLabel);
addDropHandler(metadataEntry, onDropFileMetadataVariation.bind(Constants.DEFAULT_VARIATION).bind(metadataEntryLabel)); state.addDropHandler({component: metadataEntry, handler: onDropFileMetadataVariation.bind(Constants.DEFAULT_VARIATION).bind(metadataEntryLabel)});
metadataEntry.onMouseOver = function(_) { metadataEntry.onMouseOver = function(_event) {
metadataEntry.swapClass('upload-bg', 'upload-bg-hover'); metadataEntry.swapClass('upload-bg', 'upload-bg-hover');
Cursor.cursorMode = Pointer; Cursor.cursorMode = Pointer;
} }
@ -1459,7 +1226,7 @@ class ChartEditorDialogHandler
state.success('Success', 'Loaded chart file (${path.file}.${path.ext})'); state.success('Success', 'Loaded chart file (${path.file}.${path.ext})');
}; };
addDropHandler(importBox, onDropFile); state.addDropHandler({component: importBox, handler: onDropFile});
return dialog; return dialog;
} }
@ -1633,7 +1400,7 @@ class ChartEditorDialogHandler
*/ */
static function openDialog(state:ChartEditorState, key:String, modal:Bool = true, closable:Bool = true):Null<Dialog> static function openDialog(state:ChartEditorState, key:String, modal:Bool = true, closable:Bool = true):Null<Dialog>
{ {
var dialog:Null<Dialog> = cast state.buildComponent(key); var dialog:Null<Dialog> = cast RuntimeComponentBuilder.fromAsset(key);
if (dialog == null) return null; if (dialog == null) return null;
dialog.destroyOnClose = true; dialog.destroyOnClose = true;
@ -1650,14 +1417,10 @@ class ChartEditorDialogHandler
return dialog; return dialog;
} }
// ========== // ===============
// DROP HANDLERS // DROP HANDLERS
// ========== // ===============
static var dropHandlers:Array< static var dropHandlers:Array<DialogDropTarget> = [];
{
component:Component,
handler:(String->Void)
}> = [];
/** /**
* Add a callback for when a file is dropped on a component. * Add a callback for when a file is dropped on a component.
@ -1665,32 +1428,33 @@ class ChartEditorDialogHandler
* On OS X you cant drop on the application window, but rather only the app icon * On OS X you cant drop on the application window, but rather only the app icon
* (either in the dock while running or the icon on the hard drive) so this must be disabled * (either in the dock while running or the icon on the hard drive) so this must be disabled
* and UI updated appropriately. * and UI updated appropriately.
* @param component
* @param handler
*/ */
static function addDropHandler(component:Component, handler:String->Void):Void public static function addDropHandler(state:ChartEditorState, dropTarget:DialogDropTarget):Void
{ {
#if desktop #if desktop
if (!FlxG.stage.window.onDropFile.has(onDropFile)) FlxG.stage.window.onDropFile.add(onDropFile); if (!FlxG.stage.window.onDropFile.has(onDropFile)) FlxG.stage.window.onDropFile.add(onDropFile);
dropHandlers.push( dropHandlers.push(dropTarget);
{
component: component,
handler: handler
});
#else #else
trace('addDropHandler not implemented for this platform'); trace('addDropHandler not implemented for this platform');
#end #end
} }
static function removeDropHandler(handler:String->Void):Void /**
* Remove a callback for when a file is dropped on a component.
*/
public static function removeDropHandler(state:ChartEditorState, dropTarget:DialogDropTarget):Void
{ {
#if desktop #if desktop
FlxG.stage.window.onDropFile.remove(handler); dropHandlers.remove(dropTarget);
#end #end
} }
static function clearDropHandlers():Void /**
* Clear ALL drop handlers, including the core handler.
* Call this only when leaving the chart editor entirely.
*/
public static function clearDropHandlers(state:ChartEditorState):Void
{ {
#if desktop #if desktop
dropHandlers = []; dropHandlers = [];
@ -1698,10 +1462,12 @@ class ChartEditorDialogHandler
#end #end
} }
static final EPSILON:Float = 0.01;
static function onDropFile(path:String):Void static function onDropFile(path:String):Void
{ {
// a VERY short timer to wait for the mouse position to update // a VERY short timer to wait for the mouse position to update
new FlxTimer().start(0.01, function(_) { new FlxTimer().start(EPSILON, function(_) {
for (handler in dropHandlers) for (handler in dropHandlers)
{ {
if (handler.component.hitTest(FlxG.mouse.screenX, FlxG.mouse.screenY)) if (handler.component.hitTest(FlxG.mouse.screenX, FlxG.mouse.screenY))

View file

@ -7,28 +7,28 @@ class ChartEditorShortcutHandler
{ {
public static function applyPlatformShortcutText(state:ChartEditorState):Void public static function applyPlatformShortcutText(state:ChartEditorState):Void
{ {
state.setComponentShortcutText('menubarItemNewChart', ctrlOrCmd('N')); state.menubarItemNewChart.shortcutText = ctrlOrCmd('N');
state.setComponentShortcutText('menubarItemOpenChart', ctrlOrCmd('O')); state.menubarItemOpenChart.shortcutText = ctrlOrCmd('O');
state.setComponentShortcutText('menubarItemSaveChartAs', ctrlOrCmd(shift('S'))); state.menubarItemSaveChartAs.shortcutText = ctrlOrCmd(shift('S'));
state.setComponentShortcutText('menubarItemExit', ctrlOrCmd('Q')); state.menubarItemExit.shortcutText = ctrlOrCmd('Q');
state.setComponentShortcutText('menubarItemUndo', ctrlOrCmd('Z')); state.menubarItemUndo.shortcutText = ctrlOrCmd('Z');
state.setComponentShortcutText('menubarItemRedo', ctrlOrCmd('Y')); state.menubarItemRedo.shortcutText = ctrlOrCmd('Y');
state.setComponentShortcutText('menubarItemCut', ctrlOrCmd('X')); state.menubarItemCut.shortcutText = ctrlOrCmd('X');
state.setComponentShortcutText('menubarItemCopy', ctrlOrCmd('C')); state.menubarItemCopy.shortcutText = ctrlOrCmd('C');
state.setComponentShortcutText('menubarItemPaste', ctrlOrCmd('V')); state.menubarItemPaste.shortcutText = ctrlOrCmd('V');
state.setComponentShortcutText('menubarItemSelectAll', ctrlOrCmd('A')); state.menubarItemSelectAll.shortcutText = ctrlOrCmd('A');
state.setComponentShortcutText('menubarItemSelectInverse', ctrlOrCmd('I')); state.menubarItemSelectInverse.shortcutText = ctrlOrCmd('I');
state.setComponentShortcutText('menubarItemSelectNone', ctrlOrCmd('D')); state.menubarItemSelectNone.shortcutText = ctrlOrCmd('D');
state.setComponentShortcutText('menubarItemSelectBeforeCursor', shift('Home')); state.menubarItemSelectBeforeCursor.shortcutText = shift('Home');
state.setComponentShortcutText('menubarItemSelectAfterCursor', shift('End')); state.menubarItemSelectAfterCursor.shortcutText = shift('End');
state.setComponentShortcutText('menubarItemDifficultyDown', ctrlOrCmd('')); state.menubarItemDifficultyDown.shortcutText = ctrlOrCmd('');
state.setComponentShortcutText('menubarItemDifficultyUp', ctrlOrCmd('')); state.menubarItemDifficultyUp.shortcutText = ctrlOrCmd('');
state.setComponentShortcutText('menubarItemPlaytestFull', 'Enter'); state.menubarItemPlaytestFull.shortcutText = 'Enter';
state.setComponentShortcutText('menubarItemPlaytestMinimal', shift('Enter')); state.menubarItemPlaytestMinimal.shortcutText = shift('Enter');
} }
/** /**

View file

@ -18,6 +18,7 @@ import funkin.play.event.SongEvent;
import funkin.play.song.SongSerializer; import funkin.play.song.SongSerializer;
import funkin.play.stage.StageData; import funkin.play.stage.StageData;
import funkin.play.stage.StageData.StageDataParser; import funkin.play.stage.StageData.StageDataParser;
import haxe.ui.RuntimeComponentBuilder;
import funkin.ui.debug.charting.util.ChartEditorDropdowns; import funkin.ui.debug.charting.util.ChartEditorDropdowns;
import funkin.ui.haxeui.components.CharacterPlayer; import funkin.ui.haxeui.components.CharacterPlayer;
import funkin.util.FileUtil; import funkin.util.FileUtil;
@ -217,7 +218,7 @@ class ChartEditorToolboxHandler
static function buildToolboxNoteDataLayout(state:ChartEditorState):Null<CollapsibleDialog> static function buildToolboxNoteDataLayout(state:ChartEditorState):Null<CollapsibleDialog>
{ {
var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT); var toolbox:CollapsibleDialog = cast RuntimeComponentBuilder.fromAsset(ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT);
if (toolbox == null) return null; if (toolbox == null) return null;
@ -226,7 +227,7 @@ class ChartEditorToolboxHandler
toolbox.y = 100; toolbox.y = 100;
toolbox.onDialogClosed = function(event:DialogEvent) { toolbox.onDialogClosed = function(event:DialogEvent) {
state.setUICheckboxSelected('menubarItemToggleToolboxNotes', false); state.menubarItemToggleToolboxNotes.selected = false;
} }
var toolboxNotesNoteKind:Null<DropDown> = toolbox.findComponent('toolboxNotesNoteKind', DropDown); var toolboxNotesNoteKind:Null<DropDown> = toolbox.findComponent('toolboxNotesNoteKind', DropDown);
@ -269,7 +270,7 @@ class ChartEditorToolboxHandler
static function buildToolboxEventDataLayout(state:ChartEditorState):Null<CollapsibleDialog> static function buildToolboxEventDataLayout(state:ChartEditorState):Null<CollapsibleDialog>
{ {
var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT); var toolbox:CollapsibleDialog = cast RuntimeComponentBuilder.fromAsset(ChartEditorState.CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT);
if (toolbox == null) return null; if (toolbox == null) return null;
@ -278,7 +279,7 @@ class ChartEditorToolboxHandler
toolbox.y = 150; toolbox.y = 150;
toolbox.onDialogClosed = function(event:DialogEvent) { toolbox.onDialogClosed = function(event:DialogEvent) {
state.setUICheckboxSelected('menubarItemToggleToolboxEvents', false); state.menubarItemToggleToolboxEvents.selected = false;
} }
var toolboxEventsEventKind:Null<DropDown> = toolbox.findComponent('toolboxEventsEventKind', DropDown); var toolboxEventsEventKind:Null<DropDown> = toolbox.findComponent('toolboxEventsEventKind', DropDown);
@ -418,7 +419,7 @@ class ChartEditorToolboxHandler
static function buildToolboxDifficultyLayout(state:ChartEditorState):Null<CollapsibleDialog> static function buildToolboxDifficultyLayout(state:ChartEditorState):Null<CollapsibleDialog>
{ {
var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT); var toolbox:CollapsibleDialog = cast RuntimeComponentBuilder.fromAsset(ChartEditorState.CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT);
if (toolbox == null) return null; if (toolbox == null) return null;
@ -427,7 +428,7 @@ class ChartEditorToolboxHandler
toolbox.y = 200; toolbox.y = 200;
toolbox.onDialogClosed = function(event:UIEvent) { toolbox.onDialogClosed = function(event:UIEvent) {
state.setUICheckboxSelected('menubarItemToggleToolboxDifficulty', false); state.menubarItemToggleToolboxDifficulty.selected = false;
} }
var difficultyToolboxAddVariation:Null<Button> = toolbox.findComponent('difficultyToolboxAddVariation', Button); var difficultyToolboxAddVariation:Null<Button> = toolbox.findComponent('difficultyToolboxAddVariation', Button);
@ -505,7 +506,7 @@ class ChartEditorToolboxHandler
static function buildToolboxMetadataLayout(state:ChartEditorState):Null<CollapsibleDialog> static function buildToolboxMetadataLayout(state:ChartEditorState):Null<CollapsibleDialog>
{ {
var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT); var toolbox:CollapsibleDialog = cast RuntimeComponentBuilder.fromAsset(ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
if (toolbox == null) return null; if (toolbox == null) return null;
@ -514,7 +515,7 @@ class ChartEditorToolboxHandler
toolbox.y = 250; toolbox.y = 250;
toolbox.onDialogClosed = function(event:UIEvent) { toolbox.onDialogClosed = function(event:UIEvent) {
state.setUICheckboxSelected('menubarItemToggleToolboxMetadata', false); state.menubarItemToggleToolboxMetadata.selected = false;
} }
var inputSongName:Null<TextField> = toolbox.findComponent('inputSongName', TextField); var inputSongName:Null<TextField> = toolbox.findComponent('inputSongName', TextField);
@ -656,7 +657,7 @@ class ChartEditorToolboxHandler
static function buildToolboxPlayerPreviewLayout(state:ChartEditorState):Null<CollapsibleDialog> static function buildToolboxPlayerPreviewLayout(state:ChartEditorState):Null<CollapsibleDialog>
{ {
var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT); var toolbox:CollapsibleDialog = cast RuntimeComponentBuilder.fromAsset(ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT);
if (toolbox == null) return null; if (toolbox == null) return null;
@ -665,7 +666,7 @@ class ChartEditorToolboxHandler
toolbox.y = 350; toolbox.y = 350;
toolbox.onDialogClosed = function(event:DialogEvent) { toolbox.onDialogClosed = function(event:DialogEvent) {
state.setUICheckboxSelected('menubarItemToggleToolboxPlayerPreview', false); state.menubarItemToggleToolboxPlayerPreview.selected = false;
} }
var charPlayer:Null<CharacterPlayer> = toolbox.findComponent('charPlayer'); var charPlayer:Null<CharacterPlayer> = toolbox.findComponent('charPlayer');
@ -685,7 +686,7 @@ class ChartEditorToolboxHandler
static function buildToolboxOpponentPreviewLayout(state:ChartEditorState):Null<CollapsibleDialog> static function buildToolboxOpponentPreviewLayout(state:ChartEditorState):Null<CollapsibleDialog>
{ {
var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT); var toolbox:CollapsibleDialog = cast RuntimeComponentBuilder.fromAsset(ChartEditorState.CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT);
if (toolbox == null) return null; if (toolbox == null) return null;
@ -694,7 +695,7 @@ class ChartEditorToolboxHandler
toolbox.y = 350; toolbox.y = 350;
toolbox.onDialogClosed = (event:DialogEvent) -> { toolbox.onDialogClosed = (event:DialogEvent) -> {
state.setUICheckboxSelected('menubarItemToggleToolboxOpponentPreview', false); state.menubarItemToggleToolboxOpponentPreview.selected = false;
} }
var charPlayer:Null<CharacterPlayer> = toolbox.findComponent('charPlayer'); var charPlayer:Null<CharacterPlayer> = toolbox.findComponent('charPlayer');

View file

@ -267,7 +267,7 @@ class LatencyState extends MusicBeatSubState
function generateBeatStuff() function generateBeatStuff()
{ {
Conductor.songPosition = swagSong.getTimeWithDiff(); Conductor.update(swagSong.getTimeWithDiff());
var closestBeat:Int = Math.round(Conductor.songPosition / Conductor.beatLengthMs) % diffGrp.members.length; var closestBeat:Int = Math.round(Conductor.songPosition / Conductor.beatLengthMs) % diffGrp.members.length;
var getDiff:Float = Conductor.songPosition - (closestBeat * Conductor.beatLengthMs); var getDiff:Float = Conductor.songPosition - (closestBeat * Conductor.beatLengthMs);

View file

@ -38,12 +38,19 @@ class CapsuleText extends FlxSpriteGroup
function set_text(value:String):String function set_text(value:String):String
{ {
if (value == null) return value;
if (blurredText == null || whiteText == null)
{
trace('WARN: Capsule not initialized properly');
return text = value;
}
blurredText.text = value; blurredText.text = value;
whiteText.text = value; whiteText.text = value;
whiteText.textField.filters = [ whiteText.textField.filters = [
new openfl.filters.GlowFilter(0x00ccff, 1, 5, 5, 210, BitmapFilterQuality.MEDIUM), new openfl.filters.GlowFilter(0x00ccff, 1, 5, 5, 210, BitmapFilterQuality.MEDIUM),
// new openfl.filters.BlurFilter(5, 5, BitmapFilterQuality.LOW) // new openfl.filters.BlurFilter(5, 5, BitmapFilterQuality.LOW)
]; ];
return value; return text = value;
} }
} }

View file

@ -998,6 +998,11 @@ class FreeplayState extends MusicBeatSubState
PlayStatePlaylist.isStoryMode = false; PlayStatePlaylist.isStoryMode = false;
var targetSong:Song = SongRegistry.instance.fetchEntry(cap.songData.songId); var targetSong:Song = SongRegistry.instance.fetchEntry(cap.songData.songId);
if (targetSong == null)
{
FlxG.log.warn('WARN: could not find song with id (${cap.songData.songId})');
return;
}
var targetDifficulty:String = currentDifficulty; var targetDifficulty:String = currentDifficulty;
// TODO: Implement Pico into the interface properly. // TODO: Implement Pico into the interface properly.

View file

@ -1,24 +1,25 @@
package funkin.ui.transition; package funkin.ui.transition;
import funkin.play.PlayStatePlaylist;
import flixel.FlxSprite; import flixel.FlxSprite;
import flixel.FlxState; import flixel.FlxState;
import funkin.graphics.shaders.ScreenWipeShader;
import flixel.math.FlxMath; import flixel.math.FlxMath;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import flixel.util.FlxTimer; import flixel.util.FlxTimer;
import funkin.graphics.shaders.ScreenWipeShader;
import funkin.play.PlayState; import funkin.play.PlayState;
import funkin.play.PlayStatePlaylist;
import funkin.play.song.Song.SongDifficulty;
import funkin.ui.mainmenu.MainMenuState;
import funkin.ui.MusicBeatState;
import haxe.io.Path; import haxe.io.Path;
import lime.app.Future; import lime.app.Future;
import flixel.tweens.FlxTween;
import funkin.ui.MusicBeatState;
import lime.app.Promise; import lime.app.Promise;
import lime.utils.AssetLibrary; import lime.utils.AssetLibrary;
import flixel.tweens.FlxEase;
import lime.utils.AssetManifest; import lime.utils.AssetManifest;
import lime.utils.Assets as LimeAssets; import lime.utils.Assets as LimeAssets;
import openfl.utils.Assets;
import funkin.ui.mainmenu.MainMenuState;
import openfl.filters.ShaderFilter; import openfl.filters.ShaderFilter;
import openfl.utils.Assets;
class LoadingState extends MusicBeatState class LoadingState extends MusicBeatState
{ {
@ -59,18 +60,20 @@ class LoadingState extends MusicBeatState
initSongsManifest().onComplete(function(lib) { initSongsManifest().onComplete(function(lib) {
callbacks = new MultiCallback(onLoad); callbacks = new MultiCallback(onLoad);
var introComplete = callbacks.add('introComplete'); var introComplete = callbacks.add('introComplete');
// checkLoadSong(getSongPath());
// if (PlayState.currentSong.needsVoices) if (Std.isOfType(target, PlayState))
// { {
// var files = PlayState.currentSong.voiceList; var targetPlayState:PlayState = cast target;
// var targetChart:SongDifficulty = targetPlayState.currentChart;
// if (files == null) files = ['']; // loads with no file name assumption, to load 'Voices.ogg' or whatev normally var instPath:String = Paths.inst(targetChart.song.id);
// var voicesPaths:Array<String> = targetChart.buildVoiceList();
// for (sndFile in files)
// { checkLoadSong(instPath);
// checkLoadSong(getVocalPath(sndFile)); for (voicePath in voicesPaths)
// } {
// } checkLoadSong(voicePath);
}
}
checkLibrary('shared'); checkLibrary('shared');
checkLibrary(PlayStatePlaylist.campaignId); checkLibrary(PlayStatePlaylist.campaignId);

View file

@ -8,6 +8,10 @@ import haxe.io.Path;
import openfl.net.FileReference; import openfl.net.FileReference;
import openfl.events.Event; import openfl.events.Event;
import openfl.events.IOErrorEvent; import openfl.events.IOErrorEvent;
import haxe.ui.containers.dialogs.Dialog.DialogButton;
import haxe.ui.containers.dialogs.Dialogs;
import haxe.ui.containers.dialogs.Dialogs.SelectedFileInfo;
import haxe.ui.containers.dialogs.Dialogs.FileDialogExtensionInfo;
/** /**
* Utilities for reading and writing files on various platforms. * Utilities for reading and writing files on various platforms.
@ -17,32 +21,81 @@ class FileUtil
public static final FILE_FILTER_FNFC:FileFilter = new FileFilter("Friday Night Funkin' Chart (.fnfc)", "*.fnfc"); public static final FILE_FILTER_FNFC:FileFilter = new FileFilter("Friday Night Funkin' Chart (.fnfc)", "*.fnfc");
public static final FILE_FILTER_ZIP:FileFilter = new FileFilter("ZIP Archive (.zip)", "*.zip"); public static final FILE_FILTER_ZIP:FileFilter = new FileFilter("ZIP Archive (.zip)", "*.zip");
/** public static final FILE_EXTENSION_INFO_FNFC:FileDialogExtensionInfo =
* Browses for a single file, then calls `onSelect(path)` when a path chosen.
* Note that on HTML5 this will immediately fail, you should call `openFile(onOpen:Resource->Void)` instead.
*
* @param typeFilter Filters what kinds of files can be selected.
* @return Whether the file dialog was opened successfully.
*/
public static function browseForFile(?typeFilter:Array<FileFilter>, ?onSelect:String->Void, ?onCancel:Void->Void, ?defaultPath:String,
?dialogTitle:String):Bool
{ {
#if desktop extension: 'fnfc',
var filter:String = convertTypeFilter(typeFilter); label: 'Friday Night Funkin\' Chart',
};
public static final FILE_EXTENSION_INFO_ZIP:FileDialogExtensionInfo =
{
extension: 'zip',
label: 'ZIP Archive',
};
var fileDialog:FileDialog = new FileDialog(); /**
if (onSelect != null) fileDialog.onSelect.add(onSelect); * Browses for a single file, then calls `onSelect(fileInfo)` when a file is selected.
if (onCancel != null) fileDialog.onCancel.add(onCancel); * Powered by HaxeUI, so it works on all platforms.
* File contents will be binary, not String.
*
* @param typeFilter
* @param onSelect A callback that provides a `SelectedFileInfo` object when a file is selected.
* @param onCancel A callback that is called when the user closes the dialog without selecting a file.
*/
public static function browseForBinaryFile(dialogTitle:String, ?typeFilter:Array<FileDialogExtensionInfo>, ?onSelect:SelectedFileInfo->Void,
?onCancel:Void->Void)
{
var onComplete = function(button, selectedFiles) {
if (button == DialogButton.OK && selectedFiles.length > 0)
{
onSelect(selectedFiles[0]);
}
else
{
onCancel();
}
};
fileDialog.browse(OPEN, filter, defaultPath, dialogTitle); Dialogs.openFile(onComplete,
return true; {
#elseif html5 readContents: true,
readAsBinary: true, // Binary
multiple: false,
extensions: typeFilter ?? [],
title: dialogTitle,
});
}
/**
* Browses for a single file, then calls `onSelect(fileInfo)` when a file is selected.
* Powered by HaxeUI, so it works on all platforms.
* File contents will be a String, not binary.
*
* @param typeFilter
* @param onSelect A callback that provides a `SelectedFileInfo` object when a file is selected.
* @param onCancel A callback that is called when the user closes the dialog without selecting a file.
*/
public static function browseForTextFile(dialogTitle:String, ?typeFilter:Array<FileDialogExtensionInfo>, ?onSelect:SelectedFileInfo->Void,
?onCancel:Void->Void)
{
var onComplete = function(button, selectedFiles) {
if (button == DialogButton.OK && selectedFiles.length > 0)
{
onSelect(selectedFiles[0]);
}
else
{
onCancel(); onCancel();
return false; }
#else };
onCancel();
return false; Dialogs.openFile(onComplete,
#end {
readContents: true,
readAsBinary: false, // Text
multiple: false,
extensions: typeFilter ?? [],
title: dialogTitle,
});
} }
/** /**
@ -57,11 +110,9 @@ class FileUtil
{ {
#if desktop #if desktop
var filter:String = convertTypeFilter(typeFilter); var filter:String = convertTypeFilter(typeFilter);
var fileDialog:FileDialog = new FileDialog(); var fileDialog:FileDialog = new FileDialog();
if (onSelect != null) fileDialog.onSelect.add(onSelect); if (onSelect != null) fileDialog.onSelect.add(onSelect);
if (onCancel != null) fileDialog.onCancel.add(onCancel); if (onCancel != null) fileDialog.onCancel.add(onCancel);
fileDialog.browse(OPEN_DIRECTORY, filter, defaultPath, dialogTitle); fileDialog.browse(OPEN_DIRECTORY, filter, defaultPath, dialogTitle);
return true; return true;
#elseif html5 #elseif html5
@ -84,11 +135,9 @@ class FileUtil
{ {
#if desktop #if desktop
var filter:String = convertTypeFilter(typeFilter); var filter:String = convertTypeFilter(typeFilter);
var fileDialog:FileDialog = new FileDialog(); var fileDialog:FileDialog = new FileDialog();
if (onSelect != null) fileDialog.onSelectMultiple.add(onSelect); if (onSelect != null) fileDialog.onSelectMultiple.add(onSelect);
if (onCancel != null) fileDialog.onCancel.add(onCancel); if (onCancel != null) fileDialog.onCancel.add(onCancel);
fileDialog.browse(OPEN_MULTIPLE, filter, defaultPath, dialogTitle); fileDialog.browse(OPEN_MULTIPLE, filter, defaultPath, dialogTitle);
return true; return true;
#elseif html5 #elseif html5
@ -112,11 +161,9 @@ class FileUtil
{ {
#if desktop #if desktop
var filter:String = convertTypeFilter(typeFilter); var filter:String = convertTypeFilter(typeFilter);
var fileDialog:FileDialog = new FileDialog(); var fileDialog:FileDialog = new FileDialog();
if (onSelect != null) fileDialog.onSelect.add(onSelect); if (onSelect != null) fileDialog.onSelect.add(onSelect);
if (onCancel != null) fileDialog.onCancel.add(onCancel); if (onCancel != null) fileDialog.onCancel.add(onCancel);
fileDialog.browse(SAVE, filter, defaultPath, dialogTitle); fileDialog.browse(SAVE, filter, defaultPath, dialogTitle);
return true; return true;
#elseif html5 #elseif html5
@ -128,48 +175,6 @@ class FileUtil
#end #end
} }
/**
* Browses for a single file location, then reads it and passes it to `onOpen(resource:haxe.io.Bytes)`.
* Works great on desktop and HTML5.
*
* @param typeFilter TODO What does this do?
* @return Whether the file dialog was opened successfully.
*/
public static function openFile(?typeFilter:Array<FileFilter>, ?onOpen:Bytes->Void, ?onCancel:Void->Void, ?defaultPath:String, ?dialogTitle:String):Bool
{
#if desktop
var filter:String = convertTypeFilter(typeFilter);
var fileDialog:FileDialog = new FileDialog();
if (onOpen != null) fileDialog.onOpen.add(onOpen);
if (onCancel != null) fileDialog.onCancel.add(onCancel);
fileDialog.open(filter, defaultPath, dialogTitle);
return true;
#elseif html5
var onFileLoaded:Event->Void = function(event) {
var loadedFileRef:FileReference = event.target;
trace('Loaded file: ' + loadedFileRef.name);
onOpen(loadedFileRef.data);
}
var onFileSelected:Event->Void = function(event) {
var selectedFileRef:FileReference = event.target;
trace('Selected file: ' + selectedFileRef.name);
selectedFileRef.addEventListener(Event.COMPLETE, onFileLoaded);
selectedFileRef.load();
}
var fileRef:FileReference = new FileReference();
fileRef.addEventListener(Event.SELECT, onFileSelected);
fileRef.browse(typeFilter);
return true;
#else
onCancel();
return false;
#end
}
/** /**
* Browses for a single file location, then writes the provided `haxe.io.Bytes` data and calls `onSave(path)` when done. * 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. * Works great on desktop and HTML5.
@ -181,20 +186,16 @@ class FileUtil
{ {
#if desktop #if desktop
var filter:String = convertTypeFilter(typeFilter); var filter:String = convertTypeFilter(typeFilter);
var fileDialog:FileDialog = new FileDialog(); var fileDialog:FileDialog = new FileDialog();
if (onSave != null) fileDialog.onSave.add(onSave); if (onSave != null) fileDialog.onSave.add(onSave);
if (onCancel != null) fileDialog.onCancel.add(onCancel); if (onCancel != null) fileDialog.onCancel.add(onCancel);
fileDialog.save(data, filter, defaultFileName, dialogTitle); fileDialog.save(data, filter, defaultFileName, dialogTitle);
return true; return true;
#elseif html5 #elseif html5
var filter:String = defaultFileName != null ? Path.extension(defaultFileName) : null; var filter:String = defaultFileName != null ? Path.extension(defaultFileName) : null;
var fileDialog:FileDialog = new FileDialog(); var fileDialog:FileDialog = new FileDialog();
if (onSave != null) fileDialog.onSave.add(onSave); if (onSave != null) fileDialog.onSave.add(onSave);
if (onCancel != null) fileDialog.onCancel.add(onCancel); if (onCancel != null) fileDialog.onCancel.add(onCancel);
fileDialog.save(data, filter, defaultFileName, dialogTitle); fileDialog.save(data, filter, defaultFileName, dialogTitle);
return true; return true;
#else #else
@ -241,17 +242,14 @@ class FileUtil
} }
onSaveAll(paths); onSaveAll(paths);
} }
trace('Browsing for directory to save individual files to...'); trace('Browsing for directory to save individual files to...');
#if mac #if mac
defaultPath = null; defaultPath = null;
#end #end
browseForDirectory(null, onSelectDir, onCancel, defaultPath, 'Choose directory to save all files to...'); browseForDirectory(null, onSelectDir, onCancel, defaultPath, 'Choose directory to save all files to...');
return true; return true;
#elseif html5 #elseif html5
saveFilesAsZIP(resources, onSaveAll, onCancel, defaultPath, force); saveFilesAsZIP(resources, onSaveAll, onCancel, defaultPath, force);
return true; return true;
#else #else
onCancel(); onCancel();
@ -266,15 +264,12 @@ class FileUtil
{ {
// Create a ZIP file. // Create a ZIP file.
var zipBytes:Bytes = createZIPFromEntries(resources); var zipBytes:Bytes = createZIPFromEntries(resources);
var onSave:String->Void = function(path:String) { var onSave:String->Void = function(path:String) {
trace('Saved ${resources.length} files to ZIP at "$path".'); trace('Saved ${resources.length} files to ZIP at "$path".');
if (onSave != null) onSave([path]); if (onSave != null) onSave([path]);
}; };
// Prompt the user to save the ZIP file. // Prompt the user to save the ZIP file.
saveFile(zipBytes, [FILE_FILTER_ZIP], onSave, onCancel, defaultPath, 'Save files as ZIP...'); saveFile(zipBytes, [FILE_FILTER_ZIP], onSave, onCancel, defaultPath, 'Save files as ZIP...');
return true; return true;
} }
@ -286,15 +281,12 @@ class FileUtil
{ {
// Create a ZIP file. // Create a ZIP file.
var zipBytes:Bytes = createZIPFromEntries(resources); var zipBytes:Bytes = createZIPFromEntries(resources);
var onSave:String->Void = function(path:String) { var onSave:String->Void = function(path:String) {
trace('Saved FNF file to "$path"'); trace('Saved FNF file to "$path"');
if (onSave != null) onSave([path]); if (onSave != null) onSave([path]);
}; };
// Prompt the user to save the ZIP file. // Prompt the user to save the ZIP file.
saveFile(zipBytes, [FILE_FILTER_FNFC], onSave, onCancel, defaultPath, 'Save chart as FNFC...'); saveFile(zipBytes, [FILE_FILTER_FNFC], onSave, onCancel, defaultPath, 'Save chart as FNFC...');
return true; return true;
} }
@ -309,10 +301,8 @@ class FileUtil
#if desktop #if desktop
// Create a ZIP file. // Create a ZIP file.
var zipBytes:Bytes = createZIPFromEntries(resources); var zipBytes:Bytes = createZIPFromEntries(resources);
// Write the ZIP. // Write the ZIP.
writeBytesToPath(path, zipBytes, mode); writeBytesToPath(path, zipBytes, mode);
return true; return true;
#else #else
return false; return false;
@ -371,7 +361,6 @@ class FileUtil
public static function browseFileReference(callback:FileReference->Void) public static function browseFileReference(callback:FileReference->Void)
{ {
var file = new FileReference(); var file = new FileReference();
file.addEventListener(Event.SELECT, function(e) { file.addEventListener(Event.SELECT, function(e) {
var selectedFileRef:FileReference = e.target; var selectedFileRef:FileReference = e.target;
trace('Selected file: ' + selectedFileRef.name); trace('Selected file: ' + selectedFileRef.name);
@ -382,7 +371,6 @@ class FileUtil
}); });
selectedFileRef.load(); selectedFileRef.load();
}); });
file.browse(); file.browse();
} }
@ -439,7 +427,6 @@ class FileUtil
{ {
#if sys #if sys
createDirIfNotExists(Path.directory(path)); createDirIfNotExists(Path.directory(path));
switch (mode) switch (mode)
{ {
case Force: case Force:
@ -482,7 +469,6 @@ class FileUtil
{ {
#if sys #if sys
createDirIfNotExists(Path.directory(path)); createDirIfNotExists(Path.directory(path));
switch (mode) switch (mode)
{ {
case Force: case Force:
@ -557,19 +543,15 @@ class FileUtil
public static function getTempDir():String public static function getTempDir():String
{ {
if (tempDir != null) return tempDir; if (tempDir != null) return tempDir;
#if sys #if sys
#if windows #if windows
var path:String = null; var path:String = null;
for (envName in TEMP_ENV_VARS) for (envName in TEMP_ENV_VARS)
{ {
path = Sys.getEnv(envName); path = Sys.getEnv(envName);
if (path == '') path = null; if (path == '') path = null;
if (path != null) break; if (path != null) break;
} }
tempDir = Path.join([path, 'funkin/']); tempDir = Path.join([path, 'funkin/']);
return tempDir; return tempDir;
#else #else
@ -590,10 +572,8 @@ class FileUtil
public static function createZIPFromEntries(entries:Array<Entry>):Bytes public static function createZIPFromEntries(entries:Array<Entry>):Bytes
{ {
var o:haxe.io.BytesOutput = new haxe.io.BytesOutput(); var o:haxe.io.BytesOutput = new haxe.io.BytesOutput();
var zipWriter:haxe.zip.Writer = new haxe.zip.Writer(o); var zipWriter:haxe.zip.Writer = new haxe.zip.Writer(o);
zipWriter.write(entries.list()); zipWriter.write(entries.list());
return o.getBytes(); return o.getBytes();
} }
@ -601,10 +581,8 @@ class FileUtil
{ {
trace('TEST: ' + input.length); trace('TEST: ' + input.length);
trace(input.sub(0, 30).toHex()); trace(input.sub(0, 30).toHex());
var bytesInput = new haxe.io.BytesInput(input); var bytesInput = new haxe.io.BytesInput(input);
var zippedEntries = haxe.zip.Reader.readZip(bytesInput); var zippedEntries = haxe.zip.Reader.readZip(bytesInput);
var results:Array<Entry> = []; var results:Array<Entry> = [];
for (entry in zippedEntries) for (entry in zippedEntries)
{ {
@ -637,7 +615,6 @@ class FileUtil
public static function makeZIPEntry(name:String, content:String):Entry public static function makeZIPEntry(name:String, content:String):Entry
{ {
var data:Bytes = haxe.io.Bytes.ofString(content, UTF8); var data:Bytes = haxe.io.Bytes.ofString(content, UTF8);
return makeZIPEntryFromBytes(name, data); return makeZIPEntryFromBytes(name, data);
} }
@ -653,12 +630,9 @@ class FileUtil
return { return {
fileName: name, fileName: name,
fileSize: data.length, fileSize: data.length,
data: data, data: data,
dataSize: data.length, dataSize: data.length,
compressed: false, compressed: false,
fileTime: Date.now(), fileTime: Date.now(),
crc32: null, crc32: null,
extraFields: null, extraFields: null,
@ -677,7 +651,6 @@ class FileUtil
} }
filter = filters.join(';'); filter = filters.join(';');
} }
return filter; return filter;
} }
} }

View file

@ -0,0 +1,3 @@
package haxe.ui.backend.flixel;
typedef UIStateBase = funkin.ui.MusicBeatState;

View file

@ -0,0 +1,3 @@
package haxe.ui.backend.flixel;
typedef UISubStateBase = funkin.ui.MusicBeatSubState;