1
0
Fork 0
mirror of https://github.com/ninjamuffin99/Funkin.git synced 2025-01-04 19:28:30 +00:00

Tooltips when hovering over chart events

This commit is contained in:
EliteMasterEric 2024-01-04 10:00:39 -05:00
parent e1b92e8829
commit 336810b628
10 changed files with 217 additions and 35 deletions

View file

@ -54,14 +54,14 @@
"name": "haxeui-core",
"type": "git",
"dir": null,
"ref": "e765a3e0b7a653823e8dec765e04623f27f573f8",
"ref": "67c5700e253ff8892589a95945a7799f34ae4df0",
"url": "https://github.com/haxeui/haxeui-core"
},
{
"name": "haxeui-flixel",
"type": "git",
"dir": null,
"ref": "7a517d561eff49d8123c128bf9f5c1123b84d014",
"ref": "2b9cff727999b53ed292b1675ac1c9089ac77600",
"url": "https://github.com/haxeui/haxeui-flixel"
},
{

View file

@ -161,35 +161,71 @@ class SongEventParser
}
}
enum abstract SongEventFieldType(String) from String to String
@:forward(name, title, type, keys, min, max, step, defaultValue, iterator)
abstract SongEventSchema(SongEventSchemaRaw)
{
/**
* The STRING type will display as a text field.
*/
var STRING = "string";
public function new(?fields:Array<SongEventSchemaField>)
{
this = fields;
}
/**
* The INTEGER type will display as a text field that only accepts numbers.
*/
var INTEGER = "integer";
@:arrayAccess
public function getByName(name:String):SongEventSchemaField
{
for (field in this)
{
if (field.name == name) return field;
}
/**
* The FLOAT type will display as a text field that only accepts numbers.
*/
var FLOAT = "float";
return null;
}
/**
* The BOOL type will display as a checkbox.
*/
var BOOL = "bool";
public function getFirstField():SongEventSchemaField
{
return this[0];
}
/**
* The ENUM type will display as a dropdown.
* Make sure to specify the `keys` field in the schema.
*/
var ENUM = "enum";
public function stringifyFieldValue(name:String, value:Dynamic):String
{
var field:SongEventSchemaField = getByName(name);
if (field == null) return 'Unknown';
switch (field.type)
{
case SongEventFieldType.STRING:
return Std.string(value);
case SongEventFieldType.INTEGER:
return Std.string(value);
case SongEventFieldType.FLOAT:
return Std.string(value);
case SongEventFieldType.BOOL:
return Std.string(value);
case SongEventFieldType.ENUM:
for (key in field.keys.keys())
{
if (field.keys.get(key) == value) return key;
}
return Std.string(value);
default:
return 'Unknown';
}
}
@:arrayAccess
public inline function get(key:Int)
{
return this[key];
}
@:arrayAccess
public inline function arrayWrite(k:Int, v:SongEventSchemaField):SongEventSchemaField
{
return this[k] = v;
}
}
typedef SongEventSchemaRaw = Array<SongEventSchemaField>;
typedef SongEventSchemaField =
{
/**
@ -240,4 +276,31 @@ typedef SongEventSchemaField =
?defaultValue:Dynamic,
}
typedef SongEventSchema = Array<SongEventSchemaField>;
enum abstract SongEventFieldType(String) from String to String
{
/**
* The STRING type will display as a text field.
*/
var STRING = "string";
/**
* The INTEGER type will display as a text field that only accepts numbers.
*/
var INTEGER = "integer";
/**
* The FLOAT type will display as a text field that only accepts numbers.
*/
var FLOAT = "float";
/**
* The BOOL type will display as a checkbox.
*/
var BOOL = "bool";
/**
* The ENUM type will display as a dropdown.
* Make sure to specify the `keys` field in the schema.
*/
var ENUM = "enum";
}

View file

@ -1,5 +1,8 @@
package funkin.data.song;
import funkin.play.event.SongEvent;
import funkin.data.event.SongEventData.SongEventParser;
import funkin.data.event.SongEventData.SongEventSchema;
import funkin.data.song.SongRegistry;
import thx.semver.Version;
@ -617,6 +620,38 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
this = new SongEventDataRaw(time, event, value);
}
public inline function valueAsStruct(?defaultKey:String = "key"):Dynamic
{
if (this.value == null) return {};
if (Std.isOfType(this.value, Array))
{
var result:haxe.DynamicAccess<Dynamic> = {};
result.set(defaultKey, this.value);
return cast result;
}
else if (Reflect.isObject(this.value))
{
// We enter this case if the value is a struct.
return cast this.value;
}
else
{
var result:haxe.DynamicAccess<Dynamic> = {};
result.set(defaultKey, this.value);
return cast result;
}
}
public inline function getHandler():Null<SongEvent>
{
return SongEventParser.getEvent(this.event);
}
public inline function getSchema():Null<SongEventSchema>
{
return SongEventParser.getEventSchema(this.event);
}
public inline function getDynamic(key:String):Null<Dynamic>
{
return this.value == null ? null : Reflect.field(this.value, key);
@ -662,6 +697,32 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
return this.value == null ? null : cast Reflect.field(this.value, key);
}
public function buildTooltip():String
{
var eventHandler = getHandler();
var eventSchema = getSchema();
if (eventSchema == null) return 'Unknown Event: ${this.event}';
var result = '${eventHandler.getTitle()}';
var defaultKey = eventSchema.getFirstField()?.name;
var valueStruct:haxe.DynamicAccess<Dynamic> = valueAsStruct(defaultKey);
for (pair in valueStruct.keyValueIterator())
{
var key = pair.key;
var value = pair.value;
var title = eventSchema.getByName(key)?.title ?? 'UnknownField';
var valueStr = eventSchema.stringifyFieldValue(key, value);
result += '\n- ${title}: ${valueStr}';
}
return result;
}
public function clone():SongEventData
{
return new SongEventData(this.time, this.event, this.value);

View file

@ -132,7 +132,7 @@ class FocusCameraSongEvent extends SongEvent
*/
public override function getEventSchema():SongEventSchema
{
return [
return new SongEventSchema([
{
name: "char",
title: "Character",
@ -154,6 +154,6 @@ class FocusCameraSongEvent extends SongEvent
step: 10.0,
type: SongEventFieldType.FLOAT,
}
];
]);
}
}

View file

@ -89,7 +89,7 @@ class PlayAnimationSongEvent extends SongEvent
*/
public override function getEventSchema():SongEventSchema
{
return [
return new SongEventSchema([
{
name: 'target',
title: 'Target',
@ -108,6 +108,6 @@ class PlayAnimationSongEvent extends SongEvent
type: SongEventFieldType.BOOL,
defaultValue: false
}
];
]);
}
}

View file

@ -72,7 +72,7 @@ class SetCameraBopSongEvent extends SongEvent
*/
public override function getEventSchema():SongEventSchema
{
return [
return new SongEventSchema([
{
name: 'intensity',
title: 'Intensity',
@ -87,6 +87,6 @@ class SetCameraBopSongEvent extends SongEvent
step: 1,
type: SongEventFieldType.INTEGER,
}
];
]);
}
}

View file

@ -99,7 +99,7 @@ class ZoomCameraSongEvent extends SongEvent
*/
public override function getEventSchema():SongEventSchema
{
return [
return new SongEventSchema([
{
name: 'zoom',
title: 'Zoom Level',
@ -145,6 +145,6 @@ class ZoomCameraSongEvent extends SongEvent
'Elastic In/Out' => 'elasticInOut',
]
}
];
]);
}
}

View file

@ -2113,7 +2113,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
add(gridGhostHoldNote);
gridGhostHoldNote.zIndex = 11;
gridGhostEvent = new ChartEditorEventSprite(this);
gridGhostEvent = new ChartEditorEventSprite(this, true);
gridGhostEvent.alpha = 0.6;
gridGhostEvent.eventData = new SongEventData(-1, '', {});
gridGhostEvent.visible = false;
@ -3127,6 +3127,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// Setting event data resets position relative to the grid so we fix that.
eventSprite.x += renderedEvents.x;
eventSprite.y += renderedEvents.y;
eventSprite.updateTooltipPosition();
}
// Add hold notes that have been made visible (but not their parents)

View file

@ -11,6 +11,9 @@ import flixel.graphics.frames.FlxFramesCollection;
import flixel.graphics.frames.FlxTileFrames;
import flixel.math.FlxPoint;
import funkin.data.song.SongData.SongEventData;
import haxe.ui.tooltips.ToolTipRegionOptions;
import funkin.util.HaxeUIUtil;
import haxe.ui.tooltips.ToolTipManager;
/**
* A sprite that can be used to display a song event in a chart.
@ -36,6 +39,13 @@ class ChartEditorEventSprite extends FlxSprite
public var overrideStepTime(default, set):Null<Float> = null;
public var tooltip:ToolTipRegionOptions;
/**
* Whether this sprite is a "ghost" sprite used when hovering to place a new event.
*/
public var isGhost:Bool = false;
function set_overrideStepTime(value:Null<Float>):Null<Float>
{
if (overrideStepTime == value) return overrideStepTime;
@ -45,12 +55,14 @@ class ChartEditorEventSprite extends FlxSprite
return overrideStepTime;
}
public function new(parent:ChartEditorState)
public function new(parent:ChartEditorState, isGhost:Bool = false)
{
super();
this.parentState = parent;
this.isGhost = isGhost;
this.tooltip = HaxeUIUtil.buildTooltip('N/A');
this.frames = buildFrames();
buildAnimations();
@ -140,6 +152,7 @@ class ChartEditorEventSprite extends FlxSprite
// Disown parent. MAKE SURE TO REVIVE BEFORE REUSING
this.kill();
this.visible = false;
updateTooltipPosition();
return null;
}
else
@ -151,6 +164,8 @@ class ChartEditorEventSprite extends FlxSprite
this.eventData = value;
// Update the position to match the note data.
updateEventPosition();
// Update the tooltip text.
this.tooltip.tipData = {text: this.eventData.buildTooltip()};
return this.eventData;
}
}
@ -169,6 +184,31 @@ class ChartEditorEventSprite extends FlxSprite
this.x += origin.x;
this.y += origin.y;
}
this.updateTooltipPosition();
}
public function updateTooltipPosition():Void
{
// No tooltip for ghost sprites.
if (this.isGhost) return;
if (this.eventData == null)
{
// Disable the tooltip.
ToolTipManager.instance.unregisterTooltipRegion(this.tooltip);
}
else
{
// Update the position.
this.tooltip.left = this.x;
this.tooltip.top = this.y;
this.tooltip.width = this.width;
this.tooltip.height = this.height;
// Enable the tooltip.
ToolTipManager.instance.registerTooltipRegion(this.tooltip);
}
}
/**

View file

@ -0,0 +1,17 @@
package funkin.util;
import haxe.ui.tooltips.ToolTipRegionOptions;
class HaxeUIUtil
{
public static function buildTooltip(text:String, ?left:Float, ?top:Float, ?width:Float, ?height:Float):ToolTipRegionOptions
{
return {
tipData: {text: text},
left: left ?? 0.0,
top: top ?? 0.0,
width: width ?? 0.0,
height: height ?? 0.0
}
}
}