diff --git a/Project.xml b/Project.xml
index 46ba7f155..4c0ffdce7 100644
--- a/Project.xml
+++ b/Project.xml
@@ -162,10 +162,13 @@
-
+
+
+
+
diff --git a/assets b/assets
index 4ed2b3084..2dd4ab0eb 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 4ed2b3084d54899e10d10a97eaafe210158768be
+Subproject commit 2dd4ab0eb9979422c1c4cb849ebe899b7bf1758a
diff --git a/docs/style-guide.md b/docs/style-guide.md
index 1131cca2b..4c4e01492 100644
--- a/docs/style-guide.md
+++ b/docs/style-guide.md
@@ -32,6 +32,10 @@ Example:
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
Do not include headers specifying code license on individual files in the repo, since the main `LICENSE.md` file covers all of them.
diff --git a/source/funkin/play/PauseSubState.hx b/source/funkin/play/PauseSubState.hx
index f5555b66e..c9039ce40 100644
--- a/source/funkin/play/PauseSubState.hx
+++ b/source/funkin/play/PauseSubState.hx
@@ -233,6 +233,7 @@ class PauseSubState extends MusicBeatSubState
if (PlayStatePlaylist.isStoryMode)
{
+ PlayStatePlaylist.reset();
openSubState(new funkin.ui.transition.StickerSubState(null, STORY));
}
else
diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index c9512aa93..7d20f4697 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -1354,8 +1354,7 @@ class PlayState extends MusicBeatSubState
else
{
// 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.',
- 'Stage Error');
+ lime.app.Application.current.window.alert('Unable to load stage ${id}, is its data corrupted?.', 'Stage Error');
}
}
diff --git a/source/funkin/save/Save.hx b/source/funkin/save/Save.hx
index efb3ee623..db1f2b69a 100644
--- a/source/funkin/save/Save.hx
+++ b/source/funkin/save/Save.hx
@@ -177,6 +177,23 @@ abstract Save(RawSaveData)
return this.optionsChartEditor.previousFiles;
}
+ public var chartEditorHasBackup(get, set):Bool;
+
+ function get_chartEditorHasBackup():Bool
+ {
+ if (this.optionsChartEditor.hasBackup == null) this.optionsChartEditor.hasBackup = false;
+
+ return this.optionsChartEditor.hasBackup;
+ }
+
+ function set_chartEditorHasBackup(value:Bool):Bool
+ {
+ // Set and apply.
+ this.optionsChartEditor.hasBackup = value;
+ flush();
+ return this.optionsChartEditor.hasBackup;
+ }
+
public var chartEditorNoteQuant(get, set):Int;
function get_chartEditorNoteQuant():Int
@@ -926,6 +943,13 @@ typedef SaveControlsData =
*/
typedef SaveDataChartEditorOptions =
{
+ /**
+ * Whether the Chart Editor created a backup the last time it closed.
+ * Prompt the user to load it, then set this back to `false`.
+ * @default `false`
+ */
+ var ?hasBackup:Bool;
+
/**
* Previous files opened in the Chart Editor.
* @default `[]`
diff --git a/source/funkin/ui/debug/DebugMenuSubState.hx b/source/funkin/ui/debug/DebugMenuSubState.hx
index ef02a802e..404bf6f67 100644
--- a/source/funkin/ui/debug/DebugMenuSubState.hx
+++ b/source/funkin/ui/debug/DebugMenuSubState.hx
@@ -7,6 +7,8 @@ import funkin.ui.MusicBeatSubState;
import funkin.ui.TextMenuList;
import funkin.ui.debug.charting.ChartEditorState;
import funkin.ui.MusicBeatSubState;
+import funkin.util.logging.CrashHandler;
+import flixel.addons.transition.FlxTransitionableState;
class DebugMenuSubState extends MusicBeatSubState
{
@@ -50,7 +52,9 @@ class DebugMenuSubState extends MusicBeatSubState
createItem("ANIMATION EDITOR", openAnimationEditor);
createItem("STAGE EDITOR", openStageEditor);
createItem("TEST STICKERS", testStickers);
-
+ #if sys
+ createItem("OPEN CRASH LOG FOLDER", openLogFolder);
+ #end
FlxG.camera.focusOn(new FlxPoint(camFocusPoint.x, camFocusPoint.y));
FlxG.camera.focusOn(new FlxPoint(camFocusPoint.x, camFocusPoint.y + 500));
}
@@ -81,6 +85,8 @@ class DebugMenuSubState extends MusicBeatSubState
function openChartEditor()
{
+ FlxTransitionableState.skipNextTransIn = true;
+
FlxG.switchState(new ChartEditorState());
}
@@ -101,6 +107,22 @@ class DebugMenuSubState extends MusicBeatSubState
trace('Stage Editor');
}
+ #if sys
+ function openLogFolder()
+ {
+ #if windows
+ Sys.command('explorer', [CrashHandler.LOG_FOLDER]);
+ #elseif mac
+ // mac could be fuckie with where the log folder is relative to the game file...
+ // if this comment is still here... it means it has NOT been verified on mac yet!
+ Sys.command('open', [CrashHandler.LOG_FOLDER]);
+ #end
+
+ // TODO: implement linux
+ // some shit with xdg-open :thinking: emoji...
+ }
+ #end
+
function exitDebugMenu()
{
// TODO: Add a transition?
diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index a3aeccc02..72cd2d0d6 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -1,5 +1,6 @@
package funkin.ui.debug.charting;
+import funkin.util.logging.CrashHandler;
import haxe.ui.containers.menus.MenuBar;
import flixel.addons.display.FlxSliceSprite;
import flixel.addons.display.FlxTiledSprite;
@@ -18,17 +19,18 @@ import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import flixel.tweens.misc.VarTween;
import flixel.util.FlxColor;
-import funkin.ui.mainmenu.MainMenuState;
import flixel.util.FlxSort;
import flixel.util.FlxTimer;
import funkin.audio.visualize.PolygonSpectogram;
import funkin.audio.VoicesGroup;
import funkin.data.notestyle.NoteStyleRegistry;
+import funkin.data.song.SongData.SongCharacterData;
import funkin.data.song.SongData.SongChartData;
import funkin.data.song.SongData.SongEventData;
import funkin.data.song.SongData.SongMetadata;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongDataUtils;
+import funkin.data.song.SongRegistry;
import funkin.input.Cursor;
import funkin.input.TurboKeyHandler;
import funkin.modding.events.ScriptEvent;
@@ -47,12 +49,12 @@ import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongData.SongCharacterData;
import funkin.data.song.SongDataUtils;
import funkin.ui.debug.charting.commands.ChartEditorCommand;
-import funkin.ui.debug.charting.handlers.ChartEditorShortcutHandler;
import funkin.play.stage.StageData;
import funkin.save.Save;
import funkin.ui.debug.charting.commands.AddEventsCommand;
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.CutItemsCommand;
import funkin.ui.debug.charting.commands.DeselectAllItemsCommand;
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.ChartEditorNotePreview;
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.handlers.ChartEditorShortcutHandler;
import funkin.ui.haxeui.components.CharacterPlayer;
import funkin.ui.haxeui.HaxeUIState;
+import funkin.ui.mainmenu.MainMenuState;
import funkin.util.Constants;
+import funkin.util.FileUtil;
import funkin.util.SortUtil;
import funkin.util.WindowUtil;
import haxe.DynamicAccess;
import haxe.io.Bytes;
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.Label;
import haxe.ui.components.NumberStepper;
@@ -90,18 +98,18 @@ import haxe.ui.components.TextField;
import haxe.ui.containers.dialogs.CollapsibleDialog;
import haxe.ui.containers.Frame;
import haxe.ui.containers.menus.Menu;
+import haxe.ui.containers.menus.MenuBar;
import haxe.ui.containers.menus.MenuItem;
import haxe.ui.containers.TreeView;
import haxe.ui.containers.TreeViewNode;
import haxe.ui.core.Component;
import haxe.ui.core.Screen;
import haxe.ui.events.DragEvent;
+import haxe.ui.events.MouseEvent;
+import haxe.ui.events.UIEvent;
import haxe.ui.events.UIEvent;
import haxe.ui.focus.FocusManager;
-import haxe.ui.notifications.NotificationManager;
-import haxe.ui.notifications.NotificationType;
import openfl.display.BitmapData;
-import funkin.util.FileUtil;
using Lambda;
@@ -109,24 +117,22 @@ using Lambda;
* A state dedicated to allowing the user to create and edit song charts.
* 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
*/
-@:nullSafety
-class ChartEditorState extends HaxeUIState
+// @:nullSafety
+
+@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/main-view.xml"))
+class ChartEditorState extends UIState // UIState derives from MusicBeatState
{
/**
* CONSTANTS
*/
// ==============================
- // XML 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');
-
+ // Layouts
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_METADATA_LAYOUT:String = Paths.ui('chart-editor/toolbox/metadata');
public static final CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT:String = Paths.ui('chart-editor/toolbox/difficulty');
@@ -779,6 +785,18 @@ class ChartEditorState extends HaxeUIState
return saveDataDirty;
}
+ var shouldShowBackupAvailableDialog(get, set):Bool;
+
+ function get_shouldShowBackupAvailableDialog():Bool
+ {
+ return Save.get().chartEditorHasBackup;
+ }
+
+ function set_shouldShowBackupAvailableDialog(value:Bool):Bool
+ {
+ return Save.get().chartEditorHasBackup = value;
+ }
+
/**
* Whether the difficulty tree view in the toolbox has been modified and needs to be updated.
* This happens when we add/remove difficulties.
@@ -1199,6 +1217,110 @@ class ChartEditorState extends HaxeUIState
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 = 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
*/
@@ -1288,41 +1410,6 @@ class ChartEditorState extends HaxeUIState
*/
var menuBG:Null = null;
- /**
- * The layout containing the playbar head slider.
- */
- var playbarHeadLayout:Null = null;
-
- /**
- * The submenu in the menubar containing recently opened files.
- */
- var menubarOpenRecent:Null = null;
-
- /**
- * The item in the menubar to save the currently opened chart.
- */
- var menubarItemSaveChart:Null = null;
-
- /**
- * The playbar head slider.
- */
- var playbarHead:Null = null;
-
- /**
- * The label by the playbar telling the song position.
- */
- var playbarSongPos:Null = null;
-
- /**
- * The label by the playbar telling the song time remaining.
- */
- var playbarSongRemaining:Null = null;
-
- /**
- * The label by the playbar telling the note snap.
- */
- var playbarNoteSnap:Null = null;
-
/**
* The sprite group containing the note graphics.
* Only displays a subset of the data from `currentSongChartNoteData`,
@@ -1422,9 +1509,7 @@ class ChartEditorState extends HaxeUIState
public function new(?params:ChartEditorParams)
{
- // Load the HaxeUI XML file.
- super(CHART_EDITOR_LAYOUT);
-
+ super();
this.params = params;
}
@@ -1471,7 +1556,7 @@ class ChartEditorState extends HaxeUIState
// super.create() must be called first, the HaxeUI components get created here.
super.create();
// Set the z-index of the HaxeUI.
- this.component.zIndex = 100;
+ this.root.zIndex = 100;
// Show the mouse cursor.
Cursor.show();
@@ -1498,7 +1583,7 @@ class ChartEditorState extends HaxeUIState
buildAdditionalUI();
populateOpenRecentMenu();
- ChartEditorShortcutHandler.applyPlatformShortcutText(this);
+ this.applyPlatformShortcutText();
// Setup the onClick listeners for the UI after it's been created.
setupUIListeners();
@@ -1511,33 +1596,28 @@ class ChartEditorState extends HaxeUIState
if (params != null && params.fnfcTargetPath != null)
{
// Chart editor was opened from the command line. Open the FNFC file now!
- var result:Null> = ChartEditorImportExportHandler.loadFromFNFCPath(this, params.fnfcTargetPath);
+ var result:Null> = this.loadFromFNFCPath(params.fnfcTargetPath);
if (result != null)
{
- #if !mac
- NotificationManager.instance.addNotification(
- {
- title: 'Success',
- body: result.length == 0 ? 'Loaded chart (${params.fnfcTargetPath})' : 'Loaded chart (${params.fnfcTargetPath})\n${result.join("\n")}',
- type: result.length == 0 ? NotificationType.Success : NotificationType.Warning,
- expiryMs: Constants.NOTIFICATION_DISMISS_TIME
- });
- #end
+ if (result.length == 0)
+ {
+ this.success('Loaded Chart', 'Loaded chart (${params.fnfcTargetPath})');
+ }
+ else
+ {
+ this.warning('Loaded Chart', 'Loaded chart with issues (${params.fnfcTargetPath})\n${result.join("\n")}');
+ }
}
else
{
- #if !mac
- NotificationManager.instance.addNotification(
- {
- title: 'Failure',
- body: 'Failed to load chart (${params.fnfcTargetPath})',
- type: NotificationType.Error,
- expiryMs: Constants.NOTIFICATION_DISMISS_TIME
- });
- #end
+ this.error('Failure', 'Failed to load chart (${params.fnfcTargetPath})');
// Song failed to load, open the Welcome dialog so we aren't in a broken state.
- ChartEditorDialogHandler.openWelcomeDialog(this, false);
+ var welcomeDialog = this.openWelcomeDialog(false);
+ if (shouldShowBackupAvailableDialog)
+ {
+ this.openBackupAvailableDialog(welcomeDialog);
+ }
}
}
else if (params != null && params.targetSongId != null)
@@ -1546,13 +1626,18 @@ class ChartEditorState extends HaxeUIState
}
else
{
- ChartEditorDialogHandler.openWelcomeDialog(this, false);
+ var welcomeDialog = this.openWelcomeDialog(false);
+ if (shouldShowBackupAvailableDialog)
+ {
+ this.openBackupAvailableDialog(welcomeDialog);
+ }
}
}
function setupWelcomeMusic()
{
this.welcomeMusic.loadEmbedded(Paths.music('chartEditorLoop/chartEditorLoop'));
+ FlxG.sound.list.add(this.welcomeMusic);
this.welcomeMusic.looped = true;
}
@@ -1583,7 +1668,7 @@ class ChartEditorState extends HaxeUIState
// audioVocalTrackGroup.pitch = save.chartEditorPlaybackSpeed;
}
- public function writePreferences():Void
+ public function writePreferences(hasBackup:Bool):Void
{
var save:Save = Save.get();
@@ -1591,8 +1676,11 @@ class ChartEditorState extends HaxeUIState
var filteredWorkingFilePaths:Array = [];
for (chartPath in previousWorkingFilePaths)
if (chartPath != null) filteredWorkingFilePaths.push(chartPath);
-
save.chartEditorPreviousFiles = filteredWorkingFilePaths;
+
+ if (hasBackup) trace('Queuing backup prompt for next time!');
+ save.chartEditorHasBackup = hasBackup;
+
save.chartEditorNoteQuant = noteSnapQuantIndex;
save.chartEditorLiveInputStyle = currentLiveInputStyle;
save.chartEditorDownscroll = isViewDownscroll;
@@ -1624,36 +1712,27 @@ class ChartEditorState extends HaxeUIState
stopWelcomeMusic();
// Load chart from file
- var result:Null> = ChartEditorImportExportHandler.loadFromFNFCPath(this, chartPath);
+ var result:Null> = this.loadFromFNFCPath(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
+ if (result.length == 0)
+ {
+ this.success('Loaded Chart', 'Loaded chart (${chartPath.toString()})');
+ }
+ else
+ {
+ this.warning('Loaded Chart', 'Loaded chart with issues (${chartPath.toString()})\n${result.join("\n")}');
+ }
}
else
{
- #if !mac
- NotificationManager.instance.addNotification(
- {
- title: 'Failure',
- body: 'Failed to load chart (${chartPath.toString()})',
- type: NotificationType.Error,
- expiryMs: Constants.NOTIFICATION_DISMISS_TIME
- });
- #end
+ this.error('Failure', 'Failed to load chart (${chartPath.toString()})');
}
}
if (!FileUtil.doesFileExist(chartPath))
{
- trace('Previously loaded chart file (${chartPath}) does not exist, disabling link...');
+ trace('Previously loaded chart file (${chartPath.toString()}) does not exist, disabling link...');
menuItemRecentChart.disabled = true;
}
else
@@ -1923,25 +2002,20 @@ class ChartEditorState extends HaxeUIState
function buildAdditionalUI():Void
{
- playbarHeadLayout = buildComponent(CHART_EDITOR_PLAYBARHEAD_LAYOUT);
-
- if (playbarHeadLayout == null) throw 'ERROR: Failed to construct playbarHeadLayout! Check "${CHART_EDITOR_PLAYBARHEAD_LAYOUT}".';
+ playbarHeadLayout = new ChartEditorPlaybarHead();
playbarHeadLayout.zIndex = 110;
-
playbarHeadLayout.width = FlxG.width - 8;
playbarHeadLayout.height = 10;
playbarHeadLayout.x = 4;
playbarHeadLayout.y = FlxG.height - 48 - 8;
- playbarHead = playbarHeadLayout.findComponent('playbarHead', Slider);
- if (playbarHead == null) throw 'ERROR: Failed to fetch playbarHead from playbarHeadLayout! Check "${CHART_EDITOR_PLAYBARHEAD_LAYOUT}".';
- playbarHead.allowFocus = false;
- playbarHead.width = FlxG.width;
- playbarHead.height = 10;
- playbarHead.styleString = 'padding-left: 0px; padding-right: 0px; border-left: 0px; border-right: 0px;';
+ playbarHeadLayout.playbarHead.allowFocus = false;
+ playbarHeadLayout.playbarHead.width = FlxG.width;
+ playbarHeadLayout.playbarHead.height = 10;
+ playbarHeadLayout.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;
// If we were dragging the playhead while the song was playing, resume playing.
@@ -1956,16 +2030,21 @@ class ChartEditorState extends HaxeUIState
}
}
- playbarHead.onDragEnd = function(_:DragEvent) {
+ playbarHeadLayout.playbarHead.onDrag = function(_:DragEvent) {
+ if (playbarHeadDragging)
+ {
+ var value:Null = playbarHeadLayout.playbarHead?.value;
+
+ // Set the song position to where the playhead was moved to.
+ scrollPositionInPixels = songLengthInPixels * ((value ?? 0.0) / 100);
+ // Update the conductor and audio tracks to match.
+ moveSongToScrollPosition();
+ }
+ }
+
+ playbarHeadLayout.playbarHead.onDragEnd = function(_:DragEvent) {
playbarHeadDragging = false;
- var value:Null = playbarHead?.value;
-
- // Set the song position to where the playhead was moved to.
- scrollPositionInPixels = songLengthInPixels * ((value ?? 0.0) / 100);
- // Update the conductor and audio tracks to match.
- moveSongToScrollPosition();
-
// If we were dragging the playhead while the song was playing, resume playing.
if (playbarHeadDraggingWasPlaying)
{
@@ -1977,19 +2056,9 @@ class ChartEditorState extends HaxeUIState
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;
- // Setup notifications.
- @:privateAccess
- NotificationManager.GUTTER_SIZE = 20;
+ this.setupNotifications();
}
/**
@@ -1999,46 +2068,44 @@ class ChartEditorState extends HaxeUIState
{
// Add functionality to the playbar.
- addUIClickListener('playbarPlay', _ -> toggleAudioPlayback());
- addUIClickListener('playbarStart', _ -> playbarButtonPressed = 'playbarStart');
- addUIClickListener('playbarBack', _ -> playbarButtonPressed = 'playbarBack');
- addUIClickListener('playbarForward', _ -> playbarButtonPressed = 'playbarForward');
- addUIClickListener('playbarEnd', _ -> playbarButtonPressed = 'playbarEnd');
+ playbarStart.onClick = _ -> playbarButtonPressed = 'playbarStart';
+ playbarBack.onClick = _ -> playbarButtonPressed = 'playbarBack';
+ playbarPlay.onClick = _ -> toggleAudioPlayback();
+ playbarForward.onClick = _ -> playbarButtonPressed = 'playbarForward';
+ playbarEnd.onClick = _ -> playbarButtonPressed = 'playbarEnd';
// Cycle note snap quant.
- addUIRightClickListener('playbarNoteSnap', function(_) {
+ playbarNoteSnap.onRightClick = _ -> {
noteSnapQuantIndex--;
if (noteSnapQuantIndex < 0) noteSnapQuantIndex = SNAP_QUANTS.length - 1;
- });
- addUIClickListener('playbarNoteSnap', function(_) {
+ };
+ playbarNoteSnap.onClick = _ -> {
noteSnapQuantIndex++;
if (noteSnapQuantIndex >= SNAP_QUANTS.length) noteSnapQuantIndex = 0;
- });
+ };
// Add functionality to the menu items.
- addUIClickListener('menubarItemNewChart', _ -> ChartEditorDialogHandler.openWelcomeDialog(this, true));
- addUIClickListener('menubarItemOpenChart', _ -> ChartEditorDialogHandler.openBrowseFNFC(this, true));
- addUIClickListener('menubarItemSaveChart', _ -> {
+ // File
+ menubarItemNewChart.onClick = _ -> this.openWelcomeDialog(true);
+ menubarItemOpenChart.onClick = _ -> this.openBrowseFNFC(true);
+ menubarItemSaveChart.onClick = _ -> {
if (currentWorkingFilePath != null)
{
- ChartEditorImportExportHandler.exportAllSongData(this, true, currentWorkingFilePath);
+ this.exportAllSongData(true, currentWorkingFilePath);
}
else
{
- ChartEditorImportExportHandler.exportAllSongData(this, false);
+ this.exportAllSongData(false, null);
}
- });
- addUIClickListener('menubarItemSaveChartAs', _ -> ChartEditorImportExportHandler.exportAllSongData(this));
- addUIClickListener('menubarItemLoadInst', _ -> ChartEditorDialogHandler.openUploadInstDialog(this, true));
- addUIClickListener('menubarItemImportChart', _ -> ChartEditorDialogHandler.openImportChartDialog(this, 'legacy', true));
- addUIClickListener('menubarItemExit', _ -> quitChartEditor());
+ };
+ menubarItemSaveChartAs.onClick = _ -> this.exportAllSongData(false, null);
+ menubarItemExit.onClick = _ -> quitChartEditor();
- addUIClickListener('menubarItemUndo', _ -> undoLastCommand());
-
- addUIClickListener('menubarItemRedo', _ -> redoLastCommand());
-
- addUIClickListener('menubarItemCopy', function(_) {
+ // Edit
+ menubarItemUndo.onClick = _ -> undoLastCommand();
+ menubarItemRedo.onClick = _ -> redoLastCommand();
+ menubarItemCopy.onClick = function(_) {
// Doesn't use a command because it's not undoable.
// Calculate a single time offset for all the notes and events.
@@ -2056,24 +2123,23 @@ class ChartEditorState extends HaxeUIState
notes: SongDataUtils.buildNoteClipboard(currentNoteSelection, timeOffset),
events: SongDataUtils.buildEventClipboard(currentEventSelection, timeOffset),
});
- });
+ };
+ menubarItemCut.onClick = _ -> performCommand(new CutItemsCommand(currentNoteSelection, currentEventSelection));
- addUIClickListener('menubarItemCut', _ -> performCommand(new CutItemsCommand(currentNoteSelection, currentEventSelection)));
-
- addUIClickListener('menubarItemPaste', _ -> {
+ menubarItemPaste.onClick = _ -> {
var targetMs:Float = scrollPositionInMs + playheadPositionInMs;
var targetStep:Float = Conductor.getTimeInSteps(targetMs);
var targetSnappedStep:Float = Math.floor(targetStep / noteSnapRatio) * noteSnapRatio;
var targetSnappedMs:Float = Conductor.getStepTimeInMs(targetSnappedStep);
performCommand(new PasteItemsCommand(targetSnappedMs));
- });
+ };
- addUIClickListener('menubarItemPasteUnsnapped', _ -> {
+ menubarItemPasteUnsnapped.onClick = _ -> {
var targetMs:Float = scrollPositionInMs + playheadPositionInMs;
performCommand(new PasteItemsCommand(targetMs));
- });
+ };
- addUIClickListener('menubarItemDelete', function(_) {
+ menubarItemDelete.onClick = _ -> {
if (currentNoteSelection.length > 0 && currentEventSelection.length > 0)
{
performCommand(new RemoveItemsCommand(currentNoteSelection, currentEventSelection));
@@ -2090,120 +2156,115 @@ class ChartEditorState extends HaxeUIState
{
// 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));
- addUIClickListener('menubarItemPlaytestMinimal', _ -> testSongInPlayState(true));
+ menubarItemPlaytestFull.onClick = _ -> testSongInPlayState(false);
+ menubarItemPlaytestMinimal.onClick = _ -> testSongInPlayState(true);
- addUIClickListener('menuBarItemNoteSnapDecrease', _ -> {
+ menuBarItemNoteSnapDecrease.onClick = _ -> {
noteSnapQuantIndex--;
if (noteSnapQuantIndex < 0) noteSnapQuantIndex = SNAP_QUANTS.length - 1;
- });
- addUIClickListener('menuBarItemNoteSnapIncrease', _ -> {
+ };
+ menuBarItemNoteSnapIncrease.onClick = _ -> {
noteSnapQuantIndex++;
if (noteSnapQuantIndex >= SNAP_QUANTS.length) noteSnapQuantIndex = 0;
- });
+ };
- addUIChangeListener('menuBarItemInputStyleNone', function(event:UIEvent) {
+ menuBarItemInputStyleNone.onClick = function(event:UIEvent) {
currentLiveInputStyle = None;
- });
- addUIChangeListener('menuBarItemInputStyleNumberKeys', function(event:UIEvent) {
+ };
+ menuBarItemInputStyleNumberKeys.onClick = function(event:UIEvent) {
currentLiveInputStyle = NumberKeys;
- });
- addUIChangeListener('menuBarItemInputStyleWASD', function(event:UIEvent) {
+ };
+ menuBarItemInputStyleWASD.onClick = function(event:UIEvent) {
currentLiveInputStyle = WASD;
- });
+ };
- addUIClickListener('menubarItemAbout', _ -> this.openAboutDialog());
- addUIClickListener('menubarItemWelcomeDialog', _ -> this.openWelcomeDialog(true));
+ menubarItemAbout.onClick = _ -> this.openAboutDialog();
+ menubarItemWelcomeDialog.onClick = _ -> this.openWelcomeDialog(true);
- addUIClickListener('menubarItemUserGuide', _ -> this.openUserGuideDialog());
+ #if sys
+ menubarItemGoToBackupsFolder.onClick = _ -> this.openBackupsFolder();
+ #else
+ // Disable if no file system or command access
+ menubarItemGoToBackupsFolder.disabled = true;
+ #end
- addUIChangeListener('menubarItemDownscroll', event -> isViewDownscroll = event.value);
- setUICheckboxSelected('menubarItemDownscroll', isViewDownscroll);
+ menubarItemUserGuide.onClick = _ -> this.openUserGuideDialog();
- addUIClickListener('menubarItemDifficultyUp', _ -> incrementDifficulty(1));
- addUIClickListener('menubarItemDifficultyDown', _ -> incrementDifficulty(-1));
+ menubarItemDownscroll.onClick = event -> isViewDownscroll = event.value;
+ menubarItemDownscroll.selected = isViewDownscroll;
- addUIChangeListener('menubarItemPlaytestStartTime', event -> playtestStartTime = event.value);
- setUICheckboxSelected('menubarItemPlaytestStartTime', playtestStartTime);
+ menubarItemDifficultyUp.onClick = _ -> incrementDifficulty(1);
+ menubarItemDifficultyDown.onClick = _ -> incrementDifficulty(-1);
- addUIChangeListener('menuBarItemThemeLight', function(event:UIEvent) {
+ menubarItemPlaytestStartTime.onChange = event -> playtestStartTime = event.value;
+ menubarItemPlaytestStartTime.selected = playtestStartTime;
+
+ menuBarItemThemeLight.onChange = function(event:UIEvent) {
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;
- });
- setUICheckboxSelected('menuBarItemThemeDark', currentTheme == ChartEditorTheme.Dark);
+ };
+ menuBarItemThemeDark.selected = currentTheme == ChartEditorTheme.Dark;
- addUIClickListener('menubarItemPlayPause', _ -> toggleAudioPlayback());
+ menubarItemPlayPause.onClick = _ -> toggleAudioPlayback();
- addUIClickListener('menubarItemLoadInstrumental', _ -> this.openUploadInstDialog(true));
- addUIClickListener('menubarItemLoadVocals', _ -> this.openUploadVocalsDialog(true));
+ menubarItemLoadInstrumental.onClick = _ -> this.openUploadInstDialog(true);
+ menubarItemLoadVocals.onClick = _ -> this.openUploadVocalsDialog(true);
- addUIChangeListener('menubarItemMetronomeEnabled', event -> isMetronomeEnabled = event.value);
- setUICheckboxSelected('menubarItemMetronomeEnabled', isMetronomeEnabled);
+ menubarItemMetronomeEnabled.onChange = event -> isMetronomeEnabled = event.value;
+ menubarItemMetronomeEnabled.selected = isMetronomeEnabled;
- addUIChangeListener('menubarItemPlayerHitsounds', event -> hitsoundsEnabledPlayer = event.value);
- setUICheckboxSelected('menubarItemPlayerHitsounds', hitsoundsEnabledPlayer);
+ menubarItemPlayerHitsounds.onChange = event -> hitsoundsEnabledPlayer = event.value;
+ menubarItemPlayerHitsounds.selected = hitsoundsEnabledPlayer;
- addUIChangeListener('menubarItemOpponentHitsounds', event -> hitsoundsEnabledOpponent = event.value);
- setUICheckboxSelected('menubarItemOpponentHitsounds', hitsoundsEnabledOpponent);
+ menubarItemOpponentHitsounds.onChange = event -> hitsoundsEnabledOpponent = event.value;
+ menubarItemOpponentHitsounds.selected = hitsoundsEnabledOpponent;
- var instVolumeLabel:Null = findComponent('menubarLabelVolumeInstrumental', Label);
- if (instVolumeLabel != null)
- {
- addUIChangeListener('menubarItemVolumeInstrumental', function(event:UIEvent) {
- var volume:Float = (event?.value ?? 0) / 100.0;
- if (audioInstTrack != null) audioInstTrack.volume = volume;
- instVolumeLabel.text = 'Instrumental - ${Std.int(event.value)}%';
- });
+ menubarItemVolumeInstrumental.onChange = event -> {
+ var volume:Float = (event?.value ?? 0) / 100.0;
+ if (audioInstTrack != null) audioInstTrack.volume = volume;
+ menubarLabelVolumeInstrumental.text = 'Instrumental - ${Std.int(event.value)}%';
+ };
+
+ menubarItemVolumeVocals.onChange = event -> {
+ var volume:Float = (event?.value ?? 0) / 100.0;
+ if (audioVocalTrackGroup != null) audioVocalTrackGroup.volume = volume;
+ menubarLabelVolumeVocals.text = 'Voices - ${Std.int(event.value)}%';
}
- var vocalsVolumeLabel:Null = findComponent('menubarLabelVolumeVocals', Label);
- if (vocalsVolumeLabel != null)
- {
- addUIChangeListener('menubarItemVolumeVocals', function(event:UIEvent) {
- var volume:Float = (event?.value ?? 0) / 100.0;
- if (audioVocalTrackGroup != null) audioVocalTrackGroup.volume = volume;
- vocalsVolumeLabel.text = 'Voices - ${Std.int(event.value)}%';
- });
+ menubarItemPlaybackSpeed.onChange = event -> {
+ var pitch:Float = event.value * 2.0 / 100.0;
+ pitch = Math.floor(pitch / 0.25) * 0.25; // Round to nearest 0.25.
+ #if FLX_PITCH
+ if (audioInstTrack != null) audioInstTrack.pitch = pitch;
+ if (audioVocalTrackGroup != null) audioVocalTrackGroup.pitch = pitch;
+ #end
+ var pitchDisplay:Float = Std.int(pitch * 100) / 100; // Round to 2 decimal places.
+ menubarLabelPlaybackSpeed.text = 'Playback Speed - ${pitchDisplay}x';
}
- var playbackSpeedLabel:Null = findComponent('menubarLabelPlaybackSpeed', Label);
- if (playbackSpeedLabel != null)
- {
- addUIChangeListener('menubarItemPlaybackSpeed', function(event:UIEvent) {
- var pitch:Float = event.value * 2.0 / 100.0;
- pitch = Math.floor(pitch / 0.25) * 0.25; // Round to nearest 0.25.
- #if FLX_PITCH
- if (audioInstTrack != null) audioInstTrack.pitch = pitch;
- if (audioVocalTrackGroup != null) audioVocalTrackGroup.pitch = pitch;
- #end
- var pitchDisplay:Float = Std.int(pitch * 100) / 100; // Round to 2 decimal places.
- playbackSpeedLabel.text = 'Playback Speed - ${pitchDisplay}x';
- });
- }
-
- addUIChangeListener('menubarItemToggleToolboxDifficulty', event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT, event.value));
- addUIChangeListener('menubarItemToggleToolboxMetadata', event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_METADATA_LAYOUT, event.value));
- 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));
- addUIChangeListener('menubarItemToggleToolboxOpponentPreview', event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT, event.value));
+ menubarItemToggleToolboxDifficulty.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_DIFFICULTY_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.
- registerContextMenu(null, Paths.ui('chart-editor/context/test'));
+ // registerContextMenu(null, Paths.ui('chart-editor/context/test'));
}
/**
@@ -2227,7 +2288,13 @@ class ChartEditorState extends HaxeUIState
*/
function setupAutoSave():Void
{
+ // Called when clicking the X button on the window.
WindowUtil.windowExit.add(onWindowClose);
+
+ // Called when the game crashes.
+ CrashHandler.errorSignal.add(onWindowCrash);
+ CrashHandler.criticalErrorSignal.add(onWindowCrash);
+
saveDataDirty = false;
}
@@ -2236,10 +2303,12 @@ class ChartEditorState extends HaxeUIState
*/
function autoSave():Void
{
+ var needsAutoSave:Bool = saveDataDirty;
+
saveDataDirty = false;
// Auto-save preferences.
- writePreferences();
+ writePreferences(needsAutoSave);
// Auto-save the chart.
#if html5
@@ -2247,24 +2316,72 @@ class ChartEditorState extends HaxeUIState
// TODO: Implement this.
#else
// Auto-save to temp file.
- ChartEditorImportExportHandler.exportAllSongData(this, true);
+ if (needsAutoSave)
+ {
+ this.exportAllSongData(true, null);
+ var absoluteBackupsPath:String = Path.join([Sys.getCwd(), ChartEditorImportExportHandler.BACKUPS_PATH]);
+ this.infoWithActions('Auto-Save', 'Chart auto-saved to ${absoluteBackupsPath}.', [
+ {
+ text: "Take Me There",
+ callback: openBackupsFolder,
+ }
+ ]);
+ }
#end
}
+ /**
+ * Open the backups folder in the file explorer.
+ * Don't call this on HTML5.
+ */
+ function openBackupsFolder():Void
+ {
+ // 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]);
+ WindowUtil.openFolder(absoluteBackupsPath);
+ }
+
+ /**
+ * Called when the window was closed, to save a backup of the chart.
+ * @param exitCode The exit code of the window. We use `-1` when calling the function due to a game crash.
+ */
function onWindowClose(exitCode:Int):Void
{
trace('Window exited with exit code: $exitCode');
trace('Should save chart? $saveDataDirty');
- if (saveDataDirty)
+ var needsAutoSave:Bool = saveDataDirty;
+
+ writePreferences(needsAutoSave);
+
+ if (needsAutoSave)
{
- ChartEditorImportExportHandler.exportAllSongData(this, true);
+ this.exportAllSongData(true, null);
+ }
+ }
+
+ function onWindowCrash(message:String):Void
+ {
+ trace('Chart editor intercepted crash:');
+ trace('${message}');
+
+ trace('Should save chart? $saveDataDirty');
+
+ var needsAutoSave:Bool = saveDataDirty;
+
+ writePreferences(needsAutoSave);
+
+ if (needsAutoSave)
+ {
+ this.exportAllSongData(true, null);
}
}
function cleanupAutoSave():Void
{
WindowUtil.windowExit.remove(onWindowClose);
+ CrashHandler.errorSignal.remove(onWindowCrash);
+ CrashHandler.criticalErrorSignal.remove(onWindowCrash);
}
public override function update(elapsed:Float):Void
@@ -2951,9 +3068,10 @@ class ChartEditorState extends HaxeUIState
if (FlxG.mouse.justReleased) FlxG.sound.play(Paths.sound("chartingSounds/ClickUp"));
// Note: If a menu is open in HaxeUI, don't handle cursor behavior.
- var shouldHandleCursor:Bool = !(isHaxeUIFocused || playbarHeadDragging)
+ var shouldHandleCursor:Bool = !(isHaxeUIFocused || playbarHeadDragging || isHaxeUIDialogOpen)
|| (selectionBoxStartPos != null)
|| (dragTargetNote != null || dragTargetEvent != null);
+
var eventColumn:Int = (STRUMLINE_SIZE * 2 + 1) - 1;
// trace('shouldHandleCursor: $shouldHandleCursor');
@@ -3399,13 +3517,14 @@ class ChartEditorState extends HaxeUIState
gridGhostHoldNote.noteData = gridGhostNote.noteData;
gridGhostHoldNote.noteDirection = gridGhostNote.noteData.getDirection();
- gridGhostHoldNote.setHeightDirectly(dragLengthPixels);
+ gridGhostHoldNote.setHeightDirectly(dragLengthPixels, true);
gridGhostHoldNote.updateHoldNotePosition(renderedHoldNotes);
}
else
{
gridGhostHoldNote.visible = false;
+ gridGhostHoldNote.setHeightDirectly(0);
}
}
@@ -3421,6 +3540,11 @@ class ChartEditorState extends HaxeUIState
// Finished dragging. Release the note.
currentPlaceNoteData = null;
}
+ else
+ {
+ // Cursor should be a grabby hand.
+ if (targetCursorMode == null) targetCursorMode = Grabbing;
+ }
}
else
{
@@ -3815,7 +3939,6 @@ class ChartEditorState extends HaxeUIState
function handlePlaybar():Void
{
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.
playbarHeadLayout.x = 4;
@@ -3828,26 +3951,22 @@ class ChartEditorState extends HaxeUIState
if (!playbarHeadDragging)
{
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 songPosMinutes:String = Std.string(Math.floor((songPos / 1000) / 60)).lpad('0', 2);
var songPosString:String = '${songPosMinutes}:${songPosSeconds}';
- if (playbarSongPos == null) playbarSongPos = findComponent('playbarSongPos', Label);
- if (playbarSongPos != null && playbarSongPos.value != songPosString) playbarSongPos.value = songPosString;
+ if (playbarSongPos.value != songPosString) playbarSongPos.value = songPosString;
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 songRemainingString:String = '-${songRemainingMinutes}:${songRemainingSeconds}';
- if (playbarSongRemaining == null) playbarSongRemaining = findComponent('playbarSongRemaining', Label);
- if (playbarSongRemaining != null
- && playbarSongRemaining.value != songRemainingString) playbarSongRemaining.value = songRemainingString;
+ if (playbarSongRemaining.value != songRemainingString) playbarSongRemaining.value = songRemainingString;
- if (playbarNoteSnap == null) playbarNoteSnap = findComponent('playbarNoteSnap', Label);
- if (playbarNoteSnap != null) playbarNoteSnap.text = '1/${noteSnapQuant}';
+ playbarNoteSnap.text = '1/${noteSnapQuant}';
}
function handlePlayhead():Void
@@ -3949,35 +4068,37 @@ class ChartEditorState extends HaxeUIState
function handleFileKeybinds():Void
{
// 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);
}
// 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);
}
- 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)
{
// CTRL + SHIFT + S = Save As
- ChartEditorImportExportHandler.exportAllSongData(this, false);
+ this.exportAllSongData(false, null, function(path:String) {
+ // CTRL + SHIFT + S Successful
+ this.success('Saved Chart', 'Chart saved successfully to ${path}.');
+ }, function() {
+ // CTRL + SHIFT + S Cancelled
+ });
}
else
{
// CTRL + S = Save Chart
- ChartEditorImportExportHandler.exportAllSongData(this, true, currentWorkingFilePath);
+ this.exportAllSongData(true, currentWorkingFilePath);
+ this.success('Saved Chart', 'Chart saved successfully to ${currentWorkingFilePath}.');
}
}
- if (FlxG.keys.pressed.CONTROL && FlxG.keys.pressed.SHIFT && FlxG.keys.justPressed.S)
- {
- this.exportAllSongData(false);
- }
// CTRL + Q = Quit to Menu
if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.Q)
{
@@ -4138,6 +4259,14 @@ class ChartEditorState extends HaxeUIState
{
// F1 = Open Help
if (FlxG.keys.justPressed.F1) this.openUserGuideDialog();
+
+ // DEBUG KEYBIND: Ctrl + Alt + Shift + L = Crash the game.
+ #if debug
+ if (FlxG.keys.pressed.CONTROL && FlxG.keys.pressed.ALT && FlxG.keys.pressed.SHIFT && FlxG.keys.justPressed.L)
+ {
+ throw "DEBUG: Crashing the chart editor!";
+ }
+ #end
}
override function handleQuickWatch():Void
@@ -4308,7 +4437,7 @@ class ChartEditorState extends HaxeUIState
FlxG.camera.focusOn(new FlxPoint(FlxG.width / 2, FlxG.height / 2));
FlxG.camera.zoom = 1.0;
- add(this.component);
+ add(this.root);
}
/**
@@ -4324,7 +4453,7 @@ class ChartEditorState extends HaxeUIState
if (audioVocalTrackGroup != null) audioVocalTrackGroup.play(false, audioInstTrack.time);
}
- setComponentText('playbarPlay', '||');
+ playbarPlay.text = '||'; // Pause
}
/**
@@ -4474,15 +4603,7 @@ class ChartEditorState extends HaxeUIState
}
}
- #if !mac
- NotificationManager.instance.addNotification(
- {
- title: 'Switch Difficulty',
- body: 'Switched difficulty to ${selectedDifficulty.toTitleCase()}',
- type: NotificationType.Success,
- expiryMs: Constants.NOTIFICATION_DISMISS_TIME
- });
- #end
+ this.success('Switch Difficulty', 'Switched difficulty to ${selectedDifficulty.toTitleCase()}');
}
/**
@@ -4560,12 +4681,9 @@ class ChartEditorState extends HaxeUIState
{
moveSongToScrollPosition();
- var instVolumeSlider:Null = findComponent('menubarItemVolumeInstrumental', Slider);
- var vocalVolumeSlider:Null = findComponent('menubarItemVolumeVocals', Slider);
-
// Reapply the volume.
- var instTargetVolume:Float = instVolumeSlider?.value ?? 1.0;
- var vocalTargetVolume:Float = vocalVolumeSlider?.value ?? 1.0;
+ var instTargetVolume:Float = menubarItemVolumeInstrumental.value ?? 1.0;
+ var vocalTargetVolume:Float = menubarItemVolumeVocals.value ?? 1.0;
if (audioInstTrack != null)
{
@@ -4747,9 +4865,6 @@ class ChartEditorState extends HaxeUIState
*/
// ====================
- /**
- * Dismiss any existing HaxeUI notifications, if there are any.
- */
function handleNotePreview():Void
{
if (notePreviewDirty && notePreview != null)
@@ -4779,48 +4894,30 @@ class ChartEditorState extends HaxeUIState
commandHistoryDirty = false;
// Update the Undo and Redo buttons.
- var undoButton:Null = findComponent('menubarItemUndo', MenuItem);
-
- if (undoButton != null)
+ if (undoHistory.length == 0)
{
- if (undoHistory.length == 0)
- {
- // Disable the Undo button.
- undoButton.disabled = true;
- undoButton.text = 'Undo';
- }
- else
- {
- // Change the label to the last command.
- undoButton.disabled = false;
- undoButton.text = 'Undo ${undoHistory[undoHistory.length - 1].toString()}';
- }
+ // Disable the Undo button.
+ menubarItemUndo.disabled = true;
+ menubarItemUndo.text = 'Undo';
}
else
{
- trace('undoButton is null');
+ // Change the label to the last command.
+ menubarItemUndo.disabled = false;
+ menubarItemUndo.text = 'Undo ${undoHistory[undoHistory.length - 1].toString()}';
}
- var redoButton:Null = findComponent('menubarItemRedo', MenuItem);
-
- if (redoButton != null)
+ if (redoHistory.length == 0)
{
- if (redoHistory.length == 0)
- {
- // Disable the Redo button.
- redoButton.disabled = true;
- redoButton.text = 'Redo';
- }
- else
- {
- // Change the label to the last command.
- redoButton.disabled = false;
- redoButton.text = 'Redo ${redoHistory[redoHistory.length - 1].toString()}';
- }
+ // Disable the Redo button.
+ menubarItemRedo.disabled = true;
+ menubarItemRedo.text = 'Redo';
}
else
{
- trace('redoButton is null');
+ // Change the label to the last command.
+ menubarItemRedo.disabled = false;
+ menubarItemRedo.text = 'Redo ${redoHistory[redoHistory.length - 1].toString()}';
}
}
}
@@ -4861,9 +4958,9 @@ class ChartEditorState extends HaxeUIState
switch (noteData.getStrumlineIndex())
{
case 0: // Player
- if (hitsoundsEnabledPlayer) ChartEditorAudioHandler.playSound(this, Paths.sound('chartingSounds/hitNotePlayer'));
+ if (hitsoundsEnabledPlayer) this.playSound(Paths.sound('chartingSounds/hitNotePlayer'));
case 1: // Opponent
- if (hitsoundsEnabledOpponent) ChartEditorAudioHandler.playSound(this, Paths.sound('chartingSounds/hitNoteOpponent'));
+ if (hitsoundsEnabledOpponent) this.playSound(Paths.sound('chartingSounds/hitNoteOpponent'));
}
}
}
@@ -4873,7 +4970,7 @@ class ChartEditorState extends HaxeUIState
if (audioInstTrack != null) audioInstTrack.pause();
if (audioVocalTrackGroup != null) audioVocalTrackGroup.pause();
- setComponentText('playbarPlay', '>');
+ playbarPlay.text = '>';
}
function toggleAudioPlayback():Void
@@ -4950,14 +5047,6 @@ class ChartEditorState extends HaxeUIState
ChartEditorNoteSprite.noteFrameCollection = null;
}
- /**
- * Dismiss any existing notifications, if there are any.
- */
- function dismissNotifications():Void
- {
- NotificationManager.instance.clearNotifications();
- }
-
function applyCanQuickSave():Void
{
if (menubarItemSaveChart == null) return;
diff --git a/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx b/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx
index 12115ba8a..1857b44db 100644
--- a/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx
+++ b/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx
@@ -4,8 +4,6 @@ import funkin.data.song.SongData.SongEventData;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongDataUtils;
import funkin.data.song.SongDataUtils.SongClipboardItems;
-import haxe.ui.notifications.NotificationManager;
-import haxe.ui.notifications.NotificationType;
/**
* A command which inserts the contents of the clipboard into the chart editor.
@@ -30,15 +28,7 @@ class PasteItemsCommand implements ChartEditorCommand
if (currentClipboard.valid != true)
{
- #if !mac
- NotificationManager.instance.addNotification(
- {
- title: 'Failed to Paste',
- body: 'Could not parse clipboard contents.',
- type: NotificationType.Error,
- expiryMs: Constants.NOTIFICATION_DISMISS_TIME
- });
- #end
+ state.error('Failed to Paste', 'Could not parse clipboard contents.');
return;
}
@@ -58,15 +48,7 @@ class PasteItemsCommand implements ChartEditorCommand
state.sortChartData();
- #if !mac
- NotificationManager.instance.addNotification(
- {
- title: 'Paste Successful',
- body: 'Successfully pasted clipboard contents.',
- type: NotificationType.Success,
- expiryMs: Constants.NOTIFICATION_DISMISS_TIME
- });
- #end
+ state.success('Paste Successful', 'Successfully pasted clipboard contents.');
}
public function undo(state:ChartEditorState):Void
diff --git a/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx b/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx
index 0e8c02758..e5971db08 100644
--- a/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx
+++ b/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx
@@ -9,6 +9,7 @@ import flixel.graphics.frames.FlxTileFrames;
import flixel.math.FlxPoint;
import funkin.play.notes.SustainTrail;
import funkin.data.song.SongData.SongNoteData;
+import flixel.math.FlxMath;
/**
* A sprite that can be used to display the trail of a hold note in a chart.
@@ -42,9 +43,12 @@ class ChartEditorHoldNoteSprite extends SustainTrail
* Set the height directly, to a value in pixels.
* @param h The desired height in pixels.
*/
- public function setHeightDirectly(h:Float)
+ public function setHeightDirectly(h:Float, ?lerp:Bool = false)
{
- sustainLength = h / (getScrollSpeed() * Constants.PIXELS_PER_MS);
+ if (lerp != null && lerp) sustainLength = FlxMath.lerp(sustainLength, h / (getScrollSpeed() * Constants.PIXELS_PER_MS), 0.25);
+ else
+ sustainLength = h / (getScrollSpeed() * Constants.PIXELS_PER_MS);
+
fullSustainLength = sustainLength;
}
diff --git a/source/funkin/ui/debug/charting/components/ChartEditorPlaybarHead.hx b/source/funkin/ui/debug/charting/components/ChartEditorPlaybarHead.hx
new file mode 100644
index 000000000..e08d71029
--- /dev/null
+++ b/source/funkin/ui/debug/charting/components/ChartEditorPlaybarHead.hx
@@ -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() { }
+}
diff --git a/source/funkin/ui/debug/charting/dialogs/ChartEditorAboutDialog.hx b/source/funkin/ui/debug/charting/dialogs/ChartEditorAboutDialog.hx
new file mode 100644
index 000000000..e6f57c49f
--- /dev/null
+++ b/source/funkin/ui/debug/charting/dialogs/ChartEditorAboutDialog.hx
@@ -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;
+ }
+}
diff --git a/source/funkin/ui/debug/charting/dialogs/ChartEditorBaseDialog.hx b/source/funkin/ui/debug/charting/dialogs/ChartEditorBaseDialog.hx
new file mode 100644
index 000000000..a180825a8
--- /dev/null
+++ b/source/funkin/ui/debug/charting/dialogs/ChartEditorBaseDialog.hx
@@ -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
+}
diff --git a/source/funkin/ui/debug/charting/dialogs/ChartEditorUploadChartDialog.hx b/source/funkin/ui/debug/charting/dialogs/ChartEditorUploadChartDialog.hx
new file mode 100644
index 000000000..aef5e9feb
--- /dev/null
+++ b/source/funkin/ui/debug/charting/dialogs/ChartEditorUploadChartDialog.hx
@@ -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 = [];
+
+ 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> = 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> = 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();
+ }
+}
diff --git a/source/funkin/ui/debug/charting/dialogs/ChartEditorWelcomeDialog.hx b/source/funkin/ui/debug/charting/dialogs/ChartEditorWelcomeDialog.hx
new file mode 100644
index 000000000..0a0bb5064
--- /dev/null
+++ b/source/funkin/ui/debug/charting/dialogs/ChartEditorWelcomeDialog.hx
@@ -0,0 +1,259 @@
+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);
+ state.stopWelcomeMusic();
+ }
+
+ /**
+ * 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();
+ linkRecentChart.text = chartPath;
+ linkRecentChart.onClick = function(_event) {
+ this.hideDialog(DialogButton.CANCEL);
+ state.stopWelcomeMusic();
+
+ // Load chart from file
+ var result:Null> = 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 = SongRegistry.instance.listEntryIds();
+ songList.sort(SortUtil.alphabetically);
+
+ for (targetSongId in songList)
+ {
+ var songData:Null = SongRegistry.instance.fetchEntry(targetSongId);
+ if (songData == null) continue;
+
+ var songName:Null = 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);
+ state.stopWelcomeMusic();
+
+ // 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);
+ state.stopWelcomeMusic();
+
+ // 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);
+ state.stopWelcomeMusic();
+
+ //
+ // 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);
+ state.stopWelcomeMusic();
+
+ //
+ // 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);
+ state.stopWelcomeMusic();
+
+ //
+ // 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);
+ state.stopWelcomeMusic();
+
+ // Open the "Import Chart" dialog
+ state.openImportChartWizard('legacy', false);
+ }
+}
diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx
index 06da6ee12..79937ce6f 100644
--- a/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx
+++ b/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx
@@ -14,12 +14,18 @@ import funkin.play.character.CharacterData;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.play.song.Song;
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.util.Constants;
+import funkin.util.DateUtil;
import funkin.util.FileUtil;
import funkin.util.SerializerUtil;
import funkin.util.SortUtil;
import funkin.util.VersionUtil;
+import funkin.util.WindowUtil;
import haxe.io.Path;
import haxe.ui.components.Button;
import haxe.ui.components.DropDown;
@@ -38,6 +44,7 @@ import haxe.ui.core.Component;
import haxe.ui.events.UIEvent;
import haxe.ui.notifications.NotificationManager;
import haxe.ui.notifications.NotificationType;
+import haxe.ui.RuntimeComponentBuilder;
import thx.semver.Version;
using Lambda;
@@ -50,8 +57,6 @@ using Lambda;
class ChartEditorDialogHandler
{
// 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_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');
@@ -63,6 +68,7 @@ class ChartEditorDialogHandler
static final CHART_EDITOR_DIALOG_USER_GUIDE_LAYOUT:String = Paths.ui('chart-editor/dialogs/user-guide');
static final CHART_EDITOR_DIALOG_ADD_VARIATION_LAYOUT:String = Paths.ui('chart-editor/dialogs/add-variation');
static final CHART_EDITOR_DIALOG_ADD_DIFFICULTY_LAYOUT:String = Paths.ui('chart-editor/dialogs/add-difficulty');
+ static final CHART_EDITOR_DIALOG_BACKUP_AVAILABLE_LAYOUT:String = Paths.ui('chart-editor/dialogs/backup-available');
/**
* Builds and opens a dialog giving brief credits for the chart editor.
@@ -71,7 +77,12 @@ class ChartEditorDialogHandler
*/
public static function openAboutDialog(state:ChartEditorState):Null
{
- return openDialog(state, CHART_EDITOR_DIALOG_ABOUT_LAYOUT, true, true);
+ var dialog = ChartEditorAboutDialog.build(state);
+
+ dialog.zIndex = 1000;
+ state.isHaxeUIDialogOpen = true;
+
+ return dialog;
}
/**
@@ -82,305 +93,109 @@ class ChartEditorDialogHandler
*/
public static function openWelcomeDialog(state:ChartEditorState, closable:Bool = true):Null
{
- var dialog:Null = openDialog(state, CHART_EDITOR_DIALOG_WELCOME_LAYOUT, true, closable);
- if (dialog == null) throw 'Could not locate Welcome dialog';
+ var dialog = ChartEditorWelcomeDialog.build(state, closable);
+ dialog.zIndex = 1000;
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 = 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();
- linkRecentChart.text = chartPath;
- linkRecentChart.onClick = function(_event) {
- dialog.hideDialog(DialogButton.CANCEL);
- state.stopWelcomeMusic();
-
- // Load chart from file
- var result:Null> = 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);
- }
- #else
- var splashRecentContainer:Null = 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 = dialog.findComponent('splashCreateFromSongBasicOnly', Link);
- if (linkCreateBasic == null) throw 'Could not locate splashCreateFromSongBasicOnly link in Welcome dialog';
- linkCreateBasic.onClick = function(_event) {
- // Hide the welcome dialog
- dialog.hideDialog(DialogButton.CANCEL);
- state.stopWelcomeMusic();
-
- //
- // Create Song Wizard
- //
- openCreateSongWizardBasicOnly(state, false);
- }
-
- // Create New Song "Erect/Nightmare"
- var linkCreateErect:Null = dialog.findComponent('splashCreateFromSongErectOnly', Link);
- if (linkCreateErect == null) throw 'Could not locate splashCreateFromSongErectOnly link in Welcome dialog';
- linkCreateErect.onClick = function(_event) {
- // 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 = dialog.findComponent('splashCreateFromSongBasicErect', Link);
- if (linkCreateErect == null) throw 'Could not locate splashCreateFromSongBasicErect link in Welcome dialog';
- linkCreateErect.onClick = function(_event) {
- // Hide the welcome dialog
- dialog.hideDialog(DialogButton.CANCEL);
-
- //
- // Create Song Wizard
- //
- openCreateSongWizardBasicErect(state, false);
- }
-
- var linkImportChartLegacy:Null = dialog.findComponent('splashImportChartLegacy', Link);
- if (linkImportChartLegacy == null) throw 'Could not locate splashImportChartLegacy link in Welcome dialog';
- linkImportChartLegacy.onClick = function(_event) {
- // Hide the welcome dialog
- dialog.hideDialog(DialogButton.CANCEL);
- state.stopWelcomeMusic();
-
- // Open the "Import Chart" dialog
- openImportChartWizard(state, 'legacy', false);
- };
-
- var buttonBrowse:Null = dialog.findComponent('splashBrowse', Button);
- if (buttonBrowse == null) throw 'Could not locate splashBrowse button in Welcome dialog';
- buttonBrowse.onClick = function(_event) {
- // Hide the welcome dialog
- dialog.hideDialog(DialogButton.CANCEL);
- state.stopWelcomeMusic();
-
- // Open the "Open Chart" dialog
- openBrowseFNFC(state, false);
- }
-
- var splashTemplateContainer:Null = dialog.findComponent('splashTemplateContainer', VBox);
- if (splashTemplateContainer == null) throw 'Could not locate splashTemplateContainer in Welcome dialog';
-
- var songList:Array = SongRegistry.instance.listEntryIds();
- songList.sort(SortUtil.alphabetically);
-
- for (targetSongId in songList)
- {
- var songData:Null = SongRegistry.instance.fetchEntry(targetSongId);
- if (songData == null) continue;
-
- var songName:Null = 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(_event) {
- dialog.hideDialog(DialogButton.CANCEL);
- state.stopWelcomeMusic();
-
- // Load song from template
- state.loadSongAsTemplate(targetSongId);
- }
-
- splashTemplateContainer.addComponent(linkTemplateSong);
- }
state.fadeInWelcomeMusic();
+
return dialog;
}
- public static function openBrowseFNFC(state:ChartEditorState, closable:Bool):Null
+ /**
+ * Builds and opens a dialog letting the user know a backup is available, and prompting them to load it.
+ */
+ public static function openBackupAvailableDialog(state:ChartEditorState, welcomeDialog:Null):Null
{
- var dialog:Null = openDialog(state, CHART_EDITOR_DIALOG_UPLOAD_CHART_LAYOUT, true, closable);
- if (dialog == null) throw 'Could not locate Upload Chart dialog';
-
+ var dialog:Null = openDialog(state, CHART_EDITOR_DIALOG_BACKUP_AVAILABLE_LAYOUT, true, true);
+ if (dialog == null) throw 'Could not locate Backup Available dialog';
dialog.onDialogClosed = function(_event) {
state.isHaxeUIDialogOpen = false;
if (_event.button == DialogButton.APPLY)
{
- // Simply let the dialog close.
+ // User loaded the backup! Close the welcome dialog behind this.
+ if (welcomeDialog != null) welcomeDialog.hideDialog(DialogButton.CANCEL);
}
else
{
- // User cancelled the wizard! Back to the welcome dialog.
- openWelcomeDialog(state);
+ // User cancelled the dialog, don't close the welcome dialog so we aren't in a broken state.
}
};
- var buttonCancel:Null = dialog.findComponent('dialogCancel', Button);
- if (buttonCancel == null) throw 'Could not locate dialogCancel button in Upload Chart dialog';
-
state.isHaxeUIDialogOpen = true;
+
+ var backupTimeLabel:Null = dialog.findComponent('backupTimeLabel', Label);
+ if (backupTimeLabel == null) throw 'Could not locate backupTimeLabel button in Backup Available dialog';
+
+ var latestBackupDate:Null = ChartEditorImportExportHandler.getLatestBackupDate();
+ if (latestBackupDate != null)
+ {
+ var latestBackupDateStr:String = DateUtil.generateCleanTimestamp(latestBackupDate);
+ backupTimeLabel.text = latestBackupDateStr;
+ }
+
+ var buttonCancel:Null = dialog.findComponent('dialogCancel', Button);
+ if (buttonCancel == null) throw 'Could not locate dialogCancel button in Backup Available dialog';
buttonCancel.onClick = function(_event) {
+ // Don't hide the welcome dialog behind this.
dialog.hideDialog(DialogButton.CANCEL);
}
- var chartBox:Null = dialog.findComponent('chartBox', Box);
- if (chartBox == null) throw 'Could not locate chartBox in Upload Chart dialog';
-
- chartBox.onMouseOver = function(_event) {
- chartBox.swapClass('upload-bg', 'upload-bg-hover');
- Cursor.cursorMode = Pointer;
+ var buttonGoToFolder:Null = dialog.findComponent('buttonGoToFolder', Button);
+ if (buttonGoToFolder == null) throw 'Could not locate buttonGoToFolder button in Backup Available dialog';
+ buttonGoToFolder.onClick = function(_event) {
+ state.openBackupsFolder();
+ // Don't hide the welcome dialog behind this.
+ // dialog.hideDialog(DialogButton.CANCEL);
}
- chartBox.onMouseOut = function(_event) {
- chartBox.swapClass('upload-bg-hover', 'upload-bg');
- Cursor.cursorMode = Default;
- }
+ var buttonOpenBackup:Null = dialog.findComponent('buttonOpenBackup', Button);
+ if (buttonOpenBackup == null) throw 'Could not locate buttonOpenBackup button in Backup Available dialog';
+ buttonOpenBackup.onClick = function(_event) {
+ var latestBackupPath:Null = ChartEditorImportExportHandler.getLatestBackupPath();
- var onDropFile:String->Void;
-
- chartBox.onClick = function(_event) {
- 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> = 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;
- dialog.hideDialog(DialogButton.APPLY);
- removeDropHandler(onDropFile);
- }
- }
- 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
- }
- }
- });
- }
-
- onDropFile = function(pathStr:String) {
- var path:Path = new Path(pathStr);
- trace('Dropped file (${path})');
-
- try
+ var result:Null> = (latestBackupPath != null) ? state.loadFromFNFCPath(latestBackupPath) : null;
+ if (result != null)
{
- var result:Null> = ChartEditorImportExportHandler.loadFromFNFCPath(state, path.toString());
- if (result != null)
+ if (result.length == 0)
{
- #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
- dialog.hideDialog(DialogButton.APPLY);
- removeDropHandler(onDropFile);
+ // No warnings.
+ state.success('Loaded Chart', 'Loaded chart (${latestBackupPath})');
}
else
{
- #if !mac
- NotificationManager.instance.addNotification(
- {
- title: 'Failure',
- body: 'Failed to load chart (${path.toString()})',
- type: NotificationType.Error,
- expiryMs: Constants.NOTIFICATION_DISMISS_TIME
- });
- #end
+ // One or more warnings.
+ state.warning('Loaded Chart', 'Loaded chart (${latestBackupPath})\n${result.join("\n")}');
}
- }
- 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
- }
- };
- addDropHandler(chartBox, onDropFile);
+ // Close the welcome dialog behind this.
+ dialog.hideDialog(DialogButton.APPLY);
+ }
+ else
+ {
+ state.error('Failed to Load Chart', 'Failed to load chart (${latestBackupPath})');
+
+ // Song failed to load, don't close the Welcome dialog so we aren't in a broken state.
+ dialog.hideDialog(DialogButton.CANCEL);
+ }
+ }
+
+ 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
+ {
+ var dialog = ChartEditorUploadChartDialog.build(state, closable);
+
+ dialog.zIndex = 1000;
+ state.isHaxeUIDialogOpen = true;
return dialog;
}
@@ -418,14 +233,14 @@ class ChartEditorDialogHandler
else
{
// User cancelled the wizard! Back to the welcome dialog.
- openWelcomeDialog(state);
+ state.openWelcomeDialog(closable);
}
};
}
else
{
// User cancelled the wizard! Back to the welcome dialog.
- openWelcomeDialog(state);
+ state.openWelcomeDialog(closable);
}
};
}
@@ -459,14 +274,14 @@ class ChartEditorDialogHandler
else
{
// User cancelled the wizard! Back to the welcome dialog.
- openWelcomeDialog(state);
+ state.openWelcomeDialog(closable);
}
};
}
else
{
// User cancelled the wizard! Back to the welcome dialog.
- openWelcomeDialog(state);
+ state.openWelcomeDialog(closable);
}
};
}
@@ -498,14 +313,14 @@ class ChartEditorDialogHandler
else
{
// User cancelled the wizard at Step 2! Back to the welcome dialog.
- openWelcomeDialog(state);
+ state.openWelcomeDialog(closable);
}
};
}
else
{
// User cancelled the wizard at Step 1! Back to the welcome dialog.
- openWelcomeDialog(state);
+ state.openWelcomeDialog(closable);
}
};
}
@@ -537,14 +352,14 @@ class ChartEditorDialogHandler
else
{
// User cancelled the wizard at Step 2! Back to the welcome dialog.
- openWelcomeDialog(state);
+ state.openWelcomeDialog(closable);
}
};
}
else
{
// User cancelled the wizard at Step 1! Back to the welcome dialog.
- openWelcomeDialog(state);
+ state.openWelcomeDialog(closable);
}
};
}
@@ -596,14 +411,14 @@ class ChartEditorDialogHandler
else
{
// User cancelled the wizard at Step 5! Back to the welcome dialog.
- openWelcomeDialog(state);
+ state.openWelcomeDialog(closable);
}
};
}
else
{
// User cancelled the wizard at Step 4! Back to the welcome dialog.
- openWelcomeDialog(state);
+ state.openWelcomeDialog(closable);
}
}
}
@@ -611,14 +426,14 @@ class ChartEditorDialogHandler
else
{
// User cancelled the wizard at Step 2! Back to the welcome dialog.
- openWelcomeDialog(state);
+ state.openWelcomeDialog(closable);
}
};
}
else
{
// User cancelled the wizard at Step 1! Back to the welcome dialog.
- openWelcomeDialog(state);
+ state.openWelcomeDialog(closable);
}
};
}
@@ -657,7 +472,7 @@ class ChartEditorDialogHandler
var instId:String = state.currentInstrumentalId;
- var onDropFile:String->Void;
+ var dropHandler:DialogDropTarget = {component: instrumentalBox, handler: null};
instrumentalBox.onClick = function(_event) {
Dialogs.openBinaryFile('Open Instrumental', [
@@ -666,55 +481,31 @@ class ChartEditorDialogHandler
{
if (state.loadInstFromBytes(selectedFile.bytes, instId))
{
- #if !mac
- NotificationManager.instance.addNotification(
- {
- title: 'Success',
- body: 'Loaded instrumental track (${selectedFile.name}) for variation (${state.selectedVariation})',
- type: NotificationType.Success,
- expiryMs: Constants.NOTIFICATION_DISMISS_TIME
- });
- #end
+ state.success('Loaded Instrumental', 'Loaded instrumental track (${selectedFile.name}) for variation (${state.selectedVariation})');
state.switchToCurrentInstrumental();
dialog.hideDialog(DialogButton.APPLY);
- removeDropHandler(onDropFile);
+ state.removeDropHandler(dropHandler);
}
else
{
- #if !mac
- NotificationManager.instance.addNotification(
- {
- title: 'Failure',
- body: 'Failed to load instrumental track (${selectedFile.name}) for variation (${state.selectedVariation})',
- type: NotificationType.Error,
- expiryMs: Constants.NOTIFICATION_DISMISS_TIME
- });
- #end
+ state.error('Failed to Load Instrumental', 'Failed to load instrumental track (${selectedFile.name}) for variation (${state.selectedVariation})');
}
}
});
}
- onDropFile = function(pathStr:String) {
+ var onDropFile:String->Void = function(pathStr:String) {
var path:Path = new Path(pathStr);
trace('Dropped file (${path})');
if (state.loadInstFromPath(path, instId))
{
// Tell the user the load was successful.
- #if !mac
- NotificationManager.instance.addNotification(
- {
- title: 'Success',
- body: 'Loaded instrumental track (${path.file}.${path.ext}) for variation (${state.selectedVariation})',
- type: NotificationType.Success,
- expiryMs: Constants.NOTIFICATION_DISMISS_TIME
- });
- #end
+ state.success('Loaded Instrumental', 'Loaded instrumental track (${path.file}.${path.ext}) for variation (${state.selectedVariation})');
state.switchToCurrentInstrumental();
dialog.hideDialog(DialogButton.APPLY);
- removeDropHandler(onDropFile);
+ state.removeDropHandler(dropHandler);
}
else
{
@@ -728,19 +519,13 @@ class ChartEditorDialogHandler
}
// Tell the user the load was successful.
- #if !mac
- NotificationManager.instance.addNotification(
- {
- title: 'Failure',
- body: message,
- type: NotificationType.Error,
- expiryMs: Constants.NOTIFICATION_DISMISS_TIME
- });
- #end
+ state.error('Failed to Load Instrumental', message);
}
};
- addDropHandler(instrumentalBox, onDropFile);
+ dropHandler.handler = onDropFile;
+
+ state.addDropHandler(dropHandler);
return dialog;
}
@@ -935,7 +720,7 @@ class ChartEditorDialogHandler
var charMetadata:Null = CharacterDataParser.fetchCharacterData(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 = vocalsEntry.findComponent('vocalsEntryLabel', Label);
if (vocalsEntryLabel == null) throw 'Could not locate vocalsEntryLabel in Upload Vocals dialog';
@@ -945,6 +730,8 @@ class ChartEditorDialogHandler
vocalsEntryLabel.text = 'Click to browse for vocals for $charName.';
#end
+ var dropHandler:DialogDropTarget = {component: vocalsEntry, handler: null};
+
var onDropFile:String->Void = function(pathStr:String) {
trace('Selected file: $pathStr');
var path:Path = new Path(pathStr);
@@ -958,15 +745,7 @@ class ChartEditorDialogHandler
if (state.loadVocalsFromPath(path, charKey, instId))
{
// Tell the user the load was successful.
- #if !mac
- NotificationManager.instance.addNotification(
- {
- title: 'Success',
- body: 'Loaded vocals for $charName (${path.file}.${path.ext}), variation ${state.selectedVariation}',
- type: NotificationType.Success,
- expiryMs: Constants.NOTIFICATION_DISMISS_TIME
- });
- #end
+ state.success('Loaded Vocals', 'Loaded vocals for $charName (${path.file}.${path.ext}), variation ${state.selectedVariation}');
#if FILE_DROP_SUPPORTED
vocalsEntryLabel.text = 'Voices for $charName (drag and drop, or click to browse)\nSelected file: ${path.file}.${path.ext}';
#else
@@ -974,22 +753,13 @@ class ChartEditorDialogHandler
#end
dialogNoVocals.hidden = true;
- removeDropHandler(onDropFile);
+ state.removeDropHandler(dropHandler);
}
else
{
trace('Failed to load vocal track (${path.file}.${path.ext})');
- // Vocals failed to load.
- #if !mac
- NotificationManager.instance.addNotification(
- {
- title: 'Failure',
- body: 'Failed to load vocal track (${path.file}.${path.ext}) for variation (${state.selectedVariation})',
- type: NotificationType.Error,
- expiryMs: Constants.NOTIFICATION_DISMISS_TIME
- });
- #end
+ state.error('Failed to Load Vocals', 'Failed to load vocal track (${path.file}.${path.ext}) for variation (${state.selectedVariation})');
#if FILE_DROP_SUPPORTED
vocalsEntryLabel.text = 'Drag and drop vocals for $charName here, or click to browse.';
@@ -999,6 +769,8 @@ class ChartEditorDialogHandler
}
};
+ dropHandler.handler = onDropFile;
+
vocalsEntry.onClick = function(_event) {
Dialogs.openBinaryFile('Open $charName Vocals', [
{label: 'Audio File (.ogg)', extension: 'ogg'}], function(selectedFile) {
@@ -1013,15 +785,8 @@ class ChartEditorDialogHandler
if (state.loadVocalsFromBytes(selectedFile.bytes, charKey, instId))
{
// Tell the user the load was successful.
- #if !mac
- NotificationManager.instance.addNotification(
- {
- title: 'Success',
- body: 'Loaded vocals for $charName (${selectedFile.name}), variation ${state.selectedVariation}',
- type: NotificationType.Success,
- expiryMs: Constants.NOTIFICATION_DISMISS_TIME
- });
- #end
+ state.success('Loaded Vocals', 'Loaded vocals for $charName (${selectedFile.name}), variation ${state.selectedVariation}');
+
#if FILE_DROP_SUPPORTED
vocalsEntryLabel.text = 'Voices for $charName (drag and drop, or click to browse)\nSelected file: ${selectedFile.name}';
#else
@@ -1034,15 +799,7 @@ class ChartEditorDialogHandler
{
trace('Failed to load vocal track (${selectedFile.fullPath})');
- #if !mac
- NotificationManager.instance.addNotification(
- {
- title: 'Failure',
- body: 'Failed to load vocal track (${selectedFile.name}) for variation (${state.selectedVariation})',
- type: NotificationType.Error,
- expiryMs: Constants.NOTIFICATION_DISMISS_TIME
- });
- #end
+ state.error('Failed to Load Vocals', 'Failed to load vocal track (${selectedFile.name}) for variation (${state.selectedVariation})');
#if FILE_DROP_SUPPORTED
vocalsEntryLabel.text = 'Drag and drop vocals for $charName here, or click to browse.';
@@ -1056,7 +813,7 @@ class ChartEditorDialogHandler
// onDropFile
#if FILE_DROP_SUPPORTED
- addDropHandler(vocalsEntry, onDropFile);
+ addDropHandler(dropHandler);
#end
dialogContainer.addComponent(vocalsEntry);
}
@@ -1118,7 +875,7 @@ class ChartEditorDialogHandler
}
// 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 = songDefaultChartDataEntry.findComponent('chartEntryLabel', Label);
if (songDefaultChartDataEntryLabel == null) throw 'Could not locate chartEntryLabel in Open Chart dialog';
#if FILE_DROP_SUPPORTED
@@ -1128,13 +885,17 @@ class ChartEditorDialogHandler
#end
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);
for (variation in variations)
{
// Build entries for -metadata-.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 = songVariationMetadataEntry.findComponent('chartEntryLabel', Label);
if (songVariationMetadataEntryLabel == null) throw 'Could not locate chartEntryLabel in Open Chart dialog';
#if FILE_DROP_SUPPORTED
@@ -1158,7 +919,7 @@ class ChartEditorDialogHandler
chartContainerB.addComponent(songVariationMetadataEntry);
// Build entries for -chart-.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 = songVariationChartDataEntry.findComponent('chartEntryLabel', Label);
if (songVariationChartDataEntryLabel == null) throw 'Could not locate chartEntryLabel in Open Chart dialog';
#if FILE_DROP_SUPPORTED
@@ -1193,15 +954,7 @@ class ChartEditorDialogHandler
if (songMetadataVersion == null)
{
// Tell the user the load was not successful.
- #if !mac
- NotificationManager.instance.addNotification(
- {
- title: 'Failure',
- body: 'Could not parse metadata file version (${path.file}.${path.ext})',
- type: NotificationType.Error,
- expiryMs: Constants.NOTIFICATION_DISMISS_TIME
- });
- #end
+ state.error('Failure', 'Could not parse metadata file version (${path.file}.${path.ext})');
return;
}
@@ -1211,30 +964,14 @@ class ChartEditorDialogHandler
if (songMetadataVariation == null)
{
// Tell the user the load was not successful.
- #if !mac
- NotificationManager.instance.addNotification(
- {
- title: 'Failure',
- body: 'Could not load metadata file (${path.file}.${path.ext})',
- type: NotificationType.Error,
- expiryMs: Constants.NOTIFICATION_DISMISS_TIME
- });
- #end
+ state.error('Failure', 'Could not load metadata file (${path.file}.${path.ext})');
return;
}
songMetadata.set(variation, songMetadataVariation);
// Tell the user the load was successful.
- #if !mac
- NotificationManager.instance.addNotification(
- {
- title: 'Success',
- body: 'Loaded metadata file (${path.file}.${path.ext})',
- type: NotificationType.Success,
- expiryMs: Constants.NOTIFICATION_DISMISS_TIME
- });
- #end
+ state.success('Loaded Metadata', 'Loaded metadata file (${path.file}.${path.ext})');
#if FILE_DROP_SUPPORTED
label.text = 'Metadata file (drag and drop, or click to browse)\nSelected file: ${path.file}.${path.ext}';
@@ -1258,15 +995,7 @@ class ChartEditorDialogHandler
if (songMetadataVersion == null)
{
// Tell the user the load was not successful.
- #if !mac
- NotificationManager.instance.addNotification(
- {
- title: 'Failure',
- body: 'Could not parse metadata file version (${selectedFile.name})',
- type: NotificationType.Error,
- expiryMs: Constants.NOTIFICATION_DISMISS_TIME
- });
- #end
+ state.error('Failure', 'Could not parse metadata file version (${selectedFile.name})');
return;
}
@@ -1278,15 +1007,7 @@ class ChartEditorDialogHandler
songMetadata.set(variation, songMetadataVariation);
// Tell the user the load was successful.
- #if !mac
- NotificationManager.instance.addNotification(
- {
- title: 'Success',
- body: 'Loaded metadata file (${selectedFile.name})',
- type: NotificationType.Success,
- expiryMs: Constants.NOTIFICATION_DISMISS_TIME
- });
- #end
+ state.success('Loaded Metadata', 'Loaded metadata file (${selectedFile.name})');
#if FILE_DROP_SUPPORTED
label.text = 'Metadata file (drag and drop, or click to browse)\nSelected file: ${selectedFile.name}';
@@ -1299,15 +1020,7 @@ class ChartEditorDialogHandler
else
{
// Tell the user the load was unsuccessful.
- #if !mac
- NotificationManager.instance.addNotification(
- {
- title: 'Failure',
- body: 'Failed to load metadata file (${selectedFile.name})',
- type: NotificationType.Error,
- expiryMs: Constants.NOTIFICATION_DISMISS_TIME
- });
- #end
+ state.error('Failure', 'Failed to load metadata file (${selectedFile.name})');
}
}
});
@@ -1323,15 +1036,7 @@ class ChartEditorDialogHandler
if (songChartDataVersion == null)
{
// Tell the user the load was not successful.
- #if !mac
- NotificationManager.instance.addNotification(
- {
- title: 'Failure',
- body: 'Could not parse chart data file version (${path.file}.${path.ext})',
- type: NotificationType.Error,
- expiryMs: Constants.NOTIFICATION_DISMISS_TIME
- });
- #end
+ state.error('Failure', 'Could not parse chart data file version (${path.file}.${path.ext})');
return;
}
@@ -1346,15 +1051,7 @@ class ChartEditorDialogHandler
state.noteDisplayDirty = true;
// Tell the user the load was successful.
- #if !mac
- NotificationManager.instance.addNotification(
- {
- title: 'Success',
- body: 'Loaded chart data file (${path.file}.${path.ext})',
- type: NotificationType.Success,
- expiryMs: Constants.NOTIFICATION_DISMISS_TIME
- });
- #end
+ state.success('Loaded Chart Data', 'Loaded chart data file (${path.file}.${path.ext})');
#if FILE_DROP_SUPPORTED
label.text = 'Chart data file (drag and drop, or click to browse)\nSelected file: ${path.file}.${path.ext}';
@@ -1365,15 +1062,7 @@ class ChartEditorDialogHandler
else
{
// Tell the user the load was unsuccessful.
- #if !mac
- NotificationManager.instance.addNotification(
- {
- title: 'Failure',
- body: 'Failed to load chart data file (${path.file}.${path.ext})',
- type: NotificationType.Error,
- expiryMs: Constants.NOTIFICATION_DISMISS_TIME
- });
- #end
+ state.error('Failure', 'Failed to load chart data file (${path.file}.${path.ext})');
}
};
@@ -1390,15 +1079,7 @@ class ChartEditorDialogHandler
if (songChartDataVersion == null)
{
// Tell the user the load was not successful.
- #if !mac
- NotificationManager.instance.addNotification(
- {
- title: 'Failure',
- body: 'Could not parse chart data file version (${selectedFile.name})',
- type: NotificationType.Error,
- expiryMs: Constants.NOTIFICATION_DISMISS_TIME
- });
- #end
+ state.error('Failure', 'Could not parse chart data file version (${selectedFile.name})');
return;
}
@@ -1413,15 +1094,7 @@ class ChartEditorDialogHandler
state.noteDisplayDirty = true;
// Tell the user the load was successful.
- #if !mac
- NotificationManager.instance.addNotification(
- {
- title: 'Success',
- body: 'Loaded chart data file (${selectedFile.name})',
- type: NotificationType.Success,
- expiryMs: Constants.NOTIFICATION_DISMISS_TIME
- });
- #end
+ state.success('Loaded Chart Data', 'Loaded chart data file (${selectedFile.name})');
#if FILE_DROP_SUPPORTED
label.text = 'Chart data file (drag and drop, or click to browse)\nSelected file: ${selectedFile.name}';
@@ -1433,7 +1106,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 = metadataEntry.findComponent('chartEntryLabel', Label);
if (metadataEntryLabel == null) throw 'Could not locate chartEntryLabel in Open Chart dialog';
@@ -1444,7 +1117,7 @@ class ChartEditorDialogHandler
#end
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(_event) {
metadataEntry.swapClass('upload-bg', 'upload-bg-hover');
Cursor.cursorMode = Pointer;
@@ -1518,15 +1191,7 @@ class ChartEditorDialogHandler
if (fnfLegacyData == null)
{
- #if !mac
- NotificationManager.instance.addNotification(
- {
- title: 'Failure',
- body: 'Failed to parse FNF chart file (${selectedFile.name})',
- type: NotificationType.Error,
- expiryMs: Constants.NOTIFICATION_DISMISS_TIME
- });
- #end
+ state.error('Failure', 'Failed to parse FNF chart file (${selectedFile.name})');
return;
}
@@ -1536,15 +1201,7 @@ class ChartEditorDialogHandler
state.loadSong([Constants.DEFAULT_VARIATION => songMetadata], [Constants.DEFAULT_VARIATION => songChartData]);
dialog.hideDialog(DialogButton.APPLY);
- #if !mac
- NotificationManager.instance.addNotification(
- {
- title: 'Success',
- body: 'Loaded chart file (${selectedFile.name})',
- type: NotificationType.Success,
- expiryMs: Constants.NOTIFICATION_DISMISS_TIME
- });
- #end
+ state.success('Success', 'Loaded chart file (${selectedFile.name})');
}
});
}
@@ -1559,18 +1216,10 @@ class ChartEditorDialogHandler
state.loadSong([Constants.DEFAULT_VARIATION => songMetadata], [Constants.DEFAULT_VARIATION => songChartData]);
dialog.hideDialog(DialogButton.APPLY);
- #if !mac
- NotificationManager.instance.addNotification(
- {
- title: 'Success',
- body: 'Loaded chart file (${path.file}.${path.ext})',
- type: NotificationType.Success,
- expiryMs: Constants.NOTIFICATION_DISMISS_TIME
- });
- #end
+ state.success('Success', 'Loaded chart file (${path.file}.${path.ext})');
};
- addDropHandler(importBox, onDropFile);
+ state.addDropHandler({component: importBox, handler: onDropFile});
return dialog;
}
@@ -1667,14 +1316,9 @@ class ChartEditorDialogHandler
state.songMetadata.set(pendingVariation.variation, pendingVariation);
state.difficultySelectDirty = true; // Force the Difficulty toolbox to update.
- #if !mac
- NotificationManager.instance.addNotification(
- {
- title: "Add Variation",
- body: 'Added new variation "${pendingVariation.variation}"',
- type: NotificationType.Success
- });
- #end
+
+ state.success('Add Variation', 'Added new variation "${pendingVariation.variation}"');
+
dialog.hideDialog(DialogButton.APPLY);
}
@@ -1731,14 +1375,8 @@ class ChartEditorDialogHandler
state.createDifficulty(dialogVariation.value.id, dialogDifficultyName.text.toLowerCase(), inputScrollSpeed.value ?? 1.0);
- #if !mac
- NotificationManager.instance.addNotification(
- {
- title: "Add Difficulty",
- body: 'Added new difficulty "${dialogDifficultyName.text.toLowerCase()}"',
- type: NotificationType.Success
- });
- #end
+ state.success('Add Difficulty', 'Added new difficulty "${dialogDifficultyName.text.toLowerCase()}"');
+
dialog.hideDialog(DialogButton.APPLY);
}
@@ -1752,7 +1390,7 @@ class ChartEditorDialogHandler
*/
static function openDialog(state:ChartEditorState, key:String, modal:Bool = true, closable:Bool = true):Null
{
- var dialog:Null = cast state.buildComponent(key);
+ var dialog:Null = cast RuntimeComponentBuilder.fromAsset(key);
if (dialog == null) return null;
dialog.destroyOnClose = true;
@@ -1769,14 +1407,10 @@ class ChartEditorDialogHandler
return dialog;
}
- // ==========
- // DROP HANDLERS
- // ==========
- static var dropHandlers:Array<
- {
- component:Component,
- handler:(String->Void)
- }> = [];
+ // ===============
+ // DROP HANDLERS
+ // ===============
+ static var dropHandlers:Array = [];
/**
* Add a callback for when a file is dropped on a component.
@@ -1784,32 +1418,33 @@ class ChartEditorDialogHandler
* On OS X you can’t 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
* 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 (!FlxG.stage.window.onDropFile.has(onDropFile)) FlxG.stage.window.onDropFile.add(onDropFile);
- dropHandlers.push(
- {
- component: component,
- handler: handler
- });
+ dropHandlers.push(dropTarget);
#else
trace('addDropHandler not implemented for this platform');
#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
- FlxG.stage.window.onDropFile.remove(handler);
+ dropHandlers.remove(dropTarget);
#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
dropHandlers = [];
@@ -1817,10 +1452,12 @@ class ChartEditorDialogHandler
#end
}
+ static final EPSILON:Float = 0.01;
+
static function onDropFile(path:String):Void
{
// 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)
{
if (handler.component.hitTest(FlxG.mouse.screenX, FlxG.mouse.screenY))
diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx
index 2f396d1a3..23c864a07 100644
--- a/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx
+++ b/source/funkin/ui/debug/charting/handlers/ChartEditorImportExportHandler.hx
@@ -1,11 +1,10 @@
package funkin.ui.debug.charting.handlers;
import funkin.util.VersionUtil;
-import haxe.ui.notifications.NotificationType;
import funkin.util.DateUtil;
import haxe.io.Path;
import funkin.util.SerializerUtil;
-import haxe.ui.notifications.NotificationManager;
+import funkin.util.SortUtil;
import funkin.util.FileUtil;
import funkin.util.FileUtil.FileWriteMode;
import haxe.io.Bytes;
@@ -22,6 +21,8 @@ import funkin.data.song.importer.ChartManifestData;
@:access(funkin.ui.debug.charting.ChartEditorState)
class ChartEditorImportExportHandler
{
+ public static final BACKUPS_PATH:String = './backups/';
+
/**
* Fetch's a song's existing chart and audio and loads it, replacing the current song.
*/
@@ -100,15 +101,7 @@ class ChartEditorImportExportHandler
state.refreshMetadataToolbox();
- #if !mac
- NotificationManager.instance.addNotification(
- {
- title: 'Success',
- body: 'Loaded song (${rawSongMetadata[0].songName})',
- type: NotificationType.Success,
- expiryMs: Constants.NOTIFICATION_DISMISS_TIME
- });
- #end
+ state.success('Success', 'Loaded song (${rawSongMetadata[0].songName})');
}
/**
@@ -318,11 +311,56 @@ class ChartEditorImportExportHandler
return warnings;
}
+ public static function getLatestBackupPath():Null
+ {
+ #if sys
+ var entries:Array = sys.FileSystem.readDirectory(BACKUPS_PATH);
+ entries.sort(SortUtil.alphabetically);
+
+ var latestBackupPath:Null = entries[(entries.length - 1)];
+
+ if (latestBackupPath == null) return null;
+ return haxe.io.Path.join([BACKUPS_PATH, latestBackupPath]);
+ #else
+ return null;
+ #end
+ }
+
+ public static function getLatestBackupDate():Null
+ {
+ #if sys
+ var latestBackupPath:Null = getLatestBackupPath();
+ if (latestBackupPath == null) return null;
+
+ var latestBackupName:String = haxe.io.Path.withoutDirectory(latestBackupPath);
+ latestBackupName = haxe.io.Path.withoutExtension(latestBackupName);
+
+ var parts = latestBackupName.split('-');
+
+ // var chart:String = parts[0];
+ // var editor:String = parts[1];
+ var year:Int = Std.parseInt(parts[2] ?? '0') ?? 0;
+ var month:Int = Std.parseInt(parts[3] ?? '1') ?? 1;
+ var day:Int = Std.parseInt(parts[4] ?? '0') ?? 0;
+ var hour:Int = Std.parseInt(parts[5] ?? '0') ?? 0;
+ var minute:Int = Std.parseInt(parts[6] ?? '0') ?? 0;
+ var second:Int = Std.parseInt(parts[7] ?? '0') ?? 0;
+
+ var date:Date = new Date(year, month - 1, day, hour, minute, second);
+ return date;
+ #else
+ return null;
+ #end
+ }
+
/**
* @param force Whether to export without prompting. `false` will prompt the user for a location.
* @param targetPath where to export if `force` is `true`. If `null`, will export to the `backups` folder.
+ * @param onSaveCb Callback for when the file is saved.
+ * @param onCancelCb Callback for when saving is cancelled.
*/
- public static function exportAllSongData(state:ChartEditorState, force:Bool = false, ?targetPath:String):Void
+ public static function exportAllSongData(state:ChartEditorState, force:Bool = false, targetPath:Null, ?onSaveCb:String->Void,
+ ?onCancelCb:Void->Void):Void
{
var zipEntries:Array = [];
@@ -369,13 +407,13 @@ class ChartEditorImportExportHandler
// Force writing to a generic path (autosave or crash recovery)
targetMode = Skip;
targetPath = Path.join([
- './backups/',
+ BACKUPS_PATH,
'chart-editor-${DateUtil.generateTimestamp()}.${Constants.EXT_CHART}'
]);
// We have to force write because the program will die before the save dialog is closed.
trace('Force exporting to $targetPath...');
FileUtil.saveFilesAsZIPToPath(zipEntries, targetPath, targetMode);
-
+ if (onSaveCb != null) onSaveCb(targetPath);
}
else
{
@@ -383,7 +421,7 @@ class ChartEditorImportExportHandler
trace('Force exporting to $targetPath...');
FileUtil.saveFilesAsZIPToPath(zipEntries, targetPath, targetMode);
state.saveDataDirty = false;
-
+ if (onSaveCb != null) onSaveCb(targetPath);
}
}
else
@@ -400,11 +438,13 @@ class ChartEditorImportExportHandler
trace('Saved to "${paths[0]}"');
state.currentWorkingFilePath = paths[0];
state.applyWindowTitle();
+ if (onSaveCb != null) onSaveCb(paths[0]);
}
};
var onCancel:Void->Void = function() {
trace('Export cancelled.');
+ if (onCancelCb != null) onCancelCb();
};
trace('Exporting to user-defined location...');
diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorNotificationHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorNotificationHandler.hx
new file mode 100644
index 000000000..796e70381
--- /dev/null
+++ b/source/funkin/ui/debug/charting/handlers/ChartEditorNotificationHandler.hx
@@ -0,0 +1,149 @@
+package funkin.ui.debug.charting.handlers;
+
+import haxe.ui.components.Button;
+import haxe.ui.containers.HBox;
+import haxe.ui.notifications.Notification;
+import haxe.ui.notifications.NotificationManager;
+import haxe.ui.notifications.NotificationType;
+
+class ChartEditorNotificationHandler
+{
+ public static function setupNotifications(state:ChartEditorState):Void
+ {
+ // Setup notifications.
+ @:privateAccess
+ NotificationManager.GUTTER_SIZE = 20;
+ }
+
+ /**
+ * Send a notification with a checkmark indicating success.
+ * @param state The current state of the chart editor.
+ */
+ public static function success(state:ChartEditorState, title:String, body:String):Notification
+ {
+ return sendNotification(title, body, NotificationType.Success);
+ }
+
+ /**
+ * Send a notification with a warning icon.
+ * @param state The current state of the chart editor.
+ */
+ public static function warning(state:ChartEditorState, title:String, body:String):Notification
+ {
+ return sendNotification(title, body, NotificationType.Warning);
+ }
+
+ /**
+ * Send a notification with a warning icon.
+ * @param state The current state of the chart editor.
+ */
+ public static inline function warn(state:ChartEditorState, title:String, body:String):Notification
+ {
+ return warning(state, title, body);
+ }
+
+ /**
+ * Send a notification with a cross indicating an error.
+ * @param state The current state of the chart editor.
+ */
+ public static function error(state:ChartEditorState, title:String, body:String):Notification
+ {
+ return sendNotification(title, body, NotificationType.Error);
+ }
+
+ /**
+ * Send a notification with a cross indicating failure.
+ * @param state The current state of the chart editor.
+ */
+ public static inline function failure(state:ChartEditorState, title:String, body:String):Notification
+ {
+ return error(state, title, body);
+ }
+
+ /**
+ * Send a notification with an info icon.
+ * @param state The current state of the chart editor.
+ */
+ public static function info(state:ChartEditorState, title:String, body:String):Notification
+ {
+ return sendNotification(title, body, NotificationType.Info);
+ }
+
+ /**
+ * Send a notification with an info icon and one or more actions.
+ * @param state The current state of the chart editor.
+ * @param title The title of the notification.
+ * @param body The body of the notification.
+ * @param actions The actions to add to the notification.
+ * @return The notification that was sent.
+ */
+ public static function infoWithActions(state:ChartEditorState, title:String, body:String, actions:Array):Notification
+ {
+ return sendNotification(title, body, NotificationType.Info, actions);
+ }
+
+ /**
+ * Clear all active notifications.
+ * @param state The current state of the chart editor.
+ */
+ public static function clearNotifications(state:ChartEditorState):Void
+ {
+ NotificationManager.instance.clearNotifications();
+ }
+
+ /**
+ * Clear a specific notification.
+ * @param state The current state of the chart editor.
+ * @param notif The notification to clear.
+ */
+ public static function clearNotification(state:ChartEditorState, notif:Notification):Void
+ {
+ NotificationManager.instance.removeNotification(notif);
+ }
+
+ static function sendNotification(title:String, body:String, ?type:NotificationType, ?actions:Array):Notification
+ {
+ #if !mac
+ var actionNames:Array = actions == null ? [] : actions.map(action -> action.text);
+
+ var notif = NotificationManager.instance.addNotification(
+ {
+ title: title,
+ body: body,
+ type: type ?? NotificationType.Default,
+ expiryMs: Constants.NOTIFICATION_DISMISS_TIME,
+ actions: actionNames
+ });
+
+ if (actionNames.length > 0)
+ {
+ // TODO: Tell Ian that this is REALLY dumb.
+ var actionsContainer:HBox = notif.findComponent('actionsContainer', HBox);
+ actionsContainer.walkComponents(function(component) {
+ if (Std.isOfType(component, Button))
+ {
+ var button:Button = cast component;
+ var action:Null = actions.find(action -> action.text == button.text);
+ if (action != null && action.callback != null)
+ {
+ button.onClick = function(_) {
+ action.callback();
+ };
+ }
+ }
+ return true; // Continue walking.
+ });
+ }
+
+ return notif;
+ #else
+ trace('WARNING: Notifications are not supported on Mac OS.');
+ #end
+ }
+}
+
+typedef NotificationAction =
+{
+ text:String,
+ callback:Void->Void
+}
diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorShortcutHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorShortcutHandler.hx
index 016a181d3..f7105d2f7 100644
--- a/source/funkin/ui/debug/charting/handlers/ChartEditorShortcutHandler.hx
+++ b/source/funkin/ui/debug/charting/handlers/ChartEditorShortcutHandler.hx
@@ -7,28 +7,28 @@ class ChartEditorShortcutHandler
{
public static function applyPlatformShortcutText(state:ChartEditorState):Void
{
- state.setComponentShortcutText('menubarItemNewChart', ctrlOrCmd('N'));
- state.setComponentShortcutText('menubarItemOpenChart', ctrlOrCmd('O'));
- state.setComponentShortcutText('menubarItemSaveChartAs', ctrlOrCmd(shift('S')));
- state.setComponentShortcutText('menubarItemExit', ctrlOrCmd('Q'));
+ state.menubarItemNewChart.shortcutText = ctrlOrCmd('N');
+ state.menubarItemOpenChart.shortcutText = ctrlOrCmd('O');
+ state.menubarItemSaveChartAs.shortcutText = ctrlOrCmd(shift('S'));
+ state.menubarItemExit.shortcutText = ctrlOrCmd('Q');
- state.setComponentShortcutText('menubarItemUndo', ctrlOrCmd('Z'));
- state.setComponentShortcutText('menubarItemRedo', ctrlOrCmd('Y'));
- state.setComponentShortcutText('menubarItemCut', ctrlOrCmd('X'));
- state.setComponentShortcutText('menubarItemCopy', ctrlOrCmd('C'));
- state.setComponentShortcutText('menubarItemPaste', ctrlOrCmd('V'));
+ state.menubarItemUndo.shortcutText = ctrlOrCmd('Z');
+ state.menubarItemRedo.shortcutText = ctrlOrCmd('Y');
+ state.menubarItemCut.shortcutText = ctrlOrCmd('X');
+ state.menubarItemCopy.shortcutText = ctrlOrCmd('C');
+ state.menubarItemPaste.shortcutText = ctrlOrCmd('V');
- state.setComponentShortcutText('menubarItemSelectAll', ctrlOrCmd('A'));
- state.setComponentShortcutText('menubarItemSelectInverse', ctrlOrCmd('I'));
- state.setComponentShortcutText('menubarItemSelectNone', ctrlOrCmd('D'));
- state.setComponentShortcutText('menubarItemSelectBeforeCursor', shift('Home'));
- state.setComponentShortcutText('menubarItemSelectAfterCursor', shift('End'));
+ state.menubarItemSelectAll.shortcutText = ctrlOrCmd('A');
+ state.menubarItemSelectInverse.shortcutText = ctrlOrCmd('I');
+ state.menubarItemSelectNone.shortcutText = ctrlOrCmd('D');
+ state.menubarItemSelectBeforeCursor.shortcutText = shift('Home');
+ state.menubarItemSelectAfterCursor.shortcutText = shift('End');
- state.setComponentShortcutText('menubarItemDifficultyDown', ctrlOrCmd('←'));
- state.setComponentShortcutText('menubarItemDifficultyUp', ctrlOrCmd('→'));
+ state.menubarItemDifficultyDown.shortcutText = ctrlOrCmd('←');
+ state.menubarItemDifficultyUp.shortcutText = ctrlOrCmd('→');
- state.setComponentShortcutText('menubarItemPlaytestFull', 'Enter');
- state.setComponentShortcutText('menubarItemPlaytestMinimal', shift('Enter'));
+ state.menubarItemPlaytestFull.shortcutText = 'Enter';
+ state.menubarItemPlaytestMinimal.shortcutText = shift('Enter');
}
/**
diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx
index a43dd2989..af7cd774a 100644
--- a/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx
+++ b/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx
@@ -18,6 +18,7 @@ import funkin.play.event.SongEvent;
import funkin.play.song.SongSerializer;
import funkin.play.stage.StageData;
import funkin.play.stage.StageData.StageDataParser;
+import haxe.ui.RuntimeComponentBuilder;
import funkin.ui.debug.charting.util.ChartEditorDropdowns;
import funkin.ui.haxeui.components.CharacterPlayer;
import funkin.util.FileUtil;
@@ -217,7 +218,7 @@ class ChartEditorToolboxHandler
static function buildToolboxNoteDataLayout(state:ChartEditorState):Null
{
- 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;
@@ -226,7 +227,7 @@ class ChartEditorToolboxHandler
toolbox.y = 100;
toolbox.onDialogClosed = function(event:DialogEvent) {
- state.setUICheckboxSelected('menubarItemToggleToolboxNotes', false);
+ state.menubarItemToggleToolboxNotes.selected = false;
}
var toolboxNotesNoteKind:Null = toolbox.findComponent('toolboxNotesNoteKind', DropDown);
@@ -269,7 +270,7 @@ class ChartEditorToolboxHandler
static function buildToolboxEventDataLayout(state:ChartEditorState):Null
{
- 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;
@@ -278,7 +279,7 @@ class ChartEditorToolboxHandler
toolbox.y = 150;
toolbox.onDialogClosed = function(event:DialogEvent) {
- state.setUICheckboxSelected('menubarItemToggleToolboxEvents', false);
+ state.menubarItemToggleToolboxEvents.selected = false;
}
var toolboxEventsEventKind:Null = toolbox.findComponent('toolboxEventsEventKind', DropDown);
@@ -418,7 +419,7 @@ class ChartEditorToolboxHandler
static function buildToolboxDifficultyLayout(state:ChartEditorState):Null
{
- 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;
@@ -427,7 +428,7 @@ class ChartEditorToolboxHandler
toolbox.y = 200;
toolbox.onDialogClosed = function(event:UIEvent) {
- state.setUICheckboxSelected('menubarItemToggleToolboxDifficulty', false);
+ state.menubarItemToggleToolboxDifficulty.selected = false;
}
var difficultyToolboxAddVariation:Null = toolbox.findComponent('difficultyToolboxAddVariation', Button);
@@ -505,7 +506,7 @@ class ChartEditorToolboxHandler
static function buildToolboxMetadataLayout(state:ChartEditorState):Null
{
- 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;
@@ -514,7 +515,7 @@ class ChartEditorToolboxHandler
toolbox.y = 250;
toolbox.onDialogClosed = function(event:UIEvent) {
- state.setUICheckboxSelected('menubarItemToggleToolboxMetadata', false);
+ state.menubarItemToggleToolboxMetadata.selected = false;
}
var inputSongName:Null = toolbox.findComponent('inputSongName', TextField);
@@ -667,7 +668,7 @@ class ChartEditorToolboxHandler
static function buildToolboxPlayerPreviewLayout(state:ChartEditorState):Null
{
- 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;
@@ -676,7 +677,7 @@ class ChartEditorToolboxHandler
toolbox.y = 350;
toolbox.onDialogClosed = function(event:DialogEvent) {
- state.setUICheckboxSelected('menubarItemToggleToolboxPlayerPreview', false);
+ state.menubarItemToggleToolboxPlayerPreview.selected = false;
}
var charPlayer:Null = toolbox.findComponent('charPlayer');
@@ -696,7 +697,7 @@ class ChartEditorToolboxHandler
static function buildToolboxOpponentPreviewLayout(state:ChartEditorState):Null
{
- 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;
@@ -705,7 +706,7 @@ class ChartEditorToolboxHandler
toolbox.y = 350;
toolbox.onDialogClosed = (event:DialogEvent) -> {
- state.setUICheckboxSelected('menubarItemToggleToolboxOpponentPreview', false);
+ state.menubarItemToggleToolboxOpponentPreview.selected = false;
}
var charPlayer:Null = toolbox.findComponent('charPlayer');
diff --git a/source/funkin/ui/debug/charting/import.hx b/source/funkin/ui/debug/charting/import.hx
index 56660c37a..933eaa3a5 100644
--- a/source/funkin/ui/debug/charting/import.hx
+++ b/source/funkin/ui/debug/charting/import.hx
@@ -5,6 +5,8 @@ package funkin.ui.debug.charting;
using funkin.ui.debug.charting.handlers.ChartEditorAudioHandler;
using funkin.ui.debug.charting.handlers.ChartEditorDialogHandler;
using funkin.ui.debug.charting.handlers.ChartEditorImportExportHandler;
+using funkin.ui.debug.charting.handlers.ChartEditorNotificationHandler;
+using funkin.ui.debug.charting.handlers.ChartEditorShortcutHandler;
using funkin.ui.debug.charting.handlers.ChartEditorThemeHandler;
using funkin.ui.debug.charting.handlers.ChartEditorToolboxHandler;
#end
diff --git a/source/funkin/util/CLIUtil.hx b/source/funkin/util/CLIUtil.hx
index 0ca707c34..ecabaff06 100644
--- a/source/funkin/util/CLIUtil.hx
+++ b/source/funkin/util/CLIUtil.hx
@@ -96,7 +96,7 @@ class CLIUtil
static function printUsage():Void
{
- trace('Usage: Funkin.exe [--chart ]');
+ trace('Usage: Funkin.exe [--chart ] [--help] [--version]');
}
static function buildDefaultParams():CLIParams
diff --git a/source/funkin/util/DateUtil.hx b/source/funkin/util/DateUtil.hx
index 2d08fd48b..f899ffeb2 100644
--- a/source/funkin/util/DateUtil.hx
+++ b/source/funkin/util/DateUtil.hx
@@ -12,4 +12,11 @@ class DateUtil
return
'${date.getFullYear()}-${Std.string(date.getMonth() + 1).lpad('0', 2)}-${Std.string(date.getDate()).lpad('0', 2)}-${Std.string(date.getHours()).lpad('0', 2)}-${Std.string(date.getMinutes()).lpad('0', 2)}-${Std.string(date.getSeconds()).lpad('0', 2)}';
}
+
+ public static function generateCleanTimestamp(?date:Date = null):String
+ {
+ if (date == null) date = Date.now();
+
+ return '${DateTools.format(date, '%B %d, %Y')} at ${DateTools.format(date, '%I:%M %p')}';
+ }
}
diff --git a/source/funkin/util/FileUtil.hx b/source/funkin/util/FileUtil.hx
index 8c41cc363..612737680 100644
--- a/source/funkin/util/FileUtil.hx
+++ b/source/funkin/util/FileUtil.hx
@@ -8,6 +8,10 @@ import haxe.io.Path;
import openfl.net.FileReference;
import openfl.events.Event;
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.
@@ -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_ZIP:FileFilter = new FileFilter("ZIP Archive (.zip)", "*.zip");
+ public static final FILE_EXTENSION_INFO_FNFC:FileDialogExtensionInfo =
+ {
+ extension: 'fnfc',
+ label: 'Friday Night Funkin\' Chart',
+ };
+ public static final FILE_EXTENSION_INFO_ZIP:FileDialogExtensionInfo =
+ {
+ extension: 'zip',
+ label: 'ZIP Archive',
+ };
+
/**
- * 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.
+ * 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 binary, not String.
*
- * @param typeFilter Filters what kinds of files can be selected.
- * @return Whether the file dialog was opened successfully.
+ * @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 browseForFile(?typeFilter:Array, ?onSelect:String->Void, ?onCancel:Void->Void, ?defaultPath:String,
- ?dialogTitle:String):Bool
+ public static function browseForBinaryFile(dialogTitle:String, ?typeFilter:Array, ?onSelect:SelectedFileInfo->Void,
+ ?onCancel:Void->Void)
{
- #if desktop
- var filter:String = convertTypeFilter(typeFilter);
+ var onComplete = function(button, selectedFiles) {
+ if (button == DialogButton.OK && selectedFiles.length > 0)
+ {
+ onSelect(selectedFiles[0]);
+ }
+ else
+ {
+ onCancel();
+ }
+ };
- var fileDialog:FileDialog = new FileDialog();
- if (onSelect != null) fileDialog.onSelect.add(onSelect);
- if (onCancel != null) fileDialog.onCancel.add(onCancel);
+ Dialogs.openFile(onComplete,
+ {
+ readContents: true,
+ readAsBinary: true, // Binary
+ multiple: false,
+ extensions: typeFilter ?? [],
+ title: dialogTitle,
+ });
+ }
- fileDialog.browse(OPEN, filter, defaultPath, dialogTitle);
- return true;
- #elseif html5
- onCancel();
- return false;
- #else
- onCancel();
- return false;
- #end
+ /**
+ * 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, ?onSelect:SelectedFileInfo->Void,
+ ?onCancel:Void->Void)
+ {
+ var onComplete = function(button, selectedFiles) {
+ if (button == DialogButton.OK && selectedFiles.length > 0)
+ {
+ onSelect(selectedFiles[0]);
+ }
+ else
+ {
+ onCancel();
+ }
+ };
+
+ Dialogs.openFile(onComplete,
+ {
+ readContents: true,
+ readAsBinary: false, // Text
+ multiple: false,
+ extensions: typeFilter ?? [],
+ title: dialogTitle,
+ });
}
/**
@@ -57,11 +110,9 @@ class FileUtil
{
#if desktop
var filter:String = convertTypeFilter(typeFilter);
-
var fileDialog:FileDialog = new FileDialog();
if (onSelect != null) fileDialog.onSelect.add(onSelect);
if (onCancel != null) fileDialog.onCancel.add(onCancel);
-
fileDialog.browse(OPEN_DIRECTORY, filter, defaultPath, dialogTitle);
return true;
#elseif html5
@@ -84,11 +135,9 @@ class FileUtil
{
#if desktop
var filter:String = convertTypeFilter(typeFilter);
-
var fileDialog:FileDialog = new FileDialog();
if (onSelect != null) fileDialog.onSelectMultiple.add(onSelect);
if (onCancel != null) fileDialog.onCancel.add(onCancel);
-
fileDialog.browse(OPEN_MULTIPLE, filter, defaultPath, dialogTitle);
return true;
#elseif html5
@@ -112,11 +161,9 @@ class FileUtil
{
#if desktop
var filter:String = convertTypeFilter(typeFilter);
-
var fileDialog:FileDialog = new FileDialog();
if (onSelect != null) fileDialog.onSelect.add(onSelect);
if (onCancel != null) fileDialog.onCancel.add(onCancel);
-
fileDialog.browse(SAVE, filter, defaultPath, dialogTitle);
return true;
#elseif html5
@@ -128,48 +175,6 @@ class FileUtil
#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, ?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.
* Works great on desktop and HTML5.
@@ -181,20 +186,16 @@ class FileUtil
{
#if desktop
var filter:String = convertTypeFilter(typeFilter);
-
var fileDialog:FileDialog = new FileDialog();
if (onSave != null) fileDialog.onSave.add(onSave);
if (onCancel != null) fileDialog.onCancel.add(onCancel);
-
fileDialog.save(data, filter, defaultFileName, dialogTitle);
return true;
#elseif html5
var filter:String = defaultFileName != null ? Path.extension(defaultFileName) : null;
-
var fileDialog:FileDialog = new FileDialog();
if (onSave != null) fileDialog.onSave.add(onSave);
if (onCancel != null) fileDialog.onCancel.add(onCancel);
-
fileDialog.save(data, filter, defaultFileName, dialogTitle);
return true;
#else
@@ -241,17 +242,14 @@ class FileUtil
}
onSaveAll(paths);
}
-
trace('Browsing for directory to save individual files to...');
#if mac
defaultPath = null;
#end
browseForDirectory(null, onSelectDir, onCancel, defaultPath, 'Choose directory to save all files to...');
-
return true;
#elseif html5
saveFilesAsZIP(resources, onSaveAll, onCancel, defaultPath, force);
-
return true;
#else
onCancel();
@@ -266,15 +264,12 @@ class FileUtil
{
// Create a ZIP file.
var zipBytes:Bytes = createZIPFromEntries(resources);
-
var onSave:String->Void = function(path:String) {
trace('Saved ${resources.length} files to ZIP at "$path".');
if (onSave != null) onSave([path]);
};
-
// Prompt the user to save the ZIP file.
saveFile(zipBytes, [FILE_FILTER_ZIP], onSave, onCancel, defaultPath, 'Save files as ZIP...');
-
return true;
}
@@ -286,15 +281,12 @@ class FileUtil
{
// Create a ZIP file.
var zipBytes:Bytes = createZIPFromEntries(resources);
-
var onSave:String->Void = function(path:String) {
trace('Saved FNF file to "$path"');
if (onSave != null) onSave([path]);
};
-
// Prompt the user to save the ZIP file.
saveFile(zipBytes, [FILE_FILTER_FNFC], onSave, onCancel, defaultPath, 'Save chart as FNFC...');
-
return true;
}
@@ -309,10 +301,8 @@ class FileUtil
#if desktop
// Create a ZIP file.
var zipBytes:Bytes = createZIPFromEntries(resources);
-
// Write the ZIP.
writeBytesToPath(path, zipBytes, mode);
-
return true;
#else
return false;
@@ -371,7 +361,6 @@ class FileUtil
public static function browseFileReference(callback:FileReference->Void)
{
var file = new FileReference();
-
file.addEventListener(Event.SELECT, function(e) {
var selectedFileRef:FileReference = e.target;
trace('Selected file: ' + selectedFileRef.name);
@@ -382,7 +371,6 @@ class FileUtil
});
selectedFileRef.load();
});
-
file.browse();
}
@@ -439,7 +427,6 @@ class FileUtil
{
#if sys
createDirIfNotExists(Path.directory(path));
-
switch (mode)
{
case Force:
@@ -482,7 +469,6 @@ class FileUtil
{
#if sys
createDirIfNotExists(Path.directory(path));
-
switch (mode)
{
case Force:
@@ -557,19 +543,15 @@ class FileUtil
public static function getTempDir():String
{
if (tempDir != null) return tempDir;
-
#if sys
#if windows
var path:String = null;
-
for (envName in TEMP_ENV_VARS)
{
path = Sys.getEnv(envName);
-
if (path == '') path = null;
if (path != null) break;
}
-
tempDir = Path.join([path, 'funkin/']);
return tempDir;
#else
@@ -590,10 +572,8 @@ class FileUtil
public static function createZIPFromEntries(entries:Array):Bytes
{
var o:haxe.io.BytesOutput = new haxe.io.BytesOutput();
-
var zipWriter:haxe.zip.Writer = new haxe.zip.Writer(o);
zipWriter.write(entries.list());
-
return o.getBytes();
}
@@ -601,10 +581,8 @@ class FileUtil
{
trace('TEST: ' + input.length);
trace(input.sub(0, 30).toHex());
-
var bytesInput = new haxe.io.BytesInput(input);
var zippedEntries = haxe.zip.Reader.readZip(bytesInput);
-
var results:Array = [];
for (entry in zippedEntries)
{
@@ -637,7 +615,6 @@ class FileUtil
public static function makeZIPEntry(name:String, content:String):Entry
{
var data:Bytes = haxe.io.Bytes.ofString(content, UTF8);
-
return makeZIPEntryFromBytes(name, data);
}
@@ -653,12 +630,9 @@ class FileUtil
return {
fileName: name,
fileSize: data.length,
-
data: data,
dataSize: data.length,
-
compressed: false,
-
fileTime: Date.now(),
crc32: null,
extraFields: null,
@@ -677,7 +651,6 @@ class FileUtil
}
filter = filters.join(';');
}
-
return filter;
}
}
diff --git a/source/funkin/util/WindowUtil.hx b/source/funkin/util/WindowUtil.hx
index 0e9b76bc2..9f623c39d 100644
--- a/source/funkin/util/WindowUtil.hx
+++ b/source/funkin/util/WindowUtil.hx
@@ -2,6 +2,8 @@ package funkin.util;
import flixel.util.FlxSignal.FlxTypedSignal;
+using StringTools;
+
/**
* Utilities for operating on the current window, such as changing the title.
*/
@@ -18,7 +20,7 @@ class WindowUtil
* Runs platform-specific code to open a URL in a web browser.
* @param targetUrl The URL to open.
*/
- public static function openURL(targetUrl:String)
+ public static function openURL(targetUrl:String):Void
{
#if CAN_OPEN_LINKS
#if linux
@@ -32,6 +34,45 @@ class WindowUtil
#end
}
+ /**
+ * Runs platform-specific code to open a path in the file explorer.
+ * @param targetPath The path to open.
+ */
+ public static function openFolder(targetPath:String):Void
+ {
+ #if CAN_OPEN_LINKS
+ #if windows
+ Sys.command('explorer', [targetPath.replace("/", "\\")]);
+ #elseif mac
+ Sys.command('open', [targetPath]);
+ #elseif linux
+ Sys.command('open', [targetPath]);
+ #end
+ #else
+ throw 'Cannot open URLs on this platform.';
+ #end
+ }
+
+ /**
+ * Runs platform-specific code to open a file explorer and select a specific file.
+ * @param targetPath The path of the file to select.
+ */
+ public static function openSelectFile(targetPath:String):Void
+ {
+ #if CAN_OPEN_LINKS
+ #if windows
+ Sys.command('explorer', ["/select," + targetPath.replace("/", "\\")]);
+ #elseif mac
+ Sys.command('open', ["-R", targetPath]);
+ #elseif linux
+ // TODO: unsure of the linux equivalent to opening a folder and then "selecting" a file.
+ Sys.command('open', [targetPath]);
+ #end
+ #else
+ throw 'Cannot open URLs on this platform.';
+ #end
+ }
+
/**
* Dispatched when the game window is closed.
*/
diff --git a/source/funkin/util/logging/CrashHandler.hx b/source/funkin/util/logging/CrashHandler.hx
index 9d1d3f1b9..a21732048 100644
--- a/source/funkin/util/logging/CrashHandler.hx
+++ b/source/funkin/util/logging/CrashHandler.hx
@@ -2,6 +2,7 @@ package funkin.util.logging;
import openfl.Lib;
import openfl.events.UncaughtErrorEvent;
+import flixel.util.FlxSignal.FlxTypedSignal;
/**
* A custom crash handler that writes to a log file and displays a message box.
@@ -9,7 +10,20 @@ import openfl.events.UncaughtErrorEvent;
@:nullSafety
class CrashHandler
{
- static final LOG_FOLDER = 'logs';
+ public static final LOG_FOLDER = 'logs';
+
+ /**
+ * Called before exiting the game when a standard error occurs, like a thrown exception.
+ * @param message The error message.
+ */
+ public static var errorSignal(default, null):FlxTypedSignalVoid> = new FlxTypedSignalVoid>();
+
+ /**
+ * Called before exiting the game when a critical error occurs, like a stack overflow or null object reference.
+ * CAREFUL: The game may be in an unstable state when this is called.
+ * @param message The error message.
+ */
+ public static var criticalErrorSignal(default, null):FlxTypedSignalVoid> = new FlxTypedSignalVoid>();
/**
* Initializes
@@ -34,6 +48,8 @@ class CrashHandler
{
try
{
+ errorSignal.dispatch(generateErrorMessage(error));
+
#if sys
logError(error);
#end
@@ -50,6 +66,8 @@ class CrashHandler
{
try
{
+ criticalErrorSignal.dispatch(message);
+
#if sys
logErrorMessage(message, true);
#end
diff --git a/source/haxe/ui/backend/flixel/UIStateBase.hx b/source/haxe/ui/backend/flixel/UIStateBase.hx
new file mode 100644
index 000000000..9f1a53722
--- /dev/null
+++ b/source/haxe/ui/backend/flixel/UIStateBase.hx
@@ -0,0 +1,3 @@
+package haxe.ui.backend.flixel;
+
+typedef UIStateBase = funkin.ui.MusicBeatState;
diff --git a/source/haxe/ui/backend/flixel/UISubStateBase.hx b/source/haxe/ui/backend/flixel/UISubStateBase.hx
new file mode 100644
index 000000000..306c9b633
--- /dev/null
+++ b/source/haxe/ui/backend/flixel/UISubStateBase.hx
@@ -0,0 +1,3 @@
+package haxe.ui.backend.flixel;
+
+typedef UISubStateBase = funkin.ui.MusicBeatSubState;