diff --git a/OpenRA.Game/TextNotification.cs b/OpenRA.Game/TextNotification.cs index 71b8ee2a5b..8440cc6395 100644 --- a/OpenRA.Game/TextNotification.cs +++ b/OpenRA.Game/TextNotification.cs @@ -21,16 +21,18 @@ namespace OpenRA public readonly TextNotificationPool Pool; public readonly string Prefix; public readonly string Text; - public readonly Color PrefixColor; - public readonly Color TextColor; + public readonly Color? PrefixColor; + public readonly Color? TextColor; + public readonly DateTime Time; - public TextNotification(TextNotificationPool pool, string prefix, string text, Color prefixColor, Color textColor) + public TextNotification(TextNotificationPool pool, string prefix, string text, Color? prefixColor, Color? textColor) { Pool = pool; Prefix = prefix; Text = text; PrefixColor = prefixColor; TextColor = textColor; + Time = DateTime.Now; } public bool CanIncrementOnDuplicate() @@ -38,19 +40,17 @@ namespace OpenRA return Pool == TextNotificationPool.Feedback || Pool == TextNotificationPool.System; } - public bool Equals(TextNotification other) - { - return other != null && other.GetHashCode() == GetHashCode(); - } + public static bool operator ==(TextNotification me, TextNotification other) { return me.GetHashCode() == other.GetHashCode(); } - public override bool Equals(object obj) - { - return obj is TextNotification && Equals((TextNotification)obj); - } + public static bool operator !=(TextNotification me, TextNotification other) { return !(me == other); } + + public bool Equals(TextNotification other) { return other == this; } + + public override bool Equals(object obj) { return obj is TextNotification notification && Equals(notification); } public override int GetHashCode() { - return string.Format("{0}{1}{2}", Prefix, Text, Pool).GetHashCode(); + return HashCode.Combine(Prefix, Text, (int)Pool); } } } diff --git a/OpenRA.Game/TextNotificationsManager.cs b/OpenRA.Game/TextNotificationsManager.cs index 00500776fb..54af0fdb8a 100644 --- a/OpenRA.Game/TextNotificationsManager.cs +++ b/OpenRA.Game/TextNotificationsManager.cs @@ -16,33 +16,29 @@ namespace OpenRA { public static class TextNotificationsManager { - static Color systemMessageColor = Color.White; - static Color chatMessageColor = Color.White; - static string systemMessageLabel; + static readonly string SystemMessageLabel; public static long ChatDisabledUntil { get; internal set; } static TextNotificationsManager() { - ChromeMetrics.TryGet("ChatMessageColor", out chatMessageColor); - ChromeMetrics.TryGet("SystemMessageColor", out systemMessageColor); - if (!ChromeMetrics.TryGet("SystemMessageLabel", out systemMessageLabel)) - systemMessageLabel = "Battlefield Control"; + if (!ChromeMetrics.TryGet("SystemMessageLabel", out SystemMessageLabel)) + SystemMessageLabel = "Battlefield Control"; } public static void AddFeedbackLine(string text) { - AddTextNotification(TextNotificationPool.Feedback, systemMessageLabel, text, systemMessageColor, systemMessageColor); + AddTextNotification(TextNotificationPool.Feedback, SystemMessageLabel, text); } public static void AddSystemLine(string text) { - AddSystemLine(systemMessageLabel, text); + AddSystemLine(SystemMessageLabel, text); } public static void AddSystemLine(string prefix, string text) { - AddTextNotification(TextNotificationPool.System, prefix, text, systemMessageColor, systemMessageColor); + AddTextNotification(TextNotificationPool.System, prefix, text); } public static void AddChatLine(string prefix, string text, Color? prefixColor = null, Color? textColor = null) @@ -58,7 +54,7 @@ namespace OpenRA static void AddTextNotification(TextNotificationPool pool, string prefix, string text, Color? prefixColor = null, Color? textColor = null) { if (IsPoolEnabled(pool)) - Game.OrderManager.AddTextNotification(new TextNotification(pool, prefix, text, prefixColor ?? chatMessageColor, textColor ?? chatMessageColor)); + Game.OrderManager.AddTextNotification(new TextNotification(pool, prefix, text, prefixColor, textColor)); } static bool IsPoolEnabled(TextNotificationPool pool) diff --git a/OpenRA.Mods.Common/Widgets/ChatDisplayWidget.cs b/OpenRA.Mods.Common/Widgets/ChatDisplayWidget.cs deleted file mode 100644 index 99983686fd..0000000000 --- a/OpenRA.Mods.Common/Widgets/ChatDisplayWidget.cs +++ /dev/null @@ -1,143 +0,0 @@ -#region Copyright & License Information -/* - * Copyright 2007-2021 The OpenRA Developers (see AUTHORS) - * 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.Collections.Generic; -using System.Linq; -using OpenRA.Primitives; -using OpenRA.Widgets; - -namespace OpenRA.Mods.Common.Widgets -{ - public class ChatDisplayWidget : Widget - { - public readonly int RemoveTime = 0; - public readonly bool UseContrast = false; - public readonly bool UseShadow = false; - public readonly Color BackgroundColorDark = ChromeMetrics.Get("TextContrastColorDark"); - public readonly Color BackgroundColorLight = ChromeMetrics.Get("TextContrastColorLight"); - public string Notification = ""; - public readonly int TextLineBoxHeight = 16; - public readonly int Space = 4; - - const int LogLength = 9; - List recentLines = new List(); - List lineExpirations = new List(); - - public override Rectangle EventBounds => Rectangle.Empty; - - public override void Draw() - { - var pos = RenderOrigin; - var chatLogArea = new Rectangle(pos.X, pos.Y, Bounds.Width, Bounds.Height); - var chatPos = new int2(chatLogArea.X + 5, chatLogArea.Bottom - 8); - - var font = Game.Renderer.Fonts["Regular"]; - Game.Renderer.EnableScissor(chatLogArea); - - foreach (var line in recentLines.AsEnumerable().Reverse()) - { - var lineHeight = TextLineBoxHeight; - var inset = 0; - string name = null; - - if (!string.IsNullOrEmpty(line.Prefix)) - { - name = line.Prefix + ":"; - inset = font.Measure(name).X + 5; - } - - var text = WidgetUtils.WrapText(line.Text, chatLogArea.Width - inset - 6, font); - var textSize = font.Measure(text).Y; - var offset = font.TopOffset; - - if (chatPos.Y - font.TopOffset < pos.Y) - break; - - var textLineHeight = lineHeight; - - var dh = textSize - textLineHeight; - if (dh > 0) - textLineHeight += dh; - - var textOffset = textLineHeight - (textLineHeight - textSize - offset) / 2; - var textPos = new int2(chatPos.X + inset, chatPos.Y - textOffset); - - if (name != null) - { - var nameSize = font.Measure(name).Y; - var namePos = chatPos.WithY(chatPos.Y - (textLineHeight - (lineHeight - nameSize - offset) / 2)); - - if (UseContrast) - font.DrawTextWithContrast(name, namePos, - line.PrefixColor, BackgroundColorDark, BackgroundColorLight, 1); - else if (UseShadow) - font.DrawTextWithShadow(name, namePos, - line.PrefixColor, BackgroundColorDark, BackgroundColorLight, 1); - else - font.DrawText(name, namePos, line.PrefixColor); - } - - if (UseContrast) - font.DrawTextWithContrast(text, textPos, - line.TextColor, Color.Black, 1); - else if (UseShadow) - font.DrawTextWithShadow(text, textPos, - line.TextColor, Color.Black, 1); - else - font.DrawText(text, textPos, Color.White); - - chatPos = chatPos.WithY(chatPos.Y - Space - textLineHeight); - } - - Game.Renderer.DisableScissor(); - } - - public void AddLine(TextNotification chatLine) - { - recentLines.Add(chatLine); - lineExpirations.Add(Game.LocalTick + RemoveTime); - - if (Notification != null) - Game.Sound.Play(SoundType.UI, Notification); - - while (recentLines.Count > LogLength) - RemoveLine(); - } - - public void RemoveMostRecentLine() - { - if (recentLines.Count == 0) - return; - - recentLines.RemoveAt(recentLines.Count - 1); - lineExpirations.RemoveAt(lineExpirations.Count - 1); - } - - public void RemoveLine() - { - if (recentLines.Count == 0) - return; - - recentLines.RemoveAt(0); - lineExpirations.RemoveAt(0); - } - - public override void Tick() - { - if (RemoveTime == 0) - return; - - // This takes advantage of the fact that recentLines is ordered by expiration, from sooner to later - while (recentLines.Count > 0 && Game.LocalTick >= lineExpirations[0]) - RemoveLine(); - } - } -} diff --git a/OpenRA.Mods.Common/Widgets/Logic/Ingame/IngameChatLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Ingame/IngameChatLogic.cs index ed009642dd..c368fb28d9 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Ingame/IngameChatLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Ingame/IngameChatLogic.cs @@ -25,15 +25,16 @@ namespace OpenRA.Mods.Common.Widgets.Logic { readonly OrderManager orderManager; readonly Ruleset modRules; + readonly World world; readonly ContainerWidget chatOverlay; - readonly ChatDisplayWidget chatOverlayDisplay; + readonly TextNotificationsDisplayWidget chatOverlayDisplay; readonly ContainerWidget chatChrome; readonly ScrollPanelWidget chatScrollPanel; - readonly ContainerWidget chatTemplate; readonly TextFieldWidget chatText; readonly CachedTransform chatDisabledLabel; + readonly Dictionary templates = new Dictionary(); readonly TabCompletionLogic tabCompletion = new TabCompletionLogic(); @@ -43,11 +44,15 @@ namespace OpenRA.Mods.Common.Widgets.Logic int repetitions; bool chatEnabled; + readonly bool isMenuChat; + [ObjectCreator.UseCtor] public IngameChatLogic(Widget widget, OrderManager orderManager, World world, ModData modData, bool isMenuChat, Dictionary logicArgs) { this.orderManager = orderManager; modRules = modData.DefaultRules; + this.isMenuChat = isMenuChat; + this.world = world; var chatTraits = world.WorldActor.TraitsImplementing().ToArray(); var players = world.Players.Where(p => p != world.LocalPlayer && !p.NonCombatant && !p.IsBot); @@ -59,11 +64,20 @@ namespace OpenRA.Mods.Common.Widgets.Logic tabCompletion.Commands = chatTraits.OfType().SelectMany(x => x.Commands.Keys).ToList(); tabCompletion.Names = orderManager.LobbyInfo.Clients.Select(c => c.Name).Distinct().ToList(); + if (logicArgs.TryGetValue("Templates", out var templateIds)) + { + foreach (var item in templateIds.Nodes) + { + var key = FieldLoader.GetValue("key", item.Key); + templates[key] = Ui.LoadWidget(item.Value.Value, null, new WidgetArgs()); + } + } + var chatPanel = (ContainerWidget)widget; chatOverlay = chatPanel.GetOrNull("CHAT_OVERLAY"); if (chatOverlay != null) { - chatOverlayDisplay = chatOverlay.Get("CHAT_DISPLAY"); + chatOverlayDisplay = chatOverlay.Get("CHAT_DISPLAY"); chatOverlay.Visible = false; } @@ -194,14 +208,13 @@ namespace OpenRA.Mods.Common.Widgets.Logic } chatScrollPanel = chatChrome.Get("CHAT_SCROLLPANEL"); - chatTemplate = chatScrollPanel.Get("CHAT_TEMPLATE"); chatScrollPanel.RemoveChildren(); chatScrollPanel.ScrollToBottom(); foreach (var chatLine in orderManager.NotificationsCache) AddChatLine(chatLine, true); - orderManager.AddTextNotification += AddChatLineWrapper; + orderManager.AddTextNotification += AddNotificationWrapper; chatText.IsDisabled = () => !chatEnabled || (world.IsReplay && !Game.Settings.Debug.EnableDebugCommandsInReplays); @@ -248,7 +261,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic Ui.ResetTooltips(); } - public void AddChatLineWrapper(TextNotification chatLine) + public void AddNotificationWrapper(TextNotification chatLine) { var chatLineToDisplay = chatLine; @@ -263,53 +276,27 @@ namespace OpenRA.Mods.Common.Widgets.Logic chatLine.TextColor); chatScrollPanel.RemoveChild(chatScrollPanel.Children[chatScrollPanel.Children.Count - 1]); - chatOverlayDisplay?.RemoveMostRecentLine(); + chatOverlayDisplay?.RemoveMostRecentNotification(); } else repetitions = 0; lastLine = chatLine; - chatOverlayDisplay?.AddLine(chatLineToDisplay); + chatOverlayDisplay?.AddNotification(chatLineToDisplay); // HACK: Force disable the chat notification sound for the in-menu chat dialog // This works around our inability to disable the sounds for the in-game dialog when it is hidden AddChatLine(chatLineToDisplay, chatOverlay == null); } - void AddChatLine(TextNotification chatLine, bool suppressSound) + void AddChatLine(TextNotification notification, bool suppressSound) { - var template = chatTemplate.Clone(); - var nameLabel = template.Get("NAME"); - var textLabel = template.Get("TEXT"); - - var name = ""; - if (!string.IsNullOrEmpty(chatLine.Prefix)) - name = chatLine.Prefix + ":"; - - var font = Game.Renderer.Fonts[nameLabel.Font]; - var nameSize = font.Measure(chatLine.Prefix); - - nameLabel.GetColor = () => chatLine.PrefixColor; - nameLabel.GetText = () => name; - nameLabel.Bounds.Width = nameSize.X; - - textLabel.GetColor = () => chatLine.TextColor; - textLabel.Bounds.X += nameSize.X; - textLabel.Bounds.Width -= nameSize.X; - - // Hack around our hacky wordwrap behavior: need to resize the widget to fit the text - var text = WidgetUtils.WrapText(chatLine.Text, textLabel.Bounds.Width, font); - textLabel.GetText = () => text; - var dh = font.Measure(text).Y - textLabel.Bounds.Height; - if (dh > 0) - { - textLabel.Bounds.Height += dh; - template.Bounds.Height += dh; - } + var chatLine = templates[notification.Pool].Clone(); + WidgetUtils.SetupTextNotification(chatLine, notification, chatScrollPanel.Bounds.Width - chatScrollPanel.ScrollbarWidth, isMenuChat && !world.IsReplay); var scrolledToBottom = chatScrollPanel.ScrolledToBottom; - chatScrollPanel.AddChild(template); + chatScrollPanel.AddChild(chatLine); if (scrolledToBottom) chatScrollPanel.ScrollToBottom(smooth: true); @@ -343,7 +330,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic { if (!disposed) { - orderManager.AddTextNotification -= AddChatLineWrapper; + orderManager.AddTextNotification -= AddNotificationWrapper; disposed = true; } diff --git a/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs index 3f3347e5fa..f39306c693 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyLogic.cs @@ -45,7 +45,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic readonly Widget newSpectatorTemplate; readonly ScrollPanelWidget lobbyChatPanel; - readonly Widget chatTemplate; + readonly Dictionary chatTemplates = new Dictionary(); readonly TextFieldWidget chatTextField; readonly CachedTransform chatDisabledLabel; @@ -398,6 +398,15 @@ namespace OpenRA.Mods.Common.Widgets.Logic if (skirmishMode) disconnectButton.Text = "Back"; + if (logicArgs.TryGetValue("ChatTemplates", out var templateIds)) + { + foreach (var item in templateIds.Nodes) + { + var key = FieldLoader.GetValue("key", item.Key); + chatTemplates[key] = Ui.LoadWidget(item.Value.Value, null, new WidgetArgs()); + } + } + var chatMode = lobby.Get("CHAT_MODE"); chatMode.GetText = () => teamChat ? "Team" : "All"; chatMode.OnClick = () => teamChat ^= true; @@ -442,7 +451,6 @@ namespace OpenRA.Mods.Common.Widgets.Logic chatDisabledLabel = new CachedTransform(x => x > 0 ? $"Chat available in {x} seconds..." : "Chat Disabled"); lobbyChatPanel = lobby.Get("CHAT_DISPLAY"); - chatTemplate = lobbyChatPanel.Get("CHAT_TEMPLATE"); lobbyChatPanel.RemoveChildren(); var settingsButton = lobby.GetOrNull("SETTINGS_BUTTON"); @@ -510,13 +518,13 @@ namespace OpenRA.Mods.Common.Widgets.Logic } } - void AddChatLine(TextNotification chatLine) + void AddChatLine(TextNotification notification) { - var template = (ContainerWidget)chatTemplate.Clone(); - LobbyUtils.SetupChatLine(template, DateTime.Now, chatLine); + var chatLine = chatTemplates[notification.Pool].Clone(); + WidgetUtils.SetupTextNotification(chatLine, notification, lobbyChatPanel.Bounds.Width - lobbyChatPanel.ScrollbarWidth, true); var scrolledToBottom = lobbyChatPanel.ScrolledToBottom; - lobbyChatPanel.AddChild(template); + lobbyChatPanel.AddChild(chatLine); if (scrolledToBottom) lobbyChatPanel.ScrollToBottom(smooth: true); diff --git a/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyUtils.cs b/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyUtils.cs index c28ccd08ed..de5e2e73f3 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyUtils.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Lobby/LobbyUtils.cs @@ -648,37 +648,6 @@ namespace OpenRA.Mods.Common.Widgets.Logic HideChildWidget(parent, "STATUS_IMAGE"); } - public static void SetupChatLine(ContainerWidget template, DateTime time, TextNotification chatLine) - { - var nameLabel = template.Get("NAME"); - var timeLabel = template.Get("TIME"); - var textLabel = template.Get("TEXT"); - - var nameText = chatLine.Prefix + ":"; - var font = Game.Renderer.Fonts[nameLabel.Font]; - var nameSize = font.Measure(nameText); - - timeLabel.GetText = () => $"{time.Hour:D2}:{time.Minute:D2}"; - - nameLabel.GetColor = () => chatLine.PrefixColor; - nameLabel.GetText = () => nameText; - nameLabel.Bounds.Width = nameSize.X; - - textLabel.GetColor = () => chatLine.TextColor; - textLabel.Bounds.X += nameSize.X; - textLabel.Bounds.Width -= nameSize.X; - - // Hack around our hacky wordwrap behavior: need to resize the widget to fit the text - var text = WidgetUtils.WrapText(chatLine.Text, textLabel.Bounds.Width, font); - textLabel.GetText = () => text; - var dh = font.Measure(text).Y - textLabel.Bounds.Height; - if (dh > 0) - { - textLabel.Bounds.Height += dh; - template.Bounds.Height += dh; - } - } - static void HideChildWidget(Widget parent, string widgetId) { var widget = parent.GetOrNull(widgetId); diff --git a/OpenRA.Mods.Common/Widgets/TextNotificationsDisplayWidget.cs b/OpenRA.Mods.Common/Widgets/TextNotificationsDisplayWidget.cs new file mode 100644 index 0000000000..1fbe0401bc --- /dev/null +++ b/OpenRA.Mods.Common/Widgets/TextNotificationsDisplayWidget.cs @@ -0,0 +1,136 @@ +#region Copyright & License Information +/* + * Copyright 2007-2021 The OpenRA Developers (see AUTHORS) + * 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 OpenRA.Primitives; +using OpenRA.Widgets; + +namespace OpenRA.Mods.Common.Widgets +{ + public class TextNotificationsDisplayWidget : Widget + { + public readonly int RemoveTime = 0; + public readonly int ItemSpacing = 4; + public readonly int BottomSpacing = 0; + public readonly int LogLength = 8; + public readonly bool HideOverflow = true; + + public string ChatTemplate = "CHAT_LINE_TEMPLATE"; + public string SystemTemplate = "SYSTEM_LINE_TEMPLATE"; + public string MissionTemplate = "SYSTEM_LINE_TEMPLATE"; + public string FeedbackTemplate = "SYSTEM_LINE_TEMPLATE"; + readonly Dictionary templates = new Dictionary(); + + readonly List expirations = new List(); + + Rectangle overflowDrawBounds = Rectangle.Empty; + public override Rectangle EventBounds => Rectangle.Empty; + + public override void Initialize(WidgetArgs args) + { + base.Initialize(args); + + templates.Add(TextNotificationPool.Chat, Ui.LoadWidget(ChatTemplate, null, new WidgetArgs())); + templates.Add(TextNotificationPool.System, Ui.LoadWidget(SystemTemplate, null, new WidgetArgs())); + templates.Add(TextNotificationPool.Mission, Ui.LoadWidget(MissionTemplate, null, new WidgetArgs())); + templates.Add(TextNotificationPool.Feedback, Ui.LoadWidget(FeedbackTemplate, null, new WidgetArgs())); + + // HACK: Assume that all templates use the same font + var lineHeight = Game.Renderer.Fonts[templates[TextNotificationPool.Chat].Get("TEXT").Font].Measure("").Y; + var wholeLines = (int)Math.Floor((double)((Bounds.Height - BottomSpacing) / lineHeight)); + var visibleChildrenHeight = wholeLines * lineHeight; + + overflowDrawBounds = new Rectangle(RenderOrigin.X, RenderOrigin.Y, Bounds.Width, Bounds.Height); + overflowDrawBounds.Y += Bounds.Height - visibleChildrenHeight; + overflowDrawBounds.Height = visibleChildrenHeight; + } + + public override void DrawOuter() + { + if (!IsVisible() || Children.Count == 0) + return; + + var mostRecentMessageOverflows = Bounds.Height < Children[Children.Count - 1].Bounds.Height; + + if (mostRecentMessageOverflows && HideOverflow) + Game.Renderer.EnableScissor(overflowDrawBounds); + + for (var i = Children.Count - 1; i >= 0; i--) + { + if (Bounds.Contains(Children[i].Bounds) || !HideOverflow || mostRecentMessageOverflows) + Children[i].DrawOuter(); + + if (mostRecentMessageOverflows) + break; + } + + if (mostRecentMessageOverflows && HideOverflow) + Game.Renderer.DisableScissor(); + } + + public void AddNotification(TextNotification notification) + { + var notificationWidget = templates[notification.Pool].Clone(); + WidgetUtils.SetupTextNotification(notificationWidget, notification, Bounds.Width, false); + + if (Children.Count == 0) + notificationWidget.Bounds.Y = Bounds.Bottom - notificationWidget.Bounds.Height - BottomSpacing; + else + { + foreach (var line in Children) + line.Bounds.Y -= notificationWidget.Bounds.Height + ItemSpacing; + + var lastLine = Children[Children.Count - 1]; + notificationWidget.Bounds.Y = lastLine.Bounds.Bottom + ItemSpacing; + } + + AddChild(notificationWidget); + expirations.Add(Game.LocalTick + RemoveTime); + + while (Children.Count > LogLength) + RemoveNotification(); + } + + public void RemoveMostRecentNotification() + { + if (Children.Count == 0) + return; + + var mostRecentChild = Children[Children.Count - 1]; + + RemoveChild(mostRecentChild); + expirations.RemoveAt(expirations.Count - 1); + + for (var i = Children.Count - 1; i >= 0; i--) + Children[i].Bounds.Y += mostRecentChild.Bounds.Height + ItemSpacing; + } + + private void RemoveNotification() + { + if (Children.Count == 0) + return; + + RemoveChild(Children[0]); + expirations.RemoveAt(0); + } + + public override void Tick() + { + if (RemoveTime == 0) + return; + + // This takes advantage of the fact that recentLines is ordered by expiration, from sooner to later + while (Children.Count > 0 && Game.LocalTick >= expirations[0]) + RemoveNotification(); + } + } +} diff --git a/OpenRA.Mods.Common/Widgets/WidgetUtils.cs b/OpenRA.Mods.Common/Widgets/WidgetUtils.cs index 50739a325a..d4e85151f9 100644 --- a/OpenRA.Mods.Common/Widgets/WidgetUtils.cs +++ b/OpenRA.Mods.Common/Widgets/WidgetUtils.cs @@ -364,6 +364,62 @@ namespace OpenRA.Mods.Common.Widgets return name.Update((p.PlayerName, p.WinState, clientState)); }; } + + public static void SetupTextNotification(Widget notificationWidget, TextNotification notification, int boxWidth, bool withTimestamp) + { + var timeLabel = notificationWidget.GetOrNull("TIME"); + var prefixLabel = notificationWidget.GetOrNull("PREFIX"); + var textLabel = notificationWidget.Get("TEXT"); + + var textFont = Game.Renderer.Fonts[textLabel.Font]; + var textWidth = boxWidth - notificationWidget.Bounds.X - textLabel.Bounds.X; + + var hasPrefix = !string.IsNullOrEmpty(notification.Prefix) && prefixLabel != null; + var timeOffset = 0; + + if (withTimestamp && timeLabel != null) + { + var time = $"{notification.Time.Hour:D2}:{notification.Time.Minute:D2}"; + timeOffset = timeLabel.Bounds.Width + timeLabel.Bounds.X; + + timeLabel.GetText = () => time; + + textWidth -= timeOffset; + textLabel.Bounds.X += timeOffset; + + if (hasPrefix) + prefixLabel.Bounds.X += timeOffset; + } + + if (hasPrefix) + { + var prefix = notification.Prefix + ":"; + var prefixSize = Game.Renderer.Fonts[prefixLabel.Font].Measure(prefix); + var prefixOffset = prefixSize.X + prefixLabel.Bounds.X; + + prefixLabel.GetColor = () => notification.PrefixColor ?? prefixLabel.TextColor; + prefixLabel.GetText = () => prefix; + prefixLabel.Bounds.Width = prefixSize.X; + + textWidth -= prefixOffset; + textLabel.Bounds.X += prefixOffset - timeOffset; + } + + textLabel.GetColor = () => notification.TextColor ?? textLabel.TextColor; + textLabel.Bounds.Width = textWidth; + + // Hack around our hacky wordwrap behavior: need to resize the widget to fit the text + var text = WrapText(notification.Text, textLabel.Bounds.Width, textFont); + textLabel.GetText = () => text; + var dh = textFont.Measure(text).Y - textLabel.Bounds.Height; + if (dh > 0) + { + textLabel.Bounds.Height += dh; + notificationWidget.Bounds.Height += dh; + } + + notificationWidget.Bounds.Width = boxWidth - notificationWidget.Bounds.X; + } } public class CachedTransform diff --git a/mods/cnc/chrome/ingame-chat.yaml b/mods/cnc/chrome/ingame-chat.yaml index a382335715..f3fedbd320 100644 --- a/mods/cnc/chrome/ingame-chat.yaml +++ b/mods/cnc/chrome/ingame-chat.yaml @@ -6,17 +6,22 @@ Container@CHAT_PANEL: Logic: IngameChatLogic OpenTeamChatKey: OpenTeamChat OpenGeneralChatKey: OpenGeneralChat + Templates: + Chat: CHAT_LINE_TEMPLATE + System: SYSTEM_LINE_TEMPLATE + Mission: SYSTEM_LINE_TEMPLATE + Feedback: SYSTEM_LINE_TEMPLATE Children: Container@CHAT_OVERLAY: Width: PARENT_RIGHT - 24 - Height: PARENT_BOTTOM - 25 + Height: PARENT_BOTTOM - 30 Visible: false Children: - ChatDisplay@CHAT_DISPLAY: + TextNotificationsDisplay@CHAT_DISPLAY: Width: PARENT_RIGHT Height: PARENT_BOTTOM RemoveTime: 250 - UseShadow: True + BottomSpacing: 3 Container@CHAT_CHROME: Width: PARENT_RIGHT Height: PARENT_BOTTOM @@ -59,20 +64,3 @@ Container@CHAT_PANEL: TopBottomSpacing: 3 ItemSpacing: 4 Align: Bottom - Children: - Container@CHAT_TEMPLATE: - X: 2 - Width: PARENT_RIGHT - 27 - Height: 16 - Children: - Label@NAME: - X: 3 - Width: 50 - Height: 16 - Shadow: True - Label@TEXT: - X: 12 - Width: PARENT_RIGHT - 17 - Height: 16 - WordWrap: true - Shadow: True diff --git a/mods/cnc/chrome/ingame-infochat.yaml b/mods/cnc/chrome/ingame-infochat.yaml index 1747eb0e21..db99fe9dfd 100644 --- a/mods/cnc/chrome/ingame-infochat.yaml +++ b/mods/cnc/chrome/ingame-infochat.yaml @@ -3,6 +3,11 @@ Container@CHAT_CONTAINER: Width: PARENT_RIGHT Height: PARENT_BOTTOM - 20 Logic: IngameChatLogic + Templates: + Chat: CHAT_LINE_TEMPLATE + System: SYSTEM_LINE_TEMPLATE + Mission: SYSTEM_LINE_TEMPLATE + Feedback: SYSTEM_LINE_TEMPLATE Children: Container@CHAT_CHROME: X: 15 @@ -28,20 +33,3 @@ Container@CHAT_CONTAINER: Height: PARENT_BOTTOM - 30 TopBottomSpacing: 3 ItemSpacing: 2 - Children: - Container@CHAT_TEMPLATE: - X: 2 - Width: PARENT_RIGHT - 27 - Height: 16 - Children: - Label@NAME: - X: 3 - Width: 50 - Height: 16 - Shadow: True - Label@TEXT: - X: 12 - Width: PARENT_RIGHT - 17 - Height: 16 - WordWrap: true - Shadow: True diff --git a/mods/cnc/chrome/lobby.yaml b/mods/cnc/chrome/lobby.yaml index 36041c40ba..5e77cff98e 100644 --- a/mods/cnc/chrome/lobby.yaml +++ b/mods/cnc/chrome/lobby.yaml @@ -1,5 +1,10 @@ Container@SERVER_LOBBY: Logic: LobbyLogic + ChatTemplates: + Chat: CHAT_LINE_TEMPLATE + System: SYSTEM_LINE_TEMPLATE + Mission: SYSTEM_LINE_TEMPLATE + Feedback: SYSTEM_LINE_TEMPLATE X: (WINDOW_RIGHT - WIDTH) / 2 Y: (WINDOW_BOTTOM - 560) / 2 Width: 900 @@ -99,29 +104,6 @@ Container@SERVER_LOBBY: Height: PARENT_BOTTOM - 30 TopBottomSpacing: 3 ItemSpacing: 2 - Children: - Container@CHAT_TEMPLATE: - Width: PARENT_RIGHT - 27 - Height: 16 - X: 2 - Y: 0 - Children: - Label@TIME: - X: 3 - Width: 50 - Height: 16 - Shadow: True - Label@NAME: - X: 45 - Width: 50 - Height: 16 - Shadow: True - Label@TEXT: - X: 55 - Width: PARENT_RIGHT - 60 - Height: 16 - WordWrap: true - Shadow: True Button@CHAT_MODE: Y: PARENT_BOTTOM - HEIGHT Width: 50 diff --git a/mods/cnc/mod.yaml b/mods/cnc/mod.yaml index dc1ac4c716..5862f5abf1 100644 --- a/mods/cnc/mod.yaml +++ b/mods/cnc/mod.yaml @@ -138,6 +138,7 @@ ChromeLayout: cnc|chrome/assetbrowser.yaml cnc|chrome/missionbrowser.yaml cnc|chrome/editor.yaml + common|chrome/text-notifications.yaml Voices: cnc|audio/voices.yaml diff --git a/mods/common/chrome/ingame-chat.yaml b/mods/common/chrome/ingame-chat.yaml index 42ae19d1a2..580df8c82e 100644 --- a/mods/common/chrome/ingame-chat.yaml +++ b/mods/common/chrome/ingame-chat.yaml @@ -6,17 +6,22 @@ Container@CHAT_PANEL: Logic: IngameChatLogic OpenTeamChatKey: OpenTeamChat OpenGeneralChatKey: OpenGeneralChat + Templates: + Chat: CHAT_LINE_TEMPLATE + System: SYSTEM_LINE_TEMPLATE + Mission: SYSTEM_LINE_TEMPLATE + Feedback: SYSTEM_LINE_TEMPLATE Children: Container@CHAT_OVERLAY: Width: PARENT_RIGHT - 24 - Height: PARENT_BOTTOM - 25 + Height: PARENT_BOTTOM - 30 Visible: false Children: - ChatDisplay@CHAT_DISPLAY: + TextNotificationsDisplay@CHAT_DISPLAY: Width: PARENT_RIGHT Height: PARENT_BOTTOM RemoveTime: 250 - UseShadow: True + BottomSpacing: 3 Container@CHAT_CHROME: Width: PARENT_RIGHT Height: PARENT_BOTTOM @@ -54,20 +59,3 @@ Container@CHAT_PANEL: TopBottomSpacing: 3 ItemSpacing: 4 Align: Bottom - Children: - Container@CHAT_TEMPLATE: - X: 2 - Width: PARENT_RIGHT - 27 - Height: 16 - Children: - Label@NAME: - X: 3 - Width: 50 - Height: 16 - Shadow: True - Label@TEXT: - X: 12 - Width: PARENT_RIGHT - 17 - Height: 16 - WordWrap: true - Shadow: True diff --git a/mods/common/chrome/ingame-infochat.yaml b/mods/common/chrome/ingame-infochat.yaml index 0cd9c36d79..65164131f6 100644 --- a/mods/common/chrome/ingame-infochat.yaml +++ b/mods/common/chrome/ingame-infochat.yaml @@ -3,6 +3,11 @@ Container@CHAT_CONTAINER: Width: PARENT_RIGHT Height: PARENT_BOTTOM - 100 Logic: IngameChatLogic + Templates: + Chat: CHAT_LINE_TEMPLATE + System: SYSTEM_LINE_TEMPLATE + Mission: SYSTEM_LINE_TEMPLATE + Feedback: SYSTEM_LINE_TEMPLATE Children: Container@CHAT_CHROME: X: 20 @@ -28,20 +33,3 @@ Container@CHAT_CONTAINER: Height: PARENT_BOTTOM - 30 TopBottomSpacing: 3 ItemSpacing: 2 - Children: - Container@CHAT_TEMPLATE: - X: 2 - Width: PARENT_RIGHT - 27 - Height: 16 - Children: - Label@NAME: - X: 3 - Width: 50 - Height: 16 - Shadow: True - Label@TEXT: - X: 12 - Width: PARENT_RIGHT - 17 - Height: 16 - WordWrap: true - Shadow: True diff --git a/mods/common/chrome/lobby.yaml b/mods/common/chrome/lobby.yaml index 713d0f06f3..b0ea150e3a 100644 --- a/mods/common/chrome/lobby.yaml +++ b/mods/common/chrome/lobby.yaml @@ -1,5 +1,10 @@ Background@SERVER_LOBBY: Logic: LobbyLogic + ChatTemplates: + Chat: CHAT_LINE_TEMPLATE + System: SYSTEM_LINE_TEMPLATE + Mission: SYSTEM_LINE_TEMPLATE + Feedback: SYSTEM_LINE_TEMPLATE X: (WINDOW_RIGHT - WIDTH) / 2 Y: (WINDOW_BOTTOM - HEIGHT) / 2 Width: 900 @@ -103,28 +108,6 @@ Background@SERVER_LOBBY: Height: PARENT_BOTTOM - 30 TopBottomSpacing: 2 ItemSpacing: 2 - Children: - Container@CHAT_TEMPLATE: - X: 2 - Width: PARENT_RIGHT - 27 - Height: 16 - Children: - Label@TIME: - X: 3 - Width: 50 - Height: 16 - Shadow: True - Label@NAME: - X: 45 - Width: 50 - Height: 16 - Shadow: True - Label@TEXT: - X: 55 - Width: PARENT_RIGHT - 60 - Height: 16 - WordWrap: true - Shadow: True Button@CHAT_MODE: Y: PARENT_BOTTOM - HEIGHT Width: 50 diff --git a/mods/common/chrome/text-notifications.yaml b/mods/common/chrome/text-notifications.yaml new file mode 100644 index 0000000000..3ed93a09d5 --- /dev/null +++ b/mods/common/chrome/text-notifications.yaml @@ -0,0 +1,39 @@ +Container@CHAT_LINE_TEMPLATE: + Width: PARENT_RIGHT + Height: 16 + Children: + Label@TIME: + X: 5 + Width: 37 + Height: 16 + Shadow: True + Label@PREFIX: + X: 5 + Height: 16 + Shadow: True + Label@TEXT: + X: 5 + Height: 16 + WordWrap: True + Shadow: True + +Container@SYSTEM_LINE_TEMPLATE: + Width: PARENT_RIGHT + Height: 16 + Children: + Label@TIME: + X: 5 + Width: 37 + Height: 16 + Shadow: True + Label@PREFIX: + X: 5 + Height: 16 + Shadow: True + TextColor: FFFF00 + Label@TEXT: + X: 5 + Height: 16 + WordWrap: True + Shadow: True + TextColor: FFFF00 diff --git a/mods/common/metrics.yaml b/mods/common/metrics.yaml index 9cc344a516..e766239a23 100644 --- a/mods/common/metrics.yaml +++ b/mods/common/metrics.yaml @@ -49,8 +49,6 @@ Metrics: ChatLineSound: ChatLine ClickDisabledSound: ClickDisabledSound ClickSound: ClickSound - ChatMessageColor: FFFFFF - SystemMessageColor: FFFF00 NormalSelectionColor: FFFFFF AltSelectionColor: 00FFFF CtrlSelectionColor: FFFF00 diff --git a/mods/d2k/mod.yaml b/mods/d2k/mod.yaml index f46e0fe52b..b622702915 100644 --- a/mods/d2k/mod.yaml +++ b/mods/d2k/mod.yaml @@ -117,6 +117,7 @@ ChromeLayout: common|chrome/replaybrowser.yaml common|chrome/gamesave-browser.yaml common|chrome/gamesave-loading.yaml + common|chrome/text-notifications.yaml Weapons: d2k|weapons/debris.yaml diff --git a/mods/ra/mod.yaml b/mods/ra/mod.yaml index 90db419fc7..1de50d6c49 100644 --- a/mods/ra/mod.yaml +++ b/mods/ra/mod.yaml @@ -134,6 +134,7 @@ ChromeLayout: common|chrome/confirmation-dialogs.yaml common|chrome/editor.yaml common|chrome/playerprofile.yaml + common|chrome/text-notifications.yaml Weapons: ra|weapons/explosions.yaml diff --git a/mods/ts/mod.yaml b/mods/ts/mod.yaml index 9919125409..716621a199 100644 --- a/mods/ts/mod.yaml +++ b/mods/ts/mod.yaml @@ -180,6 +180,7 @@ ChromeLayout: common|chrome/missionbrowser.yaml common|chrome/confirmation-dialogs.yaml common|chrome/editor.yaml + common|chrome/text-notifications.yaml Voices: ts|audio/voices.yaml