Guidebook can POP OUT YAYYYY (#20268)

* Let them eat cake

* Reviews

* Change amoled theme to gray

* Ok nevermind im big smart
This commit is contained in:
Vasilis
2023-09-22 21:52:30 +02:00
committed by GitHub
parent 413ba8cc9a
commit e5162d4151
6 changed files with 282 additions and 202 deletions

View File

@@ -0,0 +1,36 @@
<Control xmlns="https://spacestation14.io"
xmlns:fancyTree="clr-namespace:Content.Client.UserInterface.Controls.FancyTree"
xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
SetSize="750 700"
MinSize="100 200">
<PanelContainer StyleClasses="BackgroundDark">
<SplitContainer Orientation="Horizontal" HorizontalExpand="True" Name="Split">
<!-- Guide select -->
<BoxContainer Orientation="Vertical" Name="TreeBox">
<fancyTree:FancyTree Name="Tree" VerticalExpand="True" HorizontalExpand="True" Access="Public"/>
<Button Name="PopOutButton" HorizontalAlignment="Left" VerticalAlignment="Bottom" Access="Public" Text="{Loc admin-logs-pop-out}"/>
<customControls:VSeparator StyleClasses="LowDivider" Margin="0 -2"/>
</BoxContainer>
<BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True">
<BoxContainer Name="SearchContainer" Visible="False" HorizontalExpand="True">
<LineEdit
Name="SearchBar"
PlaceHolder="{Loc 'guidebook-filter-placeholder-text'}"
HorizontalExpand="True"
Margin="0 5 10 5">
</LineEdit>
</BoxContainer>
<ScrollContainer Name="Scroll" HScrollEnabled="False" HorizontalExpand="True" VerticalExpand="True">
<Control>
<BoxContainer Orientation="Vertical" Name="EntryContainer" Margin="5 5 5 5" Visible="False"/>
<BoxContainer Orientation="Vertical" Name="Placeholder" Margin="5 5 5 5">
<Label HorizontalAlignment="Center" VerticalAlignment="Center" Text="{Loc 'guidebook-placeholder-text'}"/>
<Label HorizontalAlignment="Center" VerticalAlignment="Center" Text="{Loc 'guidebook-placeholder-text-2'}"/>
</BoxContainer>
</Control>
</ScrollContainer>
</BoxContainer>
</SplitContainer>
</PanelContainer>
</Control>

View File

@@ -0,0 +1,187 @@
using System.Linq;
using Content.Client.Guidebook.RichText;
using Content.Client.UserInterface.ControlExtensions;
using Content.Client.UserInterface.Controls.FancyTree;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.ContentPack;
namespace Content.Client.Guidebook.Controls;
[GenerateTypedNameReferences]
public sealed partial class GuidebookControl : Control, ILinkClickHandler
{
[Dependency] private readonly IResourceManager _resourceManager = default!;
[Dependency] private readonly DocumentParsingManager _parsingMan = default!;
private Dictionary<string, GuideEntry> _entries = new();
public GuidebookControl()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
Tree.OnSelectedItemChanged += OnSelectionChanged;
SearchBar.OnTextChanged += _ =>
{
HandleFilter();
};
}
private IClydeWindow? ClydeWindow { get; set; }
private void OnSelectionChanged(TreeItem? item)
{
if (item != null && item.Metadata is GuideEntry entry)
ShowGuide(entry);
else
ClearSelectedGuide();
}
public void ClearSelectedGuide()
{
Placeholder.Visible = true;
EntryContainer.Visible = false;
SearchContainer.Visible = false;
EntryContainer.RemoveAllChildren();
}
private void ShowGuide(GuideEntry entry)
{
Scroll.SetScrollValue(default);
Placeholder.Visible = false;
EntryContainer.Visible = true;
SearchBar.Text = "";
EntryContainer.RemoveAllChildren();
using var file = _resourceManager.ContentFileReadText(entry.Text);
SearchContainer.Visible = entry.FilterEnabled;
if (!_parsingMan.TryAddMarkup(EntryContainer, file.ReadToEnd()))
{
EntryContainer.AddChild(new Label() { Text = "ERROR: Failed to parse document." });
Logger.Error($"Failed to parse contents of guide document {entry.Id}.");
}
}
public void UpdateGuides(
Dictionary<string, GuideEntry> entries,
List<string>? rootEntries = null,
string? forceRoot = null,
string? selected = null)
{
_entries = entries;
RepopulateTree(rootEntries, forceRoot);
ClearSelectedGuide();
Split.State = SplitContainer.SplitState.Auto;
if (entries.Count == 1)
{
TreeBox.Visible = false;
Split.ResizeMode = SplitContainer.SplitResizeMode.NotResizable;
selected = entries.Keys.First();
}
else
{
TreeBox.Visible = true;
Split.ResizeMode = SplitContainer.SplitResizeMode.RespectChildrenMinSize;
}
if (selected != null)
{
var item = Tree.Items.FirstOrDefault(x => x.Metadata is GuideEntry entry && entry.Id == selected);
Tree.SetSelectedIndex(item?.Index);
}
}
private IEnumerable<GuideEntry> GetSortedRootEntries(List<string>? rootEntries)
{
if (rootEntries == null)
{
HashSet<string> entries = new(_entries.Keys);
foreach (var entry in _entries.Values)
{
entries.ExceptWith(entry.Children);
}
rootEntries = entries.ToList();
}
return rootEntries
.Select(x => _entries[x])
.OrderBy(x => x.Priority)
.ThenBy(x => Loc.GetString(x.Name));
}
private void RepopulateTree(List<string>? roots = null, string? forcedRoot = null)
{
Tree.Clear();
HashSet<string> addedEntries = new();
TreeItem? parent = forcedRoot == null ? null : AddEntry(forcedRoot, null, addedEntries);
foreach (var entry in GetSortedRootEntries(roots))
{
AddEntry(entry.Id, parent, addedEntries);
}
Tree.SetAllExpanded(true);
}
private TreeItem? AddEntry(string id, TreeItem? parent, HashSet<string> addedEntries)
{
if (!_entries.TryGetValue(id, out var entry))
return null;
if (!addedEntries.Add(id))
{
Logger.Error($"Adding duplicate guide entry: {id}");
return null;
}
var item = Tree.AddItem(parent);
item.Metadata = entry;
var name = Loc.GetString(entry.Name);
item.Label.Text = name;
foreach (var child in entry.Children)
{
AddEntry(child, item, addedEntries);
}
return item;
}
public void HandleClick(string link)
{
if (!_entries.TryGetValue(link, out var entry))
return;
if (Tree.TryGetIndexFromMetadata(entry, out var index))
{
Tree.ExpandParentEntries(index.Value);
Tree.SetSelectedIndex(index);
}
else
{
ShowGuide(entry);
}
}
private void HandleFilter()
{
var emptySearch = SearchBar.Text.Trim().Length == 0;
if (Tree.SelectedItem != null && Tree.SelectedItem.Metadata is GuideEntry entry && entry.FilterEnabled)
{
var foundElements = EntryContainer.GetSearchableControls();
foreach (var element in foundElements)
{
element.SetHiddenState(true, SearchBar.Text.Trim());
}
}
}
}

View File

@@ -2,34 +2,8 @@
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
xmlns:fancyTree="clr-namespace:Content.Client.UserInterface.Controls.FancyTree"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
SetSize="750 700"
MinSize="100 200"
xmlns:controls1="clr-namespace:Content.Client.Guidebook.Controls"
Resizable="True"
Title="{Loc 'guidebook-window-title'}">
<SplitContainer Orientation="Horizontal" HorizontalExpand="True" Name="Split">
<!-- Guide select -->
<BoxContainer Orientation="Horizontal" Name="TreeBox">
<fancyTree:FancyTree Name="Tree" VerticalExpand="True" HorizontalExpand="True" Access="Public"/>
<cc:VSeparator StyleClasses="LowDivider" Margin="0 -2"/>
</BoxContainer>
<BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True">
<BoxContainer Name="SearchContainer" Visible="False" HorizontalExpand="True">
<LineEdit
Name="SearchBar"
PlaceHolder="{Loc 'guidebook-filter-placeholder-text'}"
HorizontalExpand="True"
Margin="0 5 10 5">
</LineEdit>
</BoxContainer>
<ScrollContainer Name="Scroll" HScrollEnabled="False" HorizontalExpand="True" VerticalExpand="True">
<Control>
<BoxContainer Orientation="Vertical" Name="EntryContainer" Margin="5 5 5 5" Visible="False"/>
<BoxContainer Orientation="Vertical" Name="Placeholder" Margin="5 5 5 5">
<Label HorizontalAlignment="Center" VerticalAlignment="Center" Text="{Loc 'guidebook-placeholder-text'}"/>
<Label HorizontalAlignment="Center" VerticalAlignment="Center" Text="{Loc 'guidebook-placeholder-text-2'}"/>
</BoxContainer>
</Control>
</ScrollContainer>
</BoxContainer>
</SplitContainer>
<controls1:GuidebookControl Name="Guidebook" Access="Public"/>
</controls:FancyWindow>

View File

@@ -6,6 +6,7 @@ using Content.Client.UserInterface.Controls;
using Content.Client.UserInterface.Controls.FancyTree;
using JetBrains.Annotations;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
@@ -14,175 +15,6 @@ using Robust.Shared.ContentPack;
namespace Content.Client.Guidebook.Controls;
[GenerateTypedNameReferences]
public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler
public sealed partial class GuidebookWindow : FancyWindow
{
[Dependency] private readonly IResourceManager _resourceManager = default!;
[Dependency] private readonly DocumentParsingManager _parsingMan = default!;
private Dictionary<string, GuideEntry> _entries = new();
public GuidebookWindow()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
Tree.OnSelectedItemChanged += OnSelectionChanged;
SearchBar.OnTextChanged += _ =>
{
HandleFilter();
};
}
private void OnSelectionChanged(TreeItem? item)
{
if (item != null && item.Metadata is GuideEntry entry)
ShowGuide(entry);
else
ClearSelectedGuide();
}
public void ClearSelectedGuide()
{
Placeholder.Visible = true;
EntryContainer.Visible = false;
SearchContainer.Visible = false;
EntryContainer.RemoveAllChildren();
}
private void ShowGuide(GuideEntry entry)
{
Scroll.SetScrollValue(default);
Placeholder.Visible = false;
EntryContainer.Visible = true;
SearchBar.Text = "";
EntryContainer.RemoveAllChildren();
using var file = _resourceManager.ContentFileReadText(entry.Text);
SearchContainer.Visible = entry.FilterEnabled;
if (!_parsingMan.TryAddMarkup(EntryContainer, file.ReadToEnd()))
{
EntryContainer.AddChild(new Label() { Text = "ERROR: Failed to parse document." });
Logger.Error($"Failed to parse contents of guide document {entry.Id}.");
}
}
public void UpdateGuides(
Dictionary<string, GuideEntry> entries,
List<string>? rootEntries = null,
string? forceRoot = null,
string? selected = null)
{
_entries = entries;
RepopulateTree(rootEntries, forceRoot);
ClearSelectedGuide();
Split.State = SplitContainer.SplitState.Auto;
if (entries.Count == 1)
{
TreeBox.Visible = false;
Split.ResizeMode = SplitContainer.SplitResizeMode.NotResizable;
selected = entries.Keys.First();
}
else
{
TreeBox.Visible = true;
Split.ResizeMode = SplitContainer.SplitResizeMode.RespectChildrenMinSize;
}
if (selected != null)
{
var item = Tree.Items.FirstOrDefault(x => x.Metadata is GuideEntry entry && entry.Id == selected);
Tree.SetSelectedIndex(item?.Index);
}
}
private IEnumerable<GuideEntry> GetSortedRootEntries(List<string>? rootEntries)
{
if (rootEntries == null)
{
HashSet<string> entries = new(_entries.Keys);
foreach (var entry in _entries.Values)
{
entries.ExceptWith(entry.Children);
}
rootEntries = entries.ToList();
}
return rootEntries
.Select(x => _entries[x])
.OrderBy(x => x.Priority)
.ThenBy(x => Loc.GetString(x.Name));
}
private void RepopulateTree(List<string>? roots = null, string? forcedRoot = null)
{
Tree.Clear();
HashSet<string> addedEntries = new();
TreeItem? parent = forcedRoot == null ? null : AddEntry(forcedRoot, null, addedEntries);
foreach (var entry in GetSortedRootEntries(roots))
{
AddEntry(entry.Id, parent, addedEntries);
}
Tree.SetAllExpanded(true);
}
private TreeItem? AddEntry(string id, TreeItem? parent, HashSet<string> addedEntries)
{
if (!_entries.TryGetValue(id, out var entry))
return null;
if (!addedEntries.Add(id))
{
Logger.Error($"Adding duplicate guide entry: {id}");
return null;
}
var item = Tree.AddItem(parent);
item.Metadata = entry;
var name = Loc.GetString(entry.Name);
item.Label.Text = name;
foreach (var child in entry.Children)
{
AddEntry(child, item, addedEntries);
}
return item;
}
public void HandleClick(string link)
{
if (!_entries.TryGetValue(link, out var entry))
return;
if (Tree.TryGetIndexFromMetadata(entry, out var index))
{
Tree.ExpandParentEntries(index.Value);
Tree.SetSelectedIndex(index);
}
else
{
ShowGuide(entry);
}
}
private void HandleFilter()
{
var emptySearch = SearchBar.Text.Trim().Length == 0;
if (Tree.SelectedItem != null && Tree.SelectedItem.Metadata is GuideEntry entry && entry.FilterEnabled)
{
var foundElements = EntryContainer.GetSearchableControls();
foreach (var element in foundElements)
{
element.SetHiddenState(true, SearchBar.Text.Trim());
}
}
}
}

View File

@@ -5,6 +5,7 @@ using Content.Client.Guidebook.Controls;
using Content.Client.Lobby;
using Content.Client.UserInterface.Controls;
using Content.Shared.Input;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
using static Robust.Client.UserInterface.Controls.BaseButton;
@@ -17,7 +18,11 @@ namespace Content.Client.UserInterface.Systems.Guidebook;
public sealed class GuidebookUIController : UIController, IOnStateEntered<LobbyState>, IOnStateEntered<GameplayState>, IOnStateExited<LobbyState>, IOnStateExited<GameplayState>, IOnSystemChanged<GuidebookSystem>
{
[UISystemDependency] private readonly GuidebookSystem _guidebookSystem = default!;
[Dependency] private readonly IClyde _clyde = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IUserInterfaceManager _uiManager = default!;
private IClydeWindow? ClydeWindow { get; set; }
private GuidebookWindow? _guideWindow;
private MenuButton? GuidebookButton => UIManager.GetActiveUIWidgetOrNull<MenuBar.Widgets.GameTopMenuBar>()?.GuidebookButton;
@@ -40,6 +45,7 @@ public sealed class GuidebookUIController : UIController, IOnStateEntered<LobbyS
_guideWindow = UIManager.CreateWindow<GuidebookWindow>();
_guideWindow.OnClose += OnWindowClosed;
_guideWindow.OnOpen += OnWindowOpen;
_guideWindow.Guidebook.PopOutButton.OnPressed += _ => PopOut();
// setup keybinding
CommandBinds.Builder
@@ -135,7 +141,12 @@ public sealed class GuidebookUIController : UIController, IOnStateEntered<LobbyS
string? selected = null)
{
if (_guideWindow == null)
return;
{
_guideWindow = UIManager.CreateWindow<GuidebookWindow>();
_guideWindow.OnClose += OnWindowClosed;
_guideWindow.OnOpen += OnWindowOpen;
_guideWindow.Guidebook.PopOutButton.OnPressed += _ => PopOut();
}
if (_guideWindow.IsOpen)
{
@@ -161,11 +172,11 @@ public sealed class GuidebookUIController : UIController, IOnStateEntered<LobbyS
}
}
_guideWindow.UpdateGuides(guides, rootEntries, forceRoot, selected);
_guideWindow.Guidebook.UpdateGuides(guides, rootEntries, forceRoot, selected);
// Expand up to depth-2.
_guideWindow.Tree.SetAllExpanded(false);
_guideWindow.Tree.SetAllExpanded(true, 1);
_guideWindow.Guidebook.Tree.SetAllExpanded(false);
_guideWindow.Guidebook.Tree.SetAllExpanded(true, 1);
_guideWindow.OpenCenteredRight();
}
@@ -208,4 +219,43 @@ public sealed class GuidebookUIController : UIController, IOnStateEntered<LobbyS
RecursivelyAddChildren(child, guides);
}
}
private void PopOut()
{
if (_guideWindow == null)
{
return;
}
var monitor = _clyde.EnumerateMonitors().First();
ClydeWindow = _clyde.CreateWindow(new WindowCreateParameters
{
Maximized = false,
Title = "Guidebook",
Monitor = monitor,
Width = 750,
Height = 700
});
var control = _guideWindow.Guidebook;
control.Orphan();
_guideWindow.Dispose();
_guideWindow = null;
ClydeWindow.RequestClosed += OnRequestClosed;
ClydeWindow.DisposeOnClose = true;
var Root = _uiManager.CreateWindowRoot(ClydeWindow);
Root.AddChild(control);
control.PopOutButton.Disabled = true;
control.PopOutButton.Visible = false;
}
private void OnRequestClosed(WindowRequestClosedEventArgs obj)
{
ClydeWindow = null;
_guideWindow = null;
if (GuidebookButton != null)
GuidebookButton.Pressed = false;
}
}

View File

@@ -1,5 +1,6 @@
defaultWindowTitle: Space Station 14
windowIconSet: /Textures/Logo/icon
splashLogo: /Textures/Logo/logo.png
multiWindow: true
# PJB PLEASE