980 lines
36 KiB
C#
980 lines
36 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using Content.Client.Alerts.UI;
|
|
using Content.Client.Chat.Managers;
|
|
using Content.Client.Lobby;
|
|
using Content.Client.Resources;
|
|
using Content.Client.Stylesheets;
|
|
using Content.Shared.Chat;
|
|
using Content.Shared.Input;
|
|
using Robust.Client.Graphics;
|
|
using Robust.Client.ResourceManagement;
|
|
using Robust.Client.State;
|
|
using Robust.Client.UserInterface;
|
|
using Robust.Client.UserInterface.Controls;
|
|
using Robust.Shared.Input;
|
|
using Robust.Shared.IoC;
|
|
using Robust.Shared.Localization;
|
|
using Robust.Shared.Maths;
|
|
using Robust.Shared.Timing;
|
|
using Robust.Shared.Utility;
|
|
using static Robust.Client.UserInterface.Controls.BoxContainer;
|
|
|
|
namespace Content.Client.Chat.UI
|
|
{
|
|
public class ChatBox : Control
|
|
{
|
|
public const float InitialChatBottom = 235;
|
|
|
|
public delegate void TextSubmitHandler(ChatBox chatBox, string text);
|
|
|
|
public delegate void FilterToggledHandler(ChatChannel toggled, bool enabled);
|
|
|
|
public event TextSubmitHandler? TextSubmitted;
|
|
|
|
public event FilterToggledHandler? FilterToggled;
|
|
|
|
public HistoryLineEdit Input { get; private set; }
|
|
public OutputPanel Contents { get; }
|
|
|
|
public event Action<ChatResizedEventArgs>? OnResized;
|
|
|
|
// order in which the available channel filters show up when available
|
|
public static readonly IReadOnlyList<ChatChannel> ChannelFilterOrder = new List<ChatChannel>
|
|
{
|
|
ChatChannel.Local, ChatChannel.Emotes, ChatChannel.Radio, ChatChannel.OOC, ChatChannel.Dead, ChatChannel.AdminChat,
|
|
ChatChannel.Server
|
|
};
|
|
|
|
// order in which the channels show up in the channel selector
|
|
private static readonly IReadOnlyList<ChatChannel> ChannelSelectorOrder = new List<ChatChannel>
|
|
{
|
|
ChatChannel.Local, ChatChannel.Emotes, ChatChannel.Radio, ChatChannel.OOC, ChatChannel.Dead, ChatChannel.AdminChat
|
|
};
|
|
|
|
private const float FilterPopupWidth = 110;
|
|
private const int DragMarginSize = 7;
|
|
private const int MinDistanceFromBottom = 255;
|
|
private const int MinLeft = 500;
|
|
|
|
/// <summary>
|
|
/// Will be Unspecified if set to Console
|
|
/// </summary>
|
|
public ChatChannel SelectedChannel = ChatChannel.Unspecified;
|
|
|
|
/// <summary>
|
|
/// Default formatting string for the ClientChatConsole.
|
|
/// </summary>
|
|
public string DefaultChatFormat { get; set; } = string.Empty;
|
|
|
|
public bool ReleaseFocusOnEnter { get; set; } = true;
|
|
|
|
public bool ClearOnEnter { get; set; } = true;
|
|
|
|
// when channel is changed temporarily due to typing an alias
|
|
// prefix, we save the current channel selection here to restore it when
|
|
// the message is sent
|
|
private ChatChannel? _savedSelectedChannel;
|
|
|
|
private readonly Popup _channelSelectorPopup;
|
|
private readonly Button _channelSelector;
|
|
private readonly BoxContainer _channelSelectorHBox;
|
|
private readonly FilterButton _filterButton;
|
|
private readonly Popup _filterPopup;
|
|
private readonly PanelContainer _filterPopupPanel;
|
|
private readonly BoxContainer _filterVBox;
|
|
private DragMode _currentDrag = DragMode.None;
|
|
private Vector2 _dragOffsetTopLeft;
|
|
private Vector2 _dragOffsetBottomRight;
|
|
private readonly IClyde _clyde;
|
|
private readonly bool _lobbyMode;
|
|
private byte _clampIn;
|
|
// currently known selectable channels as provided by ChatManager,
|
|
// never contains Unspecified (which corresponds to Console which is always available)
|
|
public List<ChatChannel> SelectableChannels = new();
|
|
|
|
/// <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()
|
|
{
|
|
//TODO Paul needs to fix xaml ctor args so we can pass this instead of resolving it.
|
|
var stateManager = IoCManager.Resolve<IStateManager>();
|
|
_lobbyMode = stateManager.CurrentState is LobbyState;
|
|
|
|
// 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.
|
|
_clyde = IoCManager.Resolve<IClyde>();
|
|
MouseFilter = MouseFilterMode.Stop;
|
|
LayoutContainer.SetMarginLeft(this, 4);
|
|
LayoutContainer.SetMarginRight(this, 4);
|
|
MinHeight = 128;
|
|
MinWidth = 200;
|
|
|
|
AddChild(new PanelContainer
|
|
{
|
|
PanelOverride = new StyleBoxFlat {BackgroundColor = Color.FromHex("#25252aaa")},
|
|
VerticalExpand = true,
|
|
HorizontalExpand = true,
|
|
Children =
|
|
{
|
|
new BoxContainer
|
|
{
|
|
Orientation = LayoutOrientation.Vertical,
|
|
Children =
|
|
{
|
|
(Contents = new OutputPanel
|
|
{
|
|
VerticalExpand = true,
|
|
}),
|
|
new PanelContainer
|
|
{
|
|
StyleClasses = { StyleNano.StyleClassChatSubPanel },
|
|
HorizontalExpand = true,
|
|
Children =
|
|
{
|
|
new BoxContainer
|
|
{
|
|
Orientation = LayoutOrientation.Horizontal,
|
|
HorizontalExpand = true,
|
|
SeparationOverride = 4,
|
|
Children =
|
|
{
|
|
(_channelSelector = new ChannelSelectorButton
|
|
{
|
|
StyleClasses = { StyleNano.StyleClassChatChannelSelectorButton },
|
|
MinWidth = 75,
|
|
Text = Loc.GetString("hud-chatbox-ooc"),
|
|
ToggleMode = true
|
|
}),
|
|
(Input = new HistoryLineEdit
|
|
{
|
|
PlaceHolder = Loc.GetString("hud-chatbox-info"),
|
|
HorizontalExpand = true,
|
|
StyleClasses = { StyleNano.StyleClassChatLineEdit }
|
|
}),
|
|
(_filterButton = new FilterButton
|
|
{
|
|
StyleClasses = { StyleNano.StyleClassChatFilterOptionButton }
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
_filterPopup = new Popup
|
|
{
|
|
Children =
|
|
{
|
|
(_filterPopupPanel = new PanelContainer
|
|
{
|
|
StyleClasses = {StyleNano.StyleClassBorderedWindowPanel},
|
|
Children =
|
|
{
|
|
new BoxContainer
|
|
{
|
|
Orientation = LayoutOrientation.Horizontal,
|
|
Children =
|
|
{
|
|
new Control{MinSize = (4,0)},
|
|
(_filterVBox = new BoxContainer
|
|
{
|
|
Orientation = LayoutOrientation.Vertical,
|
|
SeparationOverride = 4
|
|
})
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
};
|
|
|
|
_channelSelectorPopup = new Popup
|
|
{
|
|
Children =
|
|
{
|
|
(_channelSelectorHBox = new BoxContainer
|
|
{
|
|
Orientation = LayoutOrientation.Horizontal,
|
|
SeparationOverride = 1
|
|
})
|
|
}
|
|
};
|
|
|
|
if (!_lobbyMode)
|
|
{
|
|
UserInterfaceManager.StateRoot.AddChild(this);
|
|
LayoutContainer.SetAnchorAndMarginPreset(this, LayoutContainer.LayoutPreset.TopRight, margin: 10);
|
|
LayoutContainer.SetAnchorAndMarginPreset(this, LayoutContainer.LayoutPreset.TopRight, margin: 10);
|
|
LayoutContainer.SetMarginLeft(this, -475);
|
|
LayoutContainer.SetMarginBottom(this, InitialChatBottom);
|
|
OnResized?.Invoke(new ChatResizedEventArgs(InitialChatBottom));
|
|
}
|
|
}
|
|
|
|
protected override void EnteredTree()
|
|
{
|
|
base.EnteredTree();
|
|
_channelSelector.OnToggled += OnChannelSelectorToggled;
|
|
_filterButton.OnToggled += OnFilterButtonToggled;
|
|
Input.OnKeyBindDown += InputKeyBindDown;
|
|
Input.OnTextEntered += Input_OnTextEntered;
|
|
Input.OnTextChanged += InputOnTextChanged;
|
|
Input.OnFocusExit += InputOnFocusExit;
|
|
_channelSelectorPopup.OnPopupHide += OnChannelSelectorPopupHide;
|
|
_filterPopup.OnPopupHide += OnFilterPopupHide;
|
|
_clyde.OnWindowResized += ClydeOnOnWindowResized;
|
|
}
|
|
|
|
protected override void ExitedTree()
|
|
{
|
|
base.ExitedTree();
|
|
_channelSelector.OnToggled -= OnChannelSelectorToggled;
|
|
_filterButton.OnToggled -= OnFilterButtonToggled;
|
|
Input.OnKeyBindDown -= InputKeyBindDown;
|
|
Input.OnTextEntered -= Input_OnTextEntered;
|
|
Input.OnTextChanged -= InputOnTextChanged;
|
|
Input.OnFocusExit -= InputOnFocusExit;
|
|
_channelSelectorPopup.OnPopupHide -= OnChannelSelectorPopupHide;
|
|
_filterPopup.OnPopupHide -= OnFilterPopupHide;
|
|
_clyde.OnWindowResized -= ClydeOnOnWindowResized;
|
|
UnsubFilterItems();
|
|
UnsubChannelItems();
|
|
|
|
}
|
|
|
|
private void UnsubFilterItems()
|
|
{
|
|
foreach (var child in _filterVBox.Children)
|
|
{
|
|
if (child is not ChannelFilterCheckbox checkbox) continue;
|
|
checkbox.OnToggled -= OnFilterCheckboxToggled;
|
|
}
|
|
}
|
|
|
|
private void UnsubChannelItems()
|
|
{
|
|
foreach (var child in _channelSelectorHBox.Children)
|
|
{
|
|
if (child is not ChannelItemButton button) continue;
|
|
button.OnPressed -= OnChannelSelectorItemPressed;
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Update the available filters / selectable channels and the current filter settings using the provided
|
|
/// data.
|
|
/// </summary>
|
|
/// <param name="selectableChannels">channels currently selectable to send on</param>
|
|
/// <param name="filterableChannels">channels currently able ot filter on</param>
|
|
/// <param name="channelFilters">current settings for the channel filters, this SHOULD always have an entry if
|
|
/// there is a corresponding entry in filterableChannels, but it may also have additional
|
|
/// entries (which should not be presented to the user)</param>
|
|
/// <param name="unreadMessages">unread message counts for each disabled channel, values 10 or higher will show as 9+</param>
|
|
public void SetChannelPermissions(List<ChatChannel> selectableChannels, IReadOnlySet<ChatChannel> filterableChannels,
|
|
IReadOnlyDictionary<ChatChannel, bool> channelFilters, IReadOnlyDictionary<ChatChannel, byte> unreadMessages, bool switchIfConsole)
|
|
{
|
|
SelectableChannels = selectableChannels;
|
|
// update the channel selector
|
|
UnsubChannelItems();
|
|
_channelSelectorHBox.RemoveAllChildren();
|
|
foreach (var selectableChannel in ChannelSelectorOrder)
|
|
{
|
|
if (!selectableChannels.Contains(selectableChannel)) continue;
|
|
var newButton = new ChannelItemButton(selectableChannel);
|
|
newButton.OnPressed += OnChannelSelectorItemPressed;
|
|
_channelSelectorHBox.AddChild(newButton);
|
|
}
|
|
// console channel is always selectable and represented via Unspecified
|
|
var consoleButton = new ChannelItemButton(ChatChannel.Unspecified);
|
|
consoleButton.OnPressed += OnChannelSelectorItemPressed;
|
|
_channelSelectorHBox.AddChild(consoleButton);
|
|
|
|
|
|
if (_savedSelectedChannel.HasValue && _savedSelectedChannel.Value != ChatChannel.Unspecified &&
|
|
!selectableChannels.Contains(_savedSelectedChannel.Value))
|
|
{
|
|
// we just lost our saved selected channel, the current one will become permanent
|
|
_savedSelectedChannel = null;
|
|
}
|
|
|
|
if (!selectableChannels.Contains(SelectedChannel) && (switchIfConsole || SelectedChannel != ChatChannel.Unspecified))
|
|
{
|
|
// our previously selected channel no longer exists or we are still on console channel because we just joined
|
|
if ((SelectedChannel & ChatChannel.IC) != 0 || SelectedChannel == ChatChannel.Unspecified)
|
|
{
|
|
if (!SafelySelectChannel(ChatChannel.Local))
|
|
SafelySelectChannel(ChatChannel.Dead);
|
|
}
|
|
else if (selectableChannels.Contains(ChatChannel.OOC))
|
|
{
|
|
SafelySelectChannel(ChatChannel.OOC);
|
|
}
|
|
else //This shouldn't happen but better to be safe than sorry
|
|
{
|
|
SafelySelectChannel(selectableChannels.First());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
SafelySelectChannel(SelectedChannel);
|
|
}
|
|
|
|
// update the channel filters
|
|
UnsubFilterItems();
|
|
_filterVBox.Children.Clear();
|
|
_filterVBox.AddChild(new Control {CustomMinimumSize = (10, 0)});
|
|
foreach (var channelFilter in ChannelFilterOrder)
|
|
{
|
|
if (!filterableChannels.Contains(channelFilter)) continue;
|
|
byte? unreadCount = null;
|
|
if (unreadMessages.TryGetValue(channelFilter, out var unread))
|
|
{
|
|
unreadCount = unread;
|
|
}
|
|
var newCheckBox = new ChannelFilterCheckbox(channelFilter, unreadCount)
|
|
{
|
|
// shouldn't happen, but if there's no explicit enable setting provided, default to enabled
|
|
Pressed = !channelFilters.TryGetValue(channelFilter, out var enabled) || enabled
|
|
};
|
|
newCheckBox.OnToggled += OnFilterCheckboxToggled;
|
|
_filterVBox.AddChild(newCheckBox);
|
|
}
|
|
_filterVBox.AddChild(new Control {CustomMinimumSize = (10, 0)});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update the unread message counts in the filters based on the provided data.
|
|
/// </summary>
|
|
/// <param name="unreadMessages">counts for each channel, any values above 9 will show as 9+</param>
|
|
public void UpdateUnreadMessageCounts(IReadOnlyDictionary<ChatChannel, byte> unreadMessages)
|
|
{
|
|
foreach (var channelFilter in _filterVBox.Children)
|
|
{
|
|
if (channelFilter is not ChannelFilterCheckbox filterCheckbox) continue;
|
|
if (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;
|
|
FilterToggled?.Invoke(checkbox.Channel, checkbox.Pressed);
|
|
}
|
|
|
|
|
|
private void OnFilterButtonToggled(BaseButton.ButtonToggledEventArgs args)
|
|
{
|
|
if (args.Pressed)
|
|
{
|
|
var globalPos = _filterButton.GlobalPosition;
|
|
var (minX, minY) = _filterPopupPanel.CombinedMinimumSize;
|
|
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;
|
|
SafelySelectChannel(button.Channel);
|
|
_channelSelectorPopup.Close();
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Selects the indicated channel, clearing out any temporarily-selected channel
|
|
/// (any currently entered text is preserved). If the specified channel is not selectable,
|
|
/// will just maintain current selection.
|
|
/// </summary>
|
|
public void SelectChannel(ChatChannel toSelect)
|
|
{
|
|
_savedSelectedChannel = null;
|
|
SafelySelectChannel(toSelect);
|
|
}
|
|
|
|
private bool SafelySelectChannel(ChatChannel toSelect)
|
|
{
|
|
if (toSelect == ChatChannel.Unspecified ||
|
|
SelectableChannels.Contains(toSelect))
|
|
{
|
|
SelectedChannel = toSelect;
|
|
_channelSelector.Text = ChannelSelectorName(toSelect);
|
|
_channelSelector.Modulate = ChatHelper.ChatColor(toSelect);
|
|
return true;
|
|
}
|
|
// keep current setting
|
|
return false;
|
|
}
|
|
|
|
protected override void KeyBindDown(GUIBoundKeyEventArgs args)
|
|
{
|
|
base.KeyBindDown(args);
|
|
|
|
if (args.Function == EngineKeyFunctions.UIClick && !_lobbyMode)
|
|
{
|
|
_currentDrag = GetDragModeFor(args.RelativePosition);
|
|
|
|
if (_currentDrag != DragMode.None)
|
|
{
|
|
_dragOffsetTopLeft = args.PointerLocation.Position / UIScale - Position;
|
|
_dragOffsetBottomRight = Position + Size - args.PointerLocation.Position / UIScale;
|
|
}
|
|
}
|
|
|
|
if (args.CanFocus)
|
|
{
|
|
Input.GrabKeyboardFocus();
|
|
}
|
|
}
|
|
|
|
protected override void KeyBindUp(GUIBoundKeyEventArgs args)
|
|
{
|
|
base.KeyBindUp(args);
|
|
|
|
if (args.Function != EngineKeyFunctions.UIClick || _lobbyMode)
|
|
{
|
|
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();
|
|
}
|
|
|
|
public void CycleChatChannel(bool forward)
|
|
{
|
|
Input.IgnoreNext = true;
|
|
var channels = SelectableChannels;
|
|
var idx = channels.IndexOf(SelectedChannel);
|
|
if (forward)
|
|
{
|
|
idx++;
|
|
}
|
|
else
|
|
{
|
|
idx--;
|
|
}
|
|
idx = MathHelper.Mod(idx, channels.Count);
|
|
|
|
SelectChannel(channels[idx]);
|
|
}
|
|
|
|
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();
|
|
return;
|
|
}
|
|
|
|
// if we temporarily selected another channel via a prefx, undo that when we backspace on an empty input
|
|
if (args.Function == EngineKeyFunctions.TextBackspace && Input.Text.Length == 0 && _savedSelectedChannel.HasValue)
|
|
{
|
|
SafelySelectChannel(_savedSelectedChannel.Value);
|
|
_savedSelectedChannel = null;
|
|
}
|
|
}
|
|
|
|
// 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 || _lobbyMode)
|
|
{
|
|
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) = CombinedMinimumSize;
|
|
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()
|
|
{
|
|
if (!_lobbyMode)
|
|
_clampIn = 2;
|
|
}
|
|
|
|
protected override void FrameUpdate(FrameEventArgs args)
|
|
{
|
|
base.FrameUpdate(args);
|
|
if (_lobbyMode) return;
|
|
// 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 || _lobbyMode) 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);
|
|
OnResized?.Invoke(new ChatResizedEventArgs(bottom));
|
|
}
|
|
|
|
protected override void MouseExited()
|
|
{
|
|
base.MouseExited();
|
|
|
|
if (_currentDrag == DragMode.None && !_lobbyMode)
|
|
{
|
|
DefaultCursorShape = CursorShape.Arrow;
|
|
}
|
|
}
|
|
|
|
|
|
private void InputOnTextChanged(LineEdit.LineEditEventArgs obj)
|
|
{
|
|
// switch temporarily to a different channel if an alias prefix has been entered.
|
|
|
|
// are we already temporarily switching to a channel?
|
|
if (_savedSelectedChannel.HasValue) return;
|
|
|
|
var trimmed = obj.Text.Trim();
|
|
if (trimmed.Length == 0 || trimmed.Length > 1) return;
|
|
|
|
var channel = GetChannelFromPrefix(trimmed[0]);
|
|
var prevChannel = SelectedChannel;
|
|
if (channel == null || !SafelySelectChannel(channel.Value)) return;
|
|
// we ate the prefix and auto-switched (temporarily) to the channel with that prefix
|
|
_savedSelectedChannel = prevChannel;
|
|
Input.Text = "";
|
|
}
|
|
|
|
private static ChatChannel? GetChannelFromPrefix(char prefix)
|
|
{
|
|
return prefix switch
|
|
{
|
|
ChatManager.MeAlias => ChatChannel.Emotes,
|
|
ChatManager.RadioAlias => ChatChannel.Radio,
|
|
ChatManager.AdminChatAlias => ChatChannel.AdminChat,
|
|
ChatManager.OOCAlias => ChatChannel.OOC,
|
|
ChatManager.ConCmdSlash => ChatChannel.Unspecified,
|
|
_ => null
|
|
};
|
|
}
|
|
|
|
private static string GetPrefixFromChannel(ChatChannel channel)
|
|
{
|
|
char? prefixChar = channel switch
|
|
{
|
|
ChatChannel.Emotes => ChatManager.MeAlias,
|
|
ChatChannel.Radio => ChatManager.RadioAlias,
|
|
ChatChannel.AdminChat => ChatManager.AdminChatAlias,
|
|
ChatChannel.OOC => ChatManager.OOCAlias,
|
|
ChatChannel.Unspecified => ChatManager.ConCmdSlash,
|
|
_ => null
|
|
};
|
|
|
|
return prefixChar.ToString() ?? string.Empty;
|
|
}
|
|
|
|
public static string ChannelSelectorName(ChatChannel channel)
|
|
{
|
|
return channel switch
|
|
{
|
|
ChatChannel.AdminChat => Loc.GetString("hud-chatbox-admin"),
|
|
ChatChannel.Unspecified => Loc.GetString("hud-chatbox-console"),
|
|
_ => Loc.GetString(channel.ToString())
|
|
};
|
|
}
|
|
|
|
public void AddLine(string message, ChatChannel channel, Color color)
|
|
{
|
|
if (Disposed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var formatted = new FormattedMessage(3);
|
|
formatted.PushColor(color);
|
|
formatted.AddMarkup(message);
|
|
formatted.Pop();
|
|
Contents.AddMessage(formatted);
|
|
}
|
|
|
|
private void InputOnFocusExit(LineEdit.LineEditEventArgs obj)
|
|
{
|
|
// undo the temporary selection, otherwise it will be odd if user
|
|
// comes back to it later only to have their selection cleared upon sending
|
|
if (!_savedSelectedChannel.HasValue) return;
|
|
SafelySelectChannel(_savedSelectedChannel.Value);
|
|
_savedSelectedChannel = null;
|
|
}
|
|
|
|
private void Input_OnTextEntered(LineEdit.LineEditEventArgs args)
|
|
{
|
|
// We set it there to true so it's set to false by TextSubmitted.Invoke if necessary
|
|
ClearOnEnter = true;
|
|
|
|
if (!string.IsNullOrWhiteSpace(args.Text))
|
|
{
|
|
TextSubmitted?.Invoke(this, GetPrefixFromChannel(SelectedChannel)
|
|
+ args.Text);
|
|
}
|
|
|
|
if (ClearOnEnter)
|
|
{
|
|
Input.Clear();
|
|
if (_savedSelectedChannel.HasValue)
|
|
{
|
|
SafelySelectChannel(_savedSelectedChannel.Value);
|
|
_savedSelectedChannel = null;
|
|
}
|
|
}
|
|
|
|
if (ReleaseFocusOnEnter)
|
|
{
|
|
Input.ReleaseKeyboardFocus();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <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,
|
|
SizeFlagsVertical = SizeFlags.ShrinkCenter,
|
|
SizeFlagsHorizontal = SizeFlags.ShrinkCenter
|
|
})
|
|
);
|
|
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 ChatChannel Channel;
|
|
|
|
public ChannelItemButton(ChatChannel channel)
|
|
{
|
|
Channel = channel;
|
|
AddStyleClass(StyleNano.StyleClassChatChannelSelectorButton);
|
|
Text = ChatBox.ChannelSelectorName(channel);
|
|
}
|
|
}
|
|
|
|
public sealed class ChannelFilterCheckbox : CheckBox
|
|
{
|
|
public readonly ChatChannel Channel;
|
|
|
|
public ChannelFilterCheckbox(ChatChannel channel, byte? unreadCount)
|
|
{
|
|
Channel = channel;
|
|
|
|
UpdateText(unreadCount);
|
|
}
|
|
|
|
private void UpdateText(byte? unread)
|
|
{
|
|
var name = Channel switch
|
|
{
|
|
ChatChannel.AdminChat => Loc.GetString("hud-chatbox-admin"),
|
|
ChatChannel.Unspecified => throw new InvalidOperationException(
|
|
"cannot create chat filter for Unspecified"),
|
|
_ => Loc.GetString(Channel.ToString())
|
|
};
|
|
|
|
if (unread > 0)
|
|
{
|
|
Text = name + " (" + (unread > 9 ? "9+" : unread) + ")";
|
|
}
|
|
else
|
|
{
|
|
Text = name;
|
|
}
|
|
}
|
|
|
|
public void UpdateUnreadCount(byte? 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;
|
|
}
|
|
}
|
|
}
|