Merge pull request #275 from FunkinCrew/feature/chart-editor-drag-edit-holds

Click and drag on a sustain to edit it.
This commit is contained in:
Cameron Taylor 2024-01-12 08:15:34 -05:00 committed by GitHub
commit 772c1b2626
11 changed files with 357 additions and 37 deletions

View file

@ -54,14 +54,14 @@
"name": "haxeui-core", "name": "haxeui-core",
"type": "git", "type": "git",
"dir": null, "dir": null,
"ref": "5086e59e7551d775ed4d1fb0188e31de22d1312b", "ref": "2561076c5abeee0a60f3a2a65a8ecb7832a6a62a",
"url": "https://github.com/haxeui/haxeui-core" "url": "https://github.com/haxeui/haxeui-core"
}, },
{ {
"name": "haxeui-flixel", "name": "haxeui-flixel",
"type": "git", "type": "git",
"dir": null, "dir": null,
"ref": "2b9cff727999b53ed292b1675ac1c9089ac77600", "ref": "9c8ab039524086f5a8c8f35b9fb14538b5bfba5d",
"url": "https://github.com/haxeui/haxeui-flixel" "url": "https://github.com/haxeui/haxeui-flixel"
}, },
{ {

View file

@ -826,7 +826,13 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
@:alias("l") @:alias("l")
@:default(0) @:default(0)
@:optional @:optional
public var length:Float; public var length(default, set):Float;
function set_length(value:Float):Float
{
_stepLength = null;
return length = value;
}
/** /**
* The kind of the note. * The kind of the note.
@ -883,6 +889,11 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
return _stepTime = Conductor.instance.getTimeInSteps(this.time); return _stepTime = Conductor.instance.getTimeInSteps(this.time);
} }
/**
* The length of the note, if applicable, in steps.
* Calculated from the length and the BPM.
* Cached for performance. Set to `null` to recalculate.
*/
@:jignored @:jignored
var _stepLength:Null<Float> = null; var _stepLength:Null<Float> = null;
@ -907,9 +918,14 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
} }
else else
{ {
var lengthMs:Float = Conductor.instance.getStepTimeInMs(value) - this.time; var endStep:Float = getStepTime() + value;
var endMs:Float = Conductor.instance.getStepTimeInMs(endStep);
var lengthMs:Float = endMs - this.time;
this.length = lengthMs; this.length = lengthMs;
} }
// Recalculate the step length next time it's requested.
_stepLength = null; _stepLength = null;
} }
@ -980,6 +996,10 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
@:op(A == B) @:op(A == B)
public function op_equals(other:SongNoteData):Bool public function op_equals(other:SongNoteData):Bool
{ {
// Handle the case where one value is null.
if (this == null) return other == null;
if (other == null) return false;
if (this.kind == '') if (this.kind == '')
{ {
if (other.kind != '' && other.kind != 'normal') return false; if (other.kind != '' && other.kind != 'normal') return false;
@ -995,6 +1015,10 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
@:op(A != B) @:op(A != B)
public function op_notEquals(other:SongNoteData):Bool public function op_notEquals(other:SongNoteData):Bool
{ {
// Handle the case where one value is null.
if (this == null) return other == null;
if (other == null) return false;
if (this.kind == '') if (this.kind == '')
{ {
if (other.kind != '' && other.kind != 'normal') return true; if (other.kind != '' && other.kind != 'normal') return true;
@ -1010,24 +1034,32 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
@:op(A > B) @:op(A > B)
public function op_greaterThan(other:SongNoteData):Bool public function op_greaterThan(other:SongNoteData):Bool
{ {
if (other == null) return false;
return this.time > other.time; return this.time > other.time;
} }
@:op(A < B) @:op(A < B)
public function op_lessThan(other:SongNoteData):Bool public function op_lessThan(other:SongNoteData):Bool
{ {
if (other == null) return false;
return this.time < other.time; return this.time < other.time;
} }
@:op(A >= B) @:op(A >= B)
public function op_greaterThanOrEquals(other:SongNoteData):Bool public function op_greaterThanOrEquals(other:SongNoteData):Bool
{ {
if (other == null) return false;
return this.time >= other.time; return this.time >= other.time;
} }
@:op(A <= B) @:op(A <= B)
public function op_lessThanOrEquals(other:SongNoteData):Bool public function op_lessThanOrEquals(other:SongNoteData):Bool
{ {
if (other == null) return false;
return this.time <= other.time; return this.time <= other.time;
} }

View file

@ -82,6 +82,9 @@ class SustainTrail extends FlxSprite
public var isPixel:Bool; public var isPixel:Bool;
var graphicWidth:Float = 0;
var graphicHeight:Float = 0;
/** /**
* Normally you would take strumTime:Float, noteData:Int, sustainLength:Float, parentNote:Note (?) * Normally you would take strumTime:Float, noteData:Int, sustainLength:Float, parentNote:Note (?)
* @param NoteData * @param NoteData
@ -110,8 +113,8 @@ class SustainTrail extends FlxSprite
zoom *= 0.7; zoom *= 0.7;
// CALCULATE SIZE // CALCULATE SIZE
width = graphic.width / 8 * zoom; // amount of notes * 2 graphicWidth = graphic.width / 8 * zoom; // amount of notes * 2
height = sustainHeight(sustainLength, getScrollSpeed()); graphicHeight = sustainHeight(sustainLength, getScrollSpeed());
// instead of scrollSpeed, PlayState.SONG.speed // instead of scrollSpeed, PlayState.SONG.speed
flipY = Preferences.downscroll; flipY = Preferences.downscroll;
@ -148,12 +151,21 @@ class SustainTrail extends FlxSprite
if (sustainLength == s) return s; if (sustainLength == s) return s;
height = sustainHeight(s, getScrollSpeed()); graphicHeight = sustainHeight(s, getScrollSpeed());
this.sustainLength = s; this.sustainLength = s;
updateClipping(); updateClipping();
updateHitbox();
return this.sustainLength; return this.sustainLength;
} }
public override function updateHitbox():Void
{
width = graphicWidth;
height = graphicHeight;
offset.set(0, 0);
origin.set(width * 0.5, height * 0.5);
}
/** /**
* Sets up new vertex and UV data to clip the trail. * Sets up new vertex and UV data to clip the trail.
* If flipY is true, top and bottom bounds swap places. * If flipY is true, top and bottom bounds swap places.
@ -161,7 +173,7 @@ class SustainTrail extends FlxSprite
*/ */
public function updateClipping(songTime:Float = 0):Void public function updateClipping(songTime:Float = 0):Void
{ {
var clipHeight:Float = FlxMath.bound(sustainHeight(sustainLength - (songTime - strumTime), getScrollSpeed()), 0, height); var clipHeight:Float = FlxMath.bound(sustainHeight(sustainLength - (songTime - strumTime), getScrollSpeed()), 0, graphicHeight);
if (clipHeight <= 0.1) if (clipHeight <= 0.1)
{ {
visible = false; visible = false;
@ -178,10 +190,10 @@ class SustainTrail extends FlxSprite
// ===HOLD VERTICES== // ===HOLD VERTICES==
// Top left // Top left
vertices[0 * 2] = 0.0; // Inline with left side vertices[0 * 2] = 0.0; // Inline with left side
vertices[0 * 2 + 1] = flipY ? clipHeight : height - clipHeight; vertices[0 * 2 + 1] = flipY ? clipHeight : graphicHeight - clipHeight;
// Top right // Top right
vertices[1 * 2] = width; vertices[1 * 2] = graphicWidth;
vertices[1 * 2 + 1] = vertices[0 * 2 + 1]; // Inline with top left vertex vertices[1 * 2 + 1] = vertices[0 * 2 + 1]; // Inline with top left vertex
// Bottom left // Bottom left
@ -197,7 +209,7 @@ class SustainTrail extends FlxSprite
} }
// Bottom right // Bottom right
vertices[3 * 2] = width; vertices[3 * 2] = graphicWidth;
vertices[3 * 2 + 1] = vertices[2 * 2 + 1]; // Inline with bottom left vertex vertices[3 * 2 + 1] = vertices[2 * 2 + 1]; // Inline with bottom left vertex
// ===HOLD UVs=== // ===HOLD UVs===
@ -233,7 +245,7 @@ class SustainTrail extends FlxSprite
// Bottom left // Bottom left
vertices[6 * 2] = vertices[2 * 2]; // Inline with left side vertices[6 * 2] = vertices[2 * 2]; // Inline with left side
vertices[6 * 2 + 1] = flipY ? (graphic.height * (-bottomClip + endOffset) * zoom) : (height + graphic.height * (bottomClip - endOffset) * zoom); vertices[6 * 2 + 1] = flipY ? (graphic.height * (-bottomClip + endOffset) * zoom) : (graphicHeight + graphic.height * (bottomClip - endOffset) * zoom);
// Bottom right // Bottom right
vertices[7 * 2] = vertices[3 * 2]; // Inline with right side vertices[7 * 2] = vertices[3 * 2]; // Inline with right side
@ -277,6 +289,10 @@ class SustainTrail extends FlxSprite
getScreenPosition(_point, camera).subtractPoint(offset); getScreenPosition(_point, camera).subtractPoint(offset);
camera.drawTriangles(processedGraphic, vertices, indices, uvtData, null, _point, blend, true, antialiasing); camera.drawTriangles(processedGraphic, vertices, indices, uvtData, null, _point, blend, true, antialiasing);
} }
#if FLX_DEBUG
if (FlxG.debugger.drawDebug) drawDebug();
#end
} }
public override function kill():Void public override function kill():Void

View file

@ -750,7 +750,14 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
* `null` if the user isn't currently placing a note. * `null` if the user isn't currently placing a note.
* As the user drags, we will update this note's sustain length, and finalize the note when they release. * As the user drags, we will update this note's sustain length, and finalize the note when they release.
*/ */
var currentPlaceNoteData:Null<SongNoteData> = null; var currentPlaceNoteData(default, set):Null<SongNoteData> = null;
function set_currentPlaceNoteData(value:Null<SongNoteData>):Null<SongNoteData>
{
noteDisplayDirty = true;
return currentPlaceNoteData = value;
}
// Note Movement // Note Movement
@ -2321,7 +2328,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
bounds.height = MIN_HEIGHT; bounds.height = MIN_HEIGHT;
} }
trace('Note preview viewport bounds: ' + bounds.toString()); // trace('Note preview viewport bounds: ' + bounds.toString());
return bounds; return bounds;
} }
@ -3172,8 +3179,16 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
{ {
if (holdNoteSprite == null || holdNoteSprite.noteData == null || !holdNoteSprite.exists || !holdNoteSprite.visible) continue; if (holdNoteSprite == null || holdNoteSprite.noteData == null || !holdNoteSprite.exists || !holdNoteSprite.visible) continue;
if (!holdNoteSprite.isHoldNoteVisible(FlxG.height - PLAYBAR_HEIGHT, MENU_BAR_HEIGHT)) if (holdNoteSprite.noteData == currentPlaceNoteData)
{ {
// This hold note is for the note we are currently dragging.
// It will be displayed by gridGhostHoldNoteSprite instead.
holdNoteSprite.kill();
}
else if (!holdNoteSprite.isHoldNoteVisible(FlxG.height - MENU_BAR_HEIGHT, GRID_TOP_PAD))
{
// This hold note is off-screen.
// Kill the hold note sprite and recycle it.
holdNoteSprite.kill(); holdNoteSprite.kill();
} }
else if (!currentSongChartNoteData.fastContains(holdNoteSprite.noteData) || holdNoteSprite.noteData.length == 0) else if (!currentSongChartNoteData.fastContains(holdNoteSprite.noteData) || holdNoteSprite.noteData.length == 0)
@ -3191,7 +3206,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
else else
{ {
displayedHoldNoteData.push(holdNoteSprite.noteData); displayedHoldNoteData.push(holdNoteSprite.noteData);
// Update the event sprite's position. // Update the event sprite's height and position.
// var holdNoteHeight = holdNoteSprite.noteData.getStepLength() * GRID_SIZE;
// holdNoteSprite.setHeightDirectly(holdNoteHeight);
holdNoteSprite.updateHoldNotePosition(renderedNotes); holdNoteSprite.updateHoldNotePosition(renderedNotes);
} }
} }
@ -3269,7 +3286,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
noteSprite.updateNotePosition(renderedNotes); noteSprite.updateNotePosition(renderedNotes);
// Add hold notes that are now visible (and not already displayed). // Add hold notes that are now visible (and not already displayed).
if (noteSprite.noteData != null && noteSprite.noteData.length > 0 && displayedHoldNoteData.indexOf(noteSprite.noteData) == -1) if (noteSprite.noteData != null
&& noteSprite.noteData.length > 0
&& displayedHoldNoteData.indexOf(noteSprite.noteData) == -1
&& noteSprite.noteData != currentPlaceNoteData)
{ {
var holdNoteSprite:ChartEditorHoldNoteSprite = renderedHoldNotes.recycle(() -> new ChartEditorHoldNoteSprite(this)); var holdNoteSprite:ChartEditorHoldNoteSprite = renderedHoldNotes.recycle(() -> new ChartEditorHoldNoteSprite(this));
// trace('Creating new HoldNote... (${renderedHoldNotes.members.length})'); // trace('Creating new HoldNote... (${renderedHoldNotes.members.length})');
@ -3282,6 +3302,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
holdNoteSprite.setHeightDirectly(noteLengthPixels); holdNoteSprite.setHeightDirectly(noteLengthPixels);
holdNoteSprite.updateHoldNotePosition(renderedHoldNotes); holdNoteSprite.updateHoldNotePosition(renderedHoldNotes);
trace(holdNoteSprite.x + ', ' + holdNoteSprite.y + ', ' + holdNoteSprite.width + ', ' + holdNoteSprite.height);
} }
} }
@ -3320,6 +3342,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// Is the note a hold note? // Is the note a hold note?
if (noteData == null || noteData.length <= 0) continue; if (noteData == null || noteData.length <= 0) continue;
// Is the note the one we are dragging? If so, ghostHoldNoteSprite will handle it.
if (noteData == currentPlaceNoteData) continue;
// Is the hold note rendered already? // Is the hold note rendered already?
if (displayedHoldNoteData.indexOf(noteData) != -1) continue; if (displayedHoldNoteData.indexOf(noteData) != -1) continue;
@ -3409,7 +3434,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
selectionSquare.x = noteSprite.x; selectionSquare.x = noteSprite.x;
selectionSquare.y = noteSprite.y; selectionSquare.y = noteSprite.y;
selectionSquare.width = GRID_SIZE; selectionSquare.width = GRID_SIZE;
selectionSquare.height = GRID_SIZE;
var stepLength = noteSprite.noteData.getStepLength();
selectionSquare.height = (stepLength <= 0) ? GRID_SIZE : ((stepLength + 1) * GRID_SIZE);
} }
} }
@ -3688,6 +3715,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
var overlapsGrid:Bool = FlxG.mouse.overlaps(gridTiledSprite); var overlapsGrid:Bool = FlxG.mouse.overlaps(gridTiledSprite);
var overlapsRenderedNotes:Bool = FlxG.mouse.overlaps(renderedNotes);
var overlapsRenderedHoldNotes:Bool = FlxG.mouse.overlaps(renderedHoldNotes);
var overlapsRenderedEvents:Bool = FlxG.mouse.overlaps(renderedEvents);
// Cursor position relative to the grid. // Cursor position relative to the grid.
var cursorX:Float = FlxG.mouse.screenX - gridTiledSprite.x; var cursorX:Float = FlxG.mouse.screenX - gridTiledSprite.x;
var cursorY:Float = FlxG.mouse.screenY - gridTiledSprite.y; var cursorY:Float = FlxG.mouse.screenY - gridTiledSprite.y;
@ -3929,12 +3960,18 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
return event.alive && FlxG.mouse.overlaps(event); return event.alive && FlxG.mouse.overlaps(event);
}); });
} }
var highlightedHoldNote:Null<ChartEditorHoldNoteSprite> = null;
if (highlightedNote == null && highlightedEvent == null)
{
highlightedHoldNote = renderedHoldNotes.members.find(function(holdNote:ChartEditorHoldNoteSprite):Bool {
return holdNote.alive && FlxG.mouse.overlaps(holdNote);
});
}
if (FlxG.keys.pressed.CONTROL) if (FlxG.keys.pressed.CONTROL)
{ {
if (highlightedNote != null && highlightedNote.noteData != null) if (highlightedNote != null && highlightedNote.noteData != null)
{ {
// TODO: Handle the case of clicking on a sustain piece.
// Control click to select/deselect an individual note. // Control click to select/deselect an individual note.
if (isNoteSelected(highlightedNote.noteData)) if (isNoteSelected(highlightedNote.noteData))
{ {
@ -3957,6 +3994,18 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
performCommand(new SelectItemsCommand([], [highlightedEvent.eventData])); performCommand(new SelectItemsCommand([], [highlightedEvent.eventData]));
} }
} }
else if (highlightedHoldNote != null && highlightedHoldNote.noteData != null)
{
// Control click to select/deselect an individual note.
if (isNoteSelected(highlightedNote.noteData))
{
performCommand(new DeselectItemsCommand([highlightedHoldNote.noteData], []));
}
else
{
performCommand(new SelectItemsCommand([highlightedHoldNote.noteData], []));
}
}
else else
{ {
// Do nothing if you control-clicked on an empty space. // Do nothing if you control-clicked on an empty space.
@ -3974,6 +4023,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// Click an event to select it. // Click an event to select it.
performCommand(new SetItemSelectionCommand([], [highlightedEvent.eventData])); performCommand(new SetItemSelectionCommand([], [highlightedEvent.eventData]));
} }
else if (highlightedHoldNote != null && highlightedHoldNote.noteData != null)
{
// Click a hold note to select it.
performCommand(new SetItemSelectionCommand([highlightedHoldNote.noteData], []));
}
else else
{ {
// Click on an empty space to deselect everything. // Click on an empty space to deselect everything.
@ -4126,7 +4180,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
var dragLengthMs:Float = dragLengthSteps * Conductor.instance.stepLengthMs; var dragLengthMs:Float = dragLengthSteps * Conductor.instance.stepLengthMs;
var dragLengthPixels:Float = dragLengthSteps * GRID_SIZE; var dragLengthPixels:Float = dragLengthSteps * GRID_SIZE;
if (gridGhostNote != null && gridGhostNote.noteData != null && gridGhostHoldNote != null) if (gridGhostHoldNote != null)
{ {
if (dragLengthSteps > 0) if (dragLengthSteps > 0)
{ {
@ -4139,8 +4193,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
} }
gridGhostHoldNote.visible = true; gridGhostHoldNote.visible = true;
gridGhostHoldNote.noteData = gridGhostNote.noteData; gridGhostHoldNote.noteData = currentPlaceNoteData;
gridGhostHoldNote.noteDirection = gridGhostNote.noteData.getDirection(); gridGhostHoldNote.noteDirection = currentPlaceNoteData.getDirection();
gridGhostHoldNote.setHeightDirectly(dragLengthPixels, true); gridGhostHoldNote.setHeightDirectly(dragLengthPixels, true);
@ -4161,6 +4215,15 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// Apply the new length. // Apply the new length.
performCommand(new ExtendNoteLengthCommand(currentPlaceNoteData, dragLengthMs)); performCommand(new ExtendNoteLengthCommand(currentPlaceNoteData, dragLengthMs));
} }
else
{
// Apply the new (zero) length if we are changing the length.
if (currentPlaceNoteData.length > 0)
{
this.playSound(Paths.sound('chartingSounds/stretchSNAP_UI'));
performCommand(new ExtendNoteLengthCommand(currentPlaceNoteData, 0));
}
}
// Finished dragging. Release the note. // Finished dragging. Release the note.
currentPlaceNoteData = null; currentPlaceNoteData = null;
@ -4193,6 +4256,14 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
return event.alive && FlxG.mouse.overlaps(event); return event.alive && FlxG.mouse.overlaps(event);
}); });
} }
var highlightedHoldNote:Null<ChartEditorHoldNoteSprite> = null;
if (highlightedNote == null && highlightedEvent == null)
{
highlightedHoldNote = renderedHoldNotes.members.find(function(holdNote:ChartEditorHoldNoteSprite):Bool {
// If holdNote.alive is false, the holdNote is dead and awaiting recycling.
return holdNote.alive && FlxG.mouse.overlaps(holdNote);
});
}
if (FlxG.keys.pressed.CONTROL) if (FlxG.keys.pressed.CONTROL)
{ {
@ -4219,6 +4290,17 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
performCommand(new SelectItemsCommand([], [highlightedEvent.eventData])); performCommand(new SelectItemsCommand([], [highlightedEvent.eventData]));
} }
} }
else if (highlightedHoldNote != null && highlightedHoldNote.noteData != null)
{
if (isNoteSelected(highlightedNote.noteData))
{
performCommand(new DeselectItemsCommand([highlightedHoldNote.noteData], []));
}
else
{
performCommand(new SelectItemsCommand([highlightedHoldNote.noteData], []));
}
}
else else
{ {
// Do nothing when control clicking nothing. // Do nothing when control clicking nothing.
@ -4252,6 +4334,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
performCommand(new SetItemSelectionCommand([], [highlightedEvent.eventData])); performCommand(new SetItemSelectionCommand([], [highlightedEvent.eventData]));
} }
} }
else if (highlightedHoldNote != null && highlightedHoldNote.noteData != null)
{
// Clicked a hold note, start dragging TO EXTEND NOTE LENGTH.
currentPlaceNoteData = highlightedHoldNote.noteData;
}
else else
{ {
// Click a blank space to place a note and select it. // Click a blank space to place a note and select it.
@ -4301,6 +4388,14 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
return event.alive && FlxG.mouse.overlaps(event); return event.alive && FlxG.mouse.overlaps(event);
}); });
} }
var highlightedHoldNote:Null<ChartEditorHoldNoteSprite> = null;
if (highlightedNote == null && highlightedEvent == null)
{
highlightedHoldNote = renderedHoldNotes.members.find(function(holdNote:ChartEditorHoldNoteSprite):Bool {
// If holdNote.alive is false, the holdNote is dead and awaiting recycling.
return holdNote.alive && FlxG.mouse.overlaps(holdNote);
});
}
if (highlightedNote != null && highlightedNote.noteData != null) if (highlightedNote != null && highlightedNote.noteData != null)
{ {
@ -4352,13 +4447,40 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
performCommand(new RemoveEventsCommand([highlightedEvent.eventData])); performCommand(new RemoveEventsCommand([highlightedEvent.eventData]));
} }
} }
else if (highlightedHoldNote != null && highlightedHoldNote.noteData != null)
{
if (FlxG.keys.pressed.SHIFT)
{
// Shift + Right click opens the context menu.
// If we are clicking a large selection, open the Selection context menu, otherwise open the Note context menu.
var isHighlightedNoteSelected:Bool = isNoteSelected(highlightedHoldNote.noteData);
var useSingleNoteContextMenu:Bool = (!isHighlightedNoteSelected)
|| (isHighlightedNoteSelected && currentNoteSelection.length == 1);
// Show the context menu connected to the note.
if (useSingleNoteContextMenu)
{
this.openHoldNoteContextMenu(FlxG.mouse.screenX, FlxG.mouse.screenY, highlightedHoldNote.noteData);
}
else
{
this.openSelectionContextMenu(FlxG.mouse.screenX, FlxG.mouse.screenY);
}
}
else
{
// Right click removes hold from the note.
this.playSound(Paths.sound('chartingSounds/stretchSNAP_UI'));
performCommand(new ExtendNoteLengthCommand(highlightedHoldNote.noteData, 0));
}
}
else else
{ {
// Right clicked on nothing. // Right clicked on nothing.
} }
} }
var isOrWillSelect = overlapsSelection || dragTargetNote != null || dragTargetEvent != null; var isOrWillSelect = overlapsSelection || dragTargetNote != null || dragTargetEvent != null || overlapsRenderedNotes || overlapsRenderedHoldNotes
|| overlapsRenderedEvents;
// Handle grid cursor. // Handle grid cursor.
if (!isCursorOverHaxeUI && overlapsGrid && !isOrWillSelect && !overlapsSelectionBorder && !gridPlayheadScrollAreaPressed) if (!isCursorOverHaxeUI && overlapsGrid && !isOrWillSelect && !overlapsSelectionBorder && !gridPlayheadScrollAreaPressed)
{ {
@ -4449,6 +4571,18 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
{ {
targetCursorMode = Crosshair; targetCursorMode = Crosshair;
} }
else if (overlapsRenderedNotes)
{
targetCursorMode = Pointer;
}
else if (overlapsRenderedHoldNotes)
{
targetCursorMode = Pointer;
}
else if (overlapsRenderedEvents)
{
targetCursorMode = Pointer;
}
else if (overlapsGrid) else if (overlapsGrid)
{ {
targetCursorMode = Cell; targetCursorMode = Cell;
@ -5177,7 +5311,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
throw "ERROR: Tried to build selection square, but selectionSquareBitmap is null! Check ChartEditorThemeHandler.updateSelectionSquare()"; throw "ERROR: Tried to build selection square, but selectionSquareBitmap is null! Check ChartEditorThemeHandler.updateSelectionSquare()";
// FlxG.bitmapLog.add(selectionSquareBitmap, "selectionSquareBitmap"); // FlxG.bitmapLog.add(selectionSquareBitmap, "selectionSquareBitmap");
var result = new ChartEditorSelectionSquareSprite(); var result = new ChartEditorSelectionSquareSprite(this);
result.loadGraphic(selectionSquareBitmap); result.loadGraphic(selectionSquareBitmap);
return result; return result;
} }
@ -5510,7 +5644,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
/** /**
* HAXEUI FUNCTIONS * HAXEUI FUNCTIONS
*/ */
// ==================== // ==================
/** /**
* Set the currently selected item in the Difficulty tree view to the node representing the current difficulty. * Set the currently selected item in the Difficulty tree view to the node representing the current difficulty.
@ -5601,7 +5735,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
/** /**
* STATIC FUNCTIONS * STATIC FUNCTIONS
*/ */
// ==================== // ==================
function handleNotePreview():Void function handleNotePreview():Void
{ {

View file

@ -13,17 +13,25 @@ class ExtendNoteLengthCommand implements ChartEditorCommand
var note:SongNoteData; var note:SongNoteData;
var oldLength:Float; var oldLength:Float;
var newLength:Float; var newLength:Float;
var unit:Unit;
public function new(note:SongNoteData, newLength:Float) public function new(note:SongNoteData, newLength:Float, unit:Unit = MILLISECONDS)
{ {
this.note = note; this.note = note;
this.oldLength = note.length; this.oldLength = note.length;
this.newLength = newLength; this.newLength = newLength;
this.unit = unit;
} }
public function execute(state:ChartEditorState):Void public function execute(state:ChartEditorState):Void
{ {
note.length = newLength; switch (unit)
{
case MILLISECONDS:
this.note.length = newLength;
case STEPS:
this.note.setStepLength(newLength);
}
state.saveDataDirty = true; state.saveDataDirty = true;
state.noteDisplayDirty = true; state.noteDisplayDirty = true;
@ -36,7 +44,8 @@ class ExtendNoteLengthCommand implements ChartEditorCommand
{ {
state.playSound(Paths.sound('chartingSounds/undo')); state.playSound(Paths.sound('chartingSounds/undo'));
note.length = oldLength; // Always use milliseconds for undoing
this.note.length = oldLength;
state.saveDataDirty = true; state.saveDataDirty = true;
state.noteDisplayDirty = true; state.noteDisplayDirty = true;
@ -53,6 +62,23 @@ class ExtendNoteLengthCommand implements ChartEditorCommand
public function toString():String public function toString():String
{ {
return 'Extend Note Length'; if (oldLength == 0)
{
return 'Add Hold to Note';
}
else if (newLength == 0)
{
return 'Remove Hold from Note';
}
else
{
return 'Extend Hold Note Length';
}
} }
} }
enum Unit
{
MILLISECONDS;
STEPS;
}

View file

@ -39,6 +39,17 @@ class ChartEditorHoldNoteSprite extends SustainTrail
setup(); setup();
} }
public override function updateHitbox():Void
{
// Expand the clickable hitbox to the full column width, then nudge to the left to re-center it.
width = ChartEditorState.GRID_SIZE;
height = graphicHeight;
var xOffset = (ChartEditorState.GRID_SIZE - graphicWidth) / 2;
offset.set(-xOffset, 0);
origin.set(width * 0.5, height * 0.5);
}
/** /**
* Set the height directly, to a value in pixels. * Set the height directly, to a value in pixels.
* @param h The desired height in pixels. * @param h The desired height in pixels.
@ -52,6 +63,23 @@ class ChartEditorHoldNoteSprite extends SustainTrail
fullSustainLength = sustainLength; fullSustainLength = sustainLength;
} }
/**
* Call this to override how debug bounding boxes are drawn for this sprite.
*/
public override function drawDebugOnCamera(camera:flixel.FlxCamera):Void
{
if (!camera.visible || !camera.exists || !isOnScreen(camera)) return;
var rect = getBoundingBox(camera);
trace('hold note bounding box: ' + rect.x + ', ' + rect.y + ', ' + rect.width + ', ' + rect.height);
var gfx = beginDrawDebug(camera);
debugBoundingBoxColor = 0xffFF66FF;
gfx.lineStyle(2, color, 0.5); // thickness, color, alpha
gfx.drawRect(rect.x, rect.y, rect.width, rect.height);
endDrawDebug(camera);
}
function setup():Void function setup():Void
{ {
strumTime = 999999999; strumTime = 999999999;
@ -60,7 +88,9 @@ class ChartEditorHoldNoteSprite extends SustainTrail
active = true; active = true;
visible = true; visible = true;
alpha = 1.0; alpha = 1.0;
width = graphic.width / 8 * zoom; // amount of notes * 2 graphicWidth = graphic.width / 8 * zoom; // amount of notes * 2
updateHitbox();
} }
public override function revive():Void public override function revive():Void
@ -154,7 +184,7 @@ class ChartEditorHoldNoteSprite extends SustainTrail
} }
this.x += ChartEditorState.GRID_SIZE / 2; this.x += ChartEditorState.GRID_SIZE / 2;
this.x -= this.width / 2; this.x -= this.graphicWidth / 2;
this.y += ChartEditorState.GRID_SIZE / 2; this.y += ChartEditorState.GRID_SIZE / 2;
@ -163,5 +193,8 @@ class ChartEditorHoldNoteSprite extends SustainTrail
this.x += origin.x; this.x += origin.x;
this.y += origin.y; this.y += origin.y;
} }
// Account for expanded clickable hitbox.
this.x += this.offset.x;
} }
} }

View file

@ -1,20 +1,33 @@
package funkin.ui.debug.charting.components; package funkin.ui.debug.charting.components;
import flixel.addons.display.FlxSliceSprite;
import flixel.FlxSprite; import flixel.FlxSprite;
import funkin.data.song.SongData.SongNoteData; import flixel.math.FlxRect;
import funkin.data.song.SongData.SongEventData; import funkin.data.song.SongData.SongEventData;
import funkin.data.song.SongData.SongNoteData;
import funkin.ui.debug.charting.handlers.ChartEditorThemeHandler;
/** /**
* A sprite that can be used to display a square over a selected note or event in the chart. * A sprite that can be used to display a square over a selected note or event in the chart.
* Designed to be used and reused efficiently. Has no gameplay functionality. * Designed to be used and reused efficiently. Has no gameplay functionality.
*/ */
class ChartEditorSelectionSquareSprite extends FlxSprite @:nullSafety
@:access(funkin.ui.debug.charting.ChartEditorState)
class ChartEditorSelectionSquareSprite extends FlxSliceSprite
{ {
public var noteData:Null<SongNoteData>; public var noteData:Null<SongNoteData>;
public var eventData:Null<SongEventData>; public var eventData:Null<SongEventData>;
public function new() public function new(chartEditorState:ChartEditorState)
{ {
super(); super(chartEditorState.selectionSquareBitmap,
new FlxRect(ChartEditorThemeHandler.SELECTION_SQUARE_BORDER_WIDTH
+ 4, ChartEditorThemeHandler.SELECTION_SQUARE_BORDER_WIDTH
+ 4,
ChartEditorState.GRID_SIZE
- (2 * ChartEditorThemeHandler.SELECTION_SQUARE_BORDER_WIDTH + 8),
ChartEditorState.GRID_SIZE
- (2 * ChartEditorThemeHandler.SELECTION_SQUARE_BORDER_WIDTH + 8)),
32, 32);
} }
} }

View file

@ -0,0 +1,43 @@
package funkin.ui.debug.charting.contextmenus;
import haxe.ui.containers.menus.Menu;
import haxe.ui.containers.menus.MenuItem;
import haxe.ui.core.Screen;
import funkin.data.song.SongData.SongNoteData;
import funkin.ui.debug.charting.commands.FlipNotesCommand;
import funkin.ui.debug.charting.commands.RemoveNotesCommand;
import funkin.ui.debug.charting.commands.ExtendNoteLengthCommand;
@:access(funkin.ui.debug.charting.ChartEditorState)
@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/context-menus/hold-note.xml"))
class ChartEditorHoldNoteContextMenu extends ChartEditorBaseContextMenu
{
var contextmenuFlip:MenuItem;
var contextmenuDelete:MenuItem;
var data:SongNoteData;
public function new(chartEditorState2:ChartEditorState, xPos2:Float = 0, yPos2:Float = 0, data:SongNoteData)
{
super(chartEditorState2, xPos2, yPos2);
this.data = data;
initialize();
}
function initialize():Void
{
// NOTE: Remember to use commands here to ensure undo/redo works properly
contextmenuFlip.onClick = function(_) {
chartEditorState.performCommand(new FlipNotesCommand([data]));
}
contextmenuRemoveHold.onClick = function(_) {
chartEditorState.performCommand(new ExtendNoteLengthCommand(data, 0));
}
contextmenuDelete.onClick = function(_) {
chartEditorState.performCommand(new RemoveNotesCommand([data]));
}
}
}

View file

@ -6,6 +6,7 @@ import haxe.ui.core.Screen;
import funkin.data.song.SongData.SongNoteData; import funkin.data.song.SongData.SongNoteData;
import funkin.ui.debug.charting.commands.FlipNotesCommand; import funkin.ui.debug.charting.commands.FlipNotesCommand;
import funkin.ui.debug.charting.commands.RemoveNotesCommand; import funkin.ui.debug.charting.commands.RemoveNotesCommand;
import funkin.ui.debug.charting.commands.ExtendNoteLengthCommand;
@:access(funkin.ui.debug.charting.ChartEditorState) @:access(funkin.ui.debug.charting.ChartEditorState)
@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/context-menus/note.xml")) @:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/context-menus/note.xml"))
@ -31,6 +32,10 @@ class ChartEditorNoteContextMenu extends ChartEditorBaseContextMenu
chartEditorState.performCommand(new FlipNotesCommand([data])); chartEditorState.performCommand(new FlipNotesCommand([data]));
} }
contextmenuAddHold.onClick = function(_) {
chartEditorState.performCommand(new ExtendNoteLengthCommand(data, 4, STEPS));
}
contextmenuDelete.onClick = function(_) { contextmenuDelete.onClick = function(_) {
chartEditorState.performCommand(new RemoveNotesCommand([data])); chartEditorState.performCommand(new RemoveNotesCommand([data]));
} }

View file

@ -2,6 +2,7 @@ package funkin.ui.debug.charting.handlers;
import funkin.ui.debug.charting.contextmenus.ChartEditorDefaultContextMenu; import funkin.ui.debug.charting.contextmenus.ChartEditorDefaultContextMenu;
import funkin.ui.debug.charting.contextmenus.ChartEditorEventContextMenu; import funkin.ui.debug.charting.contextmenus.ChartEditorEventContextMenu;
import funkin.ui.debug.charting.contextmenus.ChartEditorHoldNoteContextMenu;
import funkin.ui.debug.charting.contextmenus.ChartEditorNoteContextMenu; import funkin.ui.debug.charting.contextmenus.ChartEditorNoteContextMenu;
import funkin.ui.debug.charting.contextmenus.ChartEditorSelectionContextMenu; import funkin.ui.debug.charting.contextmenus.ChartEditorSelectionContextMenu;
import haxe.ui.containers.menus.Menu; import haxe.ui.containers.menus.Menu;
@ -23,16 +24,33 @@ class ChartEditorContextMenuHandler
displayMenu(state, new ChartEditorDefaultContextMenu(state, xPos, yPos)); displayMenu(state, new ChartEditorDefaultContextMenu(state, xPos, yPos));
} }
/**
* Opened when shift+right-clicking a selection of multiple items.
*/
public static function openSelectionContextMenu(state:ChartEditorState, xPos:Float, yPos:Float) public static function openSelectionContextMenu(state:ChartEditorState, xPos:Float, yPos:Float)
{ {
displayMenu(state, new ChartEditorSelectionContextMenu(state, xPos, yPos)); displayMenu(state, new ChartEditorSelectionContextMenu(state, xPos, yPos));
} }
/**
* Opened when shift+right-clicking a single note.
*/
public static function openNoteContextMenu(state:ChartEditorState, xPos:Float, yPos:Float, data:SongNoteData) public static function openNoteContextMenu(state:ChartEditorState, xPos:Float, yPos:Float, data:SongNoteData)
{ {
displayMenu(state, new ChartEditorNoteContextMenu(state, xPos, yPos, data)); displayMenu(state, new ChartEditorNoteContextMenu(state, xPos, yPos, data));
} }
/**
* Opened when shift+right-clicking a single hold note.
*/
public static function openHoldNoteContextMenu(state:ChartEditorState, xPos:Float, yPos:Float, data:SongNoteData)
{
displayMenu(state, new ChartEditorHoldNoteContextMenu(state, xPos, yPos, data));
}
/**
* Opened when shift+right-clicking a single event.
*/
public static function openEventContextMenu(state:ChartEditorState, xPos:Float, yPos:Float, data:SongEventData) public static function openEventContextMenu(state:ChartEditorState, xPos:Float, yPos:Float, data:SongEventData)
{ {
displayMenu(state, new ChartEditorEventContextMenu(state, xPos, yPos, data)); displayMenu(state, new ChartEditorEventContextMenu(state, xPos, yPos, data));

View file

@ -52,7 +52,7 @@ class ChartEditorThemeHandler
// Border on the square highlighting selected notes. // Border on the square highlighting selected notes.
static final SELECTION_SQUARE_BORDER_COLOR_LIGHT:FlxColor = 0xFF339933; static final SELECTION_SQUARE_BORDER_COLOR_LIGHT:FlxColor = 0xFF339933;
static final SELECTION_SQUARE_BORDER_COLOR_DARK:FlxColor = 0xFF339933; static final SELECTION_SQUARE_BORDER_COLOR_DARK:FlxColor = 0xFF339933;
static final SELECTION_SQUARE_BORDER_WIDTH:Int = 1; public static final SELECTION_SQUARE_BORDER_WIDTH:Int = 1;
// Fill on the square highlighting selected notes. // Fill on the square highlighting selected notes.
// Make sure this is transparent so you can see the notes underneath. // Make sure this is transparent so you can see the notes underneath.