diff --git a/assets b/assets index f715fd22c..c6aef3e3e 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit f715fd22cd51e71b017fae796cfe564b76dec20e +Subproject commit c6aef3e3e6abc573c75dc5dbc90059ccdb6da83c 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/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index f5fb3aeb5..983840b75 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; @@ -47,7 +48,6 @@ 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; @@ -98,8 +98,6 @@ import haxe.ui.core.Screen; import haxe.ui.events.DragEvent; 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; @@ -779,6 +777,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. @@ -1498,7 +1508,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 +1521,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,7 +1551,11 @@ class ChartEditorState extends HaxeUIState } else { - ChartEditorDialogHandler.openWelcomeDialog(this, false); + var welcomeDialog = this.openWelcomeDialog(false); + if (shouldShowBackupAvailableDialog) + { + this.openBackupAvailableDialog(welcomeDialog); + } } } @@ -1583,7 +1592,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 +1600,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 +1636,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 @@ -1992,9 +1995,7 @@ class ChartEditorState extends HaxeUIState if (menubar == null) throw "Could not find menubar!"; if (!Preferences.debugDisplay) menubar.paddingLeft = null; - // Setup notifications. - @:privateAccess - NotificationManager.GUTTER_SIZE = 20; + this.setupNotifications(); } /** @@ -2022,21 +2023,21 @@ class ChartEditorState extends HaxeUIState // Add functionality to the menu items. - addUIClickListener('menubarItemNewChart', _ -> ChartEditorDialogHandler.openWelcomeDialog(this, true)); - addUIClickListener('menubarItemOpenChart', _ -> ChartEditorDialogHandler.openBrowseFNFC(this, true)); + addUIClickListener('menubarItemNewChart', _ -> this.openWelcomeDialog(true)); + addUIClickListener('menubarItemOpenChart', _ -> this.openBrowseFNFC(true)); addUIClickListener('menubarItemSaveChart', _ -> { 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('menubarItemSaveChartAs', _ -> this.exportAllSongData(false, null)); + addUIClickListener('menubarItemLoadInst', _ -> this.openUploadInstDialog(true)); + addUIClickListener('menubarItemImportChart', _ -> this.openImportChartDialog('legacy', true)); addUIClickListener('menubarItemExit', _ -> quitChartEditor()); addUIClickListener('menubarItemUndo', _ -> undoLastCommand()); @@ -2130,6 +2131,16 @@ class ChartEditorState extends HaxeUIState addUIClickListener('menubarItemAbout', _ -> this.openAboutDialog()); addUIClickListener('menubarItemWelcomeDialog', _ -> this.openWelcomeDialog(true)); + #if sys + addUIClickListener('menubarItemGoToBackupsFolder', _ -> this.openBackupsFolder()); + #else + // Disable the menu item if we're not on a desktop platform. + var menubarItemGoToBackupsFolder = findComponent('menubarItemGoToBackupsFolder', MenuItem); + if (menubarItemGoToBackupsFolder != null) menubarItemGoToBackupsFolder.disabled = true; + + menubarItemGoToBackupsFolder.disabled = true; + #end + addUIClickListener('menubarItemUserGuide', _ -> this.openUserGuideDialog()); addUIChangeListener('menubarItemDownscroll', event -> isViewDownscroll = event.value); @@ -2232,7 +2243,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; } @@ -2241,10 +2258,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 @@ -2252,24 +2271,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 @@ -3428,6 +3495,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 { @@ -3972,19 +4044,21 @@ class ChartEditorState extends HaxeUIState 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) { @@ -4145,6 +4219,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 @@ -4481,15 +4563,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()}'); } /** @@ -4754,9 +4828,6 @@ class ChartEditorState extends HaxeUIState */ // ==================== - /** - * Dismiss any existing HaxeUI notifications, if there are any. - */ function handleNotePreview():Void { if (notePreviewDirty && notePreview != null) @@ -4868,9 +4939,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')); } } } @@ -4957,14 +5028,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/handlers/ChartEditorDialogHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx index c9229009c..23895be68 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx @@ -20,6 +20,8 @@ import funkin.util.FileUtil; import funkin.util.SerializerUtil; import funkin.util.SortUtil; import funkin.util.VersionUtil; +import funkin.util.DateUtil; +import funkin.util.WindowUtil; import haxe.io.Path; import haxe.ui.components.Button; import haxe.ui.components.DropDown; @@ -36,8 +38,6 @@ import haxe.ui.containers.Form; import haxe.ui.containers.VBox; import haxe.ui.core.Component; import haxe.ui.events.UIEvent; -import haxe.ui.notifications.NotificationManager; -import haxe.ui.notifications.NotificationType; import thx.semver.Version; using Lambda; @@ -63,6 +63,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. @@ -116,27 +117,20 @@ class ChartEditorDialogHandler 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 + if (result.length == 0) + { + // No warnings. + state.success('Loaded Chart', 'Loaded chart (${chartPath.toString()})'); + } + else + { + // One or more warnings. + state.warning('Loaded Chart', 'Loaded chart (${chartPath.toString()})\n${result.join("\n")}'); + } } else { - #if !mac - NotificationManager.instance.addNotification( - { - title: 'Failure', - body: 'Failed to load chart (${chartPath.toString()})', - type: NotificationType.Error, - expiryMs: Constants.NOTIFICATION_DISMISS_TIME - }); - #end + state.error('Failed to Load Chart', 'Failed to load chart (${chartPath.toString()})'); } } @@ -256,6 +250,89 @@ class ChartEditorDialogHandler return dialog; } + /** + * [Description] + * @param state + * @return Null + */ + public static function openBackupAvailableDialog(state:ChartEditorState, welcomeDialog:Null):Null + { + 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) + { + // User loaded the backup! Close the welcome dialog behind this. + if (welcomeDialog != null) welcomeDialog.hideDialog(DialogButton.CANCEL); + } + else + { + // User cancelled the dialog, don't close the welcome dialog so we aren't in a broken state. + } + }; + + state.isHaxeUIDialogOpen = true; + + var backupTimeLabel:Null