Files
OpenRA/OpenRA.Mods.Common/Widgets/Logic/AssetBrowserLogic.cs
RoosterDragon aa121dcb2f Use covariant return types.
Use the covariant return type feature of C# 9 to allow method overrides to be more specific about their return type. By exposing this information, e.g. for Widget.Clone(), callers will have more information. This allows various casts to be removed as the information is now available within the type system.
2025-01-15 17:29:02 +02:00

765 lines
23 KiB
C#

#region Copyright & License Information
/*
* Copyright (c) The OpenRA Developers and Contributors
* This file is part of OpenRA, which is free software. It is made
* available to you under the terms of the GNU General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using OpenRA.FileSystem;
using OpenRA.Graphics;
using OpenRA.Mods.Common.Traits;
using OpenRA.Primitives;
using OpenRA.Video;
using OpenRA.Widgets;
namespace OpenRA.Mods.Common.Widgets.Logic
{
public class AssetBrowserLogic : ChromeLogic
{
[Flags]
enum AssetType
{
Sprite = 1,
Model = 2,
Audio = 4,
Video = 8,
Unknown = 16
}
[FluentReference("length")]
const string LengthInSeconds = "label-length-in-seconds";
[FluentReference]
const string AllPackages = "label-all-packages";
readonly string[] allowedExtensions;
readonly string[] allowedSpriteExtensions;
readonly string[] allowedModelExtensions;
readonly string[] allowedAudioExtensions;
readonly string[] allowedVideoExtensions;
readonly IEnumerable<IReadOnlyPackage> acceptablePackages;
readonly string[] palettes;
readonly World world;
readonly ModData modData;
readonly Widget panel;
readonly TextFieldWidget filenameInput;
readonly SliderWidget frameSlider;
readonly ScrollPanelWidget assetList;
readonly ScrollItemWidget template;
readonly Cache<SheetType, SheetBuilder> sheetBuilders;
readonly Cache<string, Sprite[]> spriteCache;
IReadOnlyPackage assetSource = null;
bool animateFrames = false;
string currentPalette;
string currentFilename;
IReadOnlyPackage currentPackage;
Sprite[] currentSprites;
IModel currentVoxel;
ISound currentSound;
ISoundFormat currentSoundFormat;
Stream currentAudioStream;
VideoPlayerWidget player = null;
bool isVideoLoaded = false;
bool isLoadError = false;
int currentFrame;
WRot modelOrientation;
float spriteScale;
float modelScale;
AssetType assetTypesToDisplay = AssetType.Sprite | AssetType.Model | AssetType.Audio | AssetType.Video;
readonly string allPackages;
[ObjectCreator.UseCtor]
public AssetBrowserLogic(Widget widget, Action onExit, ModData modData, WorldRenderer worldRenderer)
{
sheetBuilders = new Cache<SheetType, SheetBuilder>(t => new SheetBuilder(t));
spriteCache = new Cache<string, Sprite[]>(
filename => FrameLoader.GetFrames(modData.DefaultFileSystem, filename, modData.SpriteLoaders, out _)
.Select(f => sheetBuilders[SheetBuilder.FrameTypeToSheetType(f.Type)].Add(f))
.ToArray());
world = worldRenderer.World;
this.modData = modData;
panel = widget;
allPackages = FluentProvider.GetMessage(AllPackages);
var colorPickerPalettes = world.WorldActor.TraitsImplementing<IProvidesAssetBrowserColorPickerPalettes>()
.SelectMany(p => p.ColorPickerPaletteNames)
.ToArray();
palettes = world.WorldActor.TraitsImplementing<IProvidesAssetBrowserPalettes>()
.SelectMany(p => p.PaletteNames)
.Concat(colorPickerPalettes)
.ToArray();
var ticker = panel.GetOrNull<LogicTickerWidget>("ANIMATION_TICKER");
if (ticker != null)
{
ticker.OnTick = () =>
{
if (animateFrames && currentSprites != null)
SelectNextFrame();
};
}
var sourceDropdown = panel.GetOrNull<DropDownButtonWidget>("SOURCE_SELECTOR");
if (sourceDropdown != null)
{
sourceDropdown.OnMouseDown = _ => ShowSourceDropdown(sourceDropdown);
var sourceName = new CachedTransform<IReadOnlyPackage, string>(GetSourceDisplayName);
sourceDropdown.GetText = () => sourceName.Update(assetSource);
}
var assetTypeDropdown = panel.GetOrNull<DropDownButtonWidget>("ASSET_TYPES_DROPDOWN");
if (assetTypeDropdown != null)
{
var assetTypesPanel = CreateAssetTypesPanel();
assetTypeDropdown.OnMouseDown = _ =>
{
assetTypeDropdown.RemovePanel();
assetTypeDropdown.AttachPanel(assetTypesPanel);
};
}
var spriteWidget = panel.GetOrNull<SpriteWidget>("SPRITE");
if (spriteWidget != null)
{
spriteWidget.GetSprite = () => currentSprites?.Length > 0 ? currentSprites[currentFrame] : null;
currentPalette = spriteWidget.Palette;
spriteScale = spriteWidget.Scale;
spriteWidget.GetPalette = () => currentPalette;
spriteWidget.IsVisible = () => !isVideoLoaded && !isLoadError && currentSprites != null;
spriteWidget.GetScale = () => spriteScale;
}
var playerWidget = panel.GetOrNull<VideoPlayerWidget>("PLAYER");
if (playerWidget != null)
playerWidget.IsVisible = () => isVideoLoaded && !isLoadError;
if (panel.GetOrNull<Widget>("VOXEL") is IModelWidget modelWidget)
{
currentPalette = modelWidget.Palette;
modelScale = modelWidget.Scale;
modelWidget.Setup(
() => !isVideoLoaded && !isLoadError && currentVoxel != null,
() => currentPalette,
() => currentPalette,
() => modelScale,
() => currentVoxel,
() => modelOrientation);
}
var errorLabelWidget = panel.GetOrNull("ERROR");
if (errorLabelWidget != null)
errorLabelWidget.IsVisible = () => isLoadError;
var paletteDropDown = panel.GetOrNull<DropDownButtonWidget>("PALETTE_SELECTOR");
if (paletteDropDown != null)
{
paletteDropDown.OnMouseDown = _ => ShowPaletteDropdown(paletteDropDown);
paletteDropDown.GetText = () => currentPalette;
paletteDropDown.IsVisible = () => currentSprites != null || currentVoxel != null;
panel.GetOrNull<LabelWidget>("PALETTE_DESC").IsVisible = () => currentSprites != null || currentVoxel != null;
}
var colorManager = modData.DefaultRules.Actors[SystemActors.World].TraitInfo<IColorPickerManagerInfo>();
var colorDropdown = panel.GetOrNull<DropDownButtonWidget>("COLOR");
if (colorDropdown != null)
{
var color = Game.Settings.Player.Color;
colorDropdown.IsDisabled = () => !colorPickerPalettes.Contains(currentPalette);
colorDropdown.OnMouseDown = _ => colorManager.ShowColorDropDown(colorDropdown, color, null, worldRenderer, c => color = c);
colorDropdown.IsVisible = () => currentSprites != null || currentVoxel != null;
panel.Get<ColorBlockWidget>("COLORBLOCK").GetColor = () => color;
}
filenameInput = panel.Get<TextFieldWidget>("FILENAME_INPUT");
filenameInput.OnTextEdited = () => ApplyFilter();
filenameInput.OnEscKey = _ =>
{
if (string.IsNullOrEmpty(filenameInput.Text))
filenameInput.YieldKeyboardFocus();
else
{
filenameInput.Text = "";
filenameInput.OnTextEdited();
}
return true;
};
var frameContainer = panel.GetOrNull("FRAME_SELECTOR");
if (frameContainer != null)
frameContainer.IsVisible = () => (currentSprites != null && currentSprites.Length > 1) ||
(isVideoLoaded && player != null && player.Video != null && player.Video.FrameCount > 1) ||
currentSoundFormat != null;
frameSlider = panel.GetOrNull<SliderWidget>("FRAME_SLIDER");
if (frameSlider != null)
{
frameSlider.OnChange += x =>
{
if (!isVideoLoaded)
currentFrame = (int)Math.Round(x);
};
frameSlider.GetValue = () =>
{
if (isVideoLoaded)
return player.Video.CurrentFrameIndex;
if (currentSound != null)
return currentSound.SeekPosition * currentSoundFormat.SampleRate;
return currentFrame;
};
frameSlider.IsDisabled = () => isVideoLoaded || currentSoundFormat != null;
}
var frameText = panel.GetOrNull<LabelWidget>("FRAME_COUNT");
if (frameText != null)
{
var soundLength = new CachedTransform<double, string>(p =>
FluentProvider.GetMessage(LengthInSeconds, "length", Math.Round(p, 3)));
frameText.GetText = () =>
{
if (isVideoLoaded)
return $"{player.Video.CurrentFrameIndex + 1} / {player.Video.FrameCount}";
if (currentSoundFormat != null)
return soundLength.Update(currentSoundFormat.LengthInSeconds);
return $"{currentFrame} / {currentSprites.Length - 1}";
};
}
var playButton = panel.GetOrNull<ButtonWidget>("BUTTON_PLAY");
if (playButton != null)
{
playButton.OnClick = () =>
{
if (isVideoLoaded)
player.Play();
else if (currentSoundFormat != null)
{
if (currentSound != null)
Game.Sound.StopSound(currentSound);
currentSound = Game.Sound.Play(currentSoundFormat, Game.Sound.SoundVolume);
}
else
animateFrames = true;
};
playButton.IsVisible = () => isVideoLoaded ? player.Paused : !animateFrames || currentSoundFormat != null;
}
var pauseButton = panel.GetOrNull<ButtonWidget>("BUTTON_PAUSE");
if (pauseButton != null)
{
pauseButton.OnClick = () =>
{
if (isVideoLoaded)
player.Pause();
else
animateFrames = false;
};
pauseButton.IsVisible = () => isVideoLoaded ? !player.Paused : (animateFrames && currentSoundFormat == null);
}
var stopButton = panel.GetOrNull<ButtonWidget>("BUTTON_STOP");
if (stopButton != null)
{
stopButton.OnClick = () =>
{
if (isVideoLoaded)
player.Stop();
else if (currentSound != null)
Game.Sound.StopSound(currentSound);
else
{
currentFrame = 0;
animateFrames = false;
}
if (frameSlider != null)
frameSlider.Value = 0;
};
}
var nextButton = panel.GetOrNull<ButtonWidget>("BUTTON_NEXT");
if (nextButton != null)
{
nextButton.OnClick = () =>
{
if (!isVideoLoaded)
nextButton.OnClick = SelectNextFrame;
};
nextButton.IsVisible = () => !isVideoLoaded && currentSoundFormat == null;
}
var prevButton = panel.GetOrNull<ButtonWidget>("BUTTON_PREV");
if (prevButton != null)
{
prevButton.OnClick = () =>
{
if (!isVideoLoaded)
SelectPreviousFrame();
};
prevButton.IsVisible = () => !isVideoLoaded && currentSoundFormat == null;
}
var spriteScaleSlider = panel.GetOrNull<SliderWidget>("SPRITE_SCALE_SLIDER");
if (spriteScaleSlider != null)
{
spriteScaleSlider.OnChange += x => spriteScale = x;
spriteScaleSlider.GetValue = () => spriteScale;
spriteScaleSlider.IsVisible = () => currentSprites != null;
panel.GetOrNull<LabelWidget>("SPRITE_SCALE").IsVisible = () => currentSprites != null;
}
var voxelContainer = panel.GetOrNull("VOXEL_SELECTOR");
if (voxelContainer != null)
voxelContainer.IsVisible = () => currentVoxel != null;
var rollSlider = panel.GetOrNull<SliderWidget>("ROLL_SLIDER");
if (rollSlider != null)
{
rollSlider.OnChange += x =>
{
var roll = (int)x;
modelOrientation = modelOrientation.WithRoll(new WAngle(roll));
};
rollSlider.GetValue = () => modelOrientation.Roll.Angle;
}
var pitchSlider = panel.GetOrNull<SliderWidget>("PITCH_SLIDER");
if (pitchSlider != null)
{
pitchSlider.OnChange += x =>
{
var pitch = (int)x;
modelOrientation = modelOrientation.WithPitch(new WAngle(pitch));
};
pitchSlider.GetValue = () => modelOrientation.Pitch.Angle;
}
var yawSlider = panel.GetOrNull<SliderWidget>("YAW_SLIDER");
if (yawSlider != null)
{
yawSlider.OnChange += x =>
{
var yaw = (int)x;
modelOrientation = modelOrientation.WithYaw(new WAngle(yaw));
};
yawSlider.GetValue = () => modelOrientation.Yaw.Angle;
}
var modelScaleSlider = panel.GetOrNull<SliderWidget>("MODEL_SCALE_SLIDER");
if (modelScaleSlider != null)
{
modelScaleSlider.OnChange += x => modelScale = x;
modelScaleSlider.GetValue = () => modelScale;
modelScaleSlider.IsVisible = () => currentVoxel != null;
panel.GetOrNull<LabelWidget>("MODEL_SCALE").IsVisible = () => currentVoxel != null;
}
var assetBrowserModData = modData.Manifest.Get<AssetBrowser>();
allowedSpriteExtensions = assetBrowserModData.SpriteExtensions.Select(x => x.ToLowerInvariant()).ToArray();
allowedModelExtensions = assetBrowserModData.ModelExtensions.Select(x => x.ToLowerInvariant()).ToArray();
allowedAudioExtensions = assetBrowserModData.AudioExtensions.Select(x => x.ToLowerInvariant()).ToArray();
allowedVideoExtensions = assetBrowserModData.VideoExtensions.Select(x => x.ToLowerInvariant()).ToArray();
allowedExtensions = allowedSpriteExtensions
.Union(allowedModelExtensions)
.Union(allowedAudioExtensions)
.Union(allowedVideoExtensions)
.ToArray();
acceptablePackages = modData.ModFiles.MountedPackages.Where(p =>
p.Contents.Any(c => allowedExtensions.Contains(Path.GetExtension(c).ToLowerInvariant())));
assetList = panel.Get<ScrollPanelWidget>("ASSET_LIST");
template = panel.Get<ScrollItemWidget>("ASSET_TEMPLATE");
PopulateAssetList();
var closeButton = panel.GetOrNull<ButtonWidget>("CLOSE_BUTTON");
if (closeButton != null)
closeButton.OnClick = () =>
{
ClearLoadedAssets();
Ui.CloseWindow();
onExit();
};
}
void SelectNextFrame()
{
currentFrame++;
if (currentFrame >= currentSprites.Length)
currentFrame = 0;
}
void SelectPreviousFrame()
{
currentFrame--;
if (currentFrame < 0)
currentFrame = currentSprites.Length - 1;
}
readonly Dictionary<string, bool> assetVisByName = new();
bool FilterAsset(string filename)
{
var filter = filenameInput.Text;
if (string.IsNullOrWhiteSpace(filter))
return true;
if (filename.Contains(filter, StringComparison.OrdinalIgnoreCase))
return true;
return false;
}
void ApplyFilter()
{
assetVisByName.Clear();
assetList.Layout.AdjustChildren();
assetList.ScrollToTop();
// Select the first visible
var firstVisible = assetVisByName.FirstOrDefault(kvp => kvp.Value);
if (firstVisible.Key != null && modData.DefaultFileSystem.TryGetPackageContaining(firstVisible.Key, out var package, out var filename))
LoadAsset(package, filename);
}
void AddAsset(ScrollPanelWidget list, string filepath, IReadOnlyPackage package, ScrollItemWidget template)
{
var item = ScrollItemWidget.Setup(template,
() => currentFilename == filepath && currentPackage == package,
() => LoadAsset(package, filepath));
var label = item.Get<LabelWithTooltipWidget>("TITLE");
WidgetUtils.TruncateLabelToTooltip(label, filepath);
label.GetTooltipText = () => $"{filepath}\n{package.Name}";
item.IsVisible = () =>
{
var allowed =
(assetTypesToDisplay.HasFlag(AssetType.Sprite)
&& allowedSpriteExtensions.Any(ext => filepath.EndsWith(ext, true, CultureInfo.InvariantCulture)))
|| (assetTypesToDisplay.HasFlag(AssetType.Model)
&& allowedModelExtensions.Any(ext => filepath.EndsWith(ext, true, CultureInfo.InvariantCulture)))
|| (assetTypesToDisplay.HasFlag(AssetType.Audio)
&& allowedAudioExtensions.Any(ext => filepath.EndsWith(ext, true, CultureInfo.InvariantCulture)))
|| (assetTypesToDisplay.HasFlag(AssetType.Video)
&& allowedVideoExtensions.Any(ext => filepath.EndsWith(ext, true, CultureInfo.InvariantCulture)))
|| (assetTypesToDisplay.HasFlag(AssetType.Unknown)
&& !allowedExtensions.Any(ext => filepath.EndsWith(ext, true, CultureInfo.InvariantCulture)));
if (assetVisByName.TryGetValue(filepath, out var visible))
return visible && allowed;
visible = FilterAsset(filepath);
assetVisByName.Add(filepath, visible);
return visible && allowed;
};
list.AddChild(item);
}
bool LoadAsset(IReadOnlyPackage package, string filename)
{
ClearLoadedAssets();
if (string.IsNullOrEmpty(filename))
return false;
if (!package.Contains(filename))
return false;
isLoadError = false;
try
{
currentPackage = package;
currentFilename = filename;
var prefix = "";
if (modData.DefaultFileSystem is OpenRA.FileSystem.FileSystem fs)
{
prefix = fs.GetPrefix(package);
if (prefix != null)
prefix += "|";
}
var fileExtension = Path.GetExtension(filename.ToLowerInvariant());
if (allowedSpriteExtensions.Contains(fileExtension))
{
currentSprites = spriteCache[prefix + filename];
currentFrame = 0;
if (frameSlider != null && currentSprites?.Length > 0)
{
frameSlider.MaximumValue = (float)currentSprites.Length - 1;
frameSlider.Ticks = currentSprites.Length;
}
currentVoxel = null;
}
else if (allowedModelExtensions.Contains(fileExtension))
{
var voxelName = Path.GetFileNameWithoutExtension(filename);
currentVoxel = world.WorldActor.Trait<IModelCache>().GetModel(prefix + voxelName);
currentSprites = null;
}
else if (allowedAudioExtensions.Contains(fileExtension))
{
// Mute music so it doesn't interfere with the current asset.
MuteSounds();
currentAudioStream = Game.ModData.DefaultFileSystem.Open(prefix + filename);
foreach (var modDataSoundLoader in Game.ModData.SoundLoaders)
{
if (modDataSoundLoader.TryParseSound(currentAudioStream, out currentSoundFormat))
{
if (frameSlider != null)
{
frameSlider.MaximumValue = currentSoundFormat.LengthInSeconds * currentSoundFormat.SampleRate;
frameSlider.Ticks = 0;
}
break;
}
}
}
else if (allowedVideoExtensions.Contains(fileExtension))
{
// Mute music so it doesn't interfere with the current asset.
MuteSounds();
var video = VideoLoader.GetVideo(Game.ModData.DefaultFileSystem.Open(prefix + filename), true, Game.ModData.VideoLoaders);
if (video != null)
{
player = panel.Get<VideoPlayerWidget>("PLAYER");
player.LoadAndPlay(prefix + filename);
player.DrawOverlay = false;
isVideoLoaded = true;
if (frameSlider != null)
{
frameSlider.MaximumValue = (float)player.Video.FrameCount - 1;
frameSlider.Ticks = 0;
}
}
}
else
return false;
}
catch (Exception exception)
{
isLoadError = true;
Log.AddChannel("assetbrowser", "assetbrowser.log");
Log.Write("assetbrowser", $"Error reading {filename}");
Log.Write("assetbrowser", exception);
return false;
}
return true;
}
bool ShowSourceDropdown(DropDownButtonWidget dropdown)
{
var sourceName = new CachedTransform<IReadOnlyPackage, string>(GetSourceDisplayName);
ScrollItemWidget SetupItem(IReadOnlyPackage source, ScrollItemWidget itemTemplate)
{
var item = ScrollItemWidget.Setup(itemTemplate,
() => assetSource == source,
() => { assetSource = source; PopulateAssetList(); });
item.Get<LabelWidget>("LABEL").GetText = () => sourceName.Update(source);
return item;
}
var sources = new[] { (IReadOnlyPackage)null }.Concat(acceptablePackages);
dropdown.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", 280, sources, SetupItem);
return true;
}
void PopulateAssetList()
{
assetList.RemoveChildren();
var files = new SortedList<string, List<IReadOnlyPackage>>();
if (assetSource != null)
foreach (var content in assetSource.Contents)
files.Add(content, new List<IReadOnlyPackage> { assetSource });
else
{
foreach (var mountedPackage in modData.ModFiles.MountedPackages)
{
foreach (var content in mountedPackage.Contents)
{
if (!files.TryGetValue(content, out var list))
files.Add(content, new List<IReadOnlyPackage> { mountedPackage });
else
list.Add(mountedPackage);
}
}
}
foreach (var file in files)
{
foreach (var package in file.Value)
{
// Don't show unknown files in the engine dir - it is full of code and git and IDE files.
// But do show supported types that are inside just in case.
// Also don't show "files" without extensions because those may be folders.
var fileExtension = Path.GetExtension(file.Key.ToLowerInvariant());
if (string.IsNullOrWhiteSpace(fileExtension) || (package.Name == Platform.EngineDir && !allowedExtensions.Contains(fileExtension)))
continue;
AddAsset(assetList, file.Key, package, template);
}
}
}
bool ShowPaletteDropdown(DropDownButtonWidget dropdown)
{
ScrollItemWidget SetupItem(string name, ScrollItemWidget itemTemplate)
{
var item = ScrollItemWidget.Setup(itemTemplate,
() => currentPalette == name,
() => currentPalette = name);
item.Get<LabelWidget>("LABEL").GetText = () => name;
return item;
}
dropdown.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", 280, palettes, SetupItem);
return true;
}
string GetSourceDisplayName(IReadOnlyPackage source)
{
if (source == null)
return allPackages;
// Packages that are explicitly mounted in the filesystem use their explicit mount name
var fs = (OpenRA.FileSystem.FileSystem)modData.DefaultFileSystem;
var name = fs.GetPrefix(source);
// Fall back to the path relative to the mod, engine, or support dir
if (name == null)
{
name = source.Name;
var compare = Platform.CurrentPlatform == PlatformType.Windows ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
if (name.StartsWith(modData.Manifest.Package.Name, compare))
name = "$" + modData.Manifest.Id + "/" + name[(modData.Manifest.Package.Name.Length + 1)..];
else if (name.StartsWith(Platform.EngineDir, compare))
name = "./" + name[Platform.EngineDir.Length..];
else if (name.StartsWith(Platform.SupportDir, compare))
name = "^" + name[Platform.SupportDir.Length..];
}
if (name.Length > 18)
name = "..." + name[^15..];
return name;
}
// Mute/UnMute code copied from MissionBrowserLogic.
float cachedMusicVolume;
void MuteSounds()
{
if (Game.Sound.MusicVolume > 0)
{
cachedMusicVolume = Game.Sound.MusicVolume;
Game.Sound.MusicVolume = 0;
}
}
void UnMuteSounds()
{
if (cachedMusicVolume > 0)
Game.Sound.MusicVolume = cachedMusicVolume;
}
void ClearLoadedAssets()
{
if (currentSound != null)
Game.Sound.StopSound(currentSound);
currentSprites = null;
currentFrame = 0;
currentVoxel = null;
currentSound = null;
currentSoundFormat = null;
currentAudioStream?.Dispose();
currentAudioStream = null;
player?.Stop();
player = null;
isVideoLoaded = false;
// Just in case we're switching away from a type of asset that forced the music to mute.
UnMuteSounds();
}
Widget CreateAssetTypesPanel()
{
var assetTypesPanel = Ui.LoadWidget("ASSET_TYPES_PANEL", null, new WidgetArgs());
var assetTypeTemplate = assetTypesPanel.Get<CheckboxWidget>("ASSET_TYPE_TEMPLATE");
var allAssetTypes = new[] { AssetType.Sprite, AssetType.Model, AssetType.Audio, AssetType.Video, AssetType.Unknown };
foreach (var type in allAssetTypes)
{
var assetType = assetTypeTemplate.Clone();
var text = type.ToString();
assetType.GetText = () => text;
assetType.IsChecked = () => assetTypesToDisplay.HasFlag(type);
assetType.IsVisible = () => true;
assetType.OnClick = () =>
{
assetTypesToDisplay ^= type;
PopulateAssetList();
};
assetTypesPanel.AddChild(assetType);
}
return assetTypesPanel;
}
}
}