Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add text highlighting #31442

Open
wants to merge 23 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
b5bfcbd
Add basic chat highlighting
VitusVeit Aug 25, 2024
dafe31c
Add saving of highlighted words
VitusVeit Aug 26, 2024
d0b5b89
Merge remote-tracking branch 'upstream/master' into highlight
VitusVeit Aug 26, 2024
8bb7ef0
Fix highlights not loading properly
VitusVeit Aug 27, 2024
c750526
Major refactor
VitusVeit Sep 1, 2024
d9ef0db
Add autofill highlights
VitusVeit Sep 2, 2024
d1d630d
Small fixes to the autofill
VitusVeit Sep 2, 2024
2cc311a
Update highlights.ftl
VitusVeit Sep 2, 2024
2d2766e
Make the highlighs color modifiable
VitusVeit Sep 3, 2024
68e3e04
Add highlights placeholder
VitusVeit Sep 4, 2024
7abf0ce
Highlght whole words only
VitusVeit Sep 4, 2024
e96bf65
Merge remote-tracking branch 'upstream/master' into highlight
VitusVeit Sep 6, 2024
6ffe6de
Small logic and casing fixes
VitusVeit Sep 6, 2024
2c56363
Make whole-word tag an user option
VitusVeit Sep 7, 2024
6d3fe1b
Change default highlights color
VitusVeit Sep 7, 2024
aa27968
Merge remote-tracking branch 'upstream/master' into highlight
VitusVeit Sep 10, 2024
a30dcc4
Merge remote-tracking branch 'upstream/master' into highlight
VitusVeit Oct 7, 2024
6beb87b
Add background to TextEdit
VitusVeit Oct 7, 2024
f66bfcd
Apply suggestions from code review
VitusVeit Nov 2, 2024
d397578
Merge remote-tracking branch 'upstream/master' into highlight
VitusVeit Nov 3, 2024
ed85fb1
Merge remote-tracking branch and fix merge conflict
VitusVeit Nov 14, 2024
f9740c5
Actually add the CCVars
VitusVeit Nov 14, 2024
5d64ebe
Small naming and comments fixes
VitusVeit Nov 21, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Content.Client/Options/UI/OptionColorSlider.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<Control xmlns="https://spacestation14.io">
<BoxContainer Orientation="Vertical">
<Label Name="TitleLabel" Access="Public" />
<Label Name="ExampleLabel" Access="Public" />
<ColorSelectorSliders Name="Slider" Access="Public" HorizontalExpand="True" />
</BoxContainer>
</Control>
30 changes: 30 additions & 0 deletions Content.Client/Options/UI/OptionColorSlider.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;

namespace Content.Client.Options.UI;

/// <summary>
/// Standard UI control used for color sliders in the options menu. Intended for use with <see cref="OptionsTabControlRow"/>.
/// </summary>
/// <seealso cref="OptionsTabControlRow.AddOptionColorSlider"/>
[GenerateTypedNameReferences]
public sealed partial class OptionColorSlider : Control
{
/// <summary>
/// The text describing what this slider affects.
/// </summary>
public string? Title
{
get => TitleLabel.Text;
set => TitleLabel.Text = value;
}

/// <summary>
/// The example text showing the current color of the slider.
/// </summary>
public string? Example
{
get => ExampleLabel.Text;
set => ExampleLabel.Text = value;
}
}
65 changes: 65 additions & 0 deletions Content.Client/Options/UI/OptionsTabControlRow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,19 @@ public OptionSliderFloatCVar AddOptionPercentSlider(
{
return AddOption(new OptionSliderFloatCVar(this, _cfg, cVar, slider, min, max, scale, FormatPercent));
}

/// <summary>
/// Add a color slider option, backed by a simple string CVar.
/// </summary>
/// <param name="cVar">The CVar represented by the slider.</param>
/// <param name="slider">The UI control for the option.</param>
/// <returns>The option instance backing the added option.</returns>
public OptionColorSliderCVar AddOptionColorSlider(
CVarDef<string> cVar,
OptionColorSlider slider)
{
return AddOption(new OptionColorSliderCVar(this, _cfg, cVar, slider));
}

/// <summary>
/// Add a slider option, backed by a simple integer CVar.
Expand Down Expand Up @@ -518,6 +531,58 @@ private void UpdateLabelValue()
}
}

/// <summary>
/// Implementation of a CVar option that simply corresponds with a string <see cref="OptionColorSlider"/>.
/// </summary>
/// <seealso cref="OptionsTabControlRow"/>
public sealed class OptionColorSliderCVar : BaseOptionCVar<string>
{
private readonly OptionColorSlider _slider;

protected override string Value
{
get => _slider.Slider.Color.ToHex();
set
{
_slider.Slider.Color = Color.FromHex(value);
UpdateLabelColor();
}
}

/// <summary>
/// Creates a new instance of this type.
/// </summary>
/// <remarks>
/// <para>
/// It is generally more convenient to call overloads on <see cref="OptionsTabControlRow"/>
/// such as <see cref="OptionsTabControlRow.AddOptionPercentSlider"/> instead of instantiating this type directly.
/// </para>
/// </remarks>
/// <param name="controller">The control row that owns this option.</param>
/// <param name="cfg">The configuration manager to get and set values from.</param>
/// <param name="cVar">The CVar that is being controlled by this option.</param>
/// <param name="slider">The UI control for the option.</param>
public OptionColorSliderCVar(
OptionsTabControlRow controller,
IConfigurationManager cfg,
CVarDef<string> cVar,
OptionColorSlider slider) : base(controller, cfg, cVar)
{
_slider = slider;

slider.Slider.OnColorChanged += _ =>
{
ValueChanged();
UpdateLabelColor();
};
}

private void UpdateLabelColor()
{
_slider.ExampleLabel.FontColorOverride = Color.FromHex(Value);
}
}

/// <summary>
/// Implementation of a CVar option that simply corresponds with an integer <see cref="OptionSlider"/>.
/// </summary>
Expand Down
4 changes: 4 additions & 0 deletions Content.Client/Options/UI/Tabs/AccessibilityTab.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
<CheckBox Name="ColorblindFriendlyCheckBox" Text="{Loc 'ui-options-colorblind-friendly'}" />
<ui:OptionSlider Name="ChatWindowOpacitySlider" Title="{Loc 'ui-options-chat-window-opacity'}" />
<ui:OptionSlider Name="ScreenShakeIntensitySlider" Title="{Loc 'ui-options-screen-shake-intensity'}" />
<CheckBox Name="AutoFillHighlightsCheckBox" Text="{Loc 'ui-options-auto-fill-highlights'}" />
<ui:OptionColorSlider Name="HighlightsColorSlider"
Title="{Loc 'ui-options-highlights-color'}"
Example="{Loc 'ui-options-highlights-color-example'}"/>
</BoxContainer>
</ScrollContainer>
<ui:OptionsTabControlRow Name="Control" Access="Public" />
Expand Down
2 changes: 2 additions & 0 deletions Content.Client/Options/UI/Tabs/AccessibilityTab.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ public AccessibilityTab()
Control.AddOptionCheckBox(CCVars.ReducedMotion, ReducedMotionCheckBox);
Control.AddOptionPercentSlider(CCVars.ChatWindowOpacity, ChatWindowOpacitySlider);
Control.AddOptionPercentSlider(CCVars.ScreenShakeIntensity, ScreenShakeIntensitySlider);
Control.AddOptionCheckBox(CCVars.ChatAutoFillHighlights, AutoFillHighlightsCheckBox);
Control.AddOptionColorSlider(CCVars.ChatHighlightsColor, HighlightsColorSlider);

Control.Initialize();
}
Expand Down
115 changes: 114 additions & 1 deletion Content.Client/UserInterface/Systems/Chat/ChatUIController.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Globalization;
using System.Linq;
using System.Numerics;
using Content.Client.CharacterInfo;
using Content.Client.Administration.Managers;
using Content.Client.Chat;
using Content.Client.Chat.Managers;
Expand Down Expand Up @@ -40,10 +41,12 @@
using Robust.Shared.Replays;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using static Content.Client.CharacterInfo.CharacterInfoSystem;


namespace Content.Client.UserInterface.Systems.Chat;

public sealed class ChatUIController : UIController
public sealed class ChatUIController : UIController, IOnSystemChanged<CharacterInfoSystem>
{
[Dependency] private readonly IClientAdminManager _admin = default!;
[Dependency] private readonly IChatManager _manager = default!;
Expand All @@ -65,6 +68,7 @@ public sealed class ChatUIController : UIController
[UISystemDependency] private readonly TransformSystem? _transform = default;
[UISystemDependency] private readonly MindSystem? _mindSystem = default!;
[UISystemDependency] private readonly RoleCodewordSystem? _roleCodewordSystem = default!;
[UISystemDependency] private readonly CharacterInfoSystem _characterInfo = default!;

[ValidatePrototypeId<ColorPalettePrototype>]
private const string ChatNamePalette = "ChatNames";
Expand Down Expand Up @@ -149,6 +153,23 @@ private readonly Dictionary<EntityUid, SpeechBubbleQueueData> _queuedSpeechBubbl
/// </summary>
private readonly Dictionary<ChatChannel, int> _unreadMessages = new();

/// <summary>
/// The list of words to be highlighted in the chatbox.
/// </summary>
private List<string> _highlights = new();

/// <summary>
/// The string holding the hex color used to highlight words.
/// </summary>
private string? _highlightsColor;

private bool _autoFillHighlightsEnabled;

/// <summary>
/// The boolean that keeps track of the 'OnCharacterUpdated' event, whenever it's a player attaching or opening the character info panel.
/// </summary>
private bool _charInfoIsAttach = false;

// TODO add a cap for this for non-replays
public readonly List<(GameTick Tick, ChatMessage Msg)> History = new();

Expand All @@ -172,6 +193,7 @@ private readonly Dictionary<EntityUid, SpeechBubbleQueueData> _queuedSpeechBubbl
public event Action<ChatSelectChannel>? SelectableChannelsChanged;
public event Action<ChatChannel, int?>? UnreadMessageCountsUpdated;
public event Action<ChatMessage>? MessageAdded;
public event Action<string>? HighlightsUpdated;

public override void Initialize()
{
Expand Down Expand Up @@ -240,6 +262,19 @@ public override void Initialize()

_config.OnValueChanged(CCVars.ChatWindowOpacity, OnChatWindowOpacityChanged);

_config.OnValueChanged(CCVars.ChatAutoFillHighlights, (value) => { _autoFillHighlightsEnabled = value; });
_autoFillHighlightsEnabled = _config.GetCVar(CCVars.ChatAutoFillHighlights);

_config.OnValueChanged(CCVars.ChatHighlightsColor, (value) => { _highlightsColor = value; });
_highlightsColor = _config.GetCVar(CCVars.ChatHighlightsColor);

// Load highlights if any were saved.
string highlights = _config.GetCVar(CCVars.ChatHighlights);

if (!string.IsNullOrEmpty(highlights))
{
UpdateHighlights(highlights);
}
}

public void OnScreenLoad()
Expand All @@ -257,11 +292,48 @@ public void OnScreenUnload()
SetMainChat(false);
}

public void OnSystemLoaded(CharacterInfoSystem system)
{
system.OnCharacterUpdate += OnCharacterUpdated;
}

public void OnSystemUnloaded(CharacterInfoSystem system)
{
system.OnCharacterUpdate -= OnCharacterUpdated;
}

private void OnChatWindowOpacityChanged(float opacity)
{
SetChatWindowOpacity(opacity);
}

private void OnCharacterUpdated(CharacterData data)
{
// If the _charInfoIsAttach is false then the character panel was the one
// to generate the event, dismiss it.
if (!_charInfoIsAttach)
return;

var (_, job, _, _, entityName) = data;

// If the character has a normal name (eg. "Name Surname" and not "Name Initial Surname" or a particular species name)
// subdivide it so that the name and surname individually get highlighted.
if (entityName.Count(c => c == ' ') == 1)
entityName = entityName.Replace(' ', '\n');

string newHighlights = entityName;

// Convert the job title to kebab-case and use it as a key for the loc file.
string jobKey = job.Replace(' ', '-').ToLower();

if (Loc.TryGetString($"highlights-{jobKey}", out var jobMatches))
newHighlights += '\n' + jobMatches.Replace(", ", "\n");

UpdateHighlights(newHighlights);
HighlightsUpdated?.Invoke(newHighlights);
_charInfoIsAttach = false;
}

private void SetChatWindowOpacity(float opacity)
{
var chatBox = UIManager.ActiveScreen?.GetWidget<ChatBox>() ?? UIManager.ActiveScreen?.GetWidget<ResizableChatBox>();
Expand Down Expand Up @@ -426,6 +498,14 @@ public void SetSpeechBubbleRoot(LayoutContainer root)
private void OnAttachedChanged(EntityUid uid)
{
UpdateChannelPermissions();

// If auto highlights are enabled generate a request for new character info
// that will be used to determine the highlights.
if (_autoFillHighlightsEnabled)
{
_charInfoIsAttach = true;
_characterInfo.RequestCharacterInfo();
}
}

private void AddSpeechBubble(ChatMessage msg, SpeechBubble.SpeechType speechType)
Expand Down Expand Up @@ -584,6 +664,33 @@ public void ClearUnfilteredUnreads(ChatChannel channels)
}
}

public void UpdateHighlights(string highlights)
{
// Do nothing if the provided highlighs are the same as the old ones.
if (_config.GetCVar(CCVars.ChatHighlights).Equals(highlights, StringComparison.CurrentCultureIgnoreCase))
return;

_config.SetCVar(CCVars.ChatHighlights, highlights);
_config.SaveToFile();

// Replace any " character with a whole-word regex tag,
// this tag will make the words to match are separated by spaces or punctuation.
highlights = highlights.Replace("\"", "\\b");

_highlights.Clear();

// Fill the array with the highlights separated by newlines, disregarding empty entries.
string[] arrHighlights = highlights.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
foreach (var keyword in arrHighlights)
{
_highlights.Add(keyword);
}

// Arrange the list of highlights in descending order so that when highlighting,
// the full word (eg. "Security") gets picked before the abbreviation (eg. "Sec").
_highlights.Sort((x, y) => y.Length.CompareTo(x.Length));
}

public override void FrameUpdate(FrameEventArgs delta)
{
UpdateQueuedSpeechBubbles(delta);
Expand Down Expand Up @@ -824,6 +931,12 @@ public void ProcessChatMessage(ChatMessage msg, bool speechBubble = true)
msg.WrappedMessage = SharedChatSystem.InjectTagInsideTag(msg, "Name", "color", GetNameColor(SharedChatSystem.GetStringInsideTag(msg, "Name")));
}

// Color any words choosen by the client.
foreach (var highlight in _highlights)
{
msg.WrappedMessage = SharedChatSystem.InjectTagAroundString(msg, highlight, "color", _highlightsColor);
}

// Color any codewords for minds that have roles that use them
if (_player.LocalUser != null && _mindSystem != null && _roleCodewordSystem != null)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
<controls:ChannelFilterPopup
xmlns="https://spacestation14.io"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Systems.Chat.Controls">
<PanelContainer Name="FilterPopupPanel" StyleClasses="BorderedWindowPanel">
<BoxContainer Orientation="Horizontal">
<Control MinSize="4 0"/>
<BoxContainer Name="FilterVBox" MinWidth="110" Margin="0 10" Orientation="Vertical" SeparationOverride="4"/>
<BoxContainer Orientation="Horizontal" SeparationOverride="8" Margin="10 0">
<BoxContainer Name="FilterVBox" MinWidth="105" Margin="0 10" Orientation="Vertical" SeparationOverride="4"/>
<BoxContainer Name="HighlightsVBox" MinWidth="120" Margin="0 10" Orientation="Vertical" SeparationOverride="4">
<Label Text="{Loc 'hud-chatbox-highlights'}"/>
<PanelContainer>
<!-- Begin custom background for TextEdit -->
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#323446"/>
</PanelContainer.PanelOverride>
<!-- End custom background -->
<TextEdit Name="HighlightEdit" MinHeight="150" Margin="5 5"/>
</PanelContainer>
<Button Name="HighlightButton" Text="{Loc 'hud-chatbox-highlights-button'}" ToolTip="{Loc 'hud-chatbox-highlights-tooltip'}"/>
</BoxContainer>
</BoxContainer>
</PanelContainer>
</controls:ChannelFilterPopup>
Loading
Loading