Chairbender Chat (#3794)
* #272 restructure and restyle chat line edit section * #272 no arrow, actually change id on channel changer * #272 nice round chat channel picker * #272 add chat channel selection logic, and auto-select when a prefix is entered * #272 consistent width of chat channel btn * #272 only show admin channel filter if asay perms * #272 add tutorial info on chat prefixes * #272 added chat filter button * #272 added chat filter button * #272 WIP on filter popup * #272 fix filter popup pressed / unpressed logic * #272 fix filter popup positioning and layout * #272 WIP channel filter logic * #272 WIP channel filter logic * #272 WIP refactoring how chatbox / manager manages available filters and channels to send on * #272 WIP implementing filtering UI / logic and refactoring how chat UI is managed * #272 fix various bugs with new chat filter / selector logic * #272 remove outdated todos * #272 WIP working chat window resize * #272 bounded chatbox resizing * #272 alertUI moves with resized chat * #272 WIP making alertUI not be too large when changing size / UIScale * #272 WIP fixing window / uiscale adjustment * #272 WIP hacky approach for resizing, will try another approach * #272 implement hacky approach for bounded chat resize * #272 no resizing of lobby chat * #272 WIP adding unread marker to chat filters * #272 basic working unread chat message indicators * #272 WIP adding horizontal channel selector items * #272 horizontal channel selector popup * #272 workaround for chat selector staying highlighted when right clicking it while toggled * #272 workaround for chat selector staying highlighted when right clicking it while toggled * #272 wip trying to add tests for chatbox * #272 remove test, not really possible with current system * #272 merge latest * #272 merge latest * #272 fix csproj changes * It works if you disable the lobby * Fixes lobby chat * Adds more channel focusses * Channel cycler * Address review * Address nitpicks * Address more of the review * Fix chat post-viewport * Finalize review stuff Co-authored-by: chairbender <kwhipke1@gmail.com> Co-authored-by: ike709 <sparebytes@protonmail.com>
@@ -1,29 +1,65 @@
|
|||||||
using Content.Shared.Chat;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Linq;
|
||||||
|
using Content.Client.State;
|
||||||
|
using Content.Client.UserInterface;
|
||||||
|
using Content.Client.UserInterface.Stylesheets;
|
||||||
|
using Content.Client.Utility;
|
||||||
|
using Content.Shared.Chat;
|
||||||
using Robust.Client.Graphics;
|
using Robust.Client.Graphics;
|
||||||
|
using Robust.Client.ResourceManagement;
|
||||||
|
using Robust.Client.State;
|
||||||
using Robust.Client.UserInterface;
|
using Robust.Client.UserInterface;
|
||||||
using Robust.Client.UserInterface.Controls;
|
using Robust.Client.UserInterface.Controls;
|
||||||
using Robust.Shared.Input;
|
using Robust.Shared.Input;
|
||||||
|
using Robust.Shared.IoC;
|
||||||
using Robust.Shared.Localization;
|
using Robust.Shared.Localization;
|
||||||
using Robust.Shared.Maths;
|
using Robust.Shared.Maths;
|
||||||
|
using Robust.Shared.Timing;
|
||||||
using Robust.Shared.Utility;
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
namespace Content.Client.Chat
|
namespace Content.Client.Chat
|
||||||
{
|
{
|
||||||
public class ChatBox : Control
|
public class ChatBox : Control
|
||||||
{
|
{
|
||||||
|
public const float InitialChatBottom = 235;
|
||||||
|
|
||||||
public delegate void TextSubmitHandler(ChatBox chatBox, string text);
|
public delegate void TextSubmitHandler(ChatBox chatBox, string text);
|
||||||
|
|
||||||
public delegate void FilterToggledHandler(ChatBox chatBox, BaseButton.ButtonToggledEventArgs e);
|
public delegate void FilterToggledHandler(ChatChannel toggled, bool enabled);
|
||||||
|
|
||||||
|
public event TextSubmitHandler? TextSubmitted;
|
||||||
|
|
||||||
|
public event FilterToggledHandler? FilterToggled;
|
||||||
|
|
||||||
public HistoryLineEdit Input { get; private set; }
|
public HistoryLineEdit Input { get; private set; }
|
||||||
public OutputPanel Contents { get; }
|
public OutputPanel Contents { get; }
|
||||||
|
|
||||||
// Buttons for filtering
|
public event Action<ChatResizedEventArgs>? OnResized;
|
||||||
public Button AllButton { get; }
|
|
||||||
public Button LocalButton { get; }
|
// order in which the available channel filters show up when available
|
||||||
public Button OOCButton { get; }
|
public static readonly IReadOnlyList<ChatChannel> ChannelFilterOrder = new List<ChatChannel>
|
||||||
public Button AdminButton { get; }
|
{
|
||||||
public Button DeadButton { get; }
|
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;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Default formatting string for the ClientChatConsole.
|
/// Default formatting string for the ClientChatConsole.
|
||||||
@@ -34,96 +70,431 @@ namespace Content.Client.Chat
|
|||||||
|
|
||||||
public bool ClearOnEnter { 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 HBoxContainer _channelSelectorHBox;
|
||||||
|
private readonly FilterButton _filterButton;
|
||||||
|
private readonly Popup _filterPopup;
|
||||||
|
private readonly PanelContainer _filterPopupPanel;
|
||||||
|
private readonly VBoxContainer _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()
|
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;
|
MouseFilter = MouseFilterMode.Stop;
|
||||||
|
LayoutContainer.SetMarginLeft(this, 4);
|
||||||
|
LayoutContainer.SetMarginRight(this, 4);
|
||||||
|
MinHeight = 128;
|
||||||
|
MinWidth = 200;
|
||||||
|
|
||||||
var outerVBox = new VBoxContainer();
|
AddChild(new PanelContainer
|
||||||
|
|
||||||
var panelContainer = new PanelContainer
|
|
||||||
{
|
{
|
||||||
PanelOverride = new StyleBoxFlat {BackgroundColor = Color.FromHex("#25252aaa")},
|
PanelOverride = new StyleBoxFlat {BackgroundColor = Color.FromHex("#25252aaa")},
|
||||||
VerticalExpand = true
|
VerticalExpand = true,
|
||||||
|
HorizontalExpand = true,
|
||||||
|
Children =
|
||||||
|
{
|
||||||
|
new VBoxContainer
|
||||||
|
{
|
||||||
|
Children =
|
||||||
|
{
|
||||||
|
(Contents = new OutputPanel
|
||||||
|
{
|
||||||
|
VerticalExpand = true,
|
||||||
|
}),
|
||||||
|
new PanelContainer
|
||||||
|
{
|
||||||
|
StyleClasses = { StyleNano.StyleClassChatSubPanel },
|
||||||
|
HorizontalExpand = true,
|
||||||
|
Children =
|
||||||
|
{
|
||||||
|
new HBoxContainer
|
||||||
|
{
|
||||||
|
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 HBoxContainer
|
||||||
|
{
|
||||||
|
Children =
|
||||||
|
{
|
||||||
|
new Control{MinSize = (10,0)},
|
||||||
|
(_filterVBox = new VBoxContainer
|
||||||
|
{
|
||||||
|
SeparationOverride = 10
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
};
|
};
|
||||||
var vBox = new VBoxContainer();
|
|
||||||
panelContainer.AddChild(vBox);
|
|
||||||
var hBox = new HBoxContainer();
|
|
||||||
|
|
||||||
outerVBox.AddChild(panelContainer);
|
_channelSelectorPopup = new Popup
|
||||||
outerVBox.AddChild(hBox);
|
{
|
||||||
|
Children =
|
||||||
|
{
|
||||||
|
(_channelSelectorHBox = new HBoxContainer
|
||||||
|
{
|
||||||
|
SeparationOverride = 4
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
Contents = new OutputPanel {Margin = new Thickness(4, 0), VerticalExpand = true};
|
if (!_lobbyMode)
|
||||||
vBox.AddChild(Contents);
|
{
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Input = new HistoryLineEdit();
|
protected override void EnteredTree()
|
||||||
|
{
|
||||||
|
base.EnteredTree();
|
||||||
|
_channelSelector.OnToggled += OnChannelSelectorToggled;
|
||||||
|
_filterButton.OnToggled += OnFilterButtonToggled;
|
||||||
Input.OnKeyBindDown += InputKeyBindDown;
|
Input.OnKeyBindDown += InputKeyBindDown;
|
||||||
Input.OnTextEntered += Input_OnTextEntered;
|
Input.OnTextEntered += Input_OnTextEntered;
|
||||||
vBox.AddChild(Input);
|
Input.OnTextChanged += InputOnTextChanged;
|
||||||
|
Input.OnFocusExit += InputOnFocusExit;
|
||||||
|
_channelSelectorPopup.OnPopupHide += OnChannelSelectorPopupHide;
|
||||||
|
_filterPopup.OnPopupHide += OnFilterPopupHide;
|
||||||
|
_clyde.OnWindowResized += ClydeOnOnWindowResized;
|
||||||
|
}
|
||||||
|
|
||||||
AllButton = new Button
|
protected override void ExitedTree()
|
||||||
{
|
{
|
||||||
Text = Loc.GetString("All"),
|
base.ExitedTree();
|
||||||
Name = "ALL",
|
_channelSelector.OnToggled -= OnChannelSelectorToggled;
|
||||||
HorizontalExpand = true,
|
_filterButton.OnToggled -= OnFilterButtonToggled;
|
||||||
HorizontalAlignment = HAlignment.Right,
|
Input.OnKeyBindDown -= InputKeyBindDown;
|
||||||
ToggleMode = true,
|
Input.OnTextEntered -= Input_OnTextEntered;
|
||||||
};
|
Input.OnTextChanged -= InputOnTextChanged;
|
||||||
|
Input.OnFocusExit -= InputOnFocusExit;
|
||||||
|
_channelSelectorPopup.OnPopupHide -= OnChannelSelectorPopupHide;
|
||||||
|
_filterPopup.OnPopupHide -= OnFilterPopupHide;
|
||||||
|
_clyde.OnWindowResized -= ClydeOnOnWindowResized;
|
||||||
|
UnsubFilterItems();
|
||||||
|
UnsubChannelItems();
|
||||||
|
|
||||||
LocalButton = new Button
|
}
|
||||||
|
|
||||||
|
private void UnsubFilterItems()
|
||||||
{
|
{
|
||||||
Text = Loc.GetString("Local"),
|
foreach (var child in _filterVBox.Children)
|
||||||
Name = "Local",
|
|
||||||
ToggleMode = true,
|
|
||||||
};
|
|
||||||
|
|
||||||
OOCButton = new Button
|
|
||||||
{
|
{
|
||||||
Text = Loc.GetString("OOC"),
|
if (child is not ChannelFilterCheckbox checkbox) continue;
|
||||||
Name = "OOC",
|
checkbox.OnToggled -= OnFilterCheckboxToggled;
|
||||||
ToggleMode = true,
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
AdminButton = new Button
|
private void UnsubChannelItems()
|
||||||
{
|
{
|
||||||
Text = Loc.GetString("Admin"),
|
foreach (var child in _channelSelectorHBox.Children)
|
||||||
Name = "Admin",
|
|
||||||
ToggleMode = true,
|
|
||||||
Visible = false
|
|
||||||
};
|
|
||||||
|
|
||||||
DeadButton = new Button
|
|
||||||
{
|
{
|
||||||
Text = Loc.GetString("Dead"),
|
if (child is not ChannelItemButton button) continue;
|
||||||
Name = "Dead",
|
button.OnPressed -= OnChannelSelectorItemPressed;
|
||||||
ToggleMode = true,
|
}
|
||||||
Visible = false
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <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)
|
||||||
|
{
|
||||||
|
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) && SelectedChannel != ChatChannel.Unspecified)
|
||||||
|
{
|
||||||
|
// our previously selected channel no longer exists, default back to OOC, which should always be available
|
||||||
|
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)});
|
||||||
|
}
|
||||||
|
|
||||||
AllButton.OnToggled += OnFilterToggled;
|
/// <summary>
|
||||||
LocalButton.OnToggled += OnFilterToggled;
|
/// Update the unread message counts in the filters based on the provided data.
|
||||||
OOCButton.OnToggled += OnFilterToggled;
|
/// </summary>
|
||||||
AdminButton.OnToggled += OnFilterToggled;
|
/// <param name="unreadMessages">counts for each channel, any values above 9 will show as 9+</param>
|
||||||
DeadButton.OnToggled += OnFilterToggled;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
hBox.AddChild(AllButton);
|
private void OnFilterCheckboxToggled(BaseButton.ButtonToggledEventArgs args)
|
||||||
hBox.AddChild(LocalButton);
|
{
|
||||||
hBox.AddChild(DeadButton);
|
if (args.Button is not ChannelFilterCheckbox checkbox) return;
|
||||||
hBox.AddChild(OOCButton);
|
FilterToggled?.Invoke(checkbox.Channel, checkbox.Pressed);
|
||||||
hBox.AddChild(AdminButton);
|
}
|
||||||
|
|
||||||
AddChild(outerVBox);
|
|
||||||
|
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);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// keep current setting
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void KeyBindDown(GUIBoundKeyEventArgs args)
|
protected override void KeyBindDown(GUIBoundKeyEventArgs args)
|
||||||
{
|
{
|
||||||
base.KeyBindDown(args);
|
base.KeyBindDown(args);
|
||||||
|
|
||||||
if (!args.CanFocus)
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Input.GrabKeyboardFocus();
|
_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();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InputKeyBindDown(GUIBoundKeyEventArgs args)
|
private void InputKeyBindDown(GUIBoundKeyEventArgs args)
|
||||||
@@ -134,11 +505,225 @@ namespace Content.Client.Chat
|
|||||||
args.Handle();
|
args.Handle();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if we temporarily selected another channel via a prefx, undo that when we backspace on an empty input
|
||||||
|
if (Input.Text.Length == 0 && _savedSelectedChannel.HasValue &&
|
||||||
|
args.Function == EngineKeyFunctions.TextBackspace)
|
||||||
|
{
|
||||||
|
SafelySelectChannel(_savedSelectedChannel.Value);
|
||||||
|
_savedSelectedChannel = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public event TextSubmitHandler? TextSubmitted;
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
public event FilterToggledHandler? FilterToggled;
|
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()
|
||||||
|
{
|
||||||
|
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)
|
public void AddLine(string message, ChatChannel channel, Color color)
|
||||||
{
|
{
|
||||||
@@ -154,18 +739,13 @@ namespace Content.Client.Chat
|
|||||||
Contents.AddMessage(formatted);
|
Contents.AddMessage(formatted);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddLine(FormattedMessage message, Color color)
|
private void InputOnFocusExit(LineEdit.LineEditEventArgs obj)
|
||||||
{
|
{
|
||||||
if (Disposed)
|
// undo the temporary selection, otherwise it will be odd if user
|
||||||
{
|
// comes back to it later only to have their selection cleared upon sending
|
||||||
return;
|
if (!_savedSelectedChannel.HasValue) return;
|
||||||
}
|
SafelySelectChannel(_savedSelectedChannel.Value);
|
||||||
|
_savedSelectedChannel = null;
|
||||||
var formatted = new FormattedMessage(3);
|
|
||||||
formatted.PushColor(color);
|
|
||||||
formatted.AddMessage(message);
|
|
||||||
formatted.Pop();
|
|
||||||
Contents.AddMessage(formatted);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Input_OnTextEntered(LineEdit.LineEditEventArgs args)
|
private void Input_OnTextEntered(LineEdit.LineEditEventArgs args)
|
||||||
@@ -175,12 +755,18 @@ namespace Content.Client.Chat
|
|||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(args.Text))
|
if (!string.IsNullOrWhiteSpace(args.Text))
|
||||||
{
|
{
|
||||||
TextSubmitted?.Invoke(this, args.Text);
|
TextSubmitted?.Invoke(this, GetPrefixFromChannel(SelectedChannel)
|
||||||
|
+ args.Text);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ClearOnEnter)
|
if (ClearOnEnter)
|
||||||
{
|
{
|
||||||
Input.Clear();
|
Input.Clear();
|
||||||
|
if (_savedSelectedChannel.HasValue)
|
||||||
|
{
|
||||||
|
SafelySelectChannel(_savedSelectedChannel.Value);
|
||||||
|
_savedSelectedChannel = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ReleaseFocusOnEnter)
|
if (ReleaseFocusOnEnter)
|
||||||
@@ -188,10 +774,160 @@ namespace Content.Client.Chat
|
|||||||
Input.ReleaseKeyboardFocus();
|
Input.ReleaseKeyboardFocus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void OnFilterToggled(BaseButton.ButtonToggledEventArgs args)
|
/// <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
|
||||||
{
|
{
|
||||||
FilterToggled?.Invoke(this, args);
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Content.Client.Administration;
|
using Content.Client.Administration;
|
||||||
using Content.Client.GameObjects.Components.Observer;
|
using Content.Client.GameObjects.Components.Observer;
|
||||||
@@ -22,8 +23,6 @@ namespace Content.Client.Chat
|
|||||||
{
|
{
|
||||||
internal sealed class ChatManager : IChatManager, IPostInjectInit
|
internal sealed class ChatManager : IChatManager, IPostInjectInit
|
||||||
{
|
{
|
||||||
[Dependency] private IPlayerManager _playerManager = default!;
|
|
||||||
|
|
||||||
private struct SpeechBubbleData
|
private struct SpeechBubbleData
|
||||||
{
|
{
|
||||||
public string Message;
|
public string Message;
|
||||||
@@ -55,32 +54,62 @@ namespace Content.Client.Chat
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private int _maxMessageLength = 1000;
|
private int _maxMessageLength = 1000;
|
||||||
|
|
||||||
private const char ConCmdSlash = '/';
|
public const char ConCmdSlash = '/';
|
||||||
private const char OOCAlias = '[';
|
public const char OOCAlias = '[';
|
||||||
private const char MeAlias = '@';
|
public const char MeAlias = '@';
|
||||||
private const char AdminChatAlias = ']';
|
public const char AdminChatAlias = ']';
|
||||||
|
public const char RadioAlias = ';';
|
||||||
|
|
||||||
private readonly List<StoredChatMessage> _filteredHistory = new();
|
private readonly List<StoredChatMessage> _filteredHistory = new();
|
||||||
|
|
||||||
// Filter Button States
|
// currently enabled channel filters set by the user. If an entry is not in this
|
||||||
private bool _allState;
|
// list it has not been explicitly set yet, thus will default to enabled when it first
|
||||||
private bool _localState;
|
// becomes filterable (added to _filterableChannels)
|
||||||
private bool _oocState;
|
// Note that these are persisted here, at the manager,
|
||||||
private bool _adminState;
|
// rather than the chatbox so that these settings persist between instances of different
|
||||||
private bool _deadState;
|
// chatboxes.
|
||||||
|
public readonly Dictionary<ChatChannel, bool> _channelFilters = new();
|
||||||
|
|
||||||
|
// Maintains which channels a client should be able to filter (for showing in the chatbox)
|
||||||
|
// and select (for attempting to send on).
|
||||||
|
// This may not always actually match with what the server will actually allow them to
|
||||||
|
// send / receive on, it is only what the user can select in the UI. For example,
|
||||||
|
// if a user is silenced from speaking for some reason this may still contain ChatChannel.Local, it is left up
|
||||||
|
// to the server to handle invalid attempts to use particular channels and not send messages for
|
||||||
|
// channels the user shouldn't be able to hear.
|
||||||
|
//
|
||||||
|
// Note that Command is an available selection in the chatbox channel selector,
|
||||||
|
// which is not actually a chat channel but is always available.
|
||||||
|
private readonly HashSet<ChatChannel> _filterableChannels = new();
|
||||||
|
private readonly List<ChatChannel> _selectableChannels = new();
|
||||||
|
|
||||||
// Flag Enums for holding filtered channels
|
// Flag Enums for holding filtered channels
|
||||||
private ChatChannel _filteredChannels;
|
private ChatChannel _filteredChannels;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// For currently disabled chat filters,
|
||||||
|
/// unread messages (messages received since the channel has been filtered
|
||||||
|
/// out). Never goes above 10 (9+ should be shown when at 10)
|
||||||
|
/// </summary>
|
||||||
|
private readonly Dictionary<ChatChannel, byte> _unreadMessages = new();
|
||||||
|
|
||||||
|
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||||
[Dependency] private readonly IClientNetManager _netManager = default!;
|
[Dependency] private readonly IClientNetManager _netManager = default!;
|
||||||
[Dependency] private readonly IClientConsoleHost _consoleHost = default!;
|
[Dependency] private readonly IClientConsoleHost _consoleHost = default!;
|
||||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||||
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
|
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
|
||||||
[Dependency] private readonly IClientConGroupController _groupController = default!;
|
|
||||||
[Dependency] private readonly IClientAdminManager _adminMgr = default!;
|
[Dependency] private readonly IClientAdminManager _adminMgr = default!;
|
||||||
|
|
||||||
private ChatBox? _currentChatBox;
|
/// <summary>
|
||||||
|
/// Current chat box control. This can be modified, so do not depend on saving a reference to this.
|
||||||
|
/// </summary>
|
||||||
|
public ChatBox? CurrentChatBox { get; private set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Invoked when CurrentChatBox is resized (including after setting initial default size)
|
||||||
|
/// </summary>
|
||||||
|
public event Action<ChatResizedEventArgs>? OnChatBoxResized;
|
||||||
|
|
||||||
private Control _speechBubbleRoot = null!;
|
private Control _speechBubbleRoot = null!;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -110,6 +139,123 @@ namespace Content.Client.Chat
|
|||||||
_netManager.Connected += RequestMaxLength;
|
_netManager.Connected += RequestMaxLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void PostInject()
|
||||||
|
{
|
||||||
|
_adminMgr.AdminStatusUpdated += UpdateChannelPermissions;
|
||||||
|
_playerManager.LocalPlayerChanged += OnLocalPlayerChanged;
|
||||||
|
OnLocalPlayerChanged(new LocalPlayerChangedEventArgs(null, _playerManager.LocalPlayer));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnLocalPlayerChanged(LocalPlayerChangedEventArgs obj)
|
||||||
|
{
|
||||||
|
if (obj.OldPlayer != null)
|
||||||
|
{
|
||||||
|
obj.OldPlayer.EntityAttached -= OnLocalPlayerEntityAttached;
|
||||||
|
obj.OldPlayer.EntityDetached -= OnLocalPlayerEntityDetached;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj.NewPlayer != null)
|
||||||
|
{
|
||||||
|
obj.NewPlayer.EntityAttached += OnLocalPlayerEntityAttached;
|
||||||
|
obj.NewPlayer.EntityDetached += OnLocalPlayerEntityDetached;
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateChannelPermissions();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnLocalPlayerEntityAttached(EntityAttachedEventArgs obj)
|
||||||
|
{
|
||||||
|
UpdateChannelPermissions();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnLocalPlayerEntityDetached(EntityDetachedEventArgs obj)
|
||||||
|
{
|
||||||
|
UpdateChannelPermissions();
|
||||||
|
}
|
||||||
|
|
||||||
|
// go through all of the various channels and update filter / select permissions
|
||||||
|
// appropriately, also enabling them if our enabledChannels dict doesn't have an entry
|
||||||
|
// for any newly-granted channels
|
||||||
|
private void UpdateChannelPermissions()
|
||||||
|
{
|
||||||
|
// can always send/recieve OOC
|
||||||
|
if (!_selectableChannels.Contains(ChatChannel.OOC))
|
||||||
|
{
|
||||||
|
_selectableChannels.Add(ChatChannel.OOC);
|
||||||
|
}
|
||||||
|
AddFilterableChannel(ChatChannel.OOC);
|
||||||
|
|
||||||
|
// can always hear server (nobody can actually send server messages).
|
||||||
|
AddFilterableChannel(ChatChannel.Server);
|
||||||
|
|
||||||
|
// can always hear local / radio / emote
|
||||||
|
AddFilterableChannel(ChatChannel.Local);
|
||||||
|
AddFilterableChannel(ChatChannel.Radio);
|
||||||
|
AddFilterableChannel(ChatChannel.Emotes);
|
||||||
|
|
||||||
|
// Can only send local / radio / emote when attached to a non-ghost entity.
|
||||||
|
// TODO: this logic is iffy (checking if controlling something that's NOT a ghost), is there a better way to check this?
|
||||||
|
if (!_playerManager.LocalPlayer?.ControlledEntity?.HasComponent<GhostComponent>() ?? false)
|
||||||
|
{
|
||||||
|
_selectableChannels.Add(ChatChannel.Local);
|
||||||
|
_selectableChannels.Add(ChatChannel.Radio);
|
||||||
|
_selectableChannels.Add(ChatChannel.Emotes);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_selectableChannels.Remove(ChatChannel.Local);
|
||||||
|
_selectableChannels.Remove(ChatChannel.Radio);
|
||||||
|
_selectableChannels.Remove(ChatChannel.Emotes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only ghosts and admins can send / see deadchat.
|
||||||
|
// TODO: Should spectators also be able to see deadchat?
|
||||||
|
if (_adminMgr.HasFlag(AdminFlags.Admin) ||
|
||||||
|
(_playerManager?.LocalPlayer?.ControlledEntity?.HasComponent<GhostComponent>() ?? false))
|
||||||
|
{
|
||||||
|
AddFilterableChannel(ChatChannel.Dead);
|
||||||
|
if (!_selectableChannels.Contains(ChatChannel.Dead))
|
||||||
|
{
|
||||||
|
_selectableChannels.Add(ChatChannel.Dead);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_filterableChannels.Remove(ChatChannel.Dead);
|
||||||
|
_selectableChannels.Remove(ChatChannel.Dead);
|
||||||
|
}
|
||||||
|
|
||||||
|
// only admins can see / filter asay
|
||||||
|
if (_adminMgr.HasFlag(AdminFlags.Admin))
|
||||||
|
{
|
||||||
|
AddFilterableChannel(ChatChannel.AdminChat);
|
||||||
|
if (!_selectableChannels.Contains(ChatChannel.AdminChat))
|
||||||
|
{
|
||||||
|
_selectableChannels.Add(ChatChannel.AdminChat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_selectableChannels.Remove(ChatChannel.AdminChat);
|
||||||
|
_filterableChannels.Remove(ChatChannel.AdminChat);
|
||||||
|
}
|
||||||
|
|
||||||
|
// let our chatbox know all the new settings
|
||||||
|
CurrentChatBox?.SetChannelPermissions(_selectableChannels, _filterableChannels, _channelFilters, _unreadMessages);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds the channel to the set of filterable channels, defaulting it as enabled
|
||||||
|
/// if it doesn't currently have an explicit enable/disable setting
|
||||||
|
/// </summary>
|
||||||
|
private void AddFilterableChannel(ChatChannel channel)
|
||||||
|
{
|
||||||
|
if (!_channelFilters.ContainsKey(channel))
|
||||||
|
_channelFilters[channel] = true;
|
||||||
|
_filterableChannels.Add(channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public void FrameUpdate(FrameEventArgs delta)
|
public void FrameUpdate(FrameEventArgs delta)
|
||||||
{
|
{
|
||||||
// Update queued speech bubbles.
|
// Update queued speech bubbles.
|
||||||
@@ -150,29 +296,31 @@ namespace Content.Client.Chat
|
|||||||
|
|
||||||
public void SetChatBox(ChatBox chatBox)
|
public void SetChatBox(ChatBox chatBox)
|
||||||
{
|
{
|
||||||
if (_currentChatBox != null)
|
if (CurrentChatBox != null)
|
||||||
{
|
{
|
||||||
_currentChatBox.TextSubmitted -= OnChatBoxTextSubmitted;
|
CurrentChatBox.TextSubmitted -= OnChatBoxTextSubmitted;
|
||||||
_currentChatBox.FilterToggled -= OnFilterButtonToggled;
|
CurrentChatBox.FilterToggled -= OnFilterButtonToggled;
|
||||||
|
CurrentChatBox.OnResized -= ChatBoxOnResized;
|
||||||
}
|
}
|
||||||
|
|
||||||
_currentChatBox = chatBox;
|
CurrentChatBox = chatBox;
|
||||||
if (_currentChatBox != null)
|
if (CurrentChatBox != null)
|
||||||
{
|
{
|
||||||
_currentChatBox.TextSubmitted += OnChatBoxTextSubmitted;
|
CurrentChatBox.TextSubmitted += OnChatBoxTextSubmitted;
|
||||||
_currentChatBox.FilterToggled += OnFilterButtonToggled;
|
CurrentChatBox.FilterToggled += OnFilterButtonToggled;
|
||||||
|
CurrentChatBox.OnResized += ChatBoxOnResized;
|
||||||
|
|
||||||
_currentChatBox.AllButton.Pressed = !_allState;
|
CurrentChatBox.SetChannelPermissions(_selectableChannels, _filterableChannels, _channelFilters, _unreadMessages);
|
||||||
_currentChatBox.LocalButton.Pressed = !_localState;
|
|
||||||
_currentChatBox.OOCButton.Pressed = !_oocState;
|
|
||||||
_currentChatBox.AdminButton.Pressed = !_adminState;
|
|
||||||
_currentChatBox.DeadButton.Pressed = !_deadState;
|
|
||||||
AdminStatusUpdated();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RepopulateChat(_filteredHistory);
|
RepopulateChat(_filteredHistory);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ChatBoxOnResized(ChatResizedEventArgs chatResizedEventArgs)
|
||||||
|
{
|
||||||
|
OnChatBoxResized?.Invoke(chatResizedEventArgs);
|
||||||
|
}
|
||||||
|
|
||||||
public void RemoveSpeechBubble(EntityUid entityUid, SpeechBubble bubble)
|
public void RemoveSpeechBubble(EntityUid entityUid, SpeechBubble bubble)
|
||||||
{
|
{
|
||||||
bubble.Dispose();
|
bubble.Dispose();
|
||||||
@@ -193,6 +341,15 @@ namespace Content.Client.Chat
|
|||||||
if (IsFiltered(message.Channel))
|
if (IsFiltered(message.Channel))
|
||||||
{
|
{
|
||||||
Logger.Debug($"Message filtered: {message.Channel}: {message.Message}");
|
Logger.Debug($"Message filtered: {message.Channel}: {message.Message}");
|
||||||
|
// accumulate unread
|
||||||
|
if (message.Read) return;
|
||||||
|
if (!_unreadMessages.TryGetValue(message.Channel, out var count))
|
||||||
|
{
|
||||||
|
count = 0;
|
||||||
|
}
|
||||||
|
count = (byte) Math.Min(count + 1, 10);
|
||||||
|
_unreadMessages[message.Channel] = count;
|
||||||
|
CurrentChatBox?.UpdateUnreadMessageCounts(_unreadMessages);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,12 +377,15 @@ namespace Content.Client.Chat
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
_currentChatBox?.AddLine(FormattedMessage.FromMarkup(messageText), color);
|
if (CurrentChatBox == null) return;
|
||||||
|
CurrentChatBox.AddLine(messageText, message.Channel, color);
|
||||||
|
// TODO: Can make this "smarter" later by only setting it false when the message has been scrolled to
|
||||||
|
message.Read = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnChatBoxTextSubmitted(ChatBox chatBox, string text)
|
private void OnChatBoxTextSubmitted(ChatBox chatBox, string text)
|
||||||
{
|
{
|
||||||
DebugTools.Assert(chatBox == _currentChatBox);
|
DebugTools.Assert(chatBox == CurrentChatBox);
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(text))
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
return;
|
return;
|
||||||
@@ -233,12 +393,12 @@ namespace Content.Client.Chat
|
|||||||
// Check if message is longer than the character limit
|
// Check if message is longer than the character limit
|
||||||
if (text.Length > _maxMessageLength)
|
if (text.Length > _maxMessageLength)
|
||||||
{
|
{
|
||||||
if (_currentChatBox != null)
|
if (CurrentChatBox != null)
|
||||||
{
|
{
|
||||||
string locWarning = Loc.GetString("chat-manager-max-message-length",
|
string locWarning = Loc.GetString("chat-manager-max-message-length",
|
||||||
("maxMessageLength", _maxMessageLength));
|
("maxMessageLength", _maxMessageLength));
|
||||||
_currentChatBox.AddLine(locWarning, ChatChannel.Server, Color.Orange);
|
CurrentChatBox.AddLine(locWarning, ChatChannel.Server, Color.Orange);
|
||||||
_currentChatBox.ClearOnEnter = false; // The text shouldn't be cleared if it hasn't been sent
|
CurrentChatBox.ClearOnEnter = false; // The text shouldn't be cleared if it hasn't been sent
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -265,7 +425,7 @@ namespace Content.Client.Chat
|
|||||||
var conInput = text.Substring(1);
|
var conInput = text.Substring(1);
|
||||||
if (string.IsNullOrWhiteSpace(conInput))
|
if (string.IsNullOrWhiteSpace(conInput))
|
||||||
return;
|
return;
|
||||||
if (_groupController.CanCommand("asay"))
|
if (_adminMgr.HasFlag(AdminFlags.Admin))
|
||||||
{
|
{
|
||||||
_consoleHost.ExecuteCommand($"asay \"{CommandParsing.Escape(conInput)}\"");
|
_consoleHost.ExecuteCommand($"asay \"{CommandParsing.Escape(conInput)}\"");
|
||||||
}
|
}
|
||||||
@@ -286,8 +446,8 @@ namespace Content.Client.Chat
|
|||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
{
|
{
|
||||||
var conInput = _currentChatBox?.DefaultChatFormat != null
|
var conInput = CurrentChatBox?.DefaultChatFormat != null
|
||||||
? string.Format(_currentChatBox.DefaultChatFormat, CommandParsing.Escape(text))
|
? string.Format(CurrentChatBox.DefaultChatFormat, CommandParsing.Escape(text))
|
||||||
: text;
|
: text;
|
||||||
_consoleHost.ExecuteCommand(conInput);
|
_consoleHost.ExecuteCommand(conInput);
|
||||||
break;
|
break;
|
||||||
@@ -295,63 +455,19 @@ namespace Content.Client.Chat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnFilterButtonToggled(ChatBox chatBox, BaseButton.ButtonToggledEventArgs e)
|
private void OnFilterButtonToggled(ChatChannel channel, bool enabled)
|
||||||
{
|
{
|
||||||
switch (e.Button.Name)
|
if (enabled)
|
||||||
{
|
{
|
||||||
case "Local":
|
_channelFilters[channel] = true;
|
||||||
_localState = !_localState;
|
_filteredChannels &= ~channel;
|
||||||
if (_localState)
|
_unreadMessages.Remove(channel);
|
||||||
{
|
CurrentChatBox?.UpdateUnreadMessageCounts(_unreadMessages);
|
||||||
_filteredChannels |= ChatChannel.Local;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_filteredChannels &= ~ChatChannel.Local;
|
_channelFilters[channel] = false;
|
||||||
break;
|
_filteredChannels |= channel;
|
||||||
}
|
|
||||||
|
|
||||||
case "OOC":
|
|
||||||
_oocState = !_oocState;
|
|
||||||
if (_oocState)
|
|
||||||
{
|
|
||||||
_filteredChannels |= ChatChannel.OOC;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_filteredChannels &= ~ChatChannel.OOC;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "Admin":
|
|
||||||
_adminState = !_adminState;
|
|
||||||
if (_adminState)
|
|
||||||
{
|
|
||||||
_filteredChannels |= ChatChannel.AdminChat;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_filteredChannels &= ~ChatChannel.AdminChat;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "Dead":
|
|
||||||
_deadState = !_deadState;
|
|
||||||
if (_deadState)
|
|
||||||
_filteredChannels |= ChatChannel.Dead;
|
|
||||||
else
|
|
||||||
_filteredChannels &= ~ChatChannel.Dead;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "ALL":
|
|
||||||
chatBox.LocalButton.Pressed ^= true;
|
|
||||||
chatBox.OOCButton.Pressed ^= true;
|
|
||||||
if (chatBox.AdminButton != null)
|
|
||||||
chatBox.AdminButton.Pressed ^= true;
|
|
||||||
chatBox.DeadButton.Pressed ^= true;
|
|
||||||
_allState = !_allState;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RepopulateChat(_filteredHistory);
|
RepopulateChat(_filteredHistory);
|
||||||
@@ -359,12 +475,12 @@ namespace Content.Client.Chat
|
|||||||
|
|
||||||
private void RepopulateChat(IEnumerable<StoredChatMessage> filteredMessages)
|
private void RepopulateChat(IEnumerable<StoredChatMessage> filteredMessages)
|
||||||
{
|
{
|
||||||
if (_currentChatBox == null)
|
if (CurrentChatBox == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_currentChatBox.Contents.Clear();
|
CurrentChatBox.Contents.Clear();
|
||||||
|
|
||||||
foreach (var msg in filteredMessages)
|
foreach (var msg in filteredMessages)
|
||||||
{
|
{
|
||||||
@@ -522,33 +638,7 @@ namespace Content.Client.Chat
|
|||||||
|
|
||||||
private bool IsFiltered(ChatChannel channel)
|
private bool IsFiltered(ChatChannel channel)
|
||||||
{
|
{
|
||||||
// _allState works as inverter.
|
return _filteredChannels.HasFlag(channel);
|
||||||
return _allState ^ _filteredChannels.HasFlag(channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
void IPostInjectInit.PostInject()
|
|
||||||
{
|
|
||||||
_adminMgr.AdminStatusUpdated += AdminStatusUpdated;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AdminStatusUpdated()
|
|
||||||
{
|
|
||||||
if (_currentChatBox != null)
|
|
||||||
{
|
|
||||||
_currentChatBox.AdminButton.Visible = _adminMgr.HasFlag(AdminFlags.Admin);
|
|
||||||
_currentChatBox.DeadButton.Visible = _adminMgr.HasFlag(AdminFlags.Admin);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ToggleDeadChatButtonVisibility(bool visibility)
|
|
||||||
{
|
|
||||||
if (_currentChatBox != null)
|
|
||||||
{
|
|
||||||
// If the user is an admin and returned to body, don't set the flag as null
|
|
||||||
if (!visibility && _adminMgr.HasFlag(AdminFlags.Admin))
|
|
||||||
return;
|
|
||||||
_currentChatBox.DeadButton.Visible = visibility;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class SpeechBubbleQueueData
|
private sealed class SpeechBubbleQueueData
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ namespace Content.Client.Chat
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public Color MessageColorOverride { get; set; }
|
public Color MessageColorOverride { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the user has read this message at least once.
|
||||||
|
/// </summary>
|
||||||
|
public bool Read { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Constructor to copy a net message into stored client variety
|
/// Constructor to copy a net message into stored client variety
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -83,7 +83,6 @@ namespace Content.Client.GameObjects.Components.Observer
|
|||||||
_gameHud.HandsContainer.AddChild(_gui);
|
_gameHud.HandsContainer.AddChild(_gui);
|
||||||
SetGhostVisibility(true);
|
SetGhostVisibility(true);
|
||||||
_isAttached = true;
|
_isAttached = true;
|
||||||
_chatManager.ToggleDeadChatButtonVisibility(true);
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -91,7 +90,6 @@ namespace Content.Client.GameObjects.Components.Observer
|
|||||||
_gui!.Parent?.RemoveChild(_gui);
|
_gui!.Parent?.RemoveChild(_gui);
|
||||||
SetGhostVisibility(false);
|
SetGhostVisibility(false);
|
||||||
_isAttached = false;
|
_isAttached = false;
|
||||||
_chatManager.ToggleDeadChatButtonVisibility(false);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,12 @@ namespace Content.Client.Input
|
|||||||
{
|
{
|
||||||
var common = contexts.GetContext("common");
|
var common = contexts.GetContext("common");
|
||||||
common.AddFunction(ContentKeyFunctions.FocusChat);
|
common.AddFunction(ContentKeyFunctions.FocusChat);
|
||||||
|
common.AddFunction(ContentKeyFunctions.FocusLocalChat);
|
||||||
|
common.AddFunction(ContentKeyFunctions.FocusRadio);
|
||||||
common.AddFunction(ContentKeyFunctions.FocusOOC);
|
common.AddFunction(ContentKeyFunctions.FocusOOC);
|
||||||
common.AddFunction(ContentKeyFunctions.FocusAdminChat);
|
common.AddFunction(ContentKeyFunctions.FocusAdminChat);
|
||||||
|
common.AddFunction(ContentKeyFunctions.CycleChatChannelForward);
|
||||||
|
common.AddFunction(ContentKeyFunctions.CycleChatChannelBackward);
|
||||||
common.AddFunction(ContentKeyFunctions.ExamineEntity);
|
common.AddFunction(ContentKeyFunctions.ExamineEntity);
|
||||||
common.AddFunction(ContentKeyFunctions.OpenInfo);
|
common.AddFunction(ContentKeyFunctions.OpenInfo);
|
||||||
common.AddFunction(ContentKeyFunctions.TakeScreenshot);
|
common.AddFunction(ContentKeyFunctions.TakeScreenshot);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System;
|
||||||
using Content.Client.Chat;
|
using Content.Client.Chat;
|
||||||
using Robust.Shared.GameObjects;
|
using Robust.Shared.GameObjects;
|
||||||
using Robust.Shared.Timing;
|
using Robust.Shared.Timing;
|
||||||
@@ -14,6 +15,14 @@ namespace Content.Client.Interfaces.Chat
|
|||||||
|
|
||||||
void RemoveSpeechBubble(EntityUid entityUid, SpeechBubble bubble);
|
void RemoveSpeechBubble(EntityUid entityUid, SpeechBubble bubble);
|
||||||
|
|
||||||
void ToggleDeadChatButtonVisibility(bool visibility);
|
/// <summary>
|
||||||
|
/// Current chat box control. This can be modified, so do not depend on saving a reference to this.
|
||||||
|
/// </summary>
|
||||||
|
ChatBox? CurrentChatBox { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invoked when CurrentChatBox is resized (including after setting initial default size)
|
||||||
|
/// </summary>
|
||||||
|
event Action<ChatResizedEventArgs>? OnChatBoxResized;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Linq;
|
||||||
using Content.Client.Administration;
|
using Content.Client.Administration;
|
||||||
using Content.Client.Chat;
|
using Content.Client.Chat;
|
||||||
using Content.Client.Construction;
|
using Content.Client.Construction;
|
||||||
@@ -5,6 +6,7 @@ using Content.Client.Interfaces.Chat;
|
|||||||
using Content.Client.UserInterface;
|
using Content.Client.UserInterface;
|
||||||
using Content.Client.Voting;
|
using Content.Client.Voting;
|
||||||
using Content.Shared;
|
using Content.Shared;
|
||||||
|
using Content.Shared.Chat;
|
||||||
using Content.Shared.Input;
|
using Content.Shared.Input;
|
||||||
using Robust.Client.Graphics;
|
using Robust.Client.Graphics;
|
||||||
using Robust.Client.Input;
|
using Robust.Client.Input;
|
||||||
@@ -37,9 +39,6 @@ namespace Content.Client.State
|
|||||||
[ViewVariables] private ChatBox? _gameChat;
|
[ViewVariables] private ChatBox? _gameChat;
|
||||||
private ConstructionMenuPresenter? _constructionMenu;
|
private ConstructionMenuPresenter? _constructionMenu;
|
||||||
|
|
||||||
private bool _oocEnabled;
|
|
||||||
private bool _adminOocEnabled;
|
|
||||||
|
|
||||||
public MainViewport Viewport { get; private set; } = default!;
|
public MainViewport Viewport { get; private set; } = default!;
|
||||||
|
|
||||||
public override void Startup()
|
public override void Startup()
|
||||||
@@ -59,29 +58,31 @@ namespace Content.Client.State
|
|||||||
LayoutContainer.SetAnchorPreset(Viewport, LayoutContainer.LayoutPreset.Wide);
|
LayoutContainer.SetAnchorPreset(Viewport, LayoutContainer.LayoutPreset.Wide);
|
||||||
Viewport.SetPositionFirst();
|
Viewport.SetPositionFirst();
|
||||||
|
|
||||||
_userInterfaceManager.StateRoot.AddChild(_gameChat);
|
|
||||||
LayoutContainer.SetAnchorAndMarginPreset(_gameChat, LayoutContainer.LayoutPreset.TopRight, margin: 10);
|
|
||||||
LayoutContainer.SetMarginLeft(_gameChat, -475);
|
|
||||||
LayoutContainer.SetMarginBottom(_gameChat, 235);
|
|
||||||
|
|
||||||
_userInterfaceManager.StateRoot.AddChild(_gameHud.RootControl);
|
_userInterfaceManager.StateRoot.AddChild(_gameHud.RootControl);
|
||||||
_chatManager.SetChatBox(_gameChat);
|
_chatManager.SetChatBox(_gameChat);
|
||||||
_voteManager.SetPopupContainer(_gameHud.VoteContainer);
|
_voteManager.SetPopupContainer(_gameHud.VoteContainer);
|
||||||
_gameChat.DefaultChatFormat = "say \"{0}\"";
|
_gameChat.DefaultChatFormat = "say \"{0}\"";
|
||||||
_gameChat.Input.PlaceHolder = Loc.GetString("Say something! [ for OOC");
|
|
||||||
|
|
||||||
_inputManager.SetInputCommand(ContentKeyFunctions.FocusChat,
|
_inputManager.SetInputCommand(ContentKeyFunctions.FocusChat,
|
||||||
InputCmdHandler.FromDelegate(_ => FocusChat(_gameChat)));
|
InputCmdHandler.FromDelegate(_ => FocusChat(_gameChat)));
|
||||||
|
|
||||||
_inputManager.SetInputCommand(ContentKeyFunctions.FocusOOC,
|
_inputManager.SetInputCommand(ContentKeyFunctions.FocusOOC,
|
||||||
InputCmdHandler.FromDelegate(_ => FocusOOC(_gameChat)));
|
InputCmdHandler.FromDelegate(_ => FocusChannel(_gameChat, ChatChannel.OOC)));
|
||||||
|
|
||||||
|
_inputManager.SetInputCommand(ContentKeyFunctions.FocusLocalChat,
|
||||||
|
InputCmdHandler.FromDelegate(_ => FocusChannel(_gameChat, ChatChannel.Local)));
|
||||||
|
|
||||||
|
_inputManager.SetInputCommand(ContentKeyFunctions.FocusRadio,
|
||||||
|
InputCmdHandler.FromDelegate(_ => FocusChannel(_gameChat, ChatChannel.Radio)));
|
||||||
|
|
||||||
_inputManager.SetInputCommand(ContentKeyFunctions.FocusAdminChat,
|
_inputManager.SetInputCommand(ContentKeyFunctions.FocusAdminChat,
|
||||||
InputCmdHandler.FromDelegate(_ => FocusAdminChat(_gameChat)));
|
InputCmdHandler.FromDelegate(_ => FocusChannel(_gameChat, ChatChannel.AdminChat)));
|
||||||
|
|
||||||
_configurationManager.OnValueChanged(CCVars.OocEnabled, OnOocEnabledChanged, true);
|
_inputManager.SetInputCommand(ContentKeyFunctions.CycleChatChannelForward,
|
||||||
_configurationManager.OnValueChanged(CCVars.AdminOocEnabled, OnAdminOocEnabledChanged, true);
|
InputCmdHandler.FromDelegate(_ => CycleChatChannel(_gameChat, true)));
|
||||||
_adminManager.AdminStatusUpdated += OnAdminStatusUpdated;
|
|
||||||
|
_inputManager.SetInputCommand(ContentKeyFunctions.CycleChatChannelBackward,
|
||||||
|
InputCmdHandler.FromDelegate(_ => CycleChatChannel(_gameChat, false)));
|
||||||
|
|
||||||
SetupPresenters();
|
SetupPresenters();
|
||||||
|
|
||||||
@@ -118,50 +119,9 @@ namespace Content.Client.State
|
|||||||
_constructionMenu?.Dispose();
|
_constructionMenu?.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void OnOocEnabledChanged(bool val)
|
|
||||||
{
|
|
||||||
_oocEnabled = val;
|
|
||||||
|
|
||||||
if (_adminManager.IsActive())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(_gameChat is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_gameChat.Input.PlaceHolder = Loc.GetString(_oocEnabled ? "Say something! [ for OOC" : "Say something!");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnAdminOocEnabledChanged(bool val)
|
|
||||||
{
|
|
||||||
_adminOocEnabled = val;
|
|
||||||
|
|
||||||
if (!_adminManager.IsActive())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_gameChat is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_gameChat.Input.PlaceHolder = Loc.GetString(_adminOocEnabled ? "Say something! [ for OOC" : "Say something!");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnAdminStatusUpdated()
|
|
||||||
{
|
|
||||||
if (_gameChat is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_gameChat.Input.PlaceHolder = _adminManager.IsActive()
|
|
||||||
? Loc.GetString(_adminOocEnabled ? "Say something! [ for OOC" : "Say something!")
|
|
||||||
: Loc.GetString(_oocEnabled ? "Say something! [ for OOC" : "Say something!");
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static void FocusChat(ChatBox chat)
|
internal static void FocusChat(ChatBox chat)
|
||||||
{
|
{
|
||||||
if (chat == null || chat.UserInterfaceManager.KeyboardFocused != null)
|
if (chat.UserInterfaceManager.KeyboardFocused != null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -169,28 +129,34 @@ namespace Content.Client.State
|
|||||||
chat.Input.IgnoreNext = true;
|
chat.Input.IgnoreNext = true;
|
||||||
chat.Input.GrabKeyboardFocus();
|
chat.Input.GrabKeyboardFocus();
|
||||||
}
|
}
|
||||||
internal static void FocusOOC(ChatBox chat)
|
internal static void FocusChannel(ChatBox chat, ChatChannel channel)
|
||||||
{
|
{
|
||||||
if (chat == null || chat.UserInterfaceManager.KeyboardFocused != null)
|
if (chat.UserInterfaceManager.KeyboardFocused != null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
chat.Input.IgnoreNext = true;
|
chat.Input.IgnoreNext = true;
|
||||||
chat.Input.GrabKeyboardFocus();
|
chat.SelectChannel(channel);
|
||||||
chat.Input.InsertAtCursor("[");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static void FocusAdminChat(ChatBox chat)
|
internal static void CycleChatChannel(ChatBox chat, bool forward)
|
||||||
{
|
{
|
||||||
if (chat == null || chat.UserInterfaceManager.KeyboardFocused != null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
chat.Input.IgnoreNext = true;
|
chat.Input.IgnoreNext = true;
|
||||||
chat.Input.GrabKeyboardFocus();
|
var channels = chat.SelectableChannels;
|
||||||
chat.Input.InsertAtCursor("]");
|
var idx = channels.IndexOf(chat.SelectedChannel);
|
||||||
|
if (forward)
|
||||||
|
{
|
||||||
|
idx++;
|
||||||
|
idx = MathHelper.Mod(idx, channels.Count());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
idx--;
|
||||||
|
idx = MathHelper.Mod(idx, channels.Count());
|
||||||
|
}
|
||||||
|
|
||||||
|
chat.SelectChannel(channels[idx]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void FrameUpdate(FrameEventArgs e)
|
public override void FrameUpdate(FrameEventArgs e)
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Content.Client.Interfaces;
|
using Content.Client.Interfaces;
|
||||||
using Content.Client.Interfaces.Chat;
|
using Content.Client.Interfaces.Chat;
|
||||||
using Content.Client.UserInterface;
|
using Content.Client.UserInterface;
|
||||||
using Content.Client.Voting;
|
using Content.Client.Voting;
|
||||||
|
using Content.Shared.Chat;
|
||||||
using Content.Shared.Input;
|
using Content.Shared.Input;
|
||||||
using Robust.Client;
|
using Robust.Client;
|
||||||
using Robust.Client.Console;
|
using Robust.Client.Console;
|
||||||
@@ -76,10 +77,16 @@ namespace Content.Client.State
|
|||||||
InputCmdHandler.FromDelegate(_ => GameScreen.FocusChat(_lobby.Chat)));
|
InputCmdHandler.FromDelegate(_ => GameScreen.FocusChat(_lobby.Chat)));
|
||||||
|
|
||||||
_inputManager.SetInputCommand(ContentKeyFunctions.FocusOOC,
|
_inputManager.SetInputCommand(ContentKeyFunctions.FocusOOC,
|
||||||
InputCmdHandler.FromDelegate(_ => GameScreen.FocusOOC(_lobby.Chat)));
|
InputCmdHandler.FromDelegate(_ => GameScreen.FocusChannel(_lobby.Chat, ChatChannel.OOC)));
|
||||||
|
|
||||||
_inputManager.SetInputCommand(ContentKeyFunctions.FocusAdminChat,
|
_inputManager.SetInputCommand(ContentKeyFunctions.FocusAdminChat,
|
||||||
InputCmdHandler.FromDelegate(_ => GameScreen.FocusAdminChat(_lobby.Chat)));
|
InputCmdHandler.FromDelegate(_ => GameScreen.FocusChannel(_lobby.Chat, ChatChannel.AdminChat)));
|
||||||
|
|
||||||
|
_inputManager.SetInputCommand(ContentKeyFunctions.CycleChatChannelForward,
|
||||||
|
InputCmdHandler.FromDelegate(_ => GameScreen.CycleChatChannel(_lobby.Chat, true)));
|
||||||
|
|
||||||
|
_inputManager.SetInputCommand(ContentKeyFunctions.CycleChatChannelBackward,
|
||||||
|
InputCmdHandler.FromDelegate(_ => GameScreen.CycleChatChannel(_lobby.Chat, false)));
|
||||||
|
|
||||||
UpdateLobbyUi();
|
UpdateLobbyUi();
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
using Content.Client.UserInterface.Stylesheets;
|
using Content.Client.Chat;
|
||||||
|
using Content.Client.Interfaces.Chat;
|
||||||
|
using Content.Client.UserInterface.Stylesheets;
|
||||||
using Robust.Client.UserInterface;
|
using Robust.Client.UserInterface;
|
||||||
using Robust.Client.UserInterface.Controls;
|
using Robust.Client.UserInterface.Controls;
|
||||||
|
using Robust.Shared.IoC;
|
||||||
|
using Robust.Shared.Maths;
|
||||||
|
|
||||||
namespace Content.Client.UserInterface
|
namespace Content.Client.UserInterface
|
||||||
{
|
{
|
||||||
@@ -9,6 +13,7 @@ namespace Content.Client.UserInterface
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class AlertsUI : Control
|
public sealed class AlertsUI : Control
|
||||||
{
|
{
|
||||||
|
public const float ChatSeparation = 38f;
|
||||||
public GridContainer Grid { get; }
|
public GridContainer Grid { get; }
|
||||||
|
|
||||||
public AlertsUI()
|
public AlertsUI()
|
||||||
@@ -39,6 +44,36 @@ namespace Content.Client.UserInterface
|
|||||||
MinSize = (64, 64);
|
MinSize = (64, 64);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void EnteredTree()
|
||||||
|
{
|
||||||
|
base.EnteredTree();
|
||||||
|
var _chatManager = IoCManager.Resolve<IChatManager>();
|
||||||
|
_chatManager.OnChatBoxResized += OnChatResized;
|
||||||
|
OnChatResized(new ChatResizedEventArgs(ChatBox.InitialChatBottom));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void ExitedTree()
|
||||||
|
{
|
||||||
|
base.ExitedTree();
|
||||||
|
var _chatManager = IoCManager.Resolve<IChatManager>();
|
||||||
|
_chatManager.OnChatBoxResized -= OnChatResized;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void OnChatResized(ChatResizedEventArgs chatResizedEventArgs)
|
||||||
|
{
|
||||||
|
// resize us to fit just below the chatbox
|
||||||
|
var _chatManager = IoCManager.Resolve<IChatManager>();
|
||||||
|
if (_chatManager.CurrentChatBox != null)
|
||||||
|
{
|
||||||
|
LayoutContainer.SetMarginTop(this, chatResizedEventArgs.NewBottom + ChatSeparation);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LayoutContainer.SetMarginTop(this, 250);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// This makes no sense but I'm leaving it in place in case I break anything by removing it.
|
// This makes no sense but I'm leaving it in place in case I break anything by removing it.
|
||||||
|
|
||||||
protected override void Resized()
|
protected override void Resized()
|
||||||
|
|||||||
@@ -141,8 +141,12 @@ namespace Content.Client.UserInterface
|
|||||||
|
|
||||||
AddHeader("ui-options-header-ui");
|
AddHeader("ui-options-header-ui");
|
||||||
AddButton(ContentKeyFunctions.FocusChat);
|
AddButton(ContentKeyFunctions.FocusChat);
|
||||||
|
AddButton(ContentKeyFunctions.FocusLocalChat);
|
||||||
|
AddButton(ContentKeyFunctions.FocusRadio);
|
||||||
AddButton(ContentKeyFunctions.FocusOOC);
|
AddButton(ContentKeyFunctions.FocusOOC);
|
||||||
AddButton(ContentKeyFunctions.FocusAdminChat);
|
AddButton(ContentKeyFunctions.FocusAdminChat);
|
||||||
|
AddButton(ContentKeyFunctions.CycleChatChannelForward);
|
||||||
|
AddButton(ContentKeyFunctions.CycleChatChannelBackward);
|
||||||
AddButton(ContentKeyFunctions.OpenCharacterMenu);
|
AddButton(ContentKeyFunctions.OpenCharacterMenu);
|
||||||
AddButton(ContentKeyFunctions.OpenContextMenu);
|
AddButton(ContentKeyFunctions.OpenContextMenu);
|
||||||
AddButton(ContentKeyFunctions.OpenCraftingMenu);
|
AddButton(ContentKeyFunctions.OpenCraftingMenu);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Content.Client.Chat;
|
||||||
using Content.Client.GameObjects.EntitySystems;
|
using Content.Client.GameObjects.EntitySystems;
|
||||||
using Content.Client.UserInterface.Controls;
|
using Content.Client.UserInterface.Controls;
|
||||||
using Content.Client.Utility;
|
using Content.Client.Utility;
|
||||||
@@ -18,6 +19,7 @@ namespace Content.Client.UserInterface.Stylesheets
|
|||||||
public const string StyleClassBorderedWindowPanel = "BorderedWindowPanel";
|
public const string StyleClassBorderedWindowPanel = "BorderedWindowPanel";
|
||||||
public const string StyleClassInventorySlotBackground = "InventorySlotBackground";
|
public const string StyleClassInventorySlotBackground = "InventorySlotBackground";
|
||||||
public const string StyleClassHandSlotHighlight = "HandSlotHighlight";
|
public const string StyleClassHandSlotHighlight = "HandSlotHighlight";
|
||||||
|
public const string StyleClassChatSubPanel = "ChatSubPanel";
|
||||||
public const string StyleClassTransparentBorderedWindowPanel = "TransparentBorderedWindowPanel";
|
public const string StyleClassTransparentBorderedWindowPanel = "TransparentBorderedWindowPanel";
|
||||||
public const string StyleClassHotbarPanel = "HotbarPanel";
|
public const string StyleClassHotbarPanel = "HotbarPanel";
|
||||||
public const string StyleClassTooltipPanel = "tooltipBox";
|
public const string StyleClassTooltipPanel = "tooltipBox";
|
||||||
@@ -31,6 +33,9 @@ namespace Content.Client.UserInterface.Stylesheets
|
|||||||
public const string StyleClassHotbarSlotNumber = "hotbarSlotNumber";
|
public const string StyleClassHotbarSlotNumber = "hotbarSlotNumber";
|
||||||
public const string StyleClassActionSearchBox = "actionSearchBox";
|
public const string StyleClassActionSearchBox = "actionSearchBox";
|
||||||
public const string StyleClassActionMenuItemRevoked = "actionMenuItemRevoked";
|
public const string StyleClassActionMenuItemRevoked = "actionMenuItemRevoked";
|
||||||
|
public const string StyleClassChatLineEdit = "chatLineEdit";
|
||||||
|
public const string StyleClassChatChannelSelectorButton = "chatSelectorOptionButton";
|
||||||
|
public const string StyleClassChatFilterOptionButton = "chatFilterOptionButton";
|
||||||
public const string StyleClassContextMenuCount = "contextMenuCount";
|
public const string StyleClassContextMenuCount = "contextMenuCount";
|
||||||
|
|
||||||
public const string StyleClassSliderRed = "Red";
|
public const string StyleClassSliderRed = "Red";
|
||||||
@@ -208,6 +213,22 @@ namespace Content.Client.UserInterface.Stylesheets
|
|||||||
};
|
};
|
||||||
topButtonSquare.SetPatchMargin(StyleBox.Margin.Horizontal, 0);
|
topButtonSquare.SetPatchMargin(StyleBox.Margin.Horizontal, 0);
|
||||||
|
|
||||||
|
var chatChannelButtonTex = resCache.GetTexture("/Textures/Interface/Nano/rounded_button.svg.96dpi.png");
|
||||||
|
var chatChannelButton = new StyleBoxTexture
|
||||||
|
{
|
||||||
|
Texture = chatChannelButtonTex,
|
||||||
|
};
|
||||||
|
chatChannelButton.SetPatchMargin(StyleBox.Margin.All, 5);
|
||||||
|
chatChannelButton.SetPadding(StyleBox.Margin.All, 2);
|
||||||
|
|
||||||
|
var chatFilterButtonTex = resCache.GetTexture("/Textures/Interface/Nano/rounded_button_bordered.svg.96dpi.png");
|
||||||
|
var chatFilterButton = new StyleBoxTexture
|
||||||
|
{
|
||||||
|
Texture = chatFilterButtonTex,
|
||||||
|
};
|
||||||
|
chatFilterButton.SetPatchMargin(StyleBox.Margin.All, 5);
|
||||||
|
chatFilterButton.SetPadding(StyleBox.Margin.All, 2);
|
||||||
|
|
||||||
var textureInvertedTriangle = resCache.GetTexture("/Textures/Interface/Nano/inverted_triangle.svg.png");
|
var textureInvertedTriangle = resCache.GetTexture("/Textures/Interface/Nano/inverted_triangle.svg.png");
|
||||||
|
|
||||||
var lineEditTex = resCache.GetTexture("/Textures/Interface/Nano/lineedit.png");
|
var lineEditTex = resCache.GetTexture("/Textures/Interface/Nano/lineedit.png");
|
||||||
@@ -218,6 +239,13 @@ namespace Content.Client.UserInterface.Stylesheets
|
|||||||
lineEdit.SetPatchMargin(StyleBox.Margin.All, 3);
|
lineEdit.SetPatchMargin(StyleBox.Margin.All, 3);
|
||||||
lineEdit.SetContentMarginOverride(StyleBox.Margin.Horizontal, 5);
|
lineEdit.SetContentMarginOverride(StyleBox.Margin.Horizontal, 5);
|
||||||
|
|
||||||
|
var chatSubBGTex = resCache.GetTexture("/Textures/Interface/Nano/chat_sub_background.png");
|
||||||
|
var chatSubBG = new StyleBoxTexture
|
||||||
|
{
|
||||||
|
Texture = chatSubBGTex,
|
||||||
|
};
|
||||||
|
chatSubBG.SetPatchMargin(StyleBox.Margin.All, 2);
|
||||||
|
|
||||||
var actionSearchBoxTex = resCache.GetTexture("/Textures/Interface/Nano/black_panel_dark_thin_border.png");
|
var actionSearchBoxTex = resCache.GetTexture("/Textures/Interface/Nano/black_panel_dark_thin_border.png");
|
||||||
var actionSearchBox = new StyleBoxTexture
|
var actionSearchBox = new StyleBoxTexture
|
||||||
{
|
{
|
||||||
@@ -540,6 +568,20 @@ namespace Content.Client.UserInterface.Stylesheets
|
|||||||
{
|
{
|
||||||
new StyleProperty("font-color", Color.Gray),
|
new StyleProperty("font-color", Color.Gray),
|
||||||
}),
|
}),
|
||||||
|
// Chat lineedit - we don't actually draw a stylebox around the lineedit itself, we put it around the
|
||||||
|
// input + other buttons, so we must clear the default stylebox
|
||||||
|
new StyleRule(new SelectorElement(typeof(LineEdit), new[] {StyleClassChatLineEdit}, null, null),
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
new StyleProperty(LineEdit.StylePropertyStyleBox, new StyleBoxEmpty()),
|
||||||
|
}),
|
||||||
|
|
||||||
|
// chat subpanels (chat lineedit backing, popup backings)
|
||||||
|
new StyleRule(new SelectorElement(typeof(PanelContainer), new[] {StyleClassChatSubPanel}, null, null),
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
new StyleProperty(PanelContainer.StylePropertyPanel, chatSubBG),
|
||||||
|
}),
|
||||||
|
|
||||||
// Action searchbox lineedit
|
// Action searchbox lineedit
|
||||||
new StyleRule(new SelectorElement(typeof(LineEdit), new[] {StyleClassActionSearchBox}, null, null),
|
new StyleRule(new SelectorElement(typeof(LineEdit), new[] {StyleClassActionSearchBox}, null, null),
|
||||||
@@ -931,6 +973,33 @@ namespace Content.Client.UserInterface.Stylesheets
|
|||||||
new StyleProperty(Slider.StylePropertyFill, sliderFillBlue),
|
new StyleProperty(Slider.StylePropertyFill, sliderFillBlue),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
// chat channel option selector
|
||||||
|
new StyleRule(new SelectorElement(typeof(Button), new[] {StyleClassChatChannelSelectorButton}, null, null), new[]
|
||||||
|
{
|
||||||
|
new StyleProperty(Button.StylePropertyStyleBox, chatChannelButton),
|
||||||
|
}),
|
||||||
|
// chat filter button
|
||||||
|
new StyleRule(new SelectorElement(typeof(ContainerButton), new[] {StyleClassChatFilterOptionButton}, null, null), new[]
|
||||||
|
{
|
||||||
|
new StyleProperty(ContainerButton.StylePropertyStyleBox, chatFilterButton),
|
||||||
|
}),
|
||||||
|
new StyleRule(new SelectorElement(typeof(ContainerButton), new[] {StyleClassChatFilterOptionButton}, null, new[] {ContainerButton.StylePseudoClassNormal}), new[]
|
||||||
|
{
|
||||||
|
new StyleProperty(Control.StylePropertyModulateSelf, ButtonColorDefault),
|
||||||
|
}),
|
||||||
|
new StyleRule(new SelectorElement(typeof(ContainerButton), new[] {StyleClassChatFilterOptionButton}, null, new[] {ContainerButton.StylePseudoClassHover}), new[]
|
||||||
|
{
|
||||||
|
new StyleProperty(Control.StylePropertyModulateSelf, ButtonColorHovered),
|
||||||
|
}),
|
||||||
|
new StyleRule(new SelectorElement(typeof(ContainerButton), new[] {StyleClassChatFilterOptionButton}, null, new[] {ContainerButton.StylePseudoClassPressed}), new[]
|
||||||
|
{
|
||||||
|
new StyleProperty(Control.StylePropertyModulateSelf, ButtonColorPressed),
|
||||||
|
}),
|
||||||
|
new StyleRule(new SelectorElement(typeof(ContainerButton), new[] {StyleClassChatFilterOptionButton}, null, new[] {ContainerButton.StylePseudoClassDisabled}), new[]
|
||||||
|
{
|
||||||
|
new StyleProperty(Control.StylePropertyModulateSelf, ButtonColorDisabled),
|
||||||
|
}),
|
||||||
|
|
||||||
// OptionButton
|
// OptionButton
|
||||||
new StyleRule(new SelectorElement(typeof(OptionButton), null, null, null), new[]
|
new StyleRule(new SelectorElement(typeof(OptionButton), null, null, null), new[]
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -66,7 +66,8 @@ namespace Content.Server.GameObjects.Components.Headset
|
|||||||
|
|
||||||
msg.Channel = ChatChannel.Radio;
|
msg.Channel = ChatChannel.Radio;
|
||||||
msg.Message = message;
|
msg.Message = message;
|
||||||
msg.MessageWrap = Loc.GetString("chat-radio-message-wrap", ("channel", channel), ("name", source.Name));
|
//Square brackets are added here to avoid issues with escaping
|
||||||
|
msg.MessageWrap = Loc.GetString("chat-radio-message-wrap", ("channel", $"[{channel}]"), ("name", source.Name));
|
||||||
_netManager.ServerSendMessage(msg, playerChannel);
|
_netManager.ServerSendMessage(msg, playerChannel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,13 @@ namespace Content.Shared.Input
|
|||||||
public static readonly BoundKeyFunction ActivateItemInWorld = "ActivateItemInWorld"; // default action on world entity
|
public static readonly BoundKeyFunction ActivateItemInWorld = "ActivateItemInWorld"; // default action on world entity
|
||||||
public static readonly BoundKeyFunction Drop = "Drop";
|
public static readonly BoundKeyFunction Drop = "Drop";
|
||||||
public static readonly BoundKeyFunction ExamineEntity = "ExamineEntity";
|
public static readonly BoundKeyFunction ExamineEntity = "ExamineEntity";
|
||||||
public static readonly BoundKeyFunction FocusChat = "FocusChatWindow";
|
public static readonly BoundKeyFunction FocusChat = "FocusChatInputWindow";
|
||||||
|
public static readonly BoundKeyFunction FocusLocalChat = "FocusLocalChatWindow";
|
||||||
|
public static readonly BoundKeyFunction FocusRadio = "FocusRadioWindow";
|
||||||
public static readonly BoundKeyFunction FocusOOC = "FocusOOCWindow";
|
public static readonly BoundKeyFunction FocusOOC = "FocusOOCWindow";
|
||||||
public static readonly BoundKeyFunction FocusAdminChat = "FocusAdminChatWindow";
|
public static readonly BoundKeyFunction FocusAdminChat = "FocusAdminChatWindow";
|
||||||
|
public static readonly BoundKeyFunction CycleChatChannelForward = "CycleChatChannelForward";
|
||||||
|
public static readonly BoundKeyFunction CycleChatChannelBackward = "CycleChatChannelBackward";
|
||||||
public static readonly BoundKeyFunction OpenCharacterMenu = "OpenCharacterMenu";
|
public static readonly BoundKeyFunction OpenCharacterMenu = "OpenCharacterMenu";
|
||||||
public static readonly BoundKeyFunction OpenContextMenu = "OpenContextMenu";
|
public static readonly BoundKeyFunction OpenContextMenu = "OpenContextMenu";
|
||||||
public static readonly BoundKeyFunction OpenCraftingMenu = "OpenCraftingMenu";
|
public static readonly BoundKeyFunction OpenCraftingMenu = "OpenCraftingMenu";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Chat window radio wrap (prefix and postfix)
|
# Chat window radio wrap (prefix and postfix)
|
||||||
chat-radio-message-wrap = \\[{$channel}\\] {$name} says, "{"{"}0{"}"}"
|
chat-radio-message-wrap = {$channel} {$name} says, "{"{"}0{"}"}"
|
||||||
|
|
||||||
examine-radio-frequency = It is set to broadcast over the {$frequency} frequency.
|
examine-radio-frequency = It is set to broadcast over the {$frequency} frequency.
|
||||||
|
|
||||||
|
|||||||
@@ -3,3 +3,9 @@
|
|||||||
## Combat mode
|
## Combat mode
|
||||||
hud-combat-enabled = Combat mode enabled!
|
hud-combat-enabled = Combat mode enabled!
|
||||||
hud-combat-disabled = Combat mode disabled.
|
hud-combat-disabled = Combat mode disabled.
|
||||||
|
|
||||||
|
## Chat box
|
||||||
|
hud-chatbox-info = Say something! T to talk, Tab to cycle channels.
|
||||||
|
hud-chatbox-admin = Admin
|
||||||
|
hud-chatbox-ooc = OOC
|
||||||
|
hud-chatbox-console = Console
|
||||||
|
|||||||
@@ -78,9 +78,13 @@ ui-options-function-move-pulled-object = Move pulled object
|
|||||||
ui-options-function-release-pulled-object = Release pulled object
|
ui-options-function-release-pulled-object = Release pulled object
|
||||||
ui-options-function-point = Point at location
|
ui-options-function-point = Point at location
|
||||||
|
|
||||||
ui-options-function-focus-chat-window = Focus chat
|
ui-options-function-focus-chat-input-window = Focus chat
|
||||||
|
ui-options-function-focus-local-chat-window = Focus chat (IC)
|
||||||
|
ui-options-function-focus-radio-window = Focus chat (Radio)
|
||||||
ui-options-function-focus-ooc-window = Focus chat (OOC)
|
ui-options-function-focus-ooc-window = Focus chat (OOC)
|
||||||
ui-options-function-focus-admin-chat-window = Focus chat (admin)
|
ui-options-function-focus-admin-chat-window = Focus chat (Admin)
|
||||||
|
ui-options-function-cycle-chat-channel-forward = Cycle channel (Forward)
|
||||||
|
ui-options-function-cycle-chat-channel-backward = Cycle channel (Backward)
|
||||||
ui-options-function-open-character-menu = Open character menu
|
ui-options-function-open-character-menu = Open character menu
|
||||||
ui-options-function-open-context-menu = Open context menu
|
ui-options-function-open-context-menu = Open context menu
|
||||||
ui-options-function-open-crafting-menu = Open crafting menu
|
ui-options-function-open-crafting-menu = Open crafting menu
|
||||||
|
|||||||
BIN
Resources/Textures/Interface/Nano/chat_sub_background.png
Normal file
|
After Width: | Height: | Size: 147 B |
71
Resources/Textures/Interface/Nano/filter.svg
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
width="22.252764"
|
||||||
|
height="18.237368"
|
||||||
|
viewBox="0 0 22.252764 18.237368"
|
||||||
|
fill="none"
|
||||||
|
version="1.1"
|
||||||
|
id="svg8"
|
||||||
|
sodipodi:docname="filter.svg"
|
||||||
|
inkscape:export-filename="C:\ss14\space-station-14\Resources\Textures\Interface\Nano\filter.svg.96dpi.png"
|
||||||
|
inkscape:export-xdpi="96"
|
||||||
|
inkscape:export-ydpi="96"
|
||||||
|
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)">
|
||||||
|
<metadata
|
||||||
|
id="metadata14">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<defs
|
||||||
|
id="defs12" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1"
|
||||||
|
objecttolerance="10"
|
||||||
|
gridtolerance="10"
|
||||||
|
guidetolerance="10"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-width="2560"
|
||||||
|
inkscape:window-height="1377"
|
||||||
|
id="namedview10"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:pagecheckerboard="true"
|
||||||
|
inkscape:zoom="11.879394"
|
||||||
|
inkscape:cx="13.573766"
|
||||||
|
inkscape:cy="12.823837"
|
||||||
|
inkscape:window-x="1912"
|
||||||
|
inkscape:window-y="-8"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg8"
|
||||||
|
fit-margin-top="0"
|
||||||
|
fit-margin-left="0"
|
||||||
|
fit-margin-right="0"
|
||||||
|
fit-margin-bottom="0" />
|
||||||
|
<path
|
||||||
|
d="M 1.0666567,0.5 H 21.184687 l -8.5628,10.267 -0.1508,0.1809 v 0.2355 l 0.0011,6.1228 -2.7097003,-1.7953 v -4.3276 -0.2351 l -0.1504,-0.1807 z"
|
||||||
|
stroke="#ffffff"
|
||||||
|
stroke-width="1.3"
|
||||||
|
id="path4" />
|
||||||
|
<path
|
||||||
|
d="M 4.4535967,4.1833 H 17.786887"
|
||||||
|
stroke="#ffffff"
|
||||||
|
stroke-width="1.3"
|
||||||
|
stroke-linecap="square"
|
||||||
|
id="path6" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.2 KiB |
BIN
Resources/Textures/Interface/Nano/filter.svg.96dpi.png
Normal file
|
After Width: | Height: | Size: 404 B |
64
Resources/Textures/Interface/Nano/rounded_button.svg
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
width="32"
|
||||||
|
height="32"
|
||||||
|
viewBox="0 0 32 32"
|
||||||
|
fill="none"
|
||||||
|
version="1.1"
|
||||||
|
id="svg4"
|
||||||
|
sodipodi:docname="chat_filter_button.svg"
|
||||||
|
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)">
|
||||||
|
<metadata
|
||||||
|
id="metadata10">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<defs
|
||||||
|
id="defs8" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1"
|
||||||
|
objecttolerance="10"
|
||||||
|
gridtolerance="10"
|
||||||
|
guidetolerance="10"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-width="1291"
|
||||||
|
inkscape:window-height="991"
|
||||||
|
id="namedview6"
|
||||||
|
showgrid="false"
|
||||||
|
fit-margin-top="0"
|
||||||
|
fit-margin-left="0"
|
||||||
|
fit-margin-right="0"
|
||||||
|
fit-margin-bottom="0"
|
||||||
|
inkscape:zoom="11.136932"
|
||||||
|
inkscape:cx="15.825633"
|
||||||
|
inkscape:cy="16.930202"
|
||||||
|
inkscape:window-x="2766"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="svg4" />
|
||||||
|
<rect
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width="32"
|
||||||
|
height="32"
|
||||||
|
rx="5"
|
||||||
|
fill="#ffffff"
|
||||||
|
id="rect2" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
BIN
Resources/Textures/Interface/Nano/rounded_button.svg.96dpi.png
Normal file
|
After Width: | Height: | Size: 276 B |
@@ -0,0 +1,70 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
width="32"
|
||||||
|
height="32"
|
||||||
|
viewBox="0 0 32 32"
|
||||||
|
fill="none"
|
||||||
|
version="1.1"
|
||||||
|
id="svg4"
|
||||||
|
sodipodi:docname="rounded_button_bordered.svg"
|
||||||
|
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)"
|
||||||
|
inkscape:export-filename="C:\ss14\space-station-14\Resources\Textures\Interface\Nano\rounded_button_bordered.svg.96dpi.png"
|
||||||
|
inkscape:export-xdpi="96"
|
||||||
|
inkscape:export-ydpi="96">
|
||||||
|
<metadata
|
||||||
|
id="metadata10">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<defs
|
||||||
|
id="defs8" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1"
|
||||||
|
objecttolerance="10"
|
||||||
|
gridtolerance="10"
|
||||||
|
guidetolerance="10"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-width="2560"
|
||||||
|
inkscape:window-height="1377"
|
||||||
|
id="namedview6"
|
||||||
|
showgrid="false"
|
||||||
|
fit-margin-top="0"
|
||||||
|
fit-margin-left="0"
|
||||||
|
fit-margin-right="0"
|
||||||
|
fit-margin-bottom="0"
|
||||||
|
inkscape:zoom="11.136932"
|
||||||
|
inkscape:cx="15.825633"
|
||||||
|
inkscape:cy="16.930202"
|
||||||
|
inkscape:window-x="1912"
|
||||||
|
inkscape:window-y="-8"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg4"
|
||||||
|
inkscape:document-rotation="0"
|
||||||
|
inkscape:pagecheckerboard="true" />
|
||||||
|
<rect
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width="32"
|
||||||
|
height="32"
|
||||||
|
rx="5"
|
||||||
|
fill="#ffffff"
|
||||||
|
id="rect2"
|
||||||
|
style="stroke:#cfcfcf;stroke-opacity:1;fill:#ffffff;fill-opacity:1" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 370 B |
@@ -51,15 +51,28 @@ binds:
|
|||||||
- function: ShowEscapeMenu
|
- function: ShowEscapeMenu
|
||||||
type: State
|
type: State
|
||||||
key: Escape
|
key: Escape
|
||||||
- function: FocusChatWindow
|
- function: CycleChatChannelForward
|
||||||
|
type: State
|
||||||
|
key: Tab
|
||||||
|
- function: CycleChatChannelBackward
|
||||||
|
type: State
|
||||||
|
key: Tab
|
||||||
|
mod1: Control
|
||||||
|
- function: FocusChatInputWindow
|
||||||
type: State
|
type: State
|
||||||
key: T
|
key: T
|
||||||
- function: FocusOOCWindow
|
- function: FocusLocalChatWindow
|
||||||
type: State
|
type: State
|
||||||
key: LBracket
|
key: LBracket
|
||||||
- function: FocusAdminChatWindow
|
- function: FocusRadioWindow
|
||||||
|
type: State
|
||||||
|
key: SemiColon
|
||||||
|
- function: FocusOOCWindow
|
||||||
type: State
|
type: State
|
||||||
key: RBracket
|
key: RBracket
|
||||||
|
- function: FocusAdminChatWindow
|
||||||
|
type: State
|
||||||
|
key: BackSlash
|
||||||
- function: EditorLinePlace
|
- function: EditorLinePlace
|
||||||
type: State
|
type: State
|
||||||
key: MouseLeft
|
key: MouseLeft
|
||||||
|
|||||||