Hud refactor (#7202)
Co-authored-by: DrSmugleaf <DrSmugleaf@users.noreply.github.com> Co-authored-by: Jezithyr <jmaster9999@gmail.com> Co-authored-by: Jezithyr <Jezithyr@gmail.com> Co-authored-by: Visne <39844191+Visne@users.noreply.github.com> Co-authored-by: wrexbe <wrexbe@protonmail.com> Co-authored-by: wrexbe <81056464+wrexbe@users.noreply.github.com>
This commit is contained in:
@@ -1,24 +0,0 @@
|
||||
<Control xmlns="https://spacestation14.io"
|
||||
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||
xmlns:chatUI="clr-namespace:Content.Client.Chat.UI"
|
||||
MouseFilter="Stop"
|
||||
MinSize="200 128">
|
||||
<PanelContainer>
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="#25252AAA" />
|
||||
</PanelContainer.PanelOverride>
|
||||
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<OutputPanel Name="Contents" VerticalExpand="True" />
|
||||
<PanelContainer StyleClasses="ChatSubPanel">
|
||||
<BoxContainer Orientation="Horizontal" SeparationOverride="4">
|
||||
<chatUI:ChannelSelectorButton Name="ChannelSelector" ToggleMode="True"
|
||||
StyleClasses="chatSelectorOptionButton" MinWidth="75" />
|
||||
<HistoryLineEdit Name="Input" PlaceHolder="{Loc 'hud-chatbox-info'}" HorizontalExpand="True"
|
||||
StyleClasses="chatLineEdit" />
|
||||
<chatUI:FilterButton Name="FilterButton" StyleClasses="chatFilterOptionButton" />
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</Control>
|
||||
@@ -1,754 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Client.Alerts.UI;
|
||||
using Content.Client.Chat.Managers;
|
||||
using Content.Client.Chat.TypingIndicator;
|
||||
using Content.Client.Resources;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.Input;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Chat.UI
|
||||
{
|
||||
[GenerateTypedNameReferences]
|
||||
[Virtual]
|
||||
public partial class ChatBox : Control
|
||||
{
|
||||
[Dependency] protected readonly IChatManager ChatMgr = default!;
|
||||
|
||||
// order in which the available channel filters show up when available
|
||||
private static readonly ChatChannel[] ChannelFilterOrder =
|
||||
{
|
||||
ChatChannel.Local,
|
||||
ChatChannel.Whisper,
|
||||
ChatChannel.Emotes,
|
||||
ChatChannel.Radio,
|
||||
ChatChannel.OOC,
|
||||
ChatChannel.Dead,
|
||||
ChatChannel.Admin,
|
||||
ChatChannel.Server
|
||||
};
|
||||
|
||||
// order in which the channels show up in the channel selector
|
||||
private static readonly ChatSelectChannel[] ChannelSelectorOrder =
|
||||
{
|
||||
ChatSelectChannel.Local,
|
||||
ChatSelectChannel.Whisper,
|
||||
ChatSelectChannel.Emotes,
|
||||
ChatSelectChannel.Radio,
|
||||
ChatSelectChannel.LOOC,
|
||||
ChatSelectChannel.OOC,
|
||||
ChatSelectChannel.Dead,
|
||||
ChatSelectChannel.Admin
|
||||
// NOTE: Console is not in there and it can never be permanently selected.
|
||||
// You can, however, still submit commands as console by prefixing with /.
|
||||
};
|
||||
|
||||
public const char AliasLocal = '.';
|
||||
public const char AliasConsole = '/';
|
||||
public const char AliasDead = '\\';
|
||||
public const char AliasOOC = '[';
|
||||
public const char AliasEmotes = '@';
|
||||
public const char AliasAdmin = ']';
|
||||
public const char AliasRadio = ';';
|
||||
public const char AliasWhisper = ',';
|
||||
|
||||
private static readonly Dictionary<char, ChatSelectChannel> PrefixToChannel = new()
|
||||
{
|
||||
{AliasLocal, ChatSelectChannel.Local},
|
||||
{AliasWhisper, ChatSelectChannel.Whisper},
|
||||
{AliasConsole, ChatSelectChannel.Console},
|
||||
{AliasOOC, ChatSelectChannel.OOC},
|
||||
{AliasEmotes, ChatSelectChannel.Emotes},
|
||||
{AliasAdmin, ChatSelectChannel.Admin},
|
||||
{AliasRadio, ChatSelectChannel.Radio},
|
||||
{AliasDead, ChatSelectChannel.Dead}
|
||||
};
|
||||
|
||||
private static readonly Dictionary<ChatSelectChannel, char> ChannelPrefixes =
|
||||
PrefixToChannel.ToDictionary(kv => kv.Value, kv => kv.Key);
|
||||
|
||||
private const float FilterPopupWidth = 110;
|
||||
|
||||
/// <summary>
|
||||
/// The currently default channel that will be used if no prefix is specified.
|
||||
/// </summary>
|
||||
public ChatSelectChannel SelectedChannel { get; private set; } = ChatSelectChannel.OOC;
|
||||
|
||||
/// <summary>
|
||||
/// The "preferred" channel. Will be switched to if permissions change and the channel becomes available,
|
||||
/// such as by re-entering body. Gets changed if the user manually selects a channel with the buttons.
|
||||
/// </summary>
|
||||
public ChatSelectChannel PreferredChannel { get; set; } = ChatSelectChannel.OOC;
|
||||
|
||||
public bool ReleaseFocusOnEnter { get; set; } = true;
|
||||
|
||||
private readonly Popup _channelSelectorPopup;
|
||||
private readonly BoxContainer _channelSelectorHBox;
|
||||
private readonly Popup _filterPopup;
|
||||
private readonly PanelContainer _filterPopupPanel;
|
||||
private readonly BoxContainer _filterVBox;
|
||||
|
||||
/// <summary>
|
||||
/// When lobbyMode is false, will position / add to correct location in StateRoot and
|
||||
/// be resizable.
|
||||
/// wWen true, will leave layout up to parent and not be resizable.
|
||||
/// </summary>
|
||||
public ChatBox()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
LayoutContainer.SetMarginLeft(this, 4);
|
||||
LayoutContainer.SetMarginRight(this, 4);
|
||||
|
||||
_filterPopup = new Popup
|
||||
{
|
||||
Children =
|
||||
{
|
||||
(_filterPopupPanel = new PanelContainer
|
||||
{
|
||||
StyleClasses = {StyleNano.StyleClassBorderedWindowPanel},
|
||||
Children =
|
||||
{
|
||||
new BoxContainer
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Horizontal,
|
||||
Children =
|
||||
{
|
||||
new Control {MinSize = (4, 0)},
|
||||
(_filterVBox = new BoxContainer
|
||||
{
|
||||
Margin = new Thickness(0, 10),
|
||||
Orientation = BoxContainer.LayoutOrientation.Vertical,
|
||||
SeparationOverride = 4
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
_channelSelectorPopup = new Popup
|
||||
{
|
||||
Children =
|
||||
{
|
||||
(_channelSelectorHBox = new BoxContainer
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Horizontal,
|
||||
SeparationOverride = 1
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
ChannelSelector.OnToggled += OnChannelSelectorToggled;
|
||||
FilterButton.OnToggled += OnFilterButtonToggled;
|
||||
Input.OnKeyBindDown += InputKeyBindDown;
|
||||
Input.OnTextEntered += Input_OnTextEntered;
|
||||
Input.OnTextChanged += InputOnTextChanged;
|
||||
_channelSelectorPopup.OnPopupHide += OnChannelSelectorPopupHide;
|
||||
_filterPopup.OnPopupHide += OnFilterPopupHide;
|
||||
}
|
||||
|
||||
protected override void EnteredTree()
|
||||
{
|
||||
base.EnteredTree();
|
||||
|
||||
ChatMgr.MessageAdded += WriteChatMessage;
|
||||
ChatMgr.ChatPermissionsUpdated += OnChatPermissionsUpdated;
|
||||
ChatMgr.UnreadMessageCountsUpdated += UpdateUnreadMessageCounts;
|
||||
ChatMgr.FiltersUpdated += Repopulate;
|
||||
|
||||
// The chat manager may have messages logged from before there was a chat box.
|
||||
// In this case, these messages will be marked as unread despite the filters allowing them through.
|
||||
// Tell chat manager to clear these.
|
||||
ChatMgr.ClearUnfilteredUnreads();
|
||||
|
||||
ChatPermissionsUpdated(0);
|
||||
UpdateChannelSelectButton();
|
||||
Repopulate();
|
||||
}
|
||||
|
||||
protected override void ExitedTree()
|
||||
{
|
||||
base.ExitedTree();
|
||||
|
||||
ChatMgr.MessageAdded -= WriteChatMessage;
|
||||
ChatMgr.ChatPermissionsUpdated -= OnChatPermissionsUpdated;
|
||||
ChatMgr.UnreadMessageCountsUpdated -= UpdateUnreadMessageCounts;
|
||||
ChatMgr.FiltersUpdated -= Repopulate;
|
||||
}
|
||||
|
||||
private void OnChatPermissionsUpdated(ChatPermissionsUpdatedEventArgs eventArgs)
|
||||
{
|
||||
ChatPermissionsUpdated(eventArgs.OldSelectableChannels);
|
||||
}
|
||||
|
||||
private void ChatPermissionsUpdated(ChatSelectChannel oldSelectable)
|
||||
{
|
||||
// update the channel selector
|
||||
_channelSelectorHBox.Children.Clear();
|
||||
foreach (var selectableChannel in ChannelSelectorOrder)
|
||||
{
|
||||
if ((ChatMgr.SelectableChannels & selectableChannel) == 0)
|
||||
continue;
|
||||
|
||||
var newButton = new ChannelItemButton(selectableChannel);
|
||||
newButton.OnPressed += OnChannelSelectorItemPressed;
|
||||
_channelSelectorHBox.AddChild(newButton);
|
||||
}
|
||||
|
||||
// Selected channel no longer available, switch to OOC?
|
||||
if ((ChatMgr.SelectableChannels & SelectedChannel) == 0)
|
||||
{
|
||||
// Handle local -> dead mapping when you e.g. ghost.
|
||||
// Only necessary for admins because they always have deadchat
|
||||
// so the normal preferred check won't see it as newly available and do nothing.
|
||||
var mappedSelect = MapLocalIfGhost(SelectedChannel);
|
||||
if ((ChatMgr.SelectableChannels & mappedSelect) != 0)
|
||||
SafelySelectChannel(mappedSelect);
|
||||
else
|
||||
SafelySelectChannel(ChatSelectChannel.OOC);
|
||||
}
|
||||
|
||||
// If the preferred channel just became available, switch to it.
|
||||
var pref = MapLocalIfGhost(PreferredChannel);
|
||||
if ((oldSelectable & pref) == 0 && (ChatMgr.SelectableChannels & pref) != 0)
|
||||
SafelySelectChannel(pref);
|
||||
|
||||
// update the channel filters
|
||||
_filterVBox.Children.Clear();
|
||||
foreach (var channelFilter in ChannelFilterOrder)
|
||||
{
|
||||
if ((ChatMgr.FilterableChannels & channelFilter) == 0)
|
||||
continue;
|
||||
|
||||
int? unreadCount = null;
|
||||
if (ChatMgr.UnreadMessages.TryGetValue(channelFilter, out var unread))
|
||||
unreadCount = unread;
|
||||
|
||||
var newCheckBox = new ChannelFilterCheckbox(channelFilter, unreadCount)
|
||||
{
|
||||
Pressed = (ChatMgr.ChannelFilters & channelFilter) != 0
|
||||
};
|
||||
|
||||
newCheckBox.OnToggled += OnFilterCheckboxToggled;
|
||||
_filterVBox.AddChild(newCheckBox);
|
||||
}
|
||||
|
||||
UpdateChannelSelectButton();
|
||||
}
|
||||
|
||||
private void UpdateUnreadMessageCounts()
|
||||
{
|
||||
foreach (var channelFilter in _filterVBox.Children)
|
||||
{
|
||||
if (channelFilter is not ChannelFilterCheckbox filterCheckbox) continue;
|
||||
if (ChatMgr.UnreadMessages.TryGetValue(filterCheckbox.Channel, out var unread))
|
||||
{
|
||||
filterCheckbox.UpdateUnreadCount(unread);
|
||||
}
|
||||
else
|
||||
{
|
||||
filterCheckbox.UpdateUnreadCount(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnFilterCheckboxToggled(BaseButton.ButtonToggledEventArgs args)
|
||||
{
|
||||
if (args.Button is not ChannelFilterCheckbox checkbox)
|
||||
return;
|
||||
|
||||
ChatMgr.OnFilterButtonToggled(checkbox.Channel, checkbox.Pressed);
|
||||
}
|
||||
|
||||
private void OnFilterButtonToggled(BaseButton.ButtonToggledEventArgs args)
|
||||
{
|
||||
if (args.Pressed)
|
||||
{
|
||||
var globalPos = FilterButton.GlobalPosition;
|
||||
var (minX, minY) = _filterPopupPanel.MinSize;
|
||||
var box = UIBox2.FromDimensions(globalPos - (FilterPopupWidth, 0),
|
||||
(Math.Max(minX, FilterPopupWidth), minY));
|
||||
UserInterfaceManager.ModalRoot.AddChild(_filterPopup);
|
||||
_filterPopup.Open(box);
|
||||
}
|
||||
else
|
||||
{
|
||||
_filterPopup.Close();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnChannelSelectorToggled(BaseButton.ButtonToggledEventArgs args)
|
||||
{
|
||||
if (args.Pressed)
|
||||
{
|
||||
var globalLeft = GlobalPosition.X;
|
||||
var globalBot = GlobalPosition.Y + Height;
|
||||
var box = UIBox2.FromDimensions((globalLeft, globalBot), (SizeBox.Width, AlertsUI.ChatSeparation));
|
||||
UserInterfaceManager.ModalRoot.AddChild(_channelSelectorPopup);
|
||||
_channelSelectorPopup.Open(box);
|
||||
}
|
||||
else
|
||||
{
|
||||
_channelSelectorPopup.Close();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnFilterPopupHide()
|
||||
{
|
||||
OnPopupHide(_filterPopup, FilterButton);
|
||||
}
|
||||
|
||||
private void OnChannelSelectorPopupHide()
|
||||
{
|
||||
OnPopupHide(_channelSelectorPopup, ChannelSelector);
|
||||
}
|
||||
|
||||
private void OnPopupHide(Control popup, BaseButton button)
|
||||
{
|
||||
UserInterfaceManager.ModalRoot.RemoveChild(popup);
|
||||
|
||||
// this weird check here is because the hiding of the popup happens prior to the button
|
||||
// receiving the keydown, which would cause it to then become unpressed
|
||||
// and reopen immediately. To avoid this, if the popup was hidden due to clicking on the button,
|
||||
// we will not auto-unpress the button, instead leaving it up to the button toggle logic
|
||||
// (and this requires the button to be set to EnableAllKeybinds = true)
|
||||
if (UserInterfaceManager.CurrentlyHovered != button)
|
||||
{
|
||||
button.Pressed = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnChannelSelectorItemPressed(BaseButton.ButtonEventArgs obj)
|
||||
{
|
||||
if (obj.Button is not ChannelItemButton button)
|
||||
return;
|
||||
|
||||
PreferredChannel = button.Channel;
|
||||
SafelySelectChannel(button.Channel);
|
||||
_channelSelectorPopup.Close();
|
||||
}
|
||||
|
||||
public bool SafelySelectChannel(ChatSelectChannel toSelect)
|
||||
{
|
||||
toSelect = MapLocalIfGhost(toSelect);
|
||||
if ((ChatMgr.SelectableChannels & toSelect) == 0)
|
||||
return false;
|
||||
|
||||
SelectedChannel = toSelect;
|
||||
UpdateChannelSelectButton();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void UpdateChannelSelectButton()
|
||||
{
|
||||
var (prefixChannel, _) = SplitInputContents();
|
||||
|
||||
var channel = prefixChannel == 0 ? SelectedChannel : prefixChannel;
|
||||
|
||||
ChannelSelector.Text = ChannelSelectorName(channel);
|
||||
ChannelSelector.Modulate = ChannelSelectColor(channel);
|
||||
}
|
||||
|
||||
protected override void KeyBindDown(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
base.KeyBindDown(args);
|
||||
|
||||
if (args.CanFocus)
|
||||
{
|
||||
Input.GrabKeyboardFocus();
|
||||
}
|
||||
}
|
||||
|
||||
public void CycleChatChannel(bool forward)
|
||||
{
|
||||
Input.IgnoreNext = true;
|
||||
|
||||
var idx = Array.IndexOf(ChannelSelectorOrder, SelectedChannel);
|
||||
do
|
||||
{
|
||||
// go over every channel until we find one we can actually select.
|
||||
idx += forward ? 1 : -1;
|
||||
idx = MathHelper.Mod(idx, ChannelSelectorOrder.Length);
|
||||
} while ((ChatMgr.SelectableChannels & ChannelSelectorOrder[idx]) == 0);
|
||||
|
||||
SafelySelectChannel(ChannelSelectorOrder[idx]);
|
||||
}
|
||||
|
||||
private void Repopulate()
|
||||
{
|
||||
Contents.Clear();
|
||||
|
||||
foreach (var msg in ChatMgr.History)
|
||||
{
|
||||
WriteChatMessage(msg);
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteChatMessage(StoredChatMessage message)
|
||||
{
|
||||
var messageText = FormattedMessage.EscapeText(message.Message);
|
||||
if (!string.IsNullOrEmpty(message.MessageWrap))
|
||||
{
|
||||
messageText = string.Format(message.MessageWrap, messageText);
|
||||
}
|
||||
|
||||
Logger.DebugS("chat", $"{message.Channel}: {messageText}");
|
||||
|
||||
if (IsFilteredOut(message.Channel))
|
||||
return;
|
||||
|
||||
// TODO: Can make this "smarter" later by only setting it false when the message has been scrolled to
|
||||
message.Read = true;
|
||||
|
||||
var color = message.MessageColorOverride != Color.Transparent
|
||||
? message.MessageColorOverride
|
||||
: ChatHelper.ChatColor(message.Channel);
|
||||
|
||||
AddLine(messageText, message.Channel, color);
|
||||
}
|
||||
|
||||
private bool IsFilteredOut(ChatChannel channel)
|
||||
{
|
||||
return (ChatMgr.ChannelFilters & channel) == 0;
|
||||
}
|
||||
|
||||
private void InputKeyBindDown(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
if (args.Function == EngineKeyFunctions.TextReleaseFocus)
|
||||
{
|
||||
Input.ReleaseKeyboardFocus();
|
||||
args.Handle();
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Function == ContentKeyFunctions.CycleChatChannelForward)
|
||||
{
|
||||
CycleChatChannel(true);
|
||||
args.Handle();
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Function == ContentKeyFunctions.CycleChatChannelBackward)
|
||||
{
|
||||
CycleChatChannel(false);
|
||||
args.Handle();
|
||||
}
|
||||
}
|
||||
|
||||
private (ChatSelectChannel selChannel, ReadOnlyMemory<char> text) SplitInputContents()
|
||||
{
|
||||
var text = Input.Text.AsMemory().Trim();
|
||||
if (text.Length == 0)
|
||||
return default;
|
||||
|
||||
var prefixChar = text.Span[0];
|
||||
var channel = GetChannelFromPrefix(prefixChar);
|
||||
|
||||
if ((ChatMgr.SelectableChannels & channel) != 0)
|
||||
// Cut off prefix if it's valid and we can use the channel in question.
|
||||
text = text[1..];
|
||||
else
|
||||
channel = 0;
|
||||
|
||||
channel = MapLocalIfGhost(channel);
|
||||
|
||||
// Trim from start again to cut out any whitespace between the prefix and message, if any.
|
||||
return (channel, text.TrimStart());
|
||||
}
|
||||
|
||||
private void InputOnTextChanged(LineEdit.LineEditEventArgs obj)
|
||||
{
|
||||
// Update channel select button to correct channel if we have a prefix.
|
||||
UpdateChannelSelectButton();
|
||||
|
||||
// Warn typing indicator about change
|
||||
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<TypingIndicatorSystem>().ClientChangedChatText();
|
||||
}
|
||||
|
||||
private static ChatSelectChannel GetChannelFromPrefix(char prefix)
|
||||
{
|
||||
return PrefixToChannel.GetValueOrDefault(prefix);
|
||||
}
|
||||
|
||||
public static char GetPrefixFromChannel(ChatSelectChannel channel)
|
||||
{
|
||||
return ChannelPrefixes.GetValueOrDefault(channel);
|
||||
}
|
||||
|
||||
public static string ChannelSelectorName(ChatSelectChannel channel)
|
||||
{
|
||||
return Loc.GetString($"hud-chatbox-select-channel-{channel}");
|
||||
}
|
||||
|
||||
public static Color ChannelSelectColor(ChatSelectChannel channel)
|
||||
{
|
||||
return channel switch
|
||||
{
|
||||
ChatSelectChannel.Radio => Color.LimeGreen,
|
||||
ChatSelectChannel.LOOC => Color.MediumTurquoise,
|
||||
ChatSelectChannel.OOC => Color.LightSkyBlue,
|
||||
ChatSelectChannel.Dead => Color.MediumPurple,
|
||||
ChatSelectChannel.Admin => Color.Red,
|
||||
_ => Color.DarkGray
|
||||
};
|
||||
}
|
||||
|
||||
public void AddLine(string message, ChatChannel channel, Color color)
|
||||
{
|
||||
DebugTools.Assert(!Disposed);
|
||||
|
||||
var formatted = new FormattedMessage(3);
|
||||
formatted.PushColor(color);
|
||||
formatted.AddMarkup(message);
|
||||
formatted.Pop();
|
||||
Contents.AddMessage(formatted);
|
||||
}
|
||||
|
||||
private void Input_OnTextEntered(LineEdit.LineEditEventArgs args)
|
||||
{
|
||||
// Warn typing indicator about entered text
|
||||
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<TypingIndicatorSystem>().ClientSubmittedChatText();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(args.Text))
|
||||
{
|
||||
var (prefixChannel, text) = SplitInputContents();
|
||||
|
||||
// Check if message is longer than the character limit
|
||||
if (text.Length > ChatMgr.MaxMessageLength)
|
||||
{
|
||||
string locWarning = Loc.GetString(
|
||||
"chat-manager-max-message-length",
|
||||
("maxMessageLength", ChatMgr.MaxMessageLength));
|
||||
|
||||
AddLine(locWarning, ChatChannel.Server, Color.Orange);
|
||||
return;
|
||||
}
|
||||
|
||||
ChatMgr.OnChatBoxTextSubmitted(this, text, prefixChannel == 0 ? SelectedChannel : prefixChannel);
|
||||
}
|
||||
|
||||
Input.Clear();
|
||||
UpdateChannelSelectButton();
|
||||
|
||||
if (ReleaseFocusOnEnter)
|
||||
Input.ReleaseKeyboardFocus();
|
||||
}
|
||||
|
||||
public void Focus(ChatSelectChannel? channel = null)
|
||||
{
|
||||
var selectStart = Index.End;
|
||||
if (channel != null)
|
||||
{
|
||||
channel = MapLocalIfGhost(channel.Value);
|
||||
|
||||
// Channel not selectable, just do NOTHING (not even focus).
|
||||
if (!((ChatMgr.SelectableChannels & channel.Value) != 0))
|
||||
return;
|
||||
|
||||
var (_, text) = SplitInputContents();
|
||||
|
||||
var newPrefix = GetPrefixFromChannel(channel.Value);
|
||||
DebugTools.Assert(newPrefix != default, "Focus channel must have prefix!");
|
||||
|
||||
if (channel == SelectedChannel)
|
||||
{
|
||||
// New selected channel is just the selected channel,
|
||||
// just remove prefix (if any) and leave text unchanged.
|
||||
|
||||
Input.Text = text.ToString();
|
||||
selectStart = Index.Start;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Change prefix to new focused channel prefix and leave text unchanged.
|
||||
Input.Text = string.Concat(newPrefix.ToString(), " ", text.Span);
|
||||
selectStart = Index.FromStart(2);
|
||||
}
|
||||
|
||||
UpdateChannelSelectButton();
|
||||
}
|
||||
|
||||
Input.IgnoreNext = true;
|
||||
Input.GrabKeyboardFocus();
|
||||
|
||||
Input.CursorPosition = Input.Text.Length;
|
||||
Input.SelectionStart = selectStart.GetOffset(Input.Text.Length);
|
||||
}
|
||||
|
||||
private ChatSelectChannel MapLocalIfGhost(ChatSelectChannel channel)
|
||||
{
|
||||
if (channel == ChatSelectChannel.Local && ChatMgr.IsGhost)
|
||||
return ChatSelectChannel.Dead;
|
||||
|
||||
return channel;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Only needed to avoid the issue where right click on the button closes the popup
|
||||
/// but leaves the button highlighted.
|
||||
/// </summary>
|
||||
public sealed class ChannelSelectorButton : Button
|
||||
{
|
||||
public ChannelSelectorButton()
|
||||
{
|
||||
// needed so the popup is untoggled regardless of which key is pressed when hovering this button.
|
||||
// If we don't have this, then right clicking the button while it's toggled on will hide
|
||||
// the popup but keep the button toggled on
|
||||
Mode = ActionMode.Press;
|
||||
EnableAllKeybinds = true;
|
||||
}
|
||||
|
||||
protected override void KeyBindDown(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
// needed since we need EnableAllKeybinds - don't double-send both UI click and Use
|
||||
if (args.Function == EngineKeyFunctions.Use)
|
||||
return;
|
||||
|
||||
base.KeyBindDown(args);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class FilterButton : ContainerButton
|
||||
{
|
||||
private static readonly Color ColorNormal = Color.FromHex("#7b7e9e");
|
||||
private static readonly Color ColorHovered = Color.FromHex("#9699bb");
|
||||
private static readonly Color ColorPressed = Color.FromHex("#789B8C");
|
||||
|
||||
private readonly TextureRect _textureRect;
|
||||
|
||||
public FilterButton()
|
||||
{
|
||||
var filterTexture = IoCManager.Resolve<IResourceCache>()
|
||||
.GetTexture("/Textures/Interface/Nano/filter.svg.96dpi.png");
|
||||
|
||||
// needed for same reason as ChannelSelectorButton
|
||||
Mode = ActionMode.Press;
|
||||
EnableAllKeybinds = true;
|
||||
|
||||
AddChild(
|
||||
(_textureRect = new TextureRect
|
||||
{
|
||||
Texture = filterTexture,
|
||||
HorizontalAlignment = HAlignment.Center,
|
||||
VerticalAlignment = VAlignment.Center
|
||||
})
|
||||
);
|
||||
|
||||
ToggleMode = true;
|
||||
}
|
||||
|
||||
protected override void KeyBindDown(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
// needed since we need EnableAllKeybinds - don't double-send both UI click and Use
|
||||
if (args.Function == EngineKeyFunctions.Use) return;
|
||||
base.KeyBindDown(args);
|
||||
}
|
||||
|
||||
private void UpdateChildColors()
|
||||
{
|
||||
if (_textureRect == null) return;
|
||||
switch (DrawMode)
|
||||
{
|
||||
case DrawModeEnum.Normal:
|
||||
_textureRect.ModulateSelfOverride = ColorNormal;
|
||||
break;
|
||||
|
||||
case DrawModeEnum.Pressed:
|
||||
_textureRect.ModulateSelfOverride = ColorPressed;
|
||||
break;
|
||||
|
||||
case DrawModeEnum.Hover:
|
||||
_textureRect.ModulateSelfOverride = ColorHovered;
|
||||
break;
|
||||
|
||||
case DrawModeEnum.Disabled:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void DrawModeChanged()
|
||||
{
|
||||
base.DrawModeChanged();
|
||||
UpdateChildColors();
|
||||
}
|
||||
|
||||
protected override void StylePropertiesChanged()
|
||||
{
|
||||
base.StylePropertiesChanged();
|
||||
UpdateChildColors();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ChannelItemButton : Button
|
||||
{
|
||||
public readonly ChatSelectChannel Channel;
|
||||
|
||||
public ChannelItemButton(ChatSelectChannel channel)
|
||||
{
|
||||
Channel = channel;
|
||||
AddStyleClass(StyleNano.StyleClassChatChannelSelectorButton);
|
||||
Text = ChatBox.ChannelSelectorName(channel);
|
||||
|
||||
var prefix = ChatBox.GetPrefixFromChannel(channel);
|
||||
if (prefix != default)
|
||||
Text = Loc.GetString("hud-chatbox-select-name-prefixed", ("name", Text), ("prefix", prefix));
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ChannelFilterCheckbox : CheckBox
|
||||
{
|
||||
public readonly ChatChannel Channel;
|
||||
|
||||
public ChannelFilterCheckbox(ChatChannel channel, int? unreadCount)
|
||||
{
|
||||
Channel = channel;
|
||||
|
||||
UpdateText(unreadCount);
|
||||
}
|
||||
|
||||
private void UpdateText(int? unread)
|
||||
{
|
||||
var name = Loc.GetString($"hud-chatbox-channel-{Channel}");
|
||||
|
||||
if (unread > 0)
|
||||
// todo: proper fluent stuff here.
|
||||
name += " (" + (unread > 9 ? "9+" : unread) + ")";
|
||||
|
||||
Text = name;
|
||||
}
|
||||
|
||||
public void UpdateUnreadCount(int? unread)
|
||||
{
|
||||
UpdateText(unread);
|
||||
}
|
||||
}
|
||||
|
||||
public readonly struct ChatResizedEventArgs
|
||||
{
|
||||
/// new bottom that the chat rect is going to have in virtual pixels
|
||||
/// after the imminent relayout
|
||||
public readonly float NewBottom;
|
||||
|
||||
public ChatResizedEventArgs(float newBottom)
|
||||
{
|
||||
NewBottom = newBottom;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,235 +0,0 @@
|
||||
using System;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.Chat.UI
|
||||
{
|
||||
public sealed class HudChatBox : ChatBox
|
||||
{
|
||||
// TODO: Revisit the resizing stuff after https://github.com/space-wizards/RobustToolbox/issues/1392 is done,
|
||||
// Probably not "supposed" to inject IClyde, but I give up.
|
||||
// I can't find any other way to allow this control to properly resize when the
|
||||
// window is resized. Resized() isn't reliably called when resizing the window,
|
||||
// and layoutcontainer anchor / margin don't seem to adjust how we need
|
||||
// them to when the window is resized. We need it to be able to resize
|
||||
// within some bounds so that it doesn't overlap other UI elements, while still
|
||||
// being freely resizable within those bounds.
|
||||
[Dependency] private readonly IClyde _clyde = default!;
|
||||
|
||||
public const float InitialChatBottom = 235;
|
||||
private const int DragMarginSize = 7;
|
||||
private const int MinDistanceFromBottom = 255;
|
||||
private const int MinLeft = 500;
|
||||
private DragMode _currentDrag = DragMode.None;
|
||||
private Vector2 _dragOffsetTopLeft;
|
||||
private Vector2 _dragOffsetBottomRight;
|
||||
|
||||
private byte _clampIn;
|
||||
|
||||
protected override void EnteredTree()
|
||||
{
|
||||
base.EnteredTree();
|
||||
|
||||
_clyde.OnWindowResized += ClydeOnOnWindowResized;
|
||||
}
|
||||
|
||||
protected override void ExitedTree()
|
||||
{
|
||||
base.ExitedTree();
|
||||
|
||||
_clyde.OnWindowResized -= ClydeOnOnWindowResized;
|
||||
}
|
||||
|
||||
protected override void KeyBindDown(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
if (args.Function == EngineKeyFunctions.UIClick)
|
||||
{
|
||||
_currentDrag = GetDragModeFor(args.RelativePosition);
|
||||
|
||||
if (_currentDrag != DragMode.None)
|
||||
{
|
||||
_dragOffsetTopLeft = args.PointerLocation.Position / UIScale - Position;
|
||||
_dragOffsetBottomRight = Position + Size - args.PointerLocation.Position / UIScale;
|
||||
}
|
||||
}
|
||||
|
||||
base.KeyBindDown(args);
|
||||
}
|
||||
|
||||
protected override void KeyBindUp(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
base.KeyBindUp(args);
|
||||
|
||||
if (args.Function != EngineKeyFunctions.UIClick)
|
||||
return;
|
||||
|
||||
_dragOffsetTopLeft = _dragOffsetBottomRight = Vector2.Zero;
|
||||
_currentDrag = DragMode.None;
|
||||
|
||||
// If this is done in MouseDown, Godot won't fire MouseUp as you need focus to receive MouseUps.
|
||||
UserInterfaceManager.KeyboardFocused?.ReleaseKeyboardFocus();
|
||||
}
|
||||
|
||||
|
||||
// TODO: this drag and drop stuff is somewhat duplicated from Robust BaseWindow but also modified
|
||||
[Flags]
|
||||
private enum DragMode : byte
|
||||
{
|
||||
None = 0,
|
||||
Bottom = 1 << 1,
|
||||
Left = 1 << 2
|
||||
}
|
||||
|
||||
private DragMode GetDragModeFor(Vector2 relativeMousePos)
|
||||
{
|
||||
var mode = DragMode.None;
|
||||
|
||||
if (relativeMousePos.Y > Size.Y - DragMarginSize)
|
||||
{
|
||||
mode = DragMode.Bottom;
|
||||
}
|
||||
|
||||
if (relativeMousePos.X < DragMarginSize)
|
||||
{
|
||||
mode |= DragMode.Left;
|
||||
}
|
||||
|
||||
return mode;
|
||||
}
|
||||
|
||||
protected override void MouseMove(GUIMouseMoveEventArgs args)
|
||||
{
|
||||
base.MouseMove(args);
|
||||
|
||||
if (Parent == null)
|
||||
return;
|
||||
|
||||
if (_currentDrag == DragMode.None)
|
||||
{
|
||||
var cursor = CursorShape.Arrow;
|
||||
var previewDragMode = GetDragModeFor(args.RelativePosition);
|
||||
switch (previewDragMode)
|
||||
{
|
||||
case DragMode.Bottom:
|
||||
cursor = CursorShape.VResize;
|
||||
break;
|
||||
|
||||
case DragMode.Left:
|
||||
cursor = CursorShape.HResize;
|
||||
break;
|
||||
|
||||
case DragMode.Bottom | DragMode.Left:
|
||||
cursor = CursorShape.Crosshair;
|
||||
break;
|
||||
}
|
||||
|
||||
DefaultCursorShape = cursor;
|
||||
}
|
||||
else
|
||||
{
|
||||
var top = Rect.Top;
|
||||
var bottom = Rect.Bottom;
|
||||
var left = Rect.Left;
|
||||
var right = Rect.Right;
|
||||
var (minSizeX, minSizeY) = MinSize;
|
||||
if ((_currentDrag & DragMode.Bottom) == DragMode.Bottom)
|
||||
{
|
||||
bottom = Math.Max(args.GlobalPosition.Y + _dragOffsetBottomRight.Y, top + minSizeY);
|
||||
}
|
||||
|
||||
if ((_currentDrag & DragMode.Left) == DragMode.Left)
|
||||
{
|
||||
var maxX = right - minSizeX;
|
||||
left = Math.Min(args.GlobalPosition.X - _dragOffsetTopLeft.X, maxX);
|
||||
}
|
||||
|
||||
ClampSize(left, bottom);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void UIScaleChanged()
|
||||
{
|
||||
base.UIScaleChanged();
|
||||
ClampAfterDelay();
|
||||
}
|
||||
|
||||
private void ClydeOnOnWindowResized(WindowResizedEventArgs obj)
|
||||
{
|
||||
ClampAfterDelay();
|
||||
}
|
||||
|
||||
private void ClampAfterDelay()
|
||||
{
|
||||
_clampIn = 2;
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
|
||||
// we do the clamping after a delay (after UI scale / window resize)
|
||||
// because we need to wait for our parent container to properly resize
|
||||
// first, so we can calculate where we should go. If we do it right away,
|
||||
// we won't have the correct values from the parent to know how to adjust our margins.
|
||||
if (_clampIn <= 0)
|
||||
return;
|
||||
|
||||
_clampIn -= 1;
|
||||
if (_clampIn == 0)
|
||||
ClampSize();
|
||||
}
|
||||
|
||||
private void ClampSize(float? desiredLeft = null, float? desiredBottom = null)
|
||||
{
|
||||
if (Parent == null)
|
||||
return;
|
||||
|
||||
// var top = Rect.Top;
|
||||
var right = Rect.Right;
|
||||
var left = desiredLeft ?? Rect.Left;
|
||||
var bottom = desiredBottom ?? Rect.Bottom;
|
||||
|
||||
// clamp so it doesn't go too high or low (leave space for alerts UI)
|
||||
var maxBottom = Parent.Size.Y - MinDistanceFromBottom;
|
||||
if (maxBottom <= MinHeight)
|
||||
{
|
||||
// we can't fit in our given space (window made awkwardly small), so give up
|
||||
// and overlap at our min height
|
||||
bottom = MinHeight;
|
||||
}
|
||||
else
|
||||
{
|
||||
bottom = Math.Clamp(bottom, MinHeight, maxBottom);
|
||||
}
|
||||
|
||||
var maxLeft = Parent.Size.X - MinWidth;
|
||||
if (maxLeft <= MinLeft)
|
||||
{
|
||||
// window too narrow, give up and overlap at our max left
|
||||
left = maxLeft;
|
||||
}
|
||||
else
|
||||
{
|
||||
left = Math.Clamp(left, MinLeft, maxLeft);
|
||||
}
|
||||
|
||||
LayoutContainer.SetMarginLeft(this, -((right + 10) - left));
|
||||
LayoutContainer.SetMarginBottom(this, bottom);
|
||||
|
||||
ChatMgr.ChatBoxOnResized(new ChatResizedEventArgs(bottom));
|
||||
}
|
||||
|
||||
protected override void MouseExited()
|
||||
{
|
||||
base.MouseExited();
|
||||
|
||||
if (_currentDrag == DragMode.None)
|
||||
DefaultCursorShape = CursorShape.Arrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,7 @@
|
||||
using System;
|
||||
using Content.Client.Chat.Managers;
|
||||
using Content.Client.Viewport;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.Chat.UI
|
||||
@@ -48,6 +43,9 @@ namespace Content.Client.Chat.UI
|
||||
|
||||
public Vector2 ContentSize { get; private set; }
|
||||
|
||||
// man down
|
||||
public event Action<EntityUid, SpeechBubble>? OnDied;
|
||||
|
||||
public static SpeechBubble CreateSpeechBubble(SpeechType type, string text, EntityUid senderEntity, IEyeManager eyeManager, IChatManager chatManager, IEntityManager entityManager)
|
||||
{
|
||||
switch (type)
|
||||
@@ -148,7 +146,7 @@ namespace Content.Client.Chat.UI
|
||||
return;
|
||||
}
|
||||
|
||||
_chatManager.RemoveSpeechBubble(_senderEntity, this);
|
||||
OnDied?.Invoke(_senderEntity, this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -164,7 +162,6 @@ namespace Content.Client.Chat.UI
|
||||
}
|
||||
|
||||
public sealed class TextSpeechBubble : SpeechBubble
|
||||
|
||||
{
|
||||
public TextSpeechBubble(string text, EntityUid senderEntity, IEyeManager eyeManager, IChatManager chatManager, IEntityManager entityManager, string speechStyleClass)
|
||||
: base(text, senderEntity, eyeManager, chatManager, entityManager, speechStyleClass)
|
||||
|
||||
Reference in New Issue
Block a user