diff --git a/TLM/TLM/UI/WhatsNew/MarkupKeyword.cs b/TLM/TLM/UI/WhatsNew/MarkupKeyword.cs index d42af32e4..3a5a529b3 100644 --- a/TLM/TLM/UI/WhatsNew/MarkupKeyword.cs +++ b/TLM/TLM/UI/WhatsNew/MarkupKeyword.cs @@ -4,6 +4,7 @@ public enum MarkupKeyword { Link, Released, Stable, + Meta, New, Mod, Fixed, diff --git a/TLM/TLM/UI/WhatsNew/WhatsNew.cs b/TLM/TLM/UI/WhatsNew/WhatsNew.cs index 8203df962..1f54b7b87 100644 --- a/TLM/TLM/UI/WhatsNew/WhatsNew.cs +++ b/TLM/TLM/UI/WhatsNew/WhatsNew.cs @@ -10,22 +10,26 @@ namespace TrafficManager.UI.WhatsNew { using State; public class WhatsNew { - private const string WHATS_NEW_FILE = "whats_new.txt"; - private const string RESOURCES_PREFIX = "TrafficManager.Resources."; - // bump and update what's new changelogs when new features added - internal static readonly Version CurrentVersion = new Version(11,6,4,0); + internal static readonly Version CurrentVersion = new Version(11, 6, 4, 0); + + internal static readonly Version PreviouslySeenVersion = GlobalConfig.Instance.Main.LastWhatsNewPanelVersion; - internal bool Shown => CurrentVersion == GlobalConfig.Instance.Main.LastWhatsNewPanelVersion; - public List Changelogs { get; private set; } + internal bool Shown => PreviouslySeenVersion >= CurrentVersion; + + private const string WHATS_NEW_FILE = "whats_new.txt"; + private const string RESOURCES_PREFIX = "TrafficManager.Resources."; public WhatsNew() { - LoadChangelog(); + LoadChangelogs(); } + public List Changelogs { get; private set; } + public static void OpenModal() { UIView uiView = UIView.GetAView(); if (uiView) { + MarkAsShown(); WhatsNewPanel panel = uiView.AddUIComponent(typeof(WhatsNewPanel)) as WhatsNewPanel; if (panel) { Log.Info("Opened What's New panel!"); @@ -40,14 +44,13 @@ public static void OpenModal() { } } - public void MarkAsShown() { + public static void MarkAsShown() { Log.Info($"What's New - mark as shown. Version {CurrentVersion}"); GlobalConfig.Instance.Main.LastWhatsNewPanelVersion = CurrentVersion; GlobalConfig.WriteConfig(); } - - private void LoadChangelog() { + private void LoadChangelogs() { Log.Info("Loading What's New changelogs..."); string[] lines; using (Stream st = Assembly.GetExecutingAssembly() @@ -57,42 +60,47 @@ private void LoadChangelog() { lines = sr.ReadToEnd().Split(new[] { "\n", "\r\n" }, StringSplitOptions.None); } - Changelogs = ChangelogEntry.ParseChangelogs(lines); + Changelogs = ParseChangelogs(lines); } Log.Info($"Loaded {Changelogs.Count} What's New changelogs"); } - } - public class ChangelogEntry { - public Version Version { get; private set; } - public bool Stable { get; private set; } - [CanBeNull] - public string Link { get; private set; } - [CanBeNull] - public string Released { get; private set; } - public ChangeEntry[] ChangeEntries { get; private set; } - - public static List ParseChangelogs(string[] lines) { - List entries = new List(); + /// + /// Parses the changelogs in whats_new.txt. + /// + /// The contents of whats_new.txt. + /// A list of . + /// + /// Version blocks must contain only one [Version] tag + /// and must end with a [/Version] tag. + /// + /// + /// Ensure each version block ends with [/Version]. + /// + /// . + private static List ParseChangelogs(string[] lines) { + var changelogs = new List(); int i = 0; - var keywordStrings = WhatsNewMarkup.MarkupKeywordsString; + while (i < lines.Length) { string line = lines[i]; - if (TryParseKeyword(line, out MarkupKeyword lineKeyword) && lineKeyword == MarkupKeyword.VersionStart) { - ChangelogEntry changelog = new ChangelogEntry(); - // read version - changelog.Version = new Version(lines[i++].Substring(keywordStrings[MarkupKeyword.VersionStart].Length).Trim()); + if (TryParseKeyword(line, out MarkupKeyword lineKeyword, out string text) + && lineKeyword == MarkupKeyword.VersionStart) { - //get next line keyword - TryParseKeyword(lines[i], out lineKeyword); - // parse to the end of version section - List changeEntries = new List(); + var changelog = new Changelog() { + Version = new Version(text), + }; + var items = new List(); + + // Parse contents of [Version]..[/Version] block + TryParseKeyword(lines[++i], out lineKeyword, out text); while (lineKeyword != MarkupKeyword.VersionEnd) { - string text = lines[i].Substring(keywordStrings[lineKeyword].Length).Trim(); - Log._Debug($"Keyword {lineKeyword}, text: {text}"); + + // Log._Debug($"Keyword {lineKeyword}, Text: {text}"); switch (lineKeyword) { - // TODO: Should we also check for VersionStart keyword and throw an error if encountered? (a changelog block missing the [/Version]) + case MarkupKeyword.VersionStart: + throw new FormatException($"whats_new.txt line {i}: Unexpected '[Version]' tag."); case MarkupKeyword.Stable: changelog.Stable = true; break; @@ -103,58 +111,77 @@ public static List ParseChangelogs(string[] lines) { changelog.Released = text; break; case MarkupKeyword.Unknown: - //skip unknown entries + // skip unknown entries + Log.Warning($"whats_new.txt line {i}: Unrecognised entry '{line}'"); break; default: - changeEntries.Add( - new ChangeEntry() { + items.Add( + new Changelog.Item() { Keyword = lineKeyword, - Text = text + Text = text, }); break; } - i++; - TryParseKeyword(lines[i], out lineKeyword); + TryParseKeyword(lines[++i], out lineKeyword, out text); } - changelog.ChangeEntries = changeEntries.ToArray(); - Array.Sort(changelog.ChangeEntries, ChangeEntry.KeywordComparer); - entries.Add(changelog); + changelog.Items = items.ToArray(); + Array.Sort(changelog.Items, Changelog.Item.KeywordComparer); + changelogs.Add(changelog); + + // If user already seen this version, don't bother parsing remainder of file + if (changelog.Version <= PreviouslySeenVersion) { + break; + } } i++; } - return entries; + return changelogs; } - private static bool TryParseKeyword(string line, out MarkupKeyword keyword) { - if (!string.IsNullOrEmpty(line)) { - if(line.StartsWith("[") && - WhatsNewMarkup.MarkupKeywords.TryGetValue( - line.Substring(0, line.IndexOf("]") + 1), - out keyword)) { - return true; - } - Log.Warning($"Couldn't parse line \"{line}\""); + private static bool TryParseKeyword(string line, out MarkupKeyword keyword, out string text) { + if ((!string.IsNullOrEmpty(line)) && line.StartsWith("[")) { + int pos = line.IndexOf("]") + 1; + string tag = line.Substring(0, pos); + + keyword = tag.ToKeyword(); + text = line.Substring(pos).Trim(); + + return keyword != MarkupKeyword.Unknown; } keyword = MarkupKeyword.Unknown; + text = line; return false; } + } + + /// + /// Represents the changelog for a release (ie. [Version]..[/Version] block). + /// + public class Changelog { + public Version Version { get; set; } + public bool Stable { get; set; } + [CanBeNull] + public string Link { get; set; } + [CanBeNull] + public string Released { get; set; } + public Item[] Items { get; set; } - public struct ChangeEntry { + public struct Item { public MarkupKeyword Keyword; public string Text; - private sealed class KeywordRelationalComparer : IComparer { - public int Compare(ChangeEntry x, ChangeEntry y) { + public static IComparer KeywordComparer { get; } = new KeywordRelationalComparer(); + + private sealed class KeywordRelationalComparer : IComparer { + public int Compare(Item x, Item y) { return x.Keyword.CompareTo(y.Keyword); } } - - public static IComparer KeywordComparer { get; } = new KeywordRelationalComparer(); } } } \ No newline at end of file diff --git a/TLM/TLM/UI/WhatsNew/WhatsNewMarkup.cs b/TLM/TLM/UI/WhatsNew/WhatsNewMarkup.cs index 9717233e3..41ae72998 100644 --- a/TLM/TLM/UI/WhatsNew/WhatsNewMarkup.cs +++ b/TLM/TLM/UI/WhatsNew/WhatsNewMarkup.cs @@ -1,15 +1,29 @@ namespace TrafficManager.UI.WhatsNew { using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; using CSUtil.Commons; using UnityEngine; + [SuppressMessage("Usage", "RAS0002:Readonly field for a non-readonly struct", Justification = "Not performance critical.")] public static class WhatsNewMarkup { - public static readonly Dictionary MarkupKeywords = new() { + public static MarkupKeyword ToKeyword(this string token) { + if (MarkupKeywords.TryGetValue(token, out MarkupKeyword keyword)) { + return keyword; + } + return MarkupKeyword.Unknown; + } + + public static Color32 ToColor(this MarkupKeyword keyword) { + return GetColor(keyword); + } + + private static readonly Dictionary MarkupKeywords = new() { { "[Version]", MarkupKeyword.VersionStart }, { "[/Version]", MarkupKeyword.VersionEnd }, { "[Stable]", MarkupKeyword.Stable }, { "[Link]", MarkupKeyword.Link }, { "[Released]", MarkupKeyword.Released }, + { "[Meta]", MarkupKeyword.Meta }, { "[New]", MarkupKeyword.New }, { "[Mod]", MarkupKeyword.Mod }, { "[Fixed]", MarkupKeyword.Fixed }, @@ -17,47 +31,33 @@ public static class WhatsNewMarkup { { "[Removed]", MarkupKeyword.Removed }, }; - public static readonly Dictionary MarkupKeywordsString = new() { - { MarkupKeyword.VersionStart, "[Version]" }, - { MarkupKeyword.VersionEnd, "[/Version]" }, - { MarkupKeyword.Stable, "[Stable]" }, - { MarkupKeyword.Link, "[Link]" }, - { MarkupKeyword.Released, "[Released]" }, - { MarkupKeyword.New, "[New]" }, - { MarkupKeyword.Mod, "[Mod]" }, - { MarkupKeyword.Fixed, "[Fixed]" }, - { MarkupKeyword.Updated, "[Updated]" }, - { MarkupKeyword.Removed, "[Removed]" }, - }; - - public static readonly Color32 ModColor = new Color32(255, 196, 0, 255); - public static readonly Color32 FixedOrUpdatedColor = new Color32(3, 106, 225, 255); - public static readonly Color32 NewOrAddedColor = new Color32(40, 178, 72, 255); - public static readonly Color32 RemovedColor = new Color32(224, 61, 76, 255); - public static readonly Color32 VersionColor = new Color32(119, 69, 204, 255); + private static readonly Color32 Red = new (224, 61, 76, 255); + private static readonly Color32 Amber = new (255, 196, 0, 255); + private static readonly Color32 Green = new (40, 178, 72, 255); + private static readonly Color32 Blue = new (3, 106, 225, 255); + private static readonly Color32 Purple = new (119, 69, 204, 255); - public static Color32 GetColor(MarkupKeyword keyword) { + private static Color32 GetColor(MarkupKeyword keyword) { switch (keyword) { case MarkupKeyword.Fixed: - return FixedOrUpdatedColor; + return Blue; case MarkupKeyword.New: - return NewOrAddedColor; + return Green; case MarkupKeyword.Mod: - return ModColor; + return Amber; case MarkupKeyword.Removed: - return RemovedColor; + return Red; case MarkupKeyword.Updated: - return FixedOrUpdatedColor; + return Blue; case MarkupKeyword.VersionStart: case MarkupKeyword.VersionEnd: case MarkupKeyword.Stable: - return VersionColor; + case MarkupKeyword.Meta: + return Purple; default: Log.Warning($"No custom color for markup keyword: {keyword}"); return Color.white; } } - - } } \ No newline at end of file diff --git a/TLM/TLM/UI/WhatsNew/WhatsNewPanel.cs b/TLM/TLM/UI/WhatsNew/WhatsNewPanel.cs index 35384c6d0..0a3d2d0f3 100644 --- a/TLM/TLM/UI/WhatsNew/WhatsNewPanel.cs +++ b/TLM/TLM/UI/WhatsNew/WhatsNewPanel.cs @@ -55,12 +55,13 @@ private void AddFooter() { private void AddContent() { var panel = AddUIComponent(); + panel.autoLayout = false; panel.maximumSize = GetMaxContentSize(); panel.relativePosition = new Vector2(5, 40); panel.size = new Vector2(_defaultWidth - 10, _defaultHeight - _header.height - _footerPanel.height); - panel.autoLayout = true; var content = panel.AddUIComponent(); + content.autoLayout = false; content.autoLayoutDirection = LayoutDirection.Vertical; content.scrollWheelDirection = UIOrientation.Vertical; content.clipChildren = true; @@ -71,26 +72,26 @@ private void AddContent() { var stableRelease = Util.VersionUtil.IsStableRelease; - List changelogEntries = TMPELifecycle.Instance.WhatsNew.Changelogs; - foreach (ChangelogEntry entry in changelogEntries) { - if (!stableRelease || entry.Stable) { - AddChangelogEntry(entry, content); + List changelogs = TMPELifecycle.Instance.WhatsNew.Changelogs; + foreach (Changelog changelog in changelogs) { + if (!stableRelease || changelog.Stable) { + AddChangelogContent(changelog, content); } } + panel.autoLayout = true; content.autoLayout = true; } - private void AddChangelogEntry(ChangelogEntry changelogEntry, UIScrollablePanel uiScrollablePanel) { + private void AddChangelogContent(Changelog changelog, UIScrollablePanel uiScrollablePanel) { UIPanel panel = AddRowAutoLayoutPanel(parentPanel: uiScrollablePanel, panelPadding: new RectOffset(5, 0, 0, 6), panelWidth: _defaultWidth - 5, vertical: true); + AddVersionRow(panel, changelog); - AddVersionRow(panel, changelogEntry); - - foreach (ChangelogEntry.ChangeEntry changeEntry in changelogEntry.ChangeEntries) { - AddBulletPoint(panel, changeEntry, _bulletListPadding); + foreach (Changelog.Item item in changelog.Items) { + AddBulletPoint(panel, item, _bulletListPadding); } AddSeparator(panel); @@ -104,9 +105,9 @@ private void AddSeparator(UIComponent parentPanel) { separator.height = 30; } - private void AddVersionRow(UIComponent parentPanel, ChangelogEntry changelogEntry) { - bool isCurrentVersion = WhatsNew.CurrentVersion.Equals(changelogEntry.Version); - string buildString = StableOrTest(changelogEntry); + private void AddVersionRow(UIComponent parentPanel, Changelog changelog) { + bool isCurrentVersion = WhatsNew.CurrentVersion.Equals(changelog.Version); + string buildString = StableOrTest(changelog); bool wasReleased = !string.IsNullOrEmpty(buildString); // row: [version number] released xyz @@ -117,34 +118,34 @@ private void AddVersionRow(UIComponent parentPanel, ChangelogEntry changelogEntr // part: [version number] // UILabel versionLabel = AddKeywordLabel(versionRow, versionStr, MarkupKeyword.VersionStart); - UIPanel versionLabel = AddVersionLabel(versionRow, changelogEntry, buildString, wasReleased); + UIPanel versionLabel = AddVersionLabel(versionRow, changelog, buildString, wasReleased); versionLabel.name = "Version"; // part released xyz UILabel title = versionRow.AddUIComponent(); title.name = "Released"; - title.text = string.IsNullOrEmpty(changelogEntry.Released) ? "Not released yet" : changelogEntry.Released.TrimStart(); + title.text = string.IsNullOrEmpty(changelog.Released) ? "Not released yet" : changelog.Released.TrimStart(); title.suffix = isCurrentVersion ? " - current version" : string.Empty; title.textScale = 1.3f; title.textColor = _textColor; title.minimumSize = new Vector2(0, 36); title.padding = new RectOffset(16, 0, 0, 0); title.verticalAlignment = UIVerticalAlignment.Middle; - if (!string.IsNullOrEmpty(changelogEntry.Link)) { - SetupLink(title, changelogEntry); + if (!string.IsNullOrEmpty(changelog.Link)) { + SetupLink(title, changelog); } versionRow.autoLayout = true; } - private string StableOrTest(ChangelogEntry changelogEntry) => - changelogEntry.Stable + private string StableOrTest(Changelog changelog) => + changelog.Stable ? " STABLE" - : changelogEntry.Released != null + : changelog.Released != null ? " TEST" : string.Empty; - private void SetupLink(UILabel title, ChangelogEntry changelogEntry) { - string url = $"https://github.com/CitiesSkylinesMods/TMPE/blob/master/CHANGELOG.md#{changelogEntry.Link}"; + private void SetupLink(UILabel title, Changelog changelog) { + string url = $"https://github.com/CitiesSkylinesMods/TMPE/blob/master/CHANGELOG.md#{changelog.Link}"; title.tooltip = url; title.textColor = _linkTextColor; title.eventMouseEnter += (label, _) => ((UILabel)label).textColor = _linkTextColorHover; @@ -152,20 +153,20 @@ private void SetupLink(UILabel title, ChangelogEntry changelogEntry) { title.eventClicked += (_, _) => Application.OpenURL(url); } - private void AddBulletPoint(UIComponent parentPanel, ChangelogEntry.ChangeEntry changeEntry, RectOffset bulletPadding) { + private void AddBulletPoint(UIComponent parentPanel, Changelog.Item item, RectOffset bulletPadding) { // row: [keyword] text what has been changed UIPanel panel = AddRowAutoLayoutPanel(parentPanel: parentPanel, panelPadding: bulletPadding, panelWidth: _defaultWidth - 10); - panel.name = "ChangelogEntry"; + panel.name = "ChangelogItem"; panel.padding = new RectOffset(20, 0, 0, 0); // part: [fixed/updated/removed] - AddKeywordLabel(panel, changeEntry.Keyword.ToString(), changeEntry.Keyword); + AddKeywordLabel(panel, item.Keyword.ToString(), item.Keyword); // part: text UILabel label = panel.AddUIComponent(); - label.name = "ChangelogEntryText"; + label.name = "ChangelogItemText"; label.wordWrap = true; - label.text = changeEntry.Text; + label.text = item.Text; label.textScale = 0.8f; label.textColor = _textColor; label.autoHeight = true; @@ -178,13 +179,13 @@ private void AddBulletPoint(UIComponent parentPanel, ChangelogEntry.ChangeEntry private UILabel AddKeywordLabel(UIPanel panel, string text, MarkupKeyword keyword) { UILabel label = panel.AddUIComponent(); - label.name = "ChangelogEntryKeyword"; + label.name = "ChangelogItemKeyword"; label.text = text.ToUpper(); label.textScale = 0.7f; label.textColor = Color.white; label.backgroundSprite = "TextFieldPanel"; label.colorizeSprites = true; - label.color = WhatsNewMarkup.GetColor(keyword); + label.color = keyword.ToColor(); label.minimumSize = _minKeywordLabelSize; label.textAlignment = UIHorizontalAlignment.Center; label.verticalAlignment = UIVerticalAlignment.Middle; @@ -193,20 +194,20 @@ private UILabel AddKeywordLabel(UIPanel panel, string text, MarkupKeyword keywor return label; } - private UIPanel AddVersionLabel(UIPanel parentPanel, ChangelogEntry changelogEntry, string buildString, bool wasReleased) { + private UIPanel AddVersionLabel(UIPanel parentPanel, Changelog changelog, string buildString, bool wasReleased) { UIPanel panel = AddRowAutoLayoutPanel(parentPanel: parentPanel, panelPadding: _paddingZero, panelWidth: 100, vertical: true); panel.backgroundSprite = "TextFieldPanel"; - panel.color = WhatsNewMarkup.GetColor(MarkupKeyword.VersionStart); + panel.color = MarkupKeyword.VersionStart.ToColor(); panel.minimumSize = new Vector2(75, wasReleased ? 46 : 32); panel.padding = new RectOffset(0, 0, 6, 0); panel.height = wasReleased ? 45 : 36; UILabel version = panel.AddUIComponent(); - version.name = "ChangelogEntryVersionNumber"; - version.text = changelogEntry.Version.ToString(); + version.name = "ChangelogVersionNumber"; + version.text = changelog.Version.ToString(); version.textScale = 1.2f; version.textAlignment = UIHorizontalAlignment.Center; version.verticalAlignment = UIVerticalAlignment.Top; @@ -216,7 +217,7 @@ private UIPanel AddVersionLabel(UIPanel parentPanel, ChangelogEntry changelogEnt if (wasReleased) { UILabel build = panel.AddUIComponent(); - build.name = "ChangelogEntryBuildType"; + build.name = "ChangelogBuildType"; build.text = buildString; build.textScale = 0.7f; build.textAlignment = UIHorizontalAlignment.Center; @@ -253,7 +254,7 @@ private void AddHeader() { title.anchor = UIAnchorStyle.Top; title.textAlignment = UIHorizontalAlignment.Center; title.eventTextChanged += (_, _) => title.CenterToParent(); - title.text = "What's New - " + TrafficManagerMod.ModName; + title.text = $"What's New - {TrafficManagerMod.ModName}"; title.MakePixelPerfect(); var cancel = _header.AddUIComponent(); @@ -273,25 +274,24 @@ private void AddScrollbar(UIComponent parentComponent, UIScrollablePanel scrolla scrollbar.value = 0; scrollbar.incrementAmount = 25; scrollbar.autoHide = true; - scrollbar.width = 10; + scrollbar.width = 4; scrollbar.height = _defaultHeight - _header.height - _footerPanel.height; scrollbar.scrollEasingType = EasingType.BackEaseOut; var trackSprite = scrollbar.AddUIComponent(); trackSprite.relativePosition = Vector2.zero; - trackSprite.autoSize = true; trackSprite.anchor = UIAnchorStyle.All; - trackSprite.size = trackSprite.parent.size; + trackSprite.size = scrollbar.size; trackSprite.fillDirection = UIFillDirection.Vertical; - trackSprite.spriteName = "ScrollbarTrack"; + trackSprite.spriteName = string.Empty; // "ScrollbarTrack"; scrollbar.trackObject = trackSprite; var thumbSprite = trackSprite.AddUIComponent(); thumbSprite.relativePosition = Vector2.zero; thumbSprite.fillDirection = UIFillDirection.Vertical; - thumbSprite.autoSize = true; - thumbSprite.width = thumbSprite.parent.width; - thumbSprite.spriteName = "ScrollbarThumb"; + thumbSprite.size = scrollbar.size; + thumbSprite.spriteName = "ScrollbarTrack"; // "ScrollbarThumb"; + thumbSprite.color = new Color(40, 40, 40); scrollbar.thumbObject = thumbSprite; scrollbar.eventValueChanged += (_, value) => scrollablePanel.scrollPosition = new Vector2(0, value); @@ -317,8 +317,6 @@ private Vector2 GetMaxContentSize() { private void HandleClose() { if (!gameObject) return; - TMPELifecycle.Instance.WhatsNew.MarkAsShown(); - if (UIView.GetModalComponent() == this) { UIView.PopModal(); UIComponent modal = UIView.GetModalComponent();