From a423424715c5a41393cddd6fd81344d80f6eaacc Mon Sep 17 00:00:00 2001 From: Eric Myllyoja Date: Thu, 13 Oct 2022 21:32:19 -0400 Subject: [PATCH] Notifications and more --- source/funkin/play/song/SongData.hx | 6 +- source/funkin/play/song/SongDataUtils.hx | 68 ++- .../ui/debug/charting/ChartEditorCommand.hx | 20 +- .../ui/debug/charting/ChartEditorState.hx | 504 ++++++++++++++++-- 4 files changed, 502 insertions(+), 96 deletions(-) diff --git a/source/funkin/play/song/SongData.hx b/source/funkin/play/song/SongData.hx index c11ef96b6..e5679cab9 100644 --- a/source/funkin/play/song/SongData.hx +++ b/source/funkin/play/song/SongData.hx @@ -439,7 +439,11 @@ abstract SongNoteData(RawSongNoteData) @:op(A == B) public function op_equals(other:SongNoteData):Bool { - return this.t == other.time && this.d == other.data && this.l == other.length && this.k == other.kind; + if (this.k == '') + if (other.kind != '' && other.kind != 'normal') + return false; + + return this.t == other.time && this.d == other.data && this.l == other.length; } @:op(A != B) diff --git a/source/funkin/play/song/SongDataUtils.hx b/source/funkin/play/song/SongDataUtils.hx index bb7f5af38..987c35059 100644 --- a/source/funkin/play/song/SongDataUtils.hx +++ b/source/funkin/play/song/SongDataUtils.hx @@ -1,5 +1,6 @@ package funkin.play.song; +import flixel.util.FlxSort; import funkin.play.song.SongData.SongEventData; import funkin.play.song.SongData.SongNoteData; import funkin.util.ClipboardUtil; @@ -35,11 +36,17 @@ class SongDataUtils if (notes.length == 0 || subtrahend.length == 0) return notes; - return notes.filter(function(note:SongNoteData):Bool + var result = notes.filter(function(note:SongNoteData):Bool { - // SongNoteData's == operation has been overridden so that this will work. - return !subtrahend.has(note); + for (x in subtrahend) + // SongNoteData's == operation has been overridden so that this will work. + if (x == note) + return false; + + return true; }); + + return result; } /** @@ -67,34 +74,47 @@ class SongDataUtils */ public static function buildClipboard(notes:Array):Array { - return offsetSongNoteData(notes, -Std.int(notes[0].time)); + return offsetSongNoteData(sortNotes(notes), -Std.int(notes[0].time)); } - public static function writeNotesToClipboard(notes:Array):Void - { - var notesString = SerializerUtil.toJSON(notes); + public static function sortNotes(notes:Array, ?desc:Bool = false):Array + { + // TODO: Modifies the array in place. Is this okay? + notes.sort(function(a:SongNoteData, b:SongNoteData):Int + { + return FlxSort.byValues(desc ? FlxSort.DESCENDING : FlxSort.ASCENDING, a.time, b.time); + }); + return notes; + } - ClipboardUtil.setClipboard(notesString); + public static function writeNotesToClipboard(notes:Array):Void + { + var notesString = SerializerUtil.toJSON(notes); - trace('Wrote ' + notes.length + ' notes to clipboard.'); + ClipboardUtil.setClipboard(notesString); - trace(notesString); - } + trace('Wrote ' + notes.length + ' notes to clipboard.'); - public static function readNotesFromClipboard():Array - { - var notesString = ClipboardUtil.getClipboard(); + trace(notesString); + } - trace('Read ' + notesString.length + ' characters from clipboard.'); + public static function readNotesFromClipboard():Array + { + var notesString = ClipboardUtil.getClipboard(); - var notes:Array = SerializerUtil.fromJSON(notesString); + trace('Read ' + notesString.length + ' characters from clipboard.'); - if (notes == null) { - trace('Failed to parse notes from clipboard.'); - return []; - } else { - trace('Parsed ' + notes.length + ' notes from clipboard.'); - return notes; - } - } + var notes:Array = SerializerUtil.fromJSON(notesString); + + if (notes == null) + { + trace('Failed to parse notes from clipboard.'); + return []; + } + else + { + trace('Parsed ' + notes.length + ' notes from clipboard.'); + return notes; + } + } } diff --git a/source/funkin/ui/debug/charting/ChartEditorCommand.hx b/source/funkin/ui/debug/charting/ChartEditorCommand.hx index 6892c19d6..fbac7e518 100644 --- a/source/funkin/ui/debug/charting/ChartEditorCommand.hx +++ b/source/funkin/ui/debug/charting/ChartEditorCommand.hx @@ -122,7 +122,7 @@ class RemoveNotesCommand implements ChartEditorCommand public function toString():String { - if (notes.length == 1) + if (notes.length == 1 && notes[0] != null) { var dir:String = notes[0].getDirectionName(); return 'Remove $dir Note'; @@ -315,12 +315,10 @@ class DeselectAllNotesCommand implements ChartEditorCommand class CutNotesCommand implements ChartEditorCommand { private var notes:Array; - private var previousSelection:Array; - public function new(notes:Array, ?previousSelection:Array) + public function new(notes:Array) { this.notes = notes; - this.previousSelection = previousSelection == null ? [] : previousSelection; } public function execute(state:ChartEditorState):Void @@ -357,6 +355,8 @@ class CutNotesCommand implements ChartEditorCommand class PasteNotesCommand implements ChartEditorCommand { private var targetTimestamp:Float; + // Notes we added with this command, for undo. + private var addedNotes:Array; public function new(targetTimestamp:Float) { @@ -367,10 +367,10 @@ class PasteNotesCommand implements ChartEditorCommand { var currentClipboard:Array = SongDataUtils.readNotesFromClipboard(); - var notesToAdd = SongDataUtils.offsetSongNoteData(currentClipboard, Std.int(targetTimestamp)); + addedNotes = SongDataUtils.offsetSongNoteData(currentClipboard, Std.int(targetTimestamp)); - state.currentSongChartNoteData = state.currentSongChartNoteData.concat(notesToAdd); - state.currentSelection = notesToAdd; + state.currentSongChartNoteData = state.currentSongChartNoteData.concat(addedNotes); + state.currentSelection = addedNotes.copy(); state.noteDisplayDirty = true; state.notePreviewDirty = true; @@ -380,11 +380,7 @@ class PasteNotesCommand implements ChartEditorCommand public function undo(state:ChartEditorState):Void { - // NOTE: We can assume that the previous action - // defined the clipboard, so we don't need to redundantly it here... right? - // TODO: Test that this works as expected. - var currentClipboard:Array = SongDataUtils.readNotesFromClipboard(); - state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, currentClipboard); + state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, addedNotes); state.currentSelection = []; state.noteDisplayDirty = true; diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 732c47fe2..20af63c61 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -7,39 +7,40 @@ import flixel.group.FlxSpriteGroup; import flixel.system.FlxSound; import flixel.util.FlxColor; import flixel.util.FlxSort; +import flixel.util.FlxTimer; import funkin.audio.visualize.PolygonSpectogram; import funkin.play.HealthIcon; import funkin.play.song.SongData.SongChartData; import funkin.play.song.SongData.SongEventData; import funkin.play.song.SongData.SongMetadata; import funkin.play.song.SongData.SongNoteData; -import funkin.play.song.SongSerializer; -import funkin.ui.debug.charting.ChartEditorCommand.AddNotesCommand; -import funkin.ui.debug.charting.ChartEditorCommand.CutNotesCommand; -import funkin.ui.debug.charting.ChartEditorCommand.DeselectAllNotesCommand; -import funkin.ui.debug.charting.ChartEditorCommand.DeselectNotesCommand; -import funkin.ui.debug.charting.ChartEditorCommand.PasteNotesCommand; -import funkin.ui.debug.charting.ChartEditorCommand.RemoveNotesCommand; -import funkin.ui.debug.charting.ChartEditorCommand.SelectAllNotesCommand; -import funkin.ui.debug.charting.ChartEditorCommand.SelectNotesCommand; -import funkin.ui.debug.charting.ChartEditorCommand; import funkin.play.song.SongDataUtils; +import funkin.play.song.SongSerializer; +import funkin.ui.debug.charting.ChartEditorCommand; import funkin.ui.haxeui.HaxeUIState; +import haxe.ui.components.Button; import haxe.ui.components.CheckBox; +import haxe.ui.components.HorizontalSlider; +import haxe.ui.components.Label; +import haxe.ui.components.Slider; +import haxe.ui.containers.SideBar; import haxe.ui.containers.TreeView; import haxe.ui.containers.TreeViewNode; import haxe.ui.containers.dialogs.Dialog; +import haxe.ui.containers.dialogs.MessageBox; import haxe.ui.containers.menus.Menu.MenuEvent; import haxe.ui.containers.menus.MenuBar; import haxe.ui.containers.menus.MenuCheckBox; import haxe.ui.containers.menus.MenuItem; import haxe.ui.core.Component; +import haxe.ui.events.DragEvent; import haxe.ui.events.MouseEvent; import haxe.ui.events.UIEvent; import openfl.display.BitmapData; import openfl.geom.Rectangle; using Lambda; +using StringTools; // Since Haxe 3.1.0, if access is allowed to an interface, it extends to all classes implementing that interface. // Thus, any ChartEditorCommand has access to any private field. @@ -56,6 +57,8 @@ class ChartEditorState extends HaxeUIState */ static final CHART_EDITOR_LAYOUT = Paths.ui('chart-editor/main-view'); + static final CHART_EDITOR_NOTIFBAR_LAYOUT = Paths.ui('chart-editor/components/notifbar'); + static final DEFAULT_VARIATION = 'default'; static final DEFAULT_DIFFICULTY = 'normal'; @@ -66,6 +69,8 @@ class ChartEditorState extends HaxeUIState static final GRID_TOP_PAD:Int = 8; static final SELECTION_SQUARE_BORDER_WIDTH:Int = 1; + static final NOTIFICATION_DISMISS_TIME:Float = 3.0; + // UI Element Colors static final BG_COLOR:FlxColor = 0xFF673AB7; static final GRID_ALTERNATE:Bool = true; @@ -80,6 +85,9 @@ class ChartEditorState extends HaxeUIState static final SELECTION_SQUARE_BORDER_COLOR:FlxColor = 0xFF339933; static final SELECTION_SQUARE_FILL_COLOR:FlxColor = 0x4033FF33; + static final PLAYBAR_PRIMARY_COLOR:FlxColor = 0xFF442277; + static final PLAYBAR_SECONDARY_COLOR:FlxColor = 0xFF8844DD; + /** * INSTANCE DATA */ @@ -102,17 +110,23 @@ class ChartEditorState extends HaxeUIState return scrollPosition / GRID_SIZE; } - var scrollPositionInMs(get, null):Float; - /** * scrollPosition, converted to milliseconds. * TODO: Handle BPM changes. */ + var scrollPositionInMs(get, set):Float; + function get_scrollPositionInMs():Float { return scrollPositionInSteps * Conductor.stepCrochet; } + function set_scrollPositionInMs(value:Float):Float + { + scrollPosition = value / Conductor.stepCrochet; + return value; + } + /** * The position of the playhead, in pixels, relative to the scroll position. * For example, 0 means the playhead is at the top of the grid, and 40 means the playhead is 1 step farther. @@ -169,6 +183,21 @@ class ChartEditorState extends HaxeUIState */ var isModalDialogOpen:Bool = false; + /** + * Whether a skip button has been pressed on the playbar, and which one. + */ + var playbarButtonPressed:String = null; + + /** + * Whether the head of the playbar is being dragged. + */ + var playbarHeadDragging:Bool = false; + + /** + * Whether music was playing before we started dragging the playbar head. + */ + var playbarHeadDraggingWasPlaying:Bool = false; + /** * The note kind currently being placed. Defaults to `''`. * Use the input in the sidebar to change this. @@ -187,22 +216,47 @@ class ChartEditorState extends HaxeUIState function set_isViewDownscroll(value:Bool):Bool { + isViewDownscroll = value; + // Make sure view is updated. noteDisplayDirty = true; notePreviewDirty = true; + this.scrollPosition = this.scrollPosition; - return isViewDownscroll = value; + return isViewDownscroll; } /** * The current variation ID. */ - var selectedVariation:String = DEFAULT_VARIATION; + var selectedVariation(default, set):String = DEFAULT_VARIATION; + + function set_selectedVariation(value:String):String + { + selectedVariation = value; + + // Make sure view is updated. + noteDisplayDirty = true; + notePreviewDirty = true; + + return selectedVariation; + } /** * The selected difficulty ID. */ - var selectedDifficulty:String = DEFAULT_DIFFICULTY; + var selectedDifficulty(default, set):String = DEFAULT_DIFFICULTY; + + function set_selectedDifficulty(value:String):String + { + selectedDifficulty = value; + + // Make sure view is updated. + noteDisplayDirty = true; + notePreviewDirty = true; + + return selectedDifficulty; + } /** * Whether the note display render group needs to be updated. @@ -269,6 +323,13 @@ class ChartEditorState extends HaxeUIState */ var songMetadata:Map; + var availableVariations(get, null):Array; + + function get_availableVariations():Array + { + return [for (x in songMetadata.keys()) x]; + } + /** * The song chart data. * - Keys are the variation IDs. At least one (`default`) must exist. @@ -346,7 +407,7 @@ class ChartEditorState extends HaxeUIState /** * Convenience property to get the note data for the current difficulty. */ - var currentSongChartNoteData(get, null):Array; + var currentSongChartNoteData(get, set):Array; function get_currentSongChartNoteData():Array { @@ -361,10 +422,16 @@ class ChartEditorState extends HaxeUIState return result; } + function set_currentSongChartNoteData(value:Array):Array + { + currentSongChartData.notes.set(selectedDifficulty, value); + return value; + } + /** * Convenience property to get the event data for the current difficulty. */ - var currentSongChartEventData(get, null):Array; + var currentSongChartEventData(get, set):Array; function get_currentSongChartEventData():Array { @@ -376,6 +443,12 @@ class ChartEditorState extends HaxeUIState return currentSongChartData.events; } + function set_currentSongChartEventData(value:Array):Array + { + currentSongChartData.events = value; + return value; + } + var currentSongNoteSkin(get, set):String; function get_currentSongNoteSkin():String @@ -517,6 +590,9 @@ class ChartEditorState extends HaxeUIState var renderedNoteSelectionSquares:FlxTypedSpriteGroup; + var notifBar:SideBar; + var playbarHead:Slider; + public function new() { // Load the HaxeUI XML file. @@ -536,6 +612,7 @@ class ChartEditorState extends HaxeUIState // Add the HaxeUI components after the grid so they're on top. super.create(); + buildAdditionalUI(); // Setup the onClick listeners for the UI after it's been created. setupUIListeners(); @@ -684,6 +761,59 @@ class ChartEditorState extends HaxeUIState */ } + function buildAdditionalUI():Void + { + notifBar = cast buildComponent(CHART_EDITOR_NOTIFBAR_LAYOUT); + + add(notifBar); + + playbarHead = new HorizontalSlider(); + playbarHead.width = FlxG.width; + playbarHead.height = 10; + + playbarHead.x = 0; + playbarHead.y = FlxG.height - 48 - 8; + + playbarHead.allowFocus = false; + playbarHead.styleString = "padding-left: 0px; padding-right: 0px; border-left: 0px; border-right: 0px;"; + + playbarHead.onDragStart = function(_:DragEvent) + { + playbarHeadDragging = true; + + // If we were dragging the playhead while the song was playing, resume playing. + if (audioVocalTrack.playing) + { + playbarHeadDraggingWasPlaying = true; + stopAudioPlayback(); + } + else + { + playbarHeadDraggingWasPlaying = false; + } + } + + playbarHead.onDragEnd = function(_:DragEvent) + { + trace('Seek to position: ${playbarHead.value}%'); + playbarHeadDragging = false; + + // Set the song position to where the playhead was moved to. + scrollPosition = songLength * (playbarHead.value / 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) + { + playbarHeadDraggingWasPlaying = false; + startAudioPlayback(); + } + } + + add(playbarHead); + } + /** * Sets up the onClick listeners for the UI. */ @@ -703,12 +833,66 @@ class ChartEditorState extends HaxeUIState } } + // Add functionality to the playbar. + + addUIClickListener('playbarPlay', (event:MouseEvent) -> toggleAudioPlayback()); + addUIClickListener('playbarStart', (event:MouseEvent) -> playbarButtonPressed = 'playbarStart'); + addUIClickListener('playbarBack', (event:MouseEvent) -> playbarButtonPressed = 'playbarBack'); + addUIClickListener('playbarForward', (event:MouseEvent) -> playbarButtonPressed = 'playbarForward'); + addUIClickListener('playbarEnd', (event:MouseEvent) -> playbarButtonPressed = 'playbarEnd'); + // Add functionality to the menu items. addUIClickListener('menubarItemUndo', (event:MouseEvent) -> undoLastCommand()); addUIClickListener('menubarItemRedo', (event:MouseEvent) -> redoLastCommand()); + addUIClickListener('menubarItemCopy', (event:MouseEvent) -> + { + SongDataUtils.writeNotesToClipboard(SongDataUtils.buildClipboard(currentSelection)); + }); + + addUIClickListener('menubarItemCut', (event:MouseEvent) -> + { + performCommand(new CutNotesCommand(currentSelection)); + }); + + addUIClickListener('menubarItemPaste', (event:MouseEvent) -> + { + performCommand(new PasteNotesCommand(scrollPositionInMs + playheadPositionInMs)); + }); + + addUIClickListener('menubarItemDelete', (event:MouseEvent) -> + { + performCommand(new RemoveNotesCommand(currentSelection)); + }); + + addUIClickListener('menubarItemSelectAll', (event:MouseEvent) -> + { + performCommand(new SelectAllNotesCommand(currentSelection)); + }); + + addUIClickListener('menubarItemSelectInverse', (event:MouseEvent) -> { + // TODO: Implement this. + }); + + addUIClickListener('menubarItemSelectNone', (event:MouseEvent) -> + { + performCommand(new DeselectAllNotesCommand(currentSelection)); + }); + + addUIClickListener('menubarItemSelectRegion', (event:MouseEvent) -> { + // TODO: Implement this. + }); + + addUIClickListener('menubarItemSelectBeforeCursor', (event:MouseEvent) -> { + // TODO: Implement this. + }); + + addUIClickListener('menubarItemSelectAfterCursor', (event:MouseEvent) -> { + // TODO: Implement this. + }); + addUIClickListener('menubarItemAbout', (event:MouseEvent) -> openDialog('chart-editor/dialogs/about')); addUIClickListener('menubarItemUserGuide', (event:MouseEvent) -> openDialog('chart-editor/dialogs/user-guide')); @@ -822,11 +1006,11 @@ class ChartEditorState extends HaxeUIState handleScrollKeybinds(); handleCursor(); - handlePlayheadKeybinds(); - handleMenubar(); handleSidebar(); + handlePlaybar(); + handlePlayheadKeybinds(); handleFileKeybinds(); handleEditKeybinds(); handleViewKeybinds(); @@ -834,21 +1018,9 @@ class ChartEditorState extends HaxeUIState } // DEBUG - if (FlxG.keys.justPressed.A) - { - performCommand(new SwitchDifficultyCommand(selectedDifficulty, 'easy', selectedVariation, 'default')); - } - if (FlxG.keys.justPressed.S) - { - performCommand(new SwitchDifficultyCommand(selectedDifficulty, 'normal', selectedVariation, 'default')); - } - if (FlxG.keys.justPressed.D) - { - performCommand(new SwitchDifficultyCommand(selectedDifficulty, 'hard', selectedVariation, 'default')); - } if (FlxG.keys.justPressed.F) { - performCommand(new SwitchDifficultyCommand(selectedDifficulty, 'erect', selectedVariation, 'erect')); + showNotification('Hi there :)'); } // Right align the BF health icon. @@ -922,12 +1094,22 @@ class ChartEditorState extends HaxeUIState { scrollAmount = -GRID_SIZE * 4 * Conductor.beatsPerMeasure; } + if (playbarButtonPressed == 'playbarBack') + { + playbarButtonPressed = ''; + scrollAmount = -GRID_SIZE * 4 * Conductor.beatsPerMeasure; + } // PAGE DOWN = Jump Down 1 Measure if (FlxG.keys.justPressed.PAGEDOWN) { scrollAmount = GRID_SIZE * 4 * Conductor.beatsPerMeasure; } + if (playbarButtonPressed == 'playbarForward') + { + playbarButtonPressed = ''; + scrollAmount = GRID_SIZE * 4 * Conductor.beatsPerMeasure; + } // Mouse Wheel = Scroll if (FlxG.mouse.wheel != 0) @@ -971,6 +1153,11 @@ class ChartEditorState extends HaxeUIState // Scroll amount is the difference between the current position and the top. scrollAmount = 0 - this.scrollPosition; } + if (playbarButtonPressed == 'playbarStart') + { + playbarButtonPressed = ''; + scrollAmount = 0 - this.scrollPosition; + } // END = Scroll to Bottom if (FlxG.keys.justPressed.END) @@ -978,6 +1165,11 @@ class ChartEditorState extends HaxeUIState // Scroll amount is the difference between the current position and the bottom. scrollAmount = this.songLength - this.scrollPosition; } + if (playbarButtonPressed == 'playbarEnd') + { + playbarButtonPressed = ''; + scrollAmount = this.songLength - this.scrollPosition; + } // Apply the scroll amount. this.scrollPosition += scrollAmount; @@ -1038,22 +1230,26 @@ class ChartEditorState extends HaxeUIState // Find the first note that is at the cursor position. var highlightedNote:ChartEditorNoteSprite = renderedNotes.members.find(function(note:ChartEditorNoteSprite):Bool { - // return note.step == cursorStep && note.column == cursorColumn; - return FlxG.mouse.overlaps(note); + // If note.alive is false, the note is dead and awaiting recycling. + return note.alive && FlxG.mouse.overlaps(note); }); if (FlxG.keys.pressed.CONTROL) { if (highlightedNote != null) { + // Select/deselect an individual note. if (isNoteSelected(highlightedNote.noteData)) - { - performCommand(new SelectNotesCommand([highlightedNote.noteData])); - } - else { performCommand(new DeselectNotesCommand([highlightedNote.noteData])); } + else + { + performCommand(new SelectNotesCommand([highlightedNote.noteData])); + } + } + else + { } } else @@ -1191,6 +1387,34 @@ class ChartEditorState extends HaxeUIState } } + /** + * Handles display elements for the playbar at the bottom. + */ + function handlePlaybar() + { + var songPos = Conductor.songPosition; + var songRemaining = songLengthInMs - songPos; + + // Move the playhead to match the song position, if we aren't dragging it. + if (!playbarHeadDragging) + { + var songPosPercent:Float = songPos / songLengthInMs; + playbarHead.value = songPosPercent * 100; + } + + 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}'; + + setUIValue('playbarSongPos', 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}'; + + setUIValue('playbarSongRemaining', songRemainingString); + } + /** * Handle keybinds for File menu items. */ @@ -1241,6 +1465,27 @@ class ChartEditorState extends HaxeUIState // Paste notes from clipboard, at the playhead. performCommand(new PasteNotesCommand(scrollPositionInMs + playheadPositionInMs)); } + + // DELETE = Delete + if (FlxG.keys.justPressed.DELETE) + { + // Delete selected notes. + performCommand(new RemoveNotesCommand(currentSelection)); + } + + // CTRL + A = Select All + if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.A) + { + // Select all notes. + performCommand(new SelectAllNotesCommand(currentSelection)); + } + + // CTRL + D = Select None + if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.D) + { + // Deselect all notes. + performCommand(new DeselectAllNotesCommand(currentSelection)); + } } /** @@ -1274,7 +1519,10 @@ class ChartEditorState extends HaxeUIState if (treeView != null) { - var treeSong = treeView.addNode({id: 'stv_song_dadbattle', text: "S: Dad Battle", icon: "haxeui-core/styles/default/haxeui_tiny.png"}); + // Clear the tree view so we can rebuild it. + treeView.clearNodes(); + + var treeSong = treeView.addNode({id: 'stv_song', text: 'S: $currentSongName', icon: "haxeui-core/styles/default/haxeui_tiny.png"}); treeSong.expanded = true; var treeVariationDefault = treeSong.addNode({ @@ -1282,41 +1530,87 @@ class ChartEditorState extends HaxeUIState text: "V: Default", icon: "haxeui-core/styles/default/haxeui_tiny.png" }); - var treeVariationErect = treeSong.addNode({id: 'stv_variation_erect', text: "V: Erect", icon: "haxeui-core/styles/default/haxeui_tiny.png"}); + treeVariationDefault.expanded = true; var treeDifficultyEasy = treeVariationDefault.addNode({ - id: 'stv_difficulty_easy', + id: 'stv_difficulty_default_easy', text: "D: Easy", icon: "haxeui-core/styles/default/haxeui_tiny.png" }); var treeDifficultyNormal = treeVariationDefault.addNode({ - id: 'stv_difficulty_normal', + id: 'stv_difficulty_default_normal', text: "D: Normal", icon: "haxeui-core/styles/default/haxeui_tiny.png" }); var treeDifficultyHard = treeVariationDefault.addNode({ - id: 'stv_difficulty_hard', + id: 'stv_difficulty_default_hard', text: "D: Hard", icon: "haxeui-core/styles/default/haxeui_tiny.png" }); + var treeVariationErect = treeSong.addNode({id: 'stv_variation_erect', text: "V: Erect", icon: "haxeui-core/styles/default/haxeui_tiny.png"}); + treeVariationErect.expanded = true; + var treeDifficultyErect = treeVariationErect.addNode({ - id: 'stv_difficulty_erect', + id: 'stv_difficulty_erect_erect', text: "D: Erect", icon: "haxeui-core/styles/default/haxeui_tiny.png" }); treeView.onChange = onChangeTreeDifficulty; + treeView.selectedNode = getCurrentTreeDifficultyNode(); } } } + function getCurrentTreeDifficultyNode():TreeViewNode + { + var treeView:TreeView = findComponent('sidebarDifficulties'); + + if (treeView == null) + return null; + + var result = treeView.findNodeByPath('stv_song/stv_variation_$selectedVariation/stv_difficulty_${selectedVariation}_$selectedDifficulty', 'id'); + + if (result == null) + return null; + + return result; + } + function onChangeTreeDifficulty(event:UIEvent):Void { - // Get the selected node. - var target:TreeView = cast event.target; - var targetNode:TreeViewNode = target.selectedNode; - trace('Selected node: ${targetNode.id}'); + // Get the newly selected node. + var treeView:TreeView = cast event.target; + var targetNode:TreeViewNode = treeView.selectedNode; + + if (targetNode == null) + { + trace('No target node!'); + // Reset the user's selection. + treeView.selectedNode = getCurrentTreeDifficultyNode(); + return; + } + + switch (targetNode.data.id.split('_')[1]) + { + case 'difficulty': + var variation = targetNode.data.id.split('_')[2]; + var difficulty = targetNode.data.id.split('_')[3]; + + if (variation != null && difficulty != null) + { + trace('Changing difficulty to $variation:$difficulty'); + selectedVariation = variation; + selectedDifficulty = difficulty; + } + // case 'song': + // case 'variation': + default: + // Reset the user's selection. + trace('Selected wrong node type, resetting selection.'); + treeView.selectedNode = getCurrentTreeDifficultyNode(); + } } /** @@ -1424,10 +1718,13 @@ class ChartEditorState extends HaxeUIState { if (FlxG.mouse.pressedMiddle) { - // If middle mouse panning during song playback, move ONLY the playhead. + // If middle mouse panning during song playback, we move ONLY the playhead, without scrolling. Neat! var oldStepTime = Conductor.currentStepTime; Conductor.update(audioInstTrack.time); + // Resync vocals. + if (Math.abs(audioInstTrack.time - audioVocalTrack.time) > 100) + audioVocalTrack.time = audioInstTrack.time; var diffStepTime = Conductor.currentStepTime - oldStepTime; // Move the playhead. @@ -1440,6 +1737,9 @@ class ChartEditorState extends HaxeUIState // Else, move the entire view. Conductor.update(audioInstTrack.time); + // Resync vocals. + if (Math.abs(audioInstTrack.time - audioVocalTrack.time) > 100) + audioVocalTrack.time = audioInstTrack.time; // We need time in fractional steps here to allow the song to actually play. // Also account for a potentially offset playhead. @@ -1454,16 +1754,31 @@ class ChartEditorState extends HaxeUIState if (FlxG.keys.justPressed.SPACE) { - if (audioInstTrack.playing) - { - audioInstTrack.pause(); - audioVocalTrack.pause(); - } - else - { - audioInstTrack.play(); - audioVocalTrack.play(); - } + toggleAudioPlayback(); + } + } + + function startAudioPlayback() + { + audioInstTrack.play(); + audioVocalTrack.play(); + } + + function stopAudioPlayback() + { + audioInstTrack.pause(); + audioVocalTrack.pause(); + } + + function toggleAudioPlayback() + { + if (audioInstTrack.playing) + { + stopAudioPlayback(); + } + else + { + startAudioPlayback(); } } @@ -1815,4 +2130,75 @@ class ChartEditorState extends HaxeUIState @:privateAccess ChartEditorNoteSprite.noteFrameCollection = null; } + + /** + * Displays a notification to the user. The only action is to dismiss. + */ + function showNotification(text:String) + { + var notifBarText:Label = notifBar.findComponent('notifBarText', Label); + var notifBarAction1:Button = notifBar.findComponent('notifBarAction1', Button); + + // Make it appear. + notifBar.show(); + + // Don't shift the UI up. + notifBar.method = "float"; + // Anchor to far right. + notifBar.x = FlxG.width - notifBar.width; + + // Set the message. + notifBarText.text = text; + + // Configure the action button. + notifBarAction1.text = 'Dismiss'; + notifBarAction1.onClick = (_:UIEvent) -> dismissNotification(); + + // Auto dismiss. + new FlxTimer().start(NOTIFICATION_DISMISS_TIME, (_:FlxTimer) -> dismissNotification()); + } + + /** + * Dismiss any existing notifications, if there are any. + */ + function dismissNotification():Void + { + notifBar.hide(); + } + + /** + * Displays a prompt to the user, to save their changes made to this chart, + * or to discard them. + * + * @param onComplete Function to call after the user clicks Save or Don't Save. + * If Save was clicked, we save before calling this. + * @param onCancel Function to call if the user clicks Cancel. + */ + function promptSaveChanges(onComplete:Void->Void, ?onCancel:Void->Void):Void + { + var messageBox:MessageBox = new MessageBox(); + + messageBox.title = 'Save Changes?'; + messageBox.message = 'Do you want to save the changes you made to $currentSongName?\n\nYour changes will be lost if you don\'t save them.'; + messageBox.type = 'question'; + messageBox.modal = true; + messageBox.buttons = DialogButton.SAVE | "Don't Save" | "Cancel"; + + messageBox.registerEvent(DialogEvent.DIALOG_CLOSED, function(e:DialogEvent):Void + { + trace('Pressed: ${e.button}'); + switch (e.button) + { + case 'Save': + // TODO: Make sure to actually save. + // saveChart(); + onComplete(); + case "Don't Save": + onComplete(); + case 'Cancel': + if (onCancel != null) + onCancel(); + } + }); + } }