From 7d21d809158a9b6f0dd55ab5d02f47f52bc5acb3 Mon Sep 17 00:00:00 2001 From: Eric Myllyoja Date: Tue, 11 Oct 2022 03:14:57 -0400 Subject: [PATCH] Clipboard rework --- rfc/chart-format/chartformat-7.jsonc | 2 +- source/funkin/play/song/SongData.hx | 24 ++-- source/funkin/play/song/SongDataUtils.hx | 117 +++++++++--------- .../ui/debug/charting/ChartEditorCommand.hx | 94 +++++++------- .../ui/debug/charting/ChartEditorState.hx | 82 ++++++++---- source/funkin/util/ClipboardUtil.hx | 51 ++++++++ 6 files changed, 233 insertions(+), 137 deletions(-) create mode 100644 source/funkin/util/ClipboardUtil.hx diff --git a/rfc/chart-format/chartformat-7.jsonc b/rfc/chart-format/chartformat-7.jsonc index 482bafe44..1bd88de95 100644 --- a/rfc/chart-format/chartformat-7.jsonc +++ b/rfc/chart-format/chartformat-7.jsonc @@ -1246,7 +1246,7 @@ "notes": [ {"t":300, "d":4}, {"t":600, "d":2}, - {"t":900, "d":1}, + {"t":900, "d":1},- {"t":1300, "d":3}, ] } diff --git a/source/funkin/play/song/SongData.hx b/source/funkin/play/song/SongData.hx index 77e6ef0a2..c11ef96b6 100644 --- a/source/funkin/play/song/SongData.hx +++ b/source/funkin/play/song/SongData.hx @@ -439,37 +439,37 @@ abstract SongNoteData(RawSongNoteData) @:op(A == B) public function op_equals(other:SongNoteData):Bool { - return this.t == other.t && this.d == other.d && this.l == other.l && this.k == other.k; + return this.t == other.time && this.d == other.data && this.l == other.length && this.k == other.kind; } @:op(A != B) public function op_notEquals(other:SongNoteData):Bool { - return !this.op_equals(other); + return this.t != other.time || this.d != other.data || this.l != other.length || this.k != other.kind; } @:op(A > B) public function op_greaterThan(other:SongNoteData):Bool { - return this.t > other.t; + return this.t > other.time; } @:op(A < B) public function op_lessThan(other:SongNoteData):Bool { - return this.t < other.t; + return this.t < other.time; } @:op(A >= B) public function op_greaterThanOrEquals(other:SongNoteData):Bool { - return this.t >= other.t; + return this.t >= other.time; } @:op(A <= B) public function op_lessThanOrEquals(other:SongNoteData):Bool { - return this.t <= other.t; + return this.t <= other.time; } } @@ -575,37 +575,37 @@ abstract SongEventData(RawSongEventData) @:op(A == B) public function op_equals(other:SongEventData):Bool { - return this.t == other.t && this.e == other.e && this.v == other.v; + return this.t == other.time && this.e == other.event && this.v == other.value; } @:op(A != B) public function op_notEquals(other:SongEventData):Bool { - return !this.op_equals(other); + return this.t != other.time || this.e != other.event || this.v != other.value; } @:op(A > B) public function op_greaterThan(other:SongEventData):Bool { - return this.t > other.t; + return this.t > other.time; } @:op(A < B) public function op_lessThan(other:SongEventData):Bool { - return this.t < other.t; + return this.t < other.time; } @:op(A >= B) public function op_greaterThanOrEquals(other:SongEventData):Bool { - return this.t >= other.t; + return this.t >= other.time; } @:op(A <= B) public function op_lessThanOrEquals(other:SongEventData):Bool { - return this.t <= other.t; + return this.t <= other.time; } } diff --git a/source/funkin/play/song/SongDataUtils.hx b/source/funkin/play/song/SongDataUtils.hx index d6db3ac67..bba93dbd8 100644 --- a/source/funkin/play/song/SongDataUtils.hx +++ b/source/funkin/play/song/SongDataUtils.hx @@ -1,65 +1,70 @@ package funkin.play.song; +import funkin.play.song.SongData.SongEventData; +import funkin.play.song.SongData.SongNoteData; + using Lambda; -class SongDataUtils { - /** - * Given an array of SongNoteData objects, return a new array of SongNoteData objects - * whose timestamps are shifted by the given amount. - * - * @param notes The notes to modify. - * @param offset The time difference to apply in milliseconds. - */ - public static function offsetSongNoteData(notes:Array, offset:Int):Array { - return notes.map(function(note:SongNoteData):SongNoteData { - return new SongNoteData(note.time + offset, note.data, note.length, note.kind); - }); - } +class SongDataUtils +{ + /** + * Given an array of SongNoteData objects, return a new array of SongNoteData objects + * whose timestamps are shifted by the given amount. + * + * @param notes The notes to modify. + * @param offset The time difference to apply in milliseconds. + */ + public static function offsetSongNoteData(notes:Array, offset:Int):Array + { + return notes.map(function(note:SongNoteData):SongNoteData + { + return new SongNoteData(note.time + offset, note.data, note.length, note.kind); + }); + } - /** - * Remove a certain subset of notes from an array of SongNoteData objects. - * - * @param notes The array of notes to be subtracted from. - * @param subtrahend The notes to remove from the `notes` array. Yes, subtrahend is a real word. - */ - public static function subtractNotes(notes:Array, subtrahend:Array) { - if (notes.length == 0 || subtrahend.length == 0) - return notes; + /** + * Remove a certain subset of notes from an array of SongNoteData objects. + * + * @param notes The array of notes to be subtracted from. + * @param subtrahend The notes to remove from the `notes` array. Yes, subtrahend is a real word. + */ + public static function subtractNotes(notes:Array, subtrahend:Array) + { + if (notes.length == 0 || subtrahend.length == 0) + return notes; - if (subtrahend.length == 1) - return notes.remove(subtrahend[0]); + return notes.filter(function(note:SongNoteData):Bool + { + // SongNoteData's == operation has been overridden so that this will work. + return !subtrahend.has(note); + }); + } - return notes.filter(function(note:SongNoteData):Bool { - // SongNoteData's == operation has been overridden so that this will work. - return !subtrahend.has(note); - }); - } + /** + * Remove a certain subset of events from an array of SongEventData objects. + * + * @param events The array of events to be subtracted from. + * @param subtrahend The events to remove from the `events` array. Yes, subtrahend is a real word. + */ + public static function subtractEvents(events:Array, subtrahend:Array) + { + if (events.length == 0 || subtrahend.length == 0) + return events; - /** - * Remove a certain subset of events from an array of SongEventData objects. - * - * @param events The array of events to be subtracted from. - * @param subtrahend The events to remove from the `events` array. Yes, subtrahend is a real word. - */ - public static function subtractEvents(events:Array, subtrahend:Array) { - if (events.length == 0 || subtrahend.length == 0) - return events; + return events.filter(function(event:SongEventData):Bool + { + // SongEventData's == operation has been overridden so that this will work. + return !subtrahend.has(event); + }); + } - if (subtrahend.length == 1) - return events.remove(subtrahend[0]); - - return events.filter(function(event:SongEventData):Bool { - // SongEventData's == operation has been overridden so that this will work. - return !subtrahend.has(event); - }); - } - - /** - * Prepare an array of notes to be used as the clipboard data. - * - * Offset the provided array of notes such that the first note is at 0 milliseconds. - */ - public static function buildClipboard(notes:Array):Array { - return offsetSongNoteData(notes, -notes[0].time); - } -} \ No newline at end of file + /** + * Prepare an array of notes to be used as the clipboard data. + * + * Offset the provided array of notes such that the first note is at 0 milliseconds. + */ + public static function buildClipboard(notes:Array):Array + { + return offsetSongNoteData(notes, -notes[0].time); + } +} diff --git a/source/funkin/ui/debug/charting/ChartEditorCommand.hx b/source/funkin/ui/debug/charting/ChartEditorCommand.hx index 0ef035c0f..ed357cea8 100644 --- a/source/funkin/ui/debug/charting/ChartEditorCommand.hx +++ b/source/funkin/ui/debug/charting/ChartEditorCommand.hx @@ -1,8 +1,8 @@ package funkin.ui.debug.charting; -import funkin.play.song.SongDataUtils; -import funkin.play.song.SongData.SongNoteData; import funkin.play.song.SongData.SongEventData; +import funkin.play.song.SongData.SongNoteData; +import funkin.play.song.SongDataUtils; using Lambda; @@ -37,25 +37,26 @@ interface ChartEditorCommand class AddNotesCommand implements ChartEditorCommand { - private var notes:SongNoteData; + private var notes:Array; - public function new(notes:SongNoteData) + public function new(notes:Array) { this.notes = notes; } public function execute(state:ChartEditorState):Void { - for (note in notes) { + for (note in notes) + { state.currentSongChartNoteData.push(note); } state.currentSelection = notes; state.playSound(Paths.sound('funnyNoise/funnyNoise-08')); - + state.noteDisplayDirty = true; state.notePreviewDirty = true; - + state.sortChartData(); } @@ -67,48 +68,52 @@ class AddNotesCommand implements ChartEditorCommand state.noteDisplayDirty = true; state.notePreviewDirty = true; - + state.sortChartData(); } public function toString():String { - if (notes.length == 1) { + if (notes.length == 1) + { var dir:String = notes[0].getDirectionName(); return 'Add $dir Note'; } - + return 'Add ${notes.length} Notes'; } } -class RemoveNoteCommand implements ChartEditorCommand +class RemoveNotesCommand implements ChartEditorCommand { - private var note:SongNoteData; + private var notes:Array; - public function new(note:SongNoteData) + public function new(notes:Array) { - this.note = note; + this.notes = notes; } public function execute(state:ChartEditorState):Void { - state.currentSongChartNoteData.remove(note); + state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes); state.currentSelection = []; state.playSound(Paths.sound('funnyNoise/funnyNoise-01')); - + state.noteDisplayDirty = true; state.notePreviewDirty = true; - + state.sortChartData(); } public function undo(state:ChartEditorState):Void { - state.currentSongChartNoteData.push(note); - state.currentSelection = [note]; + for (note in notes) + { + state.currentSongChartNoteData.push(note); + } + state.currentSelection = notes; state.playSound(Paths.sound('funnyNoise/funnyNoise-08')); - + state.noteDisplayDirty = true; state.notePreviewDirty = true; @@ -117,11 +122,12 @@ class RemoveNoteCommand implements ChartEditorCommand public function toString():String { - if (notes.length == 1) { + if (notes.length == 1) + { var dir:String = notes[0].getDirectionName(); return 'Remove $dir Note'; } - + return 'Remove ${notes.length} Notes'; } } @@ -176,7 +182,8 @@ class SelectNotesCommand implements ChartEditorCommand public function execute(state:ChartEditorState):Void { - for (note in this.notes) { + for (note in this.notes) + { state.currentSelection.push(note); } @@ -187,23 +194,24 @@ class SelectNotesCommand implements ChartEditorCommand public function undo(state:ChartEditorState):Void { state.currentSelection = SongDataUtils.subtractNotes(state.currentSelection, this.notes); - + state.noteDisplayDirty = true; state.notePreviewDirty = true; } public function toString():String { - if (notes.length == 1) { + if (notes.length == 1) + { var dir:String = notes[0].getDirectionName(); return 'Select $dir Note'; } - + return 'Select ${notes.length} Notes'; } } -class DeselectNoteCommand implements ChartEditorCommand +class DeselectNotesCommand implements ChartEditorCommand { private var notes:Array; @@ -222,21 +230,23 @@ class DeselectNoteCommand implements ChartEditorCommand public function undo(state:ChartEditorState):Void { - for (note in this.notes) { + for (note in this.notes) + { state.currentSelection.push(note); } - + state.noteDisplayDirty = true; state.notePreviewDirty = true; } public function toString():String { - if (notes.length == 1) { + if (notes.length == 1) + { var dir:String = notes[0].getDirectionName(); return 'Deselect $dir Note'; } - + return 'Deselect ${notes.length} Notes'; } } @@ -252,8 +262,7 @@ class SelectAllNotesCommand implements ChartEditorCommand public function execute(state:ChartEditorState):Void { - state.currentSelection = state.currentSongChartNoteData - ; + state.currentSelection = state.currentSongChartNoteData; state.noteDisplayDirty = true; state.notePreviewDirty = true; } @@ -336,7 +345,7 @@ class CutNotesCommand implements ChartEditorCommand private var notes:Array; private var previousSelection:Array; - public function new(notes:Array, ?previousSelection:Array = []) + public function new(notes:Array, ?previousSelection:Array) { this.notes = notes; this.previousSelection = previousSelection == null ? [] : previousSelection; @@ -346,7 +355,7 @@ class CutNotesCommand implements ChartEditorCommand { // Copy the notes. state.currentClipboard = SongDataUtils.buildClipboard(notes); - + // Delete the notes. state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes); state.currentSelection = []; @@ -363,7 +372,7 @@ class CutNotesCommand implements ChartEditorCommand state.noteDisplayDirty = true; state.notePreviewDirty = true; - + state.sortChartData(); } @@ -392,7 +401,7 @@ class PasteNotesCommand implements ChartEditorCommand state.noteDisplayDirty = true; state.notePreviewDirty = true; - + state.sortChartData(); } @@ -405,13 +414,13 @@ class PasteNotesCommand implements ChartEditorCommand state.noteDisplayDirty = true; state.notePreviewDirty = true; - + state.sortChartData(); } public function toString():String { - var len:Int = notes.length; + var len:Int = state.currentClipboard.length; return 'Paste $len Notes from Clipboard'; } } @@ -419,6 +428,7 @@ class PasteNotesCommand implements ChartEditorCommand class AddEventsCommand implements ChartEditorCommand { private var events:Array; + // private var previousSelection:Array; public function new(events:Array, ?previousSelection:Array) @@ -435,7 +445,7 @@ class AddEventsCommand implements ChartEditorCommand state.noteDisplayDirty = true; state.notePreviewDirty = true; - + state.sortChartData(); } @@ -447,7 +457,7 @@ class AddEventsCommand implements ChartEditorCommand state.noteDisplayDirty = true; state.notePreviewDirty = true; - + state.sortChartData(); } @@ -456,4 +466,4 @@ class AddEventsCommand implements ChartEditorCommand var len:Int = events.length; return 'Add $len Events'; } -} \ No newline at end of file +} diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 752479c55..05c810b1f 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -1,7 +1,5 @@ package funkin.ui.debug.charting; -import haxe.ui.components.CheckBox; -import haxe.ui.containers.TreeViewNode; import flixel.FlxSprite; import flixel.addons.display.FlxGridOverlay; import flixel.addons.display.FlxTiledSprite; @@ -16,9 +14,20 @@ 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.CopyNotesCommand; +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.ui.haxeui.HaxeUIState; +import haxe.ui.components.CheckBox; import haxe.ui.containers.TreeView; +import haxe.ui.containers.TreeViewNode; import haxe.ui.containers.dialogs.Dialog; import haxe.ui.containers.menus.Menu.MenuEvent; import haxe.ui.containers.menus.MenuBar; @@ -30,6 +39,8 @@ import haxe.ui.events.UIEvent; import openfl.display.BitmapData; import openfl.geom.Rectangle; +using Lambda; + // 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. @:allow(funkin.ui.debug.charting.ChartEditorCommand) @@ -68,7 +79,7 @@ class ChartEditorState extends HaxeUIState static final SPECTROGRAM_COLOR:FlxColor = 0xFFFF0000; static final SELECTION_SQUARE_BORDER_COLOR:FlxColor = 0xFF339933; static final SELECTION_SQUARE_FILL_COLOR:FlxColor = 0x4033FF33; - + /** * INSTANCE DATA */ @@ -174,7 +185,8 @@ class ChartEditorState extends HaxeUIState */ var isViewDownscroll(default, set):Bool = false; - function set_isViewDownscroll(value:Bool):Bool { + function set_isViewDownscroll(value:Bool):Bool + { // Make sure view is updated. noteDisplayDirty = true; notePreviewDirty = true; @@ -447,7 +459,7 @@ class ChartEditorState extends HaxeUIState * The IMAGE used for the grid. */ var gridBitmap:BitmapData; - + /** * The IMAGE used for the selection squares. */ @@ -577,7 +589,8 @@ class ChartEditorState extends HaxeUIState dark ? GRID_COLOR_1_DARK : GRID_COLOR_1, dark ? GRID_COLOR_2_DARK : GRID_COLOR_2); } - function makeSelectionSquareBitmap() { + function makeSelectionSquareBitmap() + { selectionSquareBitmap = new BitmapData(GRID_SIZE, GRID_SIZE, true); selectionSquareBitmap.fillRect(new Rectangle(0, 0, GRID_SIZE, GRID_SIZE), SELECTION_SQUARE_BORDER_COLOR); @@ -710,7 +723,8 @@ class ChartEditorState extends HaxeUIState }); setUISelected('menubarItemToggleSidebar', true); - addUIChangeListener('menubarItemDownscroll', (event:UIEvent) -> { + addUIChangeListener('menubarItemDownscroll', (event:UIEvent) -> + { isViewDownscroll = event.value; }); setUISelected('menubarItemDownscroll', isViewDownscroll); @@ -1024,46 +1038,56 @@ class ChartEditorState extends HaxeUIState if (FlxG.mouse.justPressed) { // Find the first note that is at the cursor position. - var highlightedNote:ChartEditorNoteSprite = renderedNotes.find(function(note:ChartEditorNoteSprite):Bool { + var highlightedNote:ChartEditorNoteSprite = renderedNotes.find(function(note:ChartEditorNoteSprite):Bool + { // return note.step == cursorStep && note.column == cursorColumn; return FlxG.mouse.overlaps(note); }); - if (FlxG.keys.pressed.CONTROL) { - if (highlightedNote != null) { - if (isNoteSelected(highlightedNote.noteData)) { + if (FlxG.keys.pressed.CONTROL) + { + if (highlightedNote != null) + { + if (isNoteSelected(highlightedNote.noteData)) + { performCommand(new SelectNotesCommand([highlightedNote.noteData])); - } else { + } + else + { performCommand(new DeselectNotesCommand([highlightedNote.noteData])); } } - } else { - if (highlightedNote != null) { + } + else + { + if (highlightedNote != null) + { // Remove the note. performCommand(new RemoveNotesCommand([highlightedNote.noteData])); - } else { + } + else + { // Place a note. var eventColumn = (STRUMLINE_SIZE * 2 + 1) - 1; if (cursorColumn == eventColumn) { // Create an event and place it in the chart. var cursorMs = cursorStep * Conductor.stepCrochet; - + // TODO: Allow configuring the event to place from the sidebar. var newEventData:SongEventData = new SongEventData(cursorMs, "test", {}); - + performCommand(new AddEventsCommand([newEventData])); } else { // Create a note and place it in the chart. var cursorMs = cursorStep * Conductor.stepCrochet; - + var newNoteData:SongNoteData = new SongNoteData(cursorMs, cursorColumn, 0, selectedNoteKind); - + performCommand(new AddNotesCommand([newNoteData])); } - } } } @@ -1149,14 +1173,17 @@ class ChartEditorState extends HaxeUIState } // Handle selection squares. - for (member in renderedNoteSelectionSquares.members) { + for (member in renderedNoteSelectionSquares.members) + { member.kill(); } - for (noteSprite in renderedNotes.members) { - if (isNoteSelected(noteSprite.noteData)) { + for (noteSprite in renderedNotes.members) + { + if (isNoteSelected(noteSprite.noteData)) + { var selectionSquare:FlxSprite = renderedNoteSelectionSquares.recycle(FlxSprite).loadGraphic(selectionSquareBitmap); - + selectionSquare.x = noteSprite.x; selectionSquare.y = noteSprite.y; selectionSquare.width = noteSprite.width; @@ -1474,9 +1501,12 @@ class ChartEditorState extends HaxeUIState this.scrollPosition = value; // Move the grid sprite to the correct position. - if (isViewDownscroll) { + if (isViewDownscroll) + { gridTiledSprite.y = -scrollPosition + (MENU_BAR_HEIGHT + GRID_TOP_PAD); - } else { + } + else + { gridTiledSprite.y = -scrollPosition + (MENU_BAR_HEIGHT + GRID_TOP_PAD); } // Move the rendered notes to the correct position. diff --git a/source/funkin/util/ClipboardUtil.hx b/source/funkin/util/ClipboardUtil.hx new file mode 100644 index 000000000..bd81a2d4d --- /dev/null +++ b/source/funkin/util/ClipboardUtil.hx @@ -0,0 +1,51 @@ +package funkin.util; + +/** + * Utility functions for working with the system clipboard. + * On platforms that don't support interacting with the clipboard, + * an internal clipboard is used (neat!). + */ +class ClipboardUtil +{ + /** + * Add an event listener callback which executes next time the system clipboard is updated. + * + * @param callback The callback to execute when the clipboard is updated. + * @param once If true, the callback will only execute once and then be deleted. + * @param priority Set the priority at which the callback will be executed. Higher values execute first. + */ + public static function addListener(callback:Void->Void, ?once:Bool = false, ?priority:Int = 0):Void + { + lime.system.Clipboard.onUpdate.add(callback, once, priority); + } + + /** + * Remove an event listener callback from the system clipboard. + * + * @param callback The callback to remove. + */ + public static function removeListener(callback:Void->Void):Void + { + lime.system.Clipboard.onUpdate.remove(callback); + } + + /** + * Get the current contents of the system clipboard. + * + * @return The current contents of the system clipboard. + */ + public static function getClipboard():String + { + return lime.system.Clipboard.text; + } + + /** + * Set the contents of the system clipboard. + * + * @param text The text to set the system clipboard to. + */ + public static function setClipboard(text:String):String + { + return lime.system.Clipboard.text = text; + } +}