1
0

PowerMonitoringWindow.xaml.Widgets.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537
  1. using Content.Client.Stylesheets;
  2. using Content.Shared.Power;
  3. using Robust.Client.Graphics;
  4. using Robust.Client.UserInterface.Controls;
  5. using Robust.Shared.Utility;
  6. using System.Diagnostics.CodeAnalysis;
  7. using System.Linq;
  8. using System.Numerics;
  9. using Vector4 = Robust.Shared.Maths.Vector4;
  10. namespace Content.Client.Power;
  11. public sealed partial class PowerMonitoringWindow
  12. {
  13. private SpriteSpecifier.Texture _sourceIcon = new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/PowerMonitoring/source_arrow.png"));
  14. private SpriteSpecifier.Texture _loadIconPath = new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/PowerMonitoring/load_arrow.png"));
  15. private bool _autoScrollActive = false;
  16. private bool _autoScrollAwaitsUpdate = false;
  17. private void UpdateWindowConsoleEntry
  18. (BoxContainer masterContainer,
  19. int index,
  20. PowerMonitoringConsoleEntry entry,
  21. PowerMonitoringConsoleEntry[] focusSources,
  22. PowerMonitoringConsoleEntry[] focusLoads)
  23. {
  24. UpdateWindowConsoleEntry(masterContainer, index, entry);
  25. var windowEntry = masterContainer.GetChild(index) as PowerMonitoringWindowEntry;
  26. // If we exit here, something was added to the container that shouldn't have been added
  27. if (windowEntry == null)
  28. return;
  29. // Update sources and loads
  30. UpdateEntrySourcesOrLoads(masterContainer, windowEntry.SourcesContainer, focusSources, _sourceIcon);
  31. UpdateEntrySourcesOrLoads(masterContainer, windowEntry.LoadsContainer, focusLoads, _loadIconPath);
  32. windowEntry.MainContainer.Visible = true;
  33. }
  34. private void UpdateWindowConsoleEntry(BoxContainer masterContainer, int index, PowerMonitoringConsoleEntry entry)
  35. {
  36. PowerMonitoringWindowEntry? windowEntry;
  37. // Add missing children
  38. if (index >= masterContainer.ChildCount)
  39. {
  40. // Add basic entry
  41. windowEntry = new PowerMonitoringWindowEntry(entry);
  42. masterContainer.AddChild(windowEntry);
  43. // Selection action
  44. windowEntry.Button.OnButtonUp += args =>
  45. {
  46. windowEntry.SourcesContainer.DisposeAllChildren();
  47. windowEntry.LoadsContainer.DisposeAllChildren();
  48. ButtonAction(windowEntry, masterContainer);
  49. };
  50. }
  51. else
  52. {
  53. windowEntry = masterContainer.GetChild(index) as PowerMonitoringWindowEntry;
  54. }
  55. // If we exit here, something was added to the container that shouldn't have been added
  56. if (windowEntry == null)
  57. return;
  58. windowEntry.NetEntity = entry.NetEntity;
  59. windowEntry.Entry = entry;
  60. windowEntry.MainContainer.Visible = false;
  61. UpdateWindowEntryButton(entry.NetEntity, windowEntry.Button, entry);
  62. }
  63. public void UpdateWindowEntryButton(NetEntity netEntity, PowerMonitoringButton button, PowerMonitoringConsoleEntry entry)
  64. {
  65. if (!netEntity.IsValid())
  66. return;
  67. if (entry.MetaData == null)
  68. return;
  69. // Update button style
  70. if (netEntity == _focusEntity)
  71. button.AddStyleClass(StyleNano.StyleClassButtonColorGreen);
  72. else
  73. button.RemoveStyleClass(StyleNano.StyleClassButtonColorGreen);
  74. // Update sprite
  75. if (entry.MetaData.Value.SpritePath != string.Empty && entry.MetaData.Value.SpriteState != string.Empty)
  76. button.TextureRect.Texture = _spriteSystem.Frame0(new SpriteSpecifier.Rsi(new ResPath(entry.MetaData.Value.SpritePath), entry.MetaData.Value.SpriteState));
  77. // Update name
  78. var name = Loc.GetString(entry.MetaData.Value.EntityName);
  79. button.NameLocalized.Text = name;
  80. // Update tool tip
  81. button.ToolTip = Loc.GetString(name);
  82. // Update power value
  83. // Don't use SI prefixes, just give the number in W, so that it is readily apparent which consumer is using a lot of power.
  84. button.PowerValue.Text = Loc.GetString("power-monitoring-window-button-value", ("value", Math.Round(entry.PowerValue).ToString("N0")));
  85. // Update battery level if applicable
  86. if (entry.BatteryLevel != null)
  87. {
  88. button.BatteryLevel.Value = entry.BatteryLevel.Value;
  89. button.BatteryLevel.Visible = true;
  90. button.BatteryPercentage.Text = entry.BatteryLevel.Value.ToString("P0");
  91. button.BatteryPercentage.Visible = true;
  92. // Set progress bar color based on percentage
  93. var color = Color.FromHsv(new Vector4(entry.BatteryLevel.Value * 0.33f, 1, 1, 1));
  94. button.BatteryLevel.ForegroundStyleBoxOverride = new StyleBoxFlat { BackgroundColor = color };
  95. }
  96. else
  97. {
  98. button.BatteryLevel.Visible = false;
  99. button.BatteryPercentage.Visible = false;
  100. }
  101. }
  102. private void UpdateEntrySourcesOrLoads(BoxContainer masterContainer, BoxContainer currentContainer, PowerMonitoringConsoleEntry[]? entries, SpriteSpecifier.Texture icon)
  103. {
  104. if (currentContainer == null)
  105. return;
  106. if (entries == null || entries.Length == 0)
  107. {
  108. currentContainer.RemoveAllChildren();
  109. return;
  110. }
  111. // Remove excess children
  112. while (currentContainer.ChildCount > entries.Length)
  113. {
  114. currentContainer.RemoveChild(currentContainer.GetChild(currentContainer.ChildCount - 1));
  115. }
  116. // Add missing children
  117. while (currentContainer.ChildCount < entries.Length)
  118. {
  119. var entry = entries[currentContainer.ChildCount];
  120. var subEntry = new PowerMonitoringWindowSubEntry(entry);
  121. currentContainer.AddChild(subEntry);
  122. // Selection action
  123. subEntry.Button.OnButtonUp += args => { ButtonAction(subEntry, masterContainer); };
  124. }
  125. if (!_entManager.TryGetComponent<PowerMonitoringConsoleComponent>(Entity, out var console))
  126. return;
  127. // Update all children
  128. foreach (var child in currentContainer.Children)
  129. {
  130. if (child is not PowerMonitoringWindowSubEntry)
  131. continue;
  132. var castChild = (PowerMonitoringWindowSubEntry) child;
  133. if (castChild == null)
  134. continue;
  135. if (castChild.Icon != null)
  136. castChild.Icon.Texture = _spriteSystem.Frame0(icon);
  137. var entry = entries[child.GetPositionInParent()];
  138. castChild.NetEntity = entry.NetEntity;
  139. castChild.Entry = entry;
  140. UpdateWindowEntryButton(entry.NetEntity, castChild.Button, entries.ElementAt(child.GetPositionInParent()));
  141. }
  142. }
  143. private void ButtonAction(PowerMonitoringWindowBaseEntry entry, BoxContainer masterContainer)
  144. {
  145. // Toggle off button?
  146. if (entry.NetEntity == _focusEntity)
  147. {
  148. entry.Button.RemoveStyleClass(StyleNano.StyleClassButtonColorGreen);
  149. _focusEntity = null;
  150. // Request an update from the power monitoring system
  151. SendPowerMonitoringConsoleMessageAction?.Invoke(null, entry.Entry.Group);
  152. return;
  153. }
  154. // Otherwise, toggle on
  155. entry.Button.AddStyleClass(StyleNano.StyleClassButtonColorGreen);
  156. ActivateAutoScrollToFocus();
  157. // Toggle off the old button (if applicable)
  158. if (_focusEntity != null)
  159. {
  160. foreach (PowerMonitoringWindowEntry sibling in masterContainer.Children)
  161. {
  162. if (sibling.NetEntity == _focusEntity)
  163. {
  164. sibling.Button.RemoveStyleClass(StyleNano.StyleClassButtonColorGreen);
  165. break;
  166. }
  167. }
  168. }
  169. // Center the nav map on selected entity
  170. _focusEntity = entry.NetEntity;
  171. if (!NavMap.TrackedEntities.TryGetValue(entry.NetEntity, out var blip))
  172. return;
  173. NavMap.CenterToCoordinates(blip.Coordinates);
  174. // Switch tabs
  175. SwitchTabsBasedOnPowerMonitoringConsoleGroup(entry.Entry.Group);
  176. // Send an update from the power monitoring system
  177. SendPowerMonitoringConsoleMessageAction?.Invoke(_focusEntity, entry.Entry.Group);
  178. }
  179. private void ActivateAutoScrollToFocus()
  180. {
  181. _autoScrollActive = false;
  182. _autoScrollAwaitsUpdate = true;
  183. }
  184. private bool TryGetNextScrollPosition([NotNullWhen(true)] out float? nextScrollPosition)
  185. {
  186. nextScrollPosition = null;
  187. var scroll = MasterTabContainer.Children.ElementAt(MasterTabContainer.CurrentTab) as ScrollContainer;
  188. if (scroll == null)
  189. return false;
  190. var container = scroll.Children.ElementAt(0) as BoxContainer;
  191. if (container == null || container.Children.Count() == 0)
  192. return false;
  193. // Exit if the heights of the children haven't been initialized yet
  194. if (!container.Children.Any(x => x.Height > 0))
  195. return false;
  196. nextScrollPosition = 0;
  197. foreach (var control in container.Children)
  198. {
  199. if (control == null || control is not PowerMonitoringWindowEntry)
  200. continue;
  201. if (((PowerMonitoringWindowEntry) control).NetEntity == _focusEntity)
  202. return true;
  203. nextScrollPosition += control.Height;
  204. }
  205. // Failed to find control
  206. nextScrollPosition = null;
  207. return false;
  208. }
  209. private void AutoScrollToFocus()
  210. {
  211. if (!_autoScrollActive)
  212. return;
  213. var scroll = MasterTabContainer.Children.ElementAt(MasterTabContainer.CurrentTab) as ScrollContainer;
  214. if (scroll == null)
  215. return;
  216. if (!TryGetNextScrollPosition(out float? nextScrollPosition))
  217. return;
  218. scroll.VScrollTarget = nextScrollPosition.Value;
  219. if (MathHelper.CloseToPercent(scroll.VScroll, scroll.VScrollTarget))
  220. _autoScrollActive = false;
  221. }
  222. private void UpdateWarningLabel(PowerMonitoringFlags flags)
  223. {
  224. if (flags == PowerMonitoringFlags.None)
  225. {
  226. SystemWarningPanel.Visible = false;
  227. return;
  228. }
  229. var msg = new FormattedMessage();
  230. if ((flags & PowerMonitoringFlags.RoguePowerConsumer) != 0)
  231. {
  232. SystemWarningPanel.PanelOverride = new StyleBoxFlat
  233. {
  234. BackgroundColor = Color.Red,
  235. BorderColor = Color.DarkRed,
  236. BorderThickness = new Thickness(2),
  237. };
  238. msg.AddMarkupOrThrow(Loc.GetString("power-monitoring-window-rogue-power-consumer"));
  239. SystemWarningPanel.Visible = true;
  240. }
  241. else if ((flags & PowerMonitoringFlags.PowerNetAbnormalities) != 0)
  242. {
  243. SystemWarningPanel.PanelOverride = new StyleBoxFlat
  244. {
  245. BackgroundColor = Color.Orange,
  246. BorderColor = Color.DarkOrange,
  247. BorderThickness = new Thickness(2),
  248. };
  249. msg.AddMarkupOrThrow(Loc.GetString("power-monitoring-window-power-net-abnormalities"));
  250. SystemWarningPanel.Visible = true;
  251. }
  252. SystemWarningLabel.SetMessage(msg);
  253. }
  254. private void SwitchTabsBasedOnPowerMonitoringConsoleGroup(PowerMonitoringConsoleGroup group)
  255. {
  256. switch (group)
  257. {
  258. case PowerMonitoringConsoleGroup.Generator:
  259. MasterTabContainer.CurrentTab = 0; break;
  260. case PowerMonitoringConsoleGroup.SMES:
  261. MasterTabContainer.CurrentTab = 1; break;
  262. case PowerMonitoringConsoleGroup.Substation:
  263. MasterTabContainer.CurrentTab = 2; break;
  264. case PowerMonitoringConsoleGroup.APC:
  265. MasterTabContainer.CurrentTab = 3; break;
  266. }
  267. }
  268. private PowerMonitoringConsoleGroup GetCurrentPowerMonitoringConsoleGroup()
  269. {
  270. return (PowerMonitoringConsoleGroup) MasterTabContainer.CurrentTab;
  271. }
  272. }
  273. public sealed class PowerMonitoringWindowEntry : PowerMonitoringWindowBaseEntry
  274. {
  275. public BoxContainer MainContainer;
  276. public BoxContainer SourcesContainer;
  277. public BoxContainer LoadsContainer;
  278. public PowerMonitoringWindowEntry(PowerMonitoringConsoleEntry entry) : base(entry)
  279. {
  280. Entry = entry;
  281. // Alignment
  282. Orientation = LayoutOrientation.Vertical;
  283. HorizontalExpand = true;
  284. // Update selection button
  285. Button.StyleClasses.Add("OpenLeft");
  286. AddChild(Button);
  287. // Grid container to hold sub containers
  288. MainContainer = new BoxContainer()
  289. {
  290. Orientation = LayoutOrientation.Vertical,
  291. HorizontalExpand = true,
  292. Margin = new Thickness(8, 0, 0, 0),
  293. Visible = false,
  294. };
  295. AddChild(MainContainer);
  296. // Grid container to hold the list of sources when selected
  297. SourcesContainer = new BoxContainer()
  298. {
  299. Orientation = LayoutOrientation.Vertical,
  300. HorizontalExpand = true,
  301. };
  302. MainContainer.AddChild(SourcesContainer);
  303. // Grid container to hold the list of loads when selected
  304. LoadsContainer = new BoxContainer()
  305. {
  306. Orientation = LayoutOrientation.Vertical,
  307. HorizontalExpand = true,
  308. };
  309. MainContainer.AddChild(LoadsContainer);
  310. }
  311. }
  312. public sealed class PowerMonitoringWindowSubEntry : PowerMonitoringWindowBaseEntry
  313. {
  314. public TextureRect? Icon;
  315. public PowerMonitoringWindowSubEntry(PowerMonitoringConsoleEntry entry) : base(entry)
  316. {
  317. Orientation = LayoutOrientation.Horizontal;
  318. HorizontalExpand = true;
  319. // Source/load icon
  320. Icon = new TextureRect()
  321. {
  322. VerticalAlignment = VAlignment.Center,
  323. Margin = new Thickness(0, 0, 2, 0),
  324. };
  325. AddChild(Icon);
  326. // Selection button
  327. Button.StyleClasses.Add("OpenBoth");
  328. AddChild(Button);
  329. }
  330. }
  331. public abstract class PowerMonitoringWindowBaseEntry : BoxContainer
  332. {
  333. public NetEntity NetEntity;
  334. public PowerMonitoringConsoleEntry Entry;
  335. public PowerMonitoringButton Button;
  336. public PowerMonitoringWindowBaseEntry(PowerMonitoringConsoleEntry entry)
  337. {
  338. Entry = entry;
  339. // Add selection button (properties set by derivative classes)
  340. Button = new PowerMonitoringButton();
  341. }
  342. }
  343. public sealed class PowerMonitoringButton : Button
  344. {
  345. public BoxContainer MainContainer;
  346. public TextureRect TextureRect;
  347. public Label NameLocalized;
  348. public ProgressBar BatteryLevel;
  349. public PanelContainer BackgroundPanel;
  350. public Label BatteryPercentage;
  351. public Label PowerValue;
  352. public PowerMonitoringButton()
  353. {
  354. HorizontalExpand = true;
  355. VerticalExpand = true;
  356. Margin = new Thickness(0f, 1f, 0f, 1f);
  357. MainContainer = new BoxContainer()
  358. {
  359. Orientation = BoxContainer.LayoutOrientation.Horizontal,
  360. HorizontalExpand = true,
  361. SetHeight = 32f,
  362. };
  363. AddChild(MainContainer);
  364. TextureRect = new TextureRect()
  365. {
  366. HorizontalAlignment = HAlignment.Center,
  367. VerticalAlignment = VAlignment.Center,
  368. SetSize = new Vector2(32f, 32f),
  369. Margin = new Thickness(0f, 0f, 5f, 0f),
  370. };
  371. MainContainer.AddChild(TextureRect);
  372. NameLocalized = new Label()
  373. {
  374. HorizontalExpand = true,
  375. ClipText = true,
  376. };
  377. MainContainer.AddChild(NameLocalized);
  378. BatteryLevel = new ProgressBar()
  379. {
  380. SetWidth = 47f,
  381. SetHeight = 20f,
  382. Margin = new Thickness(15, 0, 0, 0),
  383. MaxValue = 1,
  384. Visible = false,
  385. BackgroundStyleBoxOverride = new StyleBoxFlat { BackgroundColor = Color.Black },
  386. };
  387. MainContainer.AddChild(BatteryLevel);
  388. BackgroundPanel = new PanelContainer
  389. {
  390. // Draw a half-transparent box over the battery level to make the text more readable.
  391. PanelOverride = new StyleBoxFlat
  392. {
  393. BackgroundColor = new Color(0, 0, 0, 0.9f)
  394. },
  395. HorizontalAlignment = HAlignment.Center,
  396. VerticalAlignment = VAlignment.Center,
  397. HorizontalExpand = true,
  398. VerticalExpand = true,
  399. // Box is undersized perfectly compared to the progress bar, so a little bit of the unaffected progress bar is visible.
  400. SetSize = new Vector2(43f, 16f)
  401. };
  402. BatteryLevel.AddChild(BackgroundPanel);
  403. BatteryPercentage = new Label()
  404. {
  405. VerticalAlignment = VAlignment.Center,
  406. HorizontalAlignment = HAlignment.Center,
  407. Align = Label.AlignMode.Center,
  408. SetWidth = 45f,
  409. MinWidth = 20f,
  410. Margin = new Thickness(10, -4, 10, 0),
  411. ClipText = true,
  412. Visible = false,
  413. };
  414. BackgroundPanel.AddChild(BatteryPercentage);
  415. PowerValue = new Label()
  416. {
  417. HorizontalAlignment = HAlignment.Right,
  418. Align = Label.AlignMode.Right,
  419. SetWidth = 80f,
  420. Margin = new Thickness(10, 0, 0, 0),
  421. ClipText = true,
  422. };
  423. MainContainer.AddChild(PowerValue);
  424. }
  425. }