Clipboard rework

This commit is contained in:
Eric Myllyoja 2022-10-11 03:14:57 -04:00
parent 175908f827
commit 7d21d80915
6 changed files with 233 additions and 137 deletions

View file

@ -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},
]
}

View file

@ -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;
}
}

View file

@ -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<SongNoteData>, offset:Int):Array<SongNoteData> {
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<SongNoteData>, offset:Int):Array<SongNoteData>
{
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<SongNoteData>, subtrahend:Array<SongNoteData>) {
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<SongNoteData>, subtrahend:Array<SongNoteData>)
{
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<SongEventData>, subtrahend:Array<SongEventData>)
{
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<SongEventData>, subtrahend:Array<SongEventData>) {
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<SongNoteData>):Array<SongNoteData> {
return offsetSongNoteData(notes, -notes[0].time);
}
}
/**
* 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<SongNoteData>):Array<SongNoteData>
{
return offsetSongNoteData(notes, -notes[0].time);
}
}

View file

@ -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<SongNoteData>;
public function new(notes:SongNoteData)
public function new(notes:Array<SongNoteData>)
{
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<SongNoteData>;
public function new(note:SongNoteData)
public function new(notes:Array<SongNoteData>)
{
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<SongNoteData>;
@ -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<SongNoteData>;
private var previousSelection:Array<SongNoteData>;
public function new(notes:Array<SongNoteData>, ?previousSelection:Array<SongNoteData> = [])
public function new(notes:Array<SongNoteData>, ?previousSelection:Array<SongNoteData>)
{
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<SongEventData>;
// private var previousSelection:Array<SongEventData>;
public function new(events:Array<SongEventData>, ?previousSelection:Array<SongEventData>)
@ -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';
}
}
}

View file

@ -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.

View file

@ -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;
}
}