From 336810b628631b0e1dcb67b25774bac1ce815395 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Thu, 4 Jan 2024 10:00:39 -0500 Subject: [PATCH] Tooltips when hovering over chart events --- hmm.json | 4 +- source/funkin/data/event/SongEventData.hx | 109 ++++++++++++++---- source/funkin/data/song/SongData.hx | 61 ++++++++++ .../funkin/play/event/FocusCameraSongEvent.hx | 4 +- .../play/event/PlayAnimationSongEvent.hx | 4 +- .../play/event/SetCameraBopSongEvent.hx | 4 +- .../funkin/play/event/ZoomCameraSongEvent.hx | 4 +- .../ui/debug/charting/ChartEditorState.hx | 3 +- .../components/ChartEditorEventSprite.hx | 42 ++++++- source/funkin/util/HaxeUIUtil.hx | 17 +++ 10 files changed, 217 insertions(+), 35 deletions(-) create mode 100644 source/funkin/util/HaxeUIUtil.hx diff --git a/hmm.json b/hmm.json index 57fbbb555..be9e2dd26 100644 --- a/hmm.json +++ b/hmm.json @@ -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" }, { diff --git a/source/funkin/data/event/SongEventData.hx b/source/funkin/data/event/SongEventData.hx index 7a167b031..a4a41e3a0 100644 --- a/source/funkin/data/event/SongEventData.hx +++ b/source/funkin/data/event/SongEventData.hx @@ -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) + { + 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; + typedef SongEventSchemaField = { /** @@ -240,4 +276,31 @@ typedef SongEventSchemaField = ?defaultValue:Dynamic, } -typedef SongEventSchema = Array; +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"; +} diff --git a/source/funkin/data/song/SongData.hx b/source/funkin/data/song/SongData.hx index 600871e2f..de73cd957 100644 --- a/source/funkin/data/song/SongData.hx +++ b/source/funkin/data/song/SongData.hx @@ -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 = {}; + 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 = {}; + result.set(defaultKey, this.value); + return cast result; + } + } + + public inline function getHandler():Null + { + return SongEventParser.getEvent(this.event); + } + + public inline function getSchema():Null + { + return SongEventParser.getEventSchema(this.event); + } + public inline function getDynamic(key:String):Null { 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 = 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); diff --git a/source/funkin/play/event/FocusCameraSongEvent.hx b/source/funkin/play/event/FocusCameraSongEvent.hx index 5f63254b0..c91769eb5 100644 --- a/source/funkin/play/event/FocusCameraSongEvent.hx +++ b/source/funkin/play/event/FocusCameraSongEvent.hx @@ -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, } - ]; + ]); } } diff --git a/source/funkin/play/event/PlayAnimationSongEvent.hx b/source/funkin/play/event/PlayAnimationSongEvent.hx index 6bc625517..0f611874b 100644 --- a/source/funkin/play/event/PlayAnimationSongEvent.hx +++ b/source/funkin/play/event/PlayAnimationSongEvent.hx @@ -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 } - ]; + ]); } } diff --git a/source/funkin/play/event/SetCameraBopSongEvent.hx b/source/funkin/play/event/SetCameraBopSongEvent.hx index 3cdeb9a67..7d5fd4699 100644 --- a/source/funkin/play/event/SetCameraBopSongEvent.hx +++ b/source/funkin/play/event/SetCameraBopSongEvent.hx @@ -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, } - ]; + ]); } } diff --git a/source/funkin/play/event/ZoomCameraSongEvent.hx b/source/funkin/play/event/ZoomCameraSongEvent.hx index 1ae76039e..9a361f71b 100644 --- a/source/funkin/play/event/ZoomCameraSongEvent.hx +++ b/source/funkin/play/event/ZoomCameraSongEvent.hx @@ -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', ] } - ]; + ]); } } diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 4f96fad69..5c12e3408 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -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) diff --git a/source/funkin/ui/debug/charting/components/ChartEditorEventSprite.hx b/source/funkin/ui/debug/charting/components/ChartEditorEventSprite.hx index 4c9d91407..cc9acf344 100644 --- a/source/funkin/ui/debug/charting/components/ChartEditorEventSprite.hx +++ b/source/funkin/ui/debug/charting/components/ChartEditorEventSprite.hx @@ -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 = 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):Null { 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); + } } /** diff --git a/source/funkin/util/HaxeUIUtil.hx b/source/funkin/util/HaxeUIUtil.hx new file mode 100644 index 000000000..1ffd9cd40 --- /dev/null +++ b/source/funkin/util/HaxeUIUtil.hx @@ -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 + } + } +}