Files
tbd-station-14/Content.Client/SprayPainter/UI/SprayPainterWindow.xaml.cs
Whatstone 9ad99cfa64 Make more objects spray paintable (Reviving #31328) (#37341)
* PaintableAirlockComponent and AirlockGroupPrototype have been replaced

* Slightly redesigned SprayPainterSystem for greater versatility

* Added handling of changes to the appearance of doors and storages

* PaintableGroup prototypes have been created

* Generating tabs with styles in the UI

* Fix error with undiscovered layer

* Slight improvement

* Removed unnecessary property

* The category for `PaintableGroup` was allocated to a separate prototype so that the engine itself would check if the category existed

* Added canisters, but repainting doesn't work

* Added localization to styles

* Fix sprite changing

* Added the ability to paint canisters

* slight ui improvement

* Fix yamllinter errors

* Fix test

* The UI now remembers which tab was open

* Fix build (?)

* Rename

* Charges have been added to the spray painter

* Added a charge texture for the spray painter

* Now spray painter can paint decals

* Increased number of charges

* Spawning dummy objects has been replaced by PrototypeManager

* added a signature about the painting of the object

* fix

* Code commenting

* Fix upstream

* Update Content.Shared/SprayPainter/Components/SprayPainterAmmo.cs

Co-authored-by: pathetic meowmeow <uhhadd@gmail.com>

* review

* Now decals can only be painted if the corresponding tab in the menu is open.

* Fixed a bug with pipe and decal tabs not being remembered

* Update EntityStorageVisualizerSystem.cs

* record

* loc

* Cleanup

* Revert electrified visuals

* more cleanup, fix charges, del ammo4

* no empty file, remove meta component

* closet exceptions, storage visualizer fixes

* enable/disable decal through alt-verb

* Fix missed merge conflicts

* fix snap offset, button event handlers

* simpler order, fix snap loc string

* Remove PaintableViz.BaseRSI, no decal item, A-Z

* State-respecting UI, BUI updates, FTL fixes

* revert DecalPlacerWindow changes

* revert unwanted changes, cleanup function order

* Limit SprayPainterAmmo write access to AmmoSystem

* Remove PaintedSystem

* spray paint ammo lathe recipe, youtool listing

* category as a list, groups as subtabs

* Restore inhand copyright in meta.json

* empty spray painter, recipe produces an empty one

* allow alpha on spray painter decals

* add comments

* paintable wall lockers

* Restrict painting more objects

* Suggested event changes, event cleanup

* component comments, fix ammo inhands

* uncleanable decals, dirty styles on mapinit

* organize paintables, separate emergency/closet grp

* fix categories newline at EOF

* airlock group whitespace cleanup

* realphabetize

* Clean up EntityStorageViz merge conflict markers

* Apply requested changes

* Apply suggestions from sowelipililimute's review

Co-authored-by: pathetic meowmeow <uhhadd@gmail.com>

* betrayal most foul

* Remove members from EntityPaintedEvent

* No emerg. group, steelsec to secure, locker/closet

* Enable repainting the medical wall locker

* comments, no flags on PaintableVisuals

* Remove locked variants from closets/wall closets

* removable decals

* off value consistency

* can't paint away those bones

* fix precedence

* Remove AirlockDepartment, AirlockGroup protos

Both unused.

* whitelist consistency re: ammo component

* add standing emergency closet styles

* alphabetize the spray painter listings

---------

Co-authored-by: Ertanic <black.ikra.14@gmail.com>
Co-authored-by: Эдуард <36124833+Ertanic@users.noreply.github.com>
Co-authored-by: pathetic meowmeow <uhhadd@gmail.com>
2025-07-10 20:36:57 -04:00

305 lines
11 KiB
C#

using System.Linq;
using Content.Client.UserInterface.Controls;
using Content.Shared.Decals;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client.SprayPainter.UI;
/// <summary>
/// A window to select spray painter settings by object type, as well as pipe colours and decals.
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class SprayPainterWindow : DefaultWindow
{
[Dependency] private readonly IEntitySystemManager _sysMan = default!;
[Dependency] private readonly ILocalizationManager _loc = default!;
private readonly SpriteSystem _spriteSystem;
// Events
public event Action<string, string>? OnSpritePicked;
public event Action<int, bool>? OnTabChanged;
public event Action<ProtoId<DecalPrototype>>? OnDecalChanged;
public event Action<ItemList.ItemListSelectedEventArgs>? OnSetPipeColor;
public event Action<Color?>? OnDecalColorChanged;
public event Action<int>? OnDecalAngleChanged;
public event Action<bool>? OnDecalSnapChanged;
// Pipe color data
private ItemList _colorList = default!;
public Dictionary<string, int> ItemColorIndex = new();
private Dictionary<string, Color> _currentPalette = new();
private const string ColorLocKeyPrefix = "pipe-painter-color-";
// Paintable objects
private Dictionary<string, Dictionary<string, EntProtoId>> _currentStylesByGroup = new();
private Dictionary<string, List<string>> _currentGroupsByCategory = new();
// Tab controls
private Dictionary<string, SprayPainterGroup> _paintableControls = new();
private BoxContainer? _pipeControl;
// Decals
private List<SprayPainterDecalEntry> _currentDecals = [];
private SprayPainterDecals? _sprayPainterDecals;
private readonly SpriteSpecifier _colorEntryIconTexture = new SpriteSpecifier.Rsi(
new ResPath("Structures/Piping/Atmospherics/pipe.rsi"),
"pipeStraight");
public SprayPainterWindow()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_spriteSystem = _sysMan.GetEntitySystem<SpriteSystem>();
Tabs.OnTabChanged += (index) => OnTabChanged?.Invoke(index, _sprayPainterDecals?.GetPositionInParent() == index);
}
private string GetColorLocString(string? colorKey)
{
if (string.IsNullOrEmpty(colorKey))
return Loc.GetString("pipe-painter-no-color-selected");
var locKey = ColorLocKeyPrefix + colorKey;
if (!_loc.TryGetString(locKey, out var locString))
locString = colorKey;
return locString;
}
public string? IndexToColorKey(int index)
{
return _colorList[index].Text;
}
private void OnStyleSelected(ListData data)
{
if (data is SpriteListData listData)
OnSpritePicked?.Invoke(listData.Group, listData.Style);
}
/// <summary>
/// Wrapper to allow for selecting/deselecting the event to avoid loops
/// </summary>
private void OnColorPicked(ItemList.ItemListSelectedEventArgs args)
{
OnSetPipeColor?.Invoke(args);
}
/// <summary>
/// Setup function for the window.
/// </summary>
/// <param name="stylesByGroup">Each group, mapped by name to the set of named styles by their associated entity prototype.</param>
/// <param name="groupsByCategory">The set of categories and the groups associated with them.</param>
/// <param name="decals">A list of each decal.</param>
public void PopulateCategories(Dictionary<string, Dictionary<string, EntProtoId>> stylesByGroup, Dictionary<string, List<string>> groupsByCategory, List<SprayPainterDecalEntry> decals)
{
bool tabsCleared = false;
var lastTab = Tabs.CurrentTab;
if (!_currentGroupsByCategory.Equals(groupsByCategory))
{
// Destroy all existing tabs
tabsCleared = true;
_paintableControls.Clear();
_pipeControl = null;
_sprayPainterDecals = null;
Tabs.RemoveAllChildren();
}
// Only clear if the entries change. Otherwise the list would "jump" after selecting an item
if (tabsCleared || !_currentStylesByGroup.Equals(stylesByGroup))
{
_currentStylesByGroup = stylesByGroup;
var tabIndex = 0;
foreach (var (categoryName, categoryGroups) in groupsByCategory.OrderBy(c => c.Key))
{
if (categoryGroups.Count <= 0)
continue;
// Repopulating controls:
// ensure that categories with multiple groups have separate subtabs
// but single-group categories do not.
if (tabsCleared)
{
TabContainer? subTabs = null;
if (categoryGroups.Count > 1)
subTabs = new();
foreach (var group in categoryGroups)
{
if (!stylesByGroup.TryGetValue(group, out var styles))
continue;
var groupControl = new SprayPainterGroup();
groupControl.OnButtonPressed += OnStyleSelected;
_paintableControls[group] = groupControl;
if (categoryGroups.Count > 1)
{
if (subTabs != null)
{
subTabs?.AddChild(groupControl);
var subTabLocalization = Loc.GetString("spray-painter-tab-group-" + group.ToLower());
TabContainer.SetTabTitle(groupControl, subTabLocalization);
}
}
else
{
Tabs.AddChild(groupControl);
}
}
if (subTabs != null)
Tabs.AddChild(subTabs);
var tabLocalization = Loc.GetString("spray-painter-tab-category-" + categoryName.ToLower());
Tabs.SetTabTitle(tabIndex, tabLocalization);
tabIndex++;
}
// Finally, populate all groups with new data.
foreach (var group in categoryGroups)
{
if (!stylesByGroup.TryGetValue(group, out var styles) ||
!_paintableControls.TryGetValue(group, out var control))
continue;
var dataList = styles
.Select(e => new SpriteListData(group, e.Key, e.Value, 0))
.OrderBy(d => Loc.GetString($"spray-painter-style-{group.ToLower()}-{d.Style.ToLower()}"))
.ToList();
control.PopulateList(dataList);
}
}
}
PopulateColors(_currentPalette);
if (!_currentDecals.Equals(decals))
{
_currentDecals = decals;
if (_sprayPainterDecals is null)
{
_sprayPainterDecals = new SprayPainterDecals();
_sprayPainterDecals.OnDecalSelected += id => OnDecalChanged?.Invoke(id);
_sprayPainterDecals.OnColorChanged += color => OnDecalColorChanged?.Invoke(color);
_sprayPainterDecals.OnAngleChanged += angle => OnDecalAngleChanged?.Invoke(angle);
_sprayPainterDecals.OnSnapChanged += snap => OnDecalSnapChanged?.Invoke(snap);
Tabs.AddChild(_sprayPainterDecals);
TabContainer.SetTabTitle(_sprayPainterDecals, Loc.GetString("spray-painter-tab-category-decals"));
}
_sprayPainterDecals.PopulateDecals(decals, _spriteSystem);
}
if (tabsCleared)
SetSelectedTab(lastTab);
}
public void PopulateColors(Dictionary<string, Color> palette)
{
// Create pipe tab controls if they don't exist
bool tabCreated = false;
if (_pipeControl == null)
{
_pipeControl = new BoxContainer() { Orientation = BoxContainer.LayoutOrientation.Vertical };
var label = new Label() { Text = Loc.GetString("spray-painter-selected-color") };
_colorList = new ItemList() { VerticalExpand = true };
_colorList.OnItemSelected += OnColorPicked;
_pipeControl.AddChild(label);
_pipeControl.AddChild(_colorList);
Tabs.AddChild(_pipeControl);
TabContainer.SetTabTitle(_pipeControl, Loc.GetString("spray-painter-tab-category-pipes"));
tabCreated = true;
}
// Populate the tab if needed (new tab/new data)
if (tabCreated || !_currentPalette.Equals(palette))
{
_currentPalette = palette;
ItemColorIndex.Clear();
_colorList.Clear();
int index = 0;
foreach (var color in palette)
{
var locString = GetColorLocString(color.Key);
var item = _colorList.AddItem(locString, _spriteSystem.Frame0(_colorEntryIconTexture), metadata: color.Key);
item.IconModulate = color.Value;
ItemColorIndex.Add(color.Key, index);
index++;
}
}
}
# region Setters
public void SetSelectedStyles(Dictionary<string, string> selectedStyles)
{
foreach (var (group, style) in selectedStyles)
{
if (!_paintableControls.TryGetValue(group, out var control))
continue;
control.SelectItemByStyle(style);
}
}
public void SelectColor(string color)
{
if (_colorList != null && ItemColorIndex.TryGetValue(color, out var colorIdx))
{
_colorList.OnItemSelected -= OnColorPicked;
_colorList[colorIdx].Selected = true;
_colorList.OnItemSelected += OnColorPicked;
}
}
public void SetSelectedTab(int tab)
{
Tabs.CurrentTab = int.Min(tab, Tabs.ChildCount - 1);
}
public void SetSelectedDecal(string decal)
{
if (_sprayPainterDecals != null)
_sprayPainterDecals.SetSelectedDecal(decal);
}
public void SetDecalAngle(int angle)
{
if (_sprayPainterDecals != null)
_sprayPainterDecals.SetAngle(angle);
}
public void SetDecalColor(Color? color)
{
if (_sprayPainterDecals != null)
_sprayPainterDecals.SetColor(color);
}
public void SetDecalSnap(bool snap)
{
if (_sprayPainterDecals != null)
_sprayPainterDecals.SetSnap(snap);
}
# endregion
}
public record SpriteListData(string Group, string Style, EntProtoId Prototype, int SelectedIndex) : ListData;