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 e1f340770..404bf6f67 100644 --- a/source/funkin/ui/debug/DebugMenuSubState.hx +++ b/source/funkin/ui/debug/DebugMenuSubState.hx @@ -8,6 +8,7 @@ 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 { @@ -84,6 +85,8 @@ class DebugMenuSubState extends MusicBeatSubState function openChartEditor() { + FlxTransitionableState.skipNextTransIn = true; + FlxG.switchState(new ChartEditorState()); } diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 02f66e6d6..72cd2d0d6 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -1,5 +1,7 @@ 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; import flixel.addons.transition.FlxTransitionableState; @@ -39,6 +41,14 @@ import funkin.play.components.HealthIcon; import funkin.play.notes.NoteSprite; import funkin.play.PlayState; import funkin.play.song.Song; +import funkin.data.song.SongData.SongChartData; +import funkin.data.song.SongRegistry; +import funkin.data.song.SongData.SongEventData; +import funkin.data.song.SongData.SongMetadata; +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.play.stage.StageData; import funkin.save.Save; import funkin.ui.debug.charting.commands.AddEventsCommand; @@ -99,8 +109,6 @@ 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; using Lambda; @@ -777,6 +785,18 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState 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. @@ -1563,7 +1583,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState buildAdditionalUI(); populateOpenRecentMenu(); - ChartEditorShortcutHandler.applyPlatformShortcutText(this); + this.applyPlatformShortcutText(); // Setup the onClick listeners for the UI after it's been created. setupUIListeners(); @@ -1576,33 +1596,28 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState 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) @@ -1611,7 +1626,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState } else { - ChartEditorDialogHandler.openWelcomeDialog(this, false); + var welcomeDialog = this.openWelcomeDialog(false); + if (shouldShowBackupAvailableDialog) + { + this.openBackupAvailableDialog(welcomeDialog); + } } } @@ -1649,7 +1668,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // audioVocalTrackGroup.pitch = save.chartEditorPlaybackSpeed; } - public function writePreferences():Void + public function writePreferences(hasBackup:Bool):Void { var save:Save = Save.get(); @@ -1657,8 +1676,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState 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; @@ -1690,36 +1712,27 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState 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 @@ -2017,16 +2030,21 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState } } + 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 = 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(); - // If we were dragging the playhead while the song was playing, resume playing. if (playbarHeadDraggingWasPlaying) { @@ -2040,9 +2058,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState if (!Preferences.debugDisplay) menubar.paddingLeft = null; - // Setup notifications. - @:privateAccess - NotificationManager.GUTTER_SIZE = 20; + this.setupNotifications(); } /** @@ -2071,19 +2087,19 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // Add functionality to the menu items. // File - menubarItemNewChart.onClick = _ -> ChartEditorDialogHandler.openWelcomeDialog(this, true); - menubarItemOpenChart.onClick = _ -> ChartEditorDialogHandler.openBrowseFNFC(this, true); + 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); } }; - menubarItemSaveChartAs.onClick = _ -> ChartEditorImportExportHandler.exportAllSongData(this); + menubarItemSaveChartAs.onClick = _ -> this.exportAllSongData(false, null); menubarItemExit.onClick = _ -> quitChartEditor(); // Edit @@ -2175,6 +2191,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState menubarItemAbout.onClick = _ -> this.openAboutDialog(); menubarItemWelcomeDialog.onClick = _ -> this.openWelcomeDialog(true); + #if sys + menubarItemGoToBackupsFolder.onClick = _ -> this.openBackupsFolder(); + #else + // Disable if no file system or command access + menubarItemGoToBackupsFolder.disabled = true; + #end + menubarItemUserGuide.onClick = _ -> this.openUserGuideDialog(); menubarItemDownscroll.onClick = event -> isViewDownscroll = event.value; @@ -2265,7 +2288,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState */ 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; } @@ -2274,10 +2303,12 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState */ function autoSave():Void { + var needsAutoSave:Bool = saveDataDirty; + saveDataDirty = false; // Auto-save preferences. - writePreferences(); + writePreferences(needsAutoSave); // Auto-save the chart. #if html5 @@ -2285,24 +2316,72 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // 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 @@ -2989,9 +3068,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState 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'); @@ -3460,6 +3540,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // Finished dragging. Release the note. currentPlaceNoteData = null; } + else + { + // Cursor should be a grabby hand. + if (targetCursorMode == null) targetCursorMode = Grabbing; + } } else { @@ -3999,20 +4084,21 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState 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 && !isHaxeUIDialogOpen) - { - this.exportAllSongData(false); - } - // CTRL + Q = Quit to Menu if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.Q) { @@ -4173,6 +4259,14 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState { // 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 @@ -4509,15 +4603,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState } } - #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()}'); } /** @@ -4779,9 +4865,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState */ // ==================== - /** - * Dismiss any existing HaxeUI notifications, if there are any. - */ function handleNotePreview():Void { if (notePreviewDirty && notePreview != null) @@ -4964,14 +5047,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState 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 a52828e35..b67e81465 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx @@ -14,16 +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.ChartEditorBaseDialog.DialogDropTarget; 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; @@ -66,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. @@ -397,15 +400,7 @@ 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); @@ -413,15 +408,7 @@ class ChartEditorDialogHandler } 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})'); } } }); @@ -433,15 +420,7 @@ class ChartEditorDialogHandler 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); @@ -459,15 +438,7 @@ 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); } }; @@ -693,15 +664,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 @@ -715,16 +678,7 @@ class ChartEditorDialogHandler { 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.'; @@ -750,15 +704,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 @@ -771,15 +718,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.'; @@ -934,15 +873,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; } @@ -952,30 +883,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}'; @@ -999,15 +914,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; } @@ -1019,15 +926,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}'; @@ -1040,15 +939,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})'); } } }); @@ -1064,15 +955,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; } @@ -1087,15 +970,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}'; @@ -1106,15 +981,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})'); } }; @@ -1131,15 +998,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; } @@ -1154,15 +1013,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}'; @@ -1259,15 +1110,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; } @@ -1277,15 +1120,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})'); } }); } @@ -1300,15 +1135,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 (${path.file}.${path.ext})', - type: NotificationType.Success, - expiryMs: Constants.NOTIFICATION_DISMISS_TIME - }); - #end + state.success('Success', 'Loaded chart file (${path.file}.${path.ext})'); }; state.addDropHandler({component: importBox, handler: onDropFile}); @@ -1408,14 +1235,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); } @@ -1472,14 +1294,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); } 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/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/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 e254909eb..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. @@ -11,6 +12,19 @@ class CrashHandler { 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