mirror of
https://github.com/FunkinCrew/Funkin.git
synced 2024-11-27 01:55:52 -05:00
Merge branch 'feature/improved-cursor' into develop
This commit is contained in:
commit
a02058b260
13 changed files with 599 additions and 76 deletions
2
hmm.json
2
hmm.json
|
@ -139,7 +139,7 @@
|
|||
"name": "openfl",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "ef43deb2c68d8a4bcd73abfbd77324fc8220d0c1",
|
||||
"ref": "d33d489a137ff8fdece4994cf1302f0b6334ed08",
|
||||
"url": "https://github.com/EliteMasterEric/openfl"
|
||||
},
|
||||
{
|
||||
|
|
|
@ -4,9 +4,34 @@ import openfl.utils.Assets;
|
|||
import lime.app.Future;
|
||||
import openfl.display.BitmapData;
|
||||
|
||||
@:nullSafety
|
||||
class Cursor
|
||||
{
|
||||
public static var cursorMode(default, set):CursorMode;
|
||||
/**
|
||||
* The current cursor mode.
|
||||
* Set this value to change the cursor graphic.
|
||||
*/
|
||||
public static var cursorMode(default, set):Null<CursorMode> = null;
|
||||
|
||||
/**
|
||||
* Show the cursor.
|
||||
*/
|
||||
public static inline function show():Void
|
||||
{
|
||||
FlxG.mouse.visible = true;
|
||||
// Reset the cursor mode.
|
||||
Cursor.cursorMode = Default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the cursor.
|
||||
*/
|
||||
public static inline function hide():Void
|
||||
{
|
||||
FlxG.mouse.visible = false;
|
||||
// Reset the cursor mode.
|
||||
Cursor.cursorMode = null;
|
||||
}
|
||||
|
||||
static final CURSOR_DEFAULT_PARAMS:CursorParams =
|
||||
{
|
||||
|
@ -15,7 +40,7 @@ class Cursor
|
|||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
};
|
||||
static var assetCursorDefault:BitmapData = null;
|
||||
static var assetCursorDefault:Null<BitmapData> = null;
|
||||
|
||||
static final CURSOR_CROSS_PARAMS:CursorParams =
|
||||
{
|
||||
|
@ -24,7 +49,7 @@ class Cursor
|
|||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
};
|
||||
static var assetCursorCross:BitmapData = null;
|
||||
static var assetCursorCross:Null<BitmapData> = null;
|
||||
|
||||
static final CURSOR_ERASER_PARAMS:CursorParams =
|
||||
{
|
||||
|
@ -33,16 +58,16 @@ class Cursor
|
|||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
};
|
||||
static var assetCursorEraser:BitmapData = null;
|
||||
static var assetCursorEraser:Null<BitmapData> = null;
|
||||
|
||||
static final CURSOR_GRABBING_PARAMS:CursorParams =
|
||||
{
|
||||
graphic: "assets/images/cursor/cursor-grabbing.png",
|
||||
scale: 1.0,
|
||||
offsetX: 32,
|
||||
offsetX: -8,
|
||||
offsetY: 0,
|
||||
};
|
||||
static var assetCursorGrabbing:BitmapData = null;
|
||||
static var assetCursorGrabbing:Null<BitmapData> = null;
|
||||
|
||||
static final CURSOR_HOURGLASS_PARAMS:CursorParams =
|
||||
{
|
||||
|
@ -51,25 +76,34 @@ class Cursor
|
|||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
};
|
||||
static var assetCursorHourglass:BitmapData = null;
|
||||
static var assetCursorHourglass:Null<BitmapData> = null;
|
||||
|
||||
static final CURSOR_POINTER_PARAMS:CursorParams =
|
||||
{
|
||||
graphic: "assets/images/cursor/cursor-pointer.png",
|
||||
scale: 1.0,
|
||||
offsetX: 8,
|
||||
offsetX: -8,
|
||||
offsetY: 0,
|
||||
};
|
||||
static var assetCursorPointer:BitmapData = null;
|
||||
static var assetCursorPointer:Null<BitmapData> = null;
|
||||
|
||||
static final CURSOR_TEXT_PARAMS:CursorParams =
|
||||
{
|
||||
graphic: "assets/images/cursor/cursor-text.png",
|
||||
scale: 1.0,
|
||||
scale: 0.2,
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
};
|
||||
static var assetCursorText:BitmapData = null;
|
||||
static var assetCursorText:Null<BitmapData> = null;
|
||||
|
||||
static final CURSOR_TEXT_VERTICAL_PARAMS:CursorParams =
|
||||
{
|
||||
graphic: "assets/images/cursor/cursor-text-vertical.png",
|
||||
scale: 0.2,
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
};
|
||||
static var assetCursorTextVertical:Null<BitmapData> = null;
|
||||
|
||||
static final CURSOR_ZOOM_IN_PARAMS:CursorParams =
|
||||
{
|
||||
|
@ -78,7 +112,7 @@ class Cursor
|
|||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
};
|
||||
static var assetCursorZoomIn:BitmapData = null;
|
||||
static var assetCursorZoomIn:Null<BitmapData> = null;
|
||||
|
||||
static final CURSOR_ZOOM_OUT_PARAMS:CursorParams =
|
||||
{
|
||||
|
@ -87,11 +121,36 @@ class Cursor
|
|||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
};
|
||||
static var assetCursorZoomOut:BitmapData = null;
|
||||
static var assetCursorZoomOut:Null<BitmapData> = null;
|
||||
|
||||
static function set_cursorMode(value:CursorMode):CursorMode
|
||||
static final CURSOR_CROSSHAIR_PARAMS:CursorParams =
|
||||
{
|
||||
if (cursorMode != value)
|
||||
graphic: "assets/images/cursor/cursor-crosshair.png",
|
||||
scale: 1.0,
|
||||
offsetX: -16,
|
||||
offsetY: -16,
|
||||
};
|
||||
static var assetCursorCrosshair:Null<BitmapData> = null;
|
||||
|
||||
static final CURSOR_CELL_PARAMS:CursorParams =
|
||||
{
|
||||
graphic: "assets/images/cursor/cursor-cell.png",
|
||||
scale: 1.0,
|
||||
offsetX: -16,
|
||||
offsetY: -16,
|
||||
};
|
||||
static var assetCursorCell:Null<BitmapData> = null;
|
||||
|
||||
// DESIRED CURSOR: Resize NS (vertical)
|
||||
// DESIRED CURSOR: Resize EW (horizontal)
|
||||
// DESIRED CURSOR: Resize NESW (diagonal)
|
||||
// DESIRED CURSOR: Resize NWSE (diagonal)
|
||||
// DESIRED CURSOR: Help (Cursor with question mark)
|
||||
// DESIRED CURSOR: Menu (Cursor with menu icon)
|
||||
|
||||
static function set_cursorMode(value:Null<CursorMode>):Null<CursorMode>
|
||||
{
|
||||
if (value != null && cursorMode != value)
|
||||
{
|
||||
cursorMode = value;
|
||||
setCursorGraphic(cursorMode);
|
||||
|
@ -99,16 +158,9 @@ class Cursor
|
|||
return cursorMode;
|
||||
}
|
||||
|
||||
public static inline function show():Void
|
||||
{
|
||||
FlxG.mouse.visible = true;
|
||||
}
|
||||
|
||||
public static inline function hide():Void
|
||||
{
|
||||
FlxG.mouse.visible = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronous.
|
||||
*/
|
||||
static function setCursorGraphic(?value:CursorMode = null):Void
|
||||
{
|
||||
if (value == null)
|
||||
|
@ -117,6 +169,156 @@ class Cursor
|
|||
return;
|
||||
}
|
||||
|
||||
switch (value)
|
||||
{
|
||||
case Default:
|
||||
if (assetCursorDefault == null)
|
||||
{
|
||||
var bitmapData:BitmapData = Assets.getBitmapData(CURSOR_DEFAULT_PARAMS.graphic);
|
||||
assetCursorDefault = bitmapData;
|
||||
applyCursorParams(assetCursorDefault, CURSOR_DEFAULT_PARAMS);
|
||||
}
|
||||
else
|
||||
{
|
||||
applyCursorParams(assetCursorDefault, CURSOR_DEFAULT_PARAMS);
|
||||
}
|
||||
|
||||
case Cross:
|
||||
if (assetCursorCross == null)
|
||||
{
|
||||
var bitmapData:BitmapData = Assets.getBitmapData(CURSOR_CROSS_PARAMS.graphic);
|
||||
assetCursorCross = bitmapData;
|
||||
applyCursorParams(assetCursorCross, CURSOR_CROSS_PARAMS);
|
||||
}
|
||||
else
|
||||
{
|
||||
applyCursorParams(assetCursorCross, CURSOR_CROSS_PARAMS);
|
||||
}
|
||||
|
||||
case Eraser:
|
||||
if (assetCursorEraser == null)
|
||||
{
|
||||
var bitmapData:BitmapData = Assets.getBitmapData(CURSOR_ERASER_PARAMS.graphic);
|
||||
assetCursorEraser = bitmapData;
|
||||
applyCursorParams(assetCursorEraser, CURSOR_ERASER_PARAMS);
|
||||
}
|
||||
else
|
||||
{
|
||||
applyCursorParams(assetCursorEraser, CURSOR_ERASER_PARAMS);
|
||||
}
|
||||
|
||||
case Grabbing:
|
||||
if (assetCursorGrabbing == null)
|
||||
{
|
||||
var bitmapData:BitmapData = Assets.getBitmapData(CURSOR_GRABBING_PARAMS.graphic);
|
||||
assetCursorGrabbing = bitmapData;
|
||||
applyCursorParams(assetCursorGrabbing, CURSOR_GRABBING_PARAMS);
|
||||
}
|
||||
else
|
||||
{
|
||||
applyCursorParams(assetCursorGrabbing, CURSOR_GRABBING_PARAMS);
|
||||
}
|
||||
|
||||
case Hourglass:
|
||||
if (assetCursorHourglass == null)
|
||||
{
|
||||
var bitmapData:BitmapData = Assets.getBitmapData(CURSOR_HOURGLASS_PARAMS.graphic);
|
||||
assetCursorHourglass = bitmapData;
|
||||
applyCursorParams(assetCursorHourglass, CURSOR_HOURGLASS_PARAMS);
|
||||
}
|
||||
else
|
||||
{
|
||||
applyCursorParams(assetCursorHourglass, CURSOR_HOURGLASS_PARAMS);
|
||||
}
|
||||
|
||||
case Pointer:
|
||||
if (assetCursorPointer == null)
|
||||
{
|
||||
var bitmapData:BitmapData = Assets.getBitmapData(CURSOR_POINTER_PARAMS.graphic);
|
||||
assetCursorPointer = bitmapData;
|
||||
applyCursorParams(assetCursorPointer, CURSOR_POINTER_PARAMS);
|
||||
}
|
||||
else
|
||||
{
|
||||
applyCursorParams(assetCursorPointer, CURSOR_POINTER_PARAMS);
|
||||
}
|
||||
|
||||
case Text:
|
||||
if (assetCursorText == null)
|
||||
{
|
||||
var bitmapData:BitmapData = Assets.getBitmapData(CURSOR_TEXT_PARAMS.graphic);
|
||||
assetCursorText = bitmapData;
|
||||
applyCursorParams(assetCursorText, CURSOR_TEXT_PARAMS);
|
||||
}
|
||||
else
|
||||
{
|
||||
applyCursorParams(assetCursorText, CURSOR_TEXT_PARAMS);
|
||||
}
|
||||
|
||||
case ZoomIn:
|
||||
if (assetCursorZoomIn == null)
|
||||
{
|
||||
var bitmapData:BitmapData = Assets.getBitmapData(CURSOR_ZOOM_IN_PARAMS.graphic);
|
||||
assetCursorZoomIn = bitmapData;
|
||||
applyCursorParams(assetCursorZoomIn, CURSOR_ZOOM_IN_PARAMS);
|
||||
}
|
||||
else
|
||||
{
|
||||
applyCursorParams(assetCursorZoomIn, CURSOR_ZOOM_IN_PARAMS);
|
||||
}
|
||||
|
||||
case ZoomOut:
|
||||
if (assetCursorZoomOut == null)
|
||||
{
|
||||
var bitmapData:BitmapData = Assets.getBitmapData(CURSOR_ZOOM_OUT_PARAMS.graphic);
|
||||
assetCursorZoomOut = bitmapData;
|
||||
applyCursorParams(assetCursorZoomOut, CURSOR_ZOOM_OUT_PARAMS);
|
||||
}
|
||||
else
|
||||
{
|
||||
applyCursorParams(assetCursorZoomOut, CURSOR_ZOOM_OUT_PARAMS);
|
||||
}
|
||||
|
||||
case Crosshair:
|
||||
if (assetCursorCrosshair == null)
|
||||
{
|
||||
var bitmapData:BitmapData = Assets.getBitmapData(CURSOR_CROSSHAIR_PARAMS.graphic);
|
||||
assetCursorCrosshair = bitmapData;
|
||||
applyCursorParams(assetCursorCrosshair, CURSOR_CROSSHAIR_PARAMS);
|
||||
}
|
||||
else
|
||||
{
|
||||
applyCursorParams(assetCursorCrosshair, CURSOR_CROSSHAIR_PARAMS);
|
||||
}
|
||||
|
||||
case Cell:
|
||||
if (assetCursorCell == null)
|
||||
{
|
||||
var bitmapData:BitmapData = Assets.getBitmapData(CURSOR_CELL_PARAMS.graphic);
|
||||
assetCursorCell = bitmapData;
|
||||
applyCursorParams(assetCursorCell, CURSOR_CELL_PARAMS);
|
||||
}
|
||||
else
|
||||
{
|
||||
applyCursorParams(assetCursorCell, CURSOR_CELL_PARAMS);
|
||||
}
|
||||
|
||||
default:
|
||||
setCursorGraphic(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronous.
|
||||
*/
|
||||
static function loadCursorGraphic(?value:CursorMode = null):Void
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
FlxG.mouse.unload();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (value)
|
||||
{
|
||||
case Default:
|
||||
|
@ -127,6 +329,7 @@ class Cursor
|
|||
assetCursorDefault = bitmapData;
|
||||
applyCursorParams(assetCursorDefault, CURSOR_DEFAULT_PARAMS);
|
||||
});
|
||||
future.onError(onCursorError.bind(Default));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -141,6 +344,7 @@ class Cursor
|
|||
assetCursorCross = bitmapData;
|
||||
applyCursorParams(assetCursorCross, CURSOR_CROSS_PARAMS);
|
||||
});
|
||||
future.onError(onCursorError.bind(Cross));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -155,6 +359,7 @@ class Cursor
|
|||
assetCursorEraser = bitmapData;
|
||||
applyCursorParams(assetCursorEraser, CURSOR_ERASER_PARAMS);
|
||||
});
|
||||
future.onError(onCursorError.bind(Eraser));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -169,6 +374,7 @@ class Cursor
|
|||
assetCursorGrabbing = bitmapData;
|
||||
applyCursorParams(assetCursorGrabbing, CURSOR_GRABBING_PARAMS);
|
||||
});
|
||||
future.onError(onCursorError.bind(Grabbing));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -183,6 +389,7 @@ class Cursor
|
|||
assetCursorHourglass = bitmapData;
|
||||
applyCursorParams(assetCursorHourglass, CURSOR_HOURGLASS_PARAMS);
|
||||
});
|
||||
future.onError(onCursorError.bind(Hourglass));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -197,6 +404,7 @@ class Cursor
|
|||
assetCursorPointer = bitmapData;
|
||||
applyCursorParams(assetCursorPointer, CURSOR_POINTER_PARAMS);
|
||||
});
|
||||
future.onError(onCursorError.bind(Pointer));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -211,6 +419,7 @@ class Cursor
|
|||
assetCursorText = bitmapData;
|
||||
applyCursorParams(assetCursorText, CURSOR_TEXT_PARAMS);
|
||||
});
|
||||
future.onError(onCursorError.bind(Text));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -225,6 +434,7 @@ class Cursor
|
|||
assetCursorZoomIn = bitmapData;
|
||||
applyCursorParams(assetCursorZoomIn, CURSOR_ZOOM_IN_PARAMS);
|
||||
});
|
||||
future.onError(onCursorError.bind(ZoomIn));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -239,14 +449,45 @@ class Cursor
|
|||
assetCursorZoomOut = bitmapData;
|
||||
applyCursorParams(assetCursorZoomOut, CURSOR_ZOOM_OUT_PARAMS);
|
||||
});
|
||||
future.onError(onCursorError.bind(ZoomOut));
|
||||
}
|
||||
else
|
||||
{
|
||||
applyCursorParams(assetCursorZoomOut, CURSOR_ZOOM_OUT_PARAMS);
|
||||
}
|
||||
|
||||
case Crosshair:
|
||||
if (assetCursorCrosshair == null)
|
||||
{
|
||||
var future:Future<BitmapData> = Assets.loadBitmapData(CURSOR_CROSSHAIR_PARAMS.graphic);
|
||||
future.onComplete(function(bitmapData:BitmapData) {
|
||||
assetCursorCrosshair = bitmapData;
|
||||
applyCursorParams(assetCursorCrosshair, CURSOR_CROSSHAIR_PARAMS);
|
||||
});
|
||||
future.onError(onCursorError.bind(Crosshair));
|
||||
}
|
||||
else
|
||||
{
|
||||
applyCursorParams(assetCursorCrosshair, CURSOR_CROSSHAIR_PARAMS);
|
||||
}
|
||||
|
||||
case Cell:
|
||||
if (assetCursorCell == null)
|
||||
{
|
||||
var future:Future<BitmapData> = Assets.loadBitmapData(CURSOR_CELL_PARAMS.graphic);
|
||||
future.onComplete(function(bitmapData:BitmapData) {
|
||||
assetCursorCell = bitmapData;
|
||||
applyCursorParams(assetCursorCell, CURSOR_CELL_PARAMS);
|
||||
});
|
||||
future.onError(onCursorError.bind(Cell));
|
||||
}
|
||||
else
|
||||
{
|
||||
applyCursorParams(assetCursorCell, CURSOR_CELL_PARAMS);
|
||||
}
|
||||
|
||||
default:
|
||||
setCursorGraphic(null);
|
||||
loadCursorGraphic(null);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -254,6 +495,11 @@ class Cursor
|
|||
{
|
||||
FlxG.mouse.load(graphic, params.scale, params.offsetX, params.offsetY);
|
||||
}
|
||||
|
||||
static function onCursorError(cursorMode:CursorMode, error:String):Void
|
||||
{
|
||||
trace("Failed to load cursor graphic for cursor mode " + cursorMode + ": " + error);
|
||||
}
|
||||
}
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/CSS/cursor
|
||||
|
@ -268,6 +514,8 @@ enum CursorMode
|
|||
Text;
|
||||
ZoomIn;
|
||||
ZoomOut;
|
||||
Crosshair;
|
||||
Cell;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -179,7 +179,7 @@ class SongMigrator
|
|||
songMetadata.playData.playableChars = [];
|
||||
try
|
||||
{
|
||||
Reflect.setField(songMetadata.playData.playableChars, songData.song.player1, new SongPlayableChar('', songData.song.player2));
|
||||
songMetadata.playData.playableChars.set(songData.song.player1, new SongPlayableChar('', songData.song.player2));
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
|
|
|
@ -6,6 +6,7 @@ import funkin.util.SerializerUtil;
|
|||
import funkin.data.song.SongData.SongChartData;
|
||||
import funkin.data.song.SongData.SongMetadata;
|
||||
import flixel.util.FlxTimer;
|
||||
import funkin.ui.haxeui.components.FunkinLink;
|
||||
import funkin.util.SortUtil;
|
||||
import funkin.input.Cursor;
|
||||
import funkin.play.character.BaseCharacter;
|
||||
|
@ -128,7 +129,7 @@ class ChartEditorDialogHandler
|
|||
continue;
|
||||
}
|
||||
|
||||
var linkTemplateSong:Link = new Link();
|
||||
var linkTemplateSong:Link = new FunkinLink();
|
||||
linkTemplateSong.text = songName;
|
||||
linkTemplateSong.onClick = function(_event) {
|
||||
dialog.hideDialog(DialogButton.CANCEL);
|
||||
|
@ -835,6 +836,14 @@ class ChartEditorDialogHandler
|
|||
songVariationMetadataEntryLabel.text = 'Click to browse for <song>-metadata-${variation}.json file.';
|
||||
#end
|
||||
|
||||
songVariationMetadataEntry.onMouseOver = function(_event) {
|
||||
songVariationMetadataEntry.swapClass('upload-bg', 'upload-bg-hover');
|
||||
Cursor.cursorMode = Pointer;
|
||||
}
|
||||
songVariationMetadataEntry.onMouseOut = function(_event) {
|
||||
songVariationMetadataEntry.swapClass('upload-bg-hover', 'upload-bg');
|
||||
Cursor.cursorMode = Default;
|
||||
}
|
||||
songVariationMetadataEntry.onClick = onClickMetadataVariation.bind(variation).bind(songVariationMetadataEntryLabel);
|
||||
#if FILE_DROP_SUPPORTED
|
||||
addDropHandler(songVariationMetadataEntry, onDropFileMetadataVariation.bind(variation).bind(songVariationMetadataEntryLabel));
|
||||
|
@ -851,6 +860,14 @@ class ChartEditorDialogHandler
|
|||
songVariationChartDataEntryLabel.text = 'Click to browse for <song>-chart-${variation}.json file.';
|
||||
#end
|
||||
|
||||
songVariationChartDataEntry.onMouseOver = function(_event) {
|
||||
songVariationChartDataEntry.swapClass('upload-bg', 'upload-bg-hover');
|
||||
Cursor.cursorMode = Pointer;
|
||||
}
|
||||
songVariationChartDataEntry.onMouseOut = function(_event) {
|
||||
songVariationChartDataEntry.swapClass('upload-bg-hover', 'upload-bg');
|
||||
Cursor.cursorMode = Default;
|
||||
}
|
||||
songVariationChartDataEntry.onClick = onClickChartDataVariation.bind(variation).bind(songVariationChartDataEntryLabel);
|
||||
#if FILE_DROP_SUPPORTED
|
||||
addDropHandler(songVariationChartDataEntry, onDropFileChartDataVariation.bind(variation).bind(songVariationChartDataEntryLabel));
|
||||
|
@ -1019,6 +1036,14 @@ class ChartEditorDialogHandler
|
|||
|
||||
metadataEntry.onClick = onClickMetadataVariation.bind(Constants.DEFAULT_VARIATION).bind(metadataEntryLabel);
|
||||
addDropHandler(metadataEntry, onDropFileMetadataVariation.bind(Constants.DEFAULT_VARIATION).bind(metadataEntryLabel));
|
||||
metadataEntry.onMouseOver = function(_event) {
|
||||
metadataEntry.swapClass('upload-bg', 'upload-bg-hover');
|
||||
Cursor.cursorMode = Pointer;
|
||||
}
|
||||
metadataEntry.onMouseOut = function(_event) {
|
||||
metadataEntry.swapClass('upload-bg-hover', 'upload-bg');
|
||||
Cursor.cursorMode = Default;
|
||||
}
|
||||
|
||||
chartContainerA.addComponent(metadataEntry);
|
||||
|
||||
|
@ -1065,7 +1090,6 @@ class ChartEditorDialogHandler
|
|||
importBox.swapClass('upload-bg', 'upload-bg-hover');
|
||||
Cursor.cursorMode = Pointer;
|
||||
}
|
||||
|
||||
importBox.onMouseOut = function(_event) {
|
||||
importBox.swapClass('upload-bg-hover', 'upload-bg');
|
||||
Cursor.cursorMode = Default;
|
||||
|
|
|
@ -1220,6 +1220,9 @@ class ChartEditorState extends HaxeUIState
|
|||
// Set the z-index of the HaxeUI.
|
||||
this.component.zIndex = 100;
|
||||
|
||||
// Show the mouse cursor.
|
||||
Cursor.show();
|
||||
|
||||
fixCamera();
|
||||
|
||||
// Get rid of any music from the previous state.
|
||||
|
@ -1912,13 +1915,15 @@ class ChartEditorState extends HaxeUIState
|
|||
if (pageUpKeyHandler.activated)
|
||||
{
|
||||
var measureHeight:Float = GRID_SIZE * 4 * Conductor.beatsPerMeasure;
|
||||
var targetScrollPosition:Float = Math.floor(scrollPositionInPixels / measureHeight) * measureHeight;
|
||||
var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels;
|
||||
var targetScrollPosition:Float = Math.floor(playheadPos / measureHeight) * measureHeight;
|
||||
// If we would move less than one grid, instead move to the top of the previous measure.
|
||||
if (Math.abs(targetScrollPosition - scrollPositionInPixels) < GRID_SIZE)
|
||||
var targetScrollAmount = Math.abs(targetScrollPosition - playheadPos);
|
||||
if (targetScrollAmount < GRID_SIZE)
|
||||
{
|
||||
targetScrollPosition -= GRID_SIZE * 4 * Conductor.beatsPerMeasure;
|
||||
targetScrollPosition -= GRID_SIZE * Constants.STEPS_PER_BEAT * Conductor.beatsPerMeasure;
|
||||
}
|
||||
scrollAmount = targetScrollPosition - scrollPositionInPixels;
|
||||
scrollAmount = targetScrollPosition - playheadPos;
|
||||
|
||||
shouldPause = true;
|
||||
}
|
||||
|
@ -1933,13 +1938,15 @@ class ChartEditorState extends HaxeUIState
|
|||
if (pageDownKeyHandler.activated)
|
||||
{
|
||||
var measureHeight:Float = GRID_SIZE * 4 * Conductor.beatsPerMeasure;
|
||||
var targetScrollPosition:Float = Math.ceil(scrollPositionInPixels / measureHeight) * measureHeight;
|
||||
var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels;
|
||||
var targetScrollPosition:Float = Math.ceil(playheadPos / measureHeight) * measureHeight;
|
||||
// If we would move less than one grid, instead move to the top of the next measure.
|
||||
if (Math.abs(targetScrollPosition - scrollPositionInPixels) < GRID_SIZE)
|
||||
var targetScrollAmount = Math.abs(targetScrollPosition - playheadPos);
|
||||
if (targetScrollAmount < GRID_SIZE)
|
||||
{
|
||||
targetScrollPosition += GRID_SIZE * 4 * Conductor.beatsPerMeasure;
|
||||
targetScrollPosition += GRID_SIZE * Constants.STEPS_PER_BEAT * Conductor.beatsPerMeasure;
|
||||
}
|
||||
scrollAmount = targetScrollPosition - scrollPositionInPixels;
|
||||
scrollAmount = targetScrollPosition - playheadPos;
|
||||
|
||||
shouldPause = true;
|
||||
}
|
||||
|
@ -2058,6 +2065,11 @@ class ChartEditorState extends HaxeUIState
|
|||
|
||||
if (shouldHandleCursor)
|
||||
{
|
||||
// Over the course of this big conditional block,
|
||||
// we determine what the cursor should look like,
|
||||
// and fall back to the default cursor if none of the conditions are met.
|
||||
var targetCursorMode:Null<CursorMode> = null;
|
||||
|
||||
if (gridTiledSprite == null) throw "ERROR: Tried to handle cursor, but gridTiledSprite is null! Check ChartEditorState.buildGrid()";
|
||||
|
||||
var overlapsGrid:Bool = FlxG.mouse.overlaps(gridTiledSprite);
|
||||
|
@ -2067,9 +2079,9 @@ class ChartEditorState extends HaxeUIState
|
|||
var cursorY:Float = FlxG.mouse.screenY - gridTiledSprite.y;
|
||||
|
||||
var overlapsSelectionBorder:Bool = overlapsGrid
|
||||
&& (cursorX % 40) < (GRID_SELECTION_BORDER_WIDTH / 2)
|
||||
&& ((cursorX % 40) < (GRID_SELECTION_BORDER_WIDTH / 2)
|
||||
|| (cursorX % 40) > (40 - (GRID_SELECTION_BORDER_WIDTH / 2))
|
||||
|| (cursorY % 40) < (GRID_SELECTION_BORDER_WIDTH / 2) || (cursorY % 40) > (40 - (GRID_SELECTION_BORDER_WIDTH / 2));
|
||||
|| (cursorY % 40) < (GRID_SELECTION_BORDER_WIDTH / 2) || (cursorY % 40) > (40 - (GRID_SELECTION_BORDER_WIDTH / 2)));
|
||||
|
||||
if (FlxG.mouse.justPressed)
|
||||
{
|
||||
|
@ -2085,6 +2097,8 @@ class ChartEditorState extends HaxeUIState
|
|||
else if (!overlapsGrid || overlapsSelectionBorder)
|
||||
{
|
||||
selectionBoxStartPos = new FlxPoint(FlxG.mouse.screenX, FlxG.mouse.screenY);
|
||||
// Drawing selection box.
|
||||
targetCursorMode = Crosshair;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -2095,23 +2109,6 @@ class ChartEditorState extends HaxeUIState
|
|||
}
|
||||
}
|
||||
|
||||
if (gridPlayheadScrollAreaPressed)
|
||||
{
|
||||
Cursor.cursorMode = Grabbing;
|
||||
}
|
||||
else if (notePreviewScrollAreaStartPos != null)
|
||||
{
|
||||
Cursor.cursorMode = Pointer;
|
||||
}
|
||||
else if (gridPlayheadScrollArea != null && FlxG.mouse.overlaps(gridPlayheadScrollArea))
|
||||
{
|
||||
Cursor.cursorMode = Pointer;
|
||||
}
|
||||
else
|
||||
{
|
||||
Cursor.cursorMode = Default;
|
||||
}
|
||||
|
||||
if (gridPlayheadScrollAreaPressed && FlxG.mouse.released)
|
||||
{
|
||||
gridPlayheadScrollAreaPressed = false;
|
||||
|
@ -2128,6 +2125,9 @@ class ChartEditorState extends HaxeUIState
|
|||
// Move the playhead to the cursor position.
|
||||
this.playheadPositionInPixels = FlxG.mouse.screenY - MENU_BAR_HEIGHT - GRID_TOP_PAD;
|
||||
moveSongToScrollPosition();
|
||||
|
||||
// Cursor should be a grabby hand.
|
||||
if (targetCursorMode == null) targetCursorMode = Grabbing;
|
||||
}
|
||||
|
||||
// The song position of the cursor, in steps.
|
||||
|
@ -2286,6 +2286,8 @@ class ChartEditorState extends HaxeUIState
|
|||
selectionRect.width = Math.abs(FlxG.mouse.screenX - selectionBoxStartPos.x);
|
||||
selectionRect.height = Math.abs(FlxG.mouse.screenY - selectionBoxStartPos.y);
|
||||
setSelectionBoxBounds(selectionRect);
|
||||
|
||||
targetCursorMode = Crosshair;
|
||||
}
|
||||
}
|
||||
else if (FlxG.mouse.justReleased)
|
||||
|
@ -2377,7 +2379,9 @@ class ChartEditorState extends HaxeUIState
|
|||
}
|
||||
else if (notePreviewScrollAreaStartPos != null)
|
||||
{
|
||||
trace('Updating current song time while clicking and holding...');
|
||||
// Player is clicking and holding on note preview to scrub around.
|
||||
targetCursorMode = Grabbing;
|
||||
|
||||
var clickedPosInPixels:Float = FlxMath.remapToRange(FlxG.mouse.screenY, (notePreview?.y ?? 0.0),
|
||||
(notePreview?.y ?? 0.0) + (notePreview?.height ?? 0.0), 0, songLengthInPixels);
|
||||
|
||||
|
@ -2547,8 +2551,6 @@ class ChartEditorState extends HaxeUIState
|
|||
// Handle grid cursor.
|
||||
if (overlapsGrid && !overlapsSelectionBorder && !gridPlayheadScrollAreaPressed)
|
||||
{
|
||||
Cursor.cursorMode = Pointer;
|
||||
|
||||
// Indicate that we can place a note here.
|
||||
|
||||
if (cursorColumn == eventColumn)
|
||||
|
@ -2569,6 +2571,8 @@ class ChartEditorState extends HaxeUIState
|
|||
gridGhostEvent.visible = true;
|
||||
gridGhostEvent.eventData = eventData;
|
||||
gridGhostEvent.updateEventPosition(renderedEvents);
|
||||
|
||||
targetCursorMode = Cell;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -2589,20 +2593,8 @@ class ChartEditorState extends HaxeUIState
|
|||
gridGhostNote.visible = true;
|
||||
gridGhostNote.noteData = noteData;
|
||||
gridGhostNote.updateNotePosition(renderedNotes);
|
||||
}
|
||||
|
||||
// gridCursor.visible = true;
|
||||
// // X and Y are the cursor position relative to the grid, snapped to the top left of the grid square.
|
||||
// gridCursor.x = Math.floor(cursorX / GRID_SIZE) * GRID_SIZE + gridTiledSprite.x + (GRID_SELECTION_BORDER_WIDTH / 2);
|
||||
// gridCursor.y = cursorStep * GRID_SIZE + gridTiledSprite.y + (GRID_SELECTION_BORDER_WIDTH / 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (gridGhostNote != null) gridGhostNote.visible = false;
|
||||
if (gridGhostHoldNote != null) gridGhostHoldNote.visible = false;
|
||||
if (gridGhostEvent != null) gridGhostEvent.visible = false;
|
||||
Cursor.cursorMode = Default;
|
||||
}
|
||||
targetCursorMode = Cell;
|
||||
}
|
||||
}
|
||||
else
|
||||
|
@ -2611,10 +2603,48 @@ class ChartEditorState extends HaxeUIState
|
|||
if (gridGhostHoldNote != null) gridGhostHoldNote.visible = false;
|
||||
if (gridGhostEvent != null) gridGhostEvent.visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (isCursorOverHaxeUIButton && Cursor.cursorMode == Default)
|
||||
if (targetCursorMode == null)
|
||||
{
|
||||
Cursor.cursorMode = Pointer;
|
||||
if (FlxG.mouse.pressed)
|
||||
{
|
||||
if (overlapsSelectionBorder)
|
||||
{
|
||||
targetCursorMode = Crosshair;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (FlxG.mouse.overlaps(notePreview))
|
||||
{
|
||||
targetCursorMode = Pointer;
|
||||
}
|
||||
else if (FlxG.mouse.overlaps(gridPlayheadScrollArea))
|
||||
{
|
||||
targetCursorMode = Pointer;
|
||||
}
|
||||
else if (overlapsSelectionBorder)
|
||||
{
|
||||
targetCursorMode = Crosshair;
|
||||
}
|
||||
else if (overlapsGrid)
|
||||
{
|
||||
targetCursorMode = Cell;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Actually set the cursor mode to the one we specified earlier.
|
||||
Cursor.cursorMode = targetCursorMode ?? Default;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (gridGhostNote != null) gridGhostNote.visible = false;
|
||||
if (gridGhostHoldNote != null) gridGhostHoldNote.visible = false;
|
||||
if (gridGhostEvent != null) gridGhostEvent.visible = false;
|
||||
|
||||
// Do not set Cursor.cursorMode here, because it will be set by the HaxeUI.
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2861,10 +2891,10 @@ class ChartEditorState extends HaxeUIState
|
|||
}
|
||||
|
||||
// Sort the notes DESCENDING. This keeps the sustain behind the associated note.
|
||||
renderedNotes.sort(FlxSort.byY, FlxSort.DESCENDING);
|
||||
renderedNotes.sort(FlxSort.byY, FlxSort.DESCENDING); // TODO: .group.insertionSort()
|
||||
|
||||
// Sort the events DESCENDING. This keeps the sustain behind the associated note.
|
||||
renderedEvents.sort(FlxSort.byY, FlxSort.DESCENDING);
|
||||
renderedEvents.sort(FlxSort.byY, FlxSort.DESCENDING); // TODO: .group.insertionSort()
|
||||
}
|
||||
|
||||
// Add a debug value which displays the current size of the note pool.
|
||||
|
@ -4196,10 +4226,12 @@ class ChartEditorState extends HaxeUIState
|
|||
|
||||
function sortChartData():Void
|
||||
{
|
||||
// TODO: .insertionSort()
|
||||
currentSongChartNoteData.sort(function(a:SongNoteData, b:SongNoteData):Int {
|
||||
return FlxSort.byValues(FlxSort.ASCENDING, a.time, b.time);
|
||||
});
|
||||
|
||||
// TODO: .insertionSort()
|
||||
currentSongChartEventData.sort(function(a:SongEventData, b:SongEventData):Int {
|
||||
return FlxSort.byValues(FlxSort.ASCENDING, a.time, b.time);
|
||||
});
|
||||
|
@ -4247,6 +4279,9 @@ class ChartEditorState extends HaxeUIState
|
|||
|
||||
cleanupAutoSave();
|
||||
|
||||
// Hide the mouse cursor on other states.
|
||||
Cursor.hide();
|
||||
|
||||
@:privateAccess
|
||||
ChartEditorNoteSprite.noteFrameCollection = null;
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ typedef AnimationInfo =
|
|||
@:composite(Layout)
|
||||
class CharacterPlayer extends Box
|
||||
{
|
||||
var character:BaseCharacter;
|
||||
var character:Null<BaseCharacter>;
|
||||
|
||||
public function new(defaultToBf:Bool = true)
|
||||
{
|
||||
|
@ -47,7 +47,7 @@ class CharacterPlayer extends Box
|
|||
|
||||
function get_charId():String
|
||||
{
|
||||
return character.characterId;
|
||||
return character?.characterId ?? '';
|
||||
}
|
||||
|
||||
function set_charId(value:String):String
|
||||
|
@ -60,7 +60,7 @@ class CharacterPlayer extends Box
|
|||
|
||||
function get_charName():String
|
||||
{
|
||||
return character.characterName;
|
||||
return character?.characterName ?? "Unknown";
|
||||
}
|
||||
|
||||
// possible haxeui bug: if listener is added after event is dispatched, event is "lost"... is it smart to "collect and redispatch"? Not sure
|
||||
|
@ -86,7 +86,11 @@ class CharacterPlayer extends Box
|
|||
|
||||
// Prevent script issues by fetching with debug=true.
|
||||
var newCharacter:BaseCharacter = CharacterDataParser.fetchCharacter(id, true);
|
||||
if (newCharacter == null) return; // Fail if character doesn't exist.
|
||||
if (newCharacter == null)
|
||||
{
|
||||
character = null;
|
||||
return; // Fail if character doesn't exist.
|
||||
}
|
||||
|
||||
// Assign character.
|
||||
character = newCharacter;
|
||||
|
|
30
source/funkin/ui/haxeui/components/FunkinButton.hx
Normal file
30
source/funkin/ui/haxeui/components/FunkinButton.hx
Normal file
|
@ -0,0 +1,30 @@
|
|||
package funkin.ui.haxeui.components;
|
||||
|
||||
import funkin.input.Cursor;
|
||||
import haxe.ui.events.MouseEvent;
|
||||
import haxe.ui.components.Button;
|
||||
|
||||
/**
|
||||
* A HaxeUI button which:
|
||||
* - Changes the current cursor when hovered over.
|
||||
*/
|
||||
class FunkinButton extends Button
|
||||
{
|
||||
public function new()
|
||||
{
|
||||
super();
|
||||
|
||||
this.onMouseOver = handleMouseOver;
|
||||
this.onMouseOut = handleMouseOut;
|
||||
}
|
||||
|
||||
private function handleMouseOver(event:MouseEvent)
|
||||
{
|
||||
Cursor.cursorMode = Pointer;
|
||||
}
|
||||
|
||||
private function handleMouseOut(event:MouseEvent)
|
||||
{
|
||||
Cursor.cursorMode = Default;
|
||||
}
|
||||
}
|
30
source/funkin/ui/haxeui/components/FunkinHorizontalSlider.hx
Normal file
30
source/funkin/ui/haxeui/components/FunkinHorizontalSlider.hx
Normal file
|
@ -0,0 +1,30 @@
|
|||
package funkin.ui.haxeui.components;
|
||||
|
||||
import haxe.ui.components.HorizontalSlider;
|
||||
import funkin.input.Cursor;
|
||||
import haxe.ui.events.MouseEvent;
|
||||
|
||||
/**
|
||||
* A HaxeUI horizontal slider which:
|
||||
* - Changes the current cursor when hovered over.
|
||||
*/
|
||||
class FunkinHorizontalSlider extends HorizontalSlider
|
||||
{
|
||||
public function new()
|
||||
{
|
||||
super();
|
||||
|
||||
this.onMouseOver = handleMouseOver;
|
||||
this.onMouseOut = handleMouseOut;
|
||||
}
|
||||
|
||||
private function handleMouseOver(event:MouseEvent)
|
||||
{
|
||||
Cursor.cursorMode = Pointer;
|
||||
}
|
||||
|
||||
private function handleMouseOut(event:MouseEvent)
|
||||
{
|
||||
Cursor.cursorMode = Default;
|
||||
}
|
||||
}
|
30
source/funkin/ui/haxeui/components/FunkinLink.hx
Normal file
30
source/funkin/ui/haxeui/components/FunkinLink.hx
Normal file
|
@ -0,0 +1,30 @@
|
|||
package funkin.ui.haxeui.components;
|
||||
|
||||
import funkin.input.Cursor;
|
||||
import haxe.ui.events.MouseEvent;
|
||||
import haxe.ui.components.Link;
|
||||
|
||||
/**
|
||||
* A HaxeUI link which:
|
||||
* - Changes the current cursor when hovered over.
|
||||
*/
|
||||
class FunkinLink extends Link
|
||||
{
|
||||
public function new()
|
||||
{
|
||||
super();
|
||||
|
||||
this.onMouseOver = handleMouseOver;
|
||||
this.onMouseOut = handleMouseOut;
|
||||
}
|
||||
|
||||
private function handleMouseOver(event:MouseEvent)
|
||||
{
|
||||
Cursor.cursorMode = Pointer;
|
||||
}
|
||||
|
||||
private function handleMouseOut(event:MouseEvent)
|
||||
{
|
||||
Cursor.cursorMode = Default;
|
||||
}
|
||||
}
|
32
source/funkin/ui/haxeui/components/FunkinMenuBar.hx
Normal file
32
source/funkin/ui/haxeui/components/FunkinMenuBar.hx
Normal file
|
@ -0,0 +1,32 @@
|
|||
package funkin.ui.haxeui.components;
|
||||
|
||||
import funkin.input.Cursor;
|
||||
import haxe.ui.events.MouseEvent;
|
||||
import haxe.ui.containers.menus.MenuBar;
|
||||
import haxe.ui.core.CompositeBuilder;
|
||||
|
||||
/**
|
||||
* A HaxeUI menu bar which:
|
||||
* - Changes the current cursor when each button is hovered over.
|
||||
*/
|
||||
class FunkinMenuBar extends MenuBar
|
||||
{
|
||||
public function new()
|
||||
{
|
||||
super();
|
||||
|
||||
registerListeners();
|
||||
}
|
||||
|
||||
private function registerListeners():Void {}
|
||||
|
||||
private function handleMouseOver(event:MouseEvent)
|
||||
{
|
||||
Cursor.cursorMode = Pointer;
|
||||
}
|
||||
|
||||
private function handleMouseOut(event:MouseEvent)
|
||||
{
|
||||
Cursor.cursorMode = Default;
|
||||
}
|
||||
}
|
30
source/funkin/ui/haxeui/components/FunkinMenuCheckBox.hx
Normal file
30
source/funkin/ui/haxeui/components/FunkinMenuCheckBox.hx
Normal file
|
@ -0,0 +1,30 @@
|
|||
package funkin.ui.haxeui.components;
|
||||
|
||||
import funkin.input.Cursor;
|
||||
import haxe.ui.events.MouseEvent;
|
||||
import haxe.ui.containers.menus.MenuCheckBox;
|
||||
|
||||
/**
|
||||
* A HaxeUI menu checkbox which:
|
||||
* - Changes the current cursor when hovered over.
|
||||
*/
|
||||
class FunkinMenuCheckBox extends MenuCheckBox
|
||||
{
|
||||
public function new()
|
||||
{
|
||||
super();
|
||||
|
||||
this.onMouseOver = handleMouseOver;
|
||||
this.onMouseOut = handleMouseOut;
|
||||
}
|
||||
|
||||
private function handleMouseOver(event:MouseEvent)
|
||||
{
|
||||
Cursor.cursorMode = Pointer;
|
||||
}
|
||||
|
||||
private function handleMouseOut(event:MouseEvent)
|
||||
{
|
||||
Cursor.cursorMode = Default;
|
||||
}
|
||||
}
|
30
source/funkin/ui/haxeui/components/FunkinMenuItem.hx
Normal file
30
source/funkin/ui/haxeui/components/FunkinMenuItem.hx
Normal file
|
@ -0,0 +1,30 @@
|
|||
package funkin.ui.haxeui.components;
|
||||
|
||||
import funkin.input.Cursor;
|
||||
import haxe.ui.events.MouseEvent;
|
||||
import haxe.ui.containers.menus.MenuItem;
|
||||
|
||||
/**
|
||||
* A HaxeUI menu item which:
|
||||
* - Changes the current cursor when hovered over.
|
||||
*/
|
||||
class FunkinMenuItem extends MenuItem
|
||||
{
|
||||
public function new()
|
||||
{
|
||||
super();
|
||||
|
||||
this.onMouseOver = handleMouseOver;
|
||||
this.onMouseOut = handleMouseOut;
|
||||
}
|
||||
|
||||
private function handleMouseOver(event:MouseEvent)
|
||||
{
|
||||
Cursor.cursorMode = Pointer;
|
||||
}
|
||||
|
||||
private function handleMouseOut(event:MouseEvent)
|
||||
{
|
||||
Cursor.cursorMode = Default;
|
||||
}
|
||||
}
|
30
source/funkin/ui/haxeui/components/FunkinMenuOptionBox.hx
Normal file
30
source/funkin/ui/haxeui/components/FunkinMenuOptionBox.hx
Normal file
|
@ -0,0 +1,30 @@
|
|||
package funkin.ui.haxeui.components;
|
||||
|
||||
import haxe.ui.containers.menus.MenuOptionBox;
|
||||
import funkin.input.Cursor;
|
||||
import haxe.ui.events.MouseEvent;
|
||||
|
||||
/**
|
||||
* A HaxeUI menu option box which:
|
||||
* - Changes the current cursor when hovered over.
|
||||
*/
|
||||
class FunkinMenuOptionBox extends MenuOptionBox
|
||||
{
|
||||
public function new()
|
||||
{
|
||||
super();
|
||||
|
||||
this.onMouseOver = handleMouseOver;
|
||||
this.onMouseOut = handleMouseOut;
|
||||
}
|
||||
|
||||
private function handleMouseOver(event:MouseEvent)
|
||||
{
|
||||
Cursor.cursorMode = Pointer;
|
||||
}
|
||||
|
||||
private function handleMouseOut(event:MouseEvent)
|
||||
{
|
||||
Cursor.cursorMode = Default;
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue