diff --git a/StarterNG.Tests/SettingKeyCatalogTests.cs b/StarterNG.Tests/SettingKeyCatalogTests.cs deleted file mode 100644 index 94c4853..0000000 --- a/StarterNG.Tests/SettingKeyCatalogTests.cs +++ /dev/null @@ -1,47 +0,0 @@ -using StarterNG.Infrastructure.Settings; - -namespace StarterNG.Tests; - -public class SettingKeyCatalogTests -{ - [Fact] - public void A_clamped_key_is_annotated_with_its_default_and_range() - { - Assert.Equal("dynamiclights = 7 (0-7)", SettingKeyCatalog.Annotate("dynamiclights")); - Assert.Equal("starter.bufferscale = 100 (50-200)", SettingKeyCatalog.Annotate("starter.bufferscale")); - } - - [Fact] - public void An_unclamped_key_is_annotated_with_its_default_alone() - { - Assert.Equal("maxtexturesize = 4096", SettingKeyCatalog.Annotate("maxtexturesize")); - Assert.Equal("vsync = false", SettingKeyCatalog.Annotate("vsync")); - } - - [Fact] - public void A_text_valued_key_is_shown_without_a_default() - { - // An absent lang means "follow the operating system", not "English", and an - // absent exe path means "go and find the binary" - neither has a default - // worth printing. - Assert.Equal("lang", SettingKeyCatalog.Annotate("lang")); - Assert.Equal("starter.exe.path", SettingKeyCatalog.Annotate("starter.exe.path")); - } - - [Fact] - public void A_composite_or_unknown_key_is_shown_as_written() - { - Assert.Equal("shadowtune", SettingKeyCatalog.Annotate("shadowtune")); - Assert.Equal("", SettingKeyCatalog.Annotate(null)); - } - - [Fact] - public void The_annotation_matches_what_the_serializer_actually_defaults_to() - { - var settings = new StarterNG.Domain.Settings.SimulatorSettings(); - new SettingsSerializer().Read(settings, new StarterNG.Classes.ConfigFile()); - - Assert.Equal($"maxtexturesize = {settings.MaxTextureSize}", SettingKeyCatalog.Annotate("maxtexturesize")); - Assert.Equal($"fieldofview = {settings.FieldOfView} (15-75)", SettingKeyCatalog.Annotate("fieldofview")); - } -} diff --git a/StarterNG/Controls/DetailRows.cs b/StarterNG/Controls/DetailRows.cs new file mode 100644 index 0000000..4d325e7 --- /dev/null +++ b/StarterNG/Controls/DetailRows.cs @@ -0,0 +1,56 @@ +using Avalonia.Controls; + +namespace StarterNG.Controls; + +/// +/// The read-only label/value lines the depot's detail tabs are built from. +/// +/// +/// Shared so every tab reads the same way: label in the left column, value in +/// the right, aligned down the panel. Previously each tab built its own rows, +/// and the texture tab glued "label: value" into a single line, which lined up +/// with nothing. +/// +public static class DetailRows +{ + /// Shown in place of a reading a vehicle does not have. + public const string NoValue = "–"; + + /// + /// The one row shape every detail tab uses. The label column negotiates its + /// width through the shared-size group on the host panel, so all rows in a + /// panel line up without anyone naming a pixel count - see the Compact styles + /// in Settings.axaml. Omitting makes it a + /// read-only line; omitting indents a bare widget to + /// the control column. + /// + public static SettingRow Row(string? label = null, Control? content = null, + string? value = null, string? description = null, + string? tip = null) + { + var row = new SettingRow + { + Label = label, + Description = description, + ValueText = value, + Content = content + }; + row.Classes.Add("Compact"); + + if (!string.IsNullOrWhiteSpace(tip)) + ToolTip.SetTip(row, tip); + + return row; + } + + /// + /// A read-only label/value line. The dash keeps the row from collapsing when a + /// vehicle has no reading for the field; the full value is on the tooltip, so + /// a trimmed one is still legible. + /// + public static SettingRow Reading(string label, string? value) + { + bool empty = string.IsNullOrWhiteSpace(value); + return Row(label, value: empty ? NoValue : value, tip: empty ? null : value); + } +} diff --git a/StarterNG/Infrastructure/Settings/SettingKeyCatalog.cs b/StarterNG/Infrastructure/Settings/SettingKeyCatalog.cs deleted file mode 100644 index c5e897b..0000000 --- a/StarterNG/Infrastructure/Settings/SettingKeyCatalog.cs +++ /dev/null @@ -1,127 +0,0 @@ -using System.Collections.Generic; - -namespace StarterNG.Infrastructure.Settings; - -/// What the settings screen can state about a key without describing it. -/// The eu07.ini key itself. -/// The value the simulator assumes when the key is absent. -/// "lo-hi" when the starter clamps the value, otherwise null. -public readonly record struct SettingKeyInfo(string Key, string? Default, string? Range); - -/// -/// The default and the accepted range of every eu07.ini key the starter writes. -/// -/// -/// Read straight off - these are the same -/// literals that method defaults and clamps with - so the settings screen can -/// state a fact about each key rather than a description of it. If a default or -/// a clamp changes in the serializer, it changes here too. -/// -/// Text-valued keys carry no default. The literal the serializer passes to -/// GetString is the last step of a chain, not the value the simulator assumes: -/// an absent lang means the starter follows the operating system, and an -/// absent starter.exe.path means it goes looking for the binary itself. -/// Printing "lang = en" beside the language picker would state something untrue. -/// -public static class SettingKeyCatalog -{ - private static readonly Dictionary Entries = new() - { - ["ai.trainman"] = new("ai.trainman", "true", null), - ["anisotropicfiltering"] = new("anisotropicfiltering", "8", null), - ["async.trainThreads"] = new("async.trainThreads", "0", null), - ["brakespeed"] = new("brakespeed", "3.0", null), - ["brakestep"] = new("brakestep", "1.0", null), - ["compresstex"] = new("compresstex", "true", null), - ["convertmodels"] = new("convertmodels", "0", null), - ["crashdamage"] = new("crashdamage", "true", null), - ["debugmode"] = new("debugmode", "false", null), - ["dynamiclights"] = new("dynamiclights", "7", "0-7"), - ["enabletraction"] = new("enabletraction", "true", null), - ["feedbackmode"] = new("feedbackmode", "1", "0-5"), - ["feedbackport"] = new("feedbackport", "888", null), - ["fieldofview"] = new("fieldofview", "45", "15-75"), - ["fpslimit"] = new("fpslimit", "30", "1-100"), - ["friction"] = new("friction", "1.0", null), - ["fullphysics"] = new("fullphysics", "true", null), - ["fullscreen"] = new("fullscreen", "false", null), - ["fullscreenwindowed"] = new("fullscreenwindowed", "false", null), - ["gfx.drawrange.factor.max"] = new("gfx.drawrange.factor.max", "3.0", null), - ["gfx.envmap.enabled"] = new("gfx.envmap.enabled", "true", null), - ["gfx.extraeffects"] = new("gfx.extraeffects", "true", null), - ["gfx.postfx.chromaticaberration.enabled"] = new("gfx.postfx.chromaticaberration.enabled", "true", null), - ["gfx.postfx.motionblur.enabled"] = new("gfx.postfx.motionblur.enabled", "true", null), - ["gfx.reflections.fidelity"] = new("gfx.reflections.fidelity", "0", "0-2"), - ["gfx.reflections.framerate"] = new("gfx.reflections.framerate", "60", null), - ["gfx.resource.move"] = new("gfx.resource.move", "false", null), - ["gfx.resource.sweep"] = new("gfx.resource.sweep", "true", null), - ["gfx.shadergamma"] = new("gfx.shadergamma", "false", null), - ["gfx.shadow.angle.min"] = new("gfx.shadow.angle.min", "0.2", null), - ["gfx.shadow.rank.cutoff"] = new("gfx.shadow.rank.cutoff", "3", null), - ["gfx.shadows.cab.range"] = new("gfx.shadows.cab.range", "0", null), - ["gfx.skippipeline"] = new("gfx.skippipeline", "false", null), - ["gfx.skiprendering"] = new("gfx.skiprendering", "false", null), - ["gfx.smoke"] = new("gfx.smoke", "true", null), - ["gfx.smoke.fidelity"] = new("gfx.smoke.fidelity", "1", "1-4"), - ["gfx.usegles"] = new("gfx.usegles", "false", null), - ["height"] = new("height", "720", null), - ["inactivepause"] = new("inactivepause", "true", null), - ["input.gamepad"] = new("input.gamepad", "true", null), - ["lang"] = new("lang", null, null), - ["livetraction"] = new("livetraction", "true", null), - ["maxcabtexturesize"] = new("maxcabtexturesize", "4096", null), - ["maxtexturesize"] = new("maxtexturesize", "4096", null), - ["multiplelogs"] = new("multiplelogs", "false", null), - ["multisampling"] = new("multisampling", "2", "0-3"), - ["pause"] = new("pause", "false", null), - ["physicslog"] = new("physicslog", "false", null), - ["python.enabled"] = new("python.enabled", "true", null), - ["python.mipmaps"] = new("python.mipmaps", "true", null), - ["python.threadedupload"] = new("python.threadedupload", "true", null), - ["python.updatetime"] = new("python.updatetime", "200", null), - ["scalespeculars"] = new("scalespeculars", "true", null), - ["shadows"] = new("shadows", "true", null), - ["sound.volume"] = new("sound.volume", "1.0", null), - ["sound.volume.ambient"] = new("sound.volume.ambient", "1.0", null), - ["sound.volume.positional"] = new("sound.volume.positional", "1.0", null), - ["sound.volume.radio"] = new("sound.volume.radio", "0.75", null), - ["sound.volume.vehicle"] = new("sound.volume.vehicle", "1.0", null), - ["soundenabled"] = new("soundenabled", "true", null), - ["splinefidelity"] = new("splinefidelity", "1", "1-4"), - ["starter.autoclose"] = new("starter.autoclose", "false", null), - ["starter.batterydefault"] = new("starter.batterydefault", "0", "0-2"), - ["starter.bufferscale"] = new("starter.bufferscale", "100", "50-200"), - ["starter.drivableonly"] = new("starter.drivableonly", "true", null), - ["starter.exe.auto"] = new("starter.exe.auto", "true", null), - ["starter.exe.path"] = new("starter.exe.path", null, null), - ["starter.expandtree"] = new("starter.expandtree", "false", null), - ["starter.hide.archivalvehicles"] = new("starter.hide.archivalvehicles", "true", null), - ["starter.ignoreirrelevant"] = new("starter.ignoreirrelevant", "false", null), - ["starter.largethumbnails"] = new("starter.largethumbnails", "false", null), - ["starter.last.scenery"] = new("starter.last.scenery", null, null), - ["starter.logmissingvehicles"] = new("starter.logmissingvehicles", "false", null), - ["starter.show.ai"] = new("starter.show.ai", "true", null), - ["starter.show.archival"] = new("starter.show.archival", "true", null), - ["starter.window.maximized"] = new("starter.window.maximized", "false", null), - ["uart"] = new("uart", null, null), - ["uartdebug"] = new("uartdebug", "false", null), - ["uarttune"] = new("uarttune", null, null), - ["usevbo"] = new("usevbo", "true", null), - ["vsync"] = new("vsync", "false", null), - ["width"] = new("width", "1280", null), - }; - - /// - /// The key annotation for a settings row: the key, its default and its range, - /// e.g. "dynamiclights = 7 (0-7)". Empty for a key we know nothing about, and - /// for the composite keys a row only partly owns. - /// - public static string Annotate(string? key) - { - if (string.IsNullOrEmpty(key) || !Entries.TryGetValue(key, out var info)) - return key ?? ""; - - string text = info.Default is null ? info.Key : $"{info.Key} = {info.Default}"; - return info.Range is null ? text : $"{text} ({info.Range})"; - } -} diff --git a/StarterNG/Styles/Settings.axaml b/StarterNG/Styles/Settings.axaml index 4c0be03..268d6ba 100644 --- a/StarterNG/Styles/Settings.axaml +++ b/StarterNG/Styles/Settings.axaml @@ -55,15 +55,15 @@ + can be copied straight into the file, and dimmed so it + stays out of the way of the setting itself. --> - + diff --git a/StarterNG/Views/Depot/VehicleBrowser.cs b/StarterNG/Views/Depot/VehicleBrowser.cs index 2a59880..e67c8b2 100644 --- a/StarterNG/Views/Depot/VehicleBrowser.cs +++ b/StarterNG/Views/Depot/VehicleBrowser.cs @@ -17,6 +17,7 @@ using StarterNG.Domain; using StarterNG.Domain.Sceneries; using StarterNG.Domain.Vehicles; using StarterNG.Application; +using StarterNG.Controls; namespace StarterNG.Views; @@ -411,37 +412,44 @@ public sealed class VehicleBrowser return menu; } + /// + /// Fills a detail panel with what is known about a livery: the skin itself, + /// then whatever its credit line carries. + /// + /// + /// Built from the same as the general tab, so the + /// labels line up in a column instead of each line being its own sentence. + /// Fields the credit line leaves out are dropped rather than shown as + /// blanks - most liveries fill in only a few. + /// public void ShowTextureInfo(VehicleTexture texture, StackPanel target) { target.Children.Clear(); - var full = new List(); - void Row(string label, string? value) + var tooltip = new List(); + + void Reading(string label, string? value) { - if (string.IsNullOrWhiteSpace(value)) return; - string line = $"{label}: {value}"; - full.Add(line); - target.Children.Add(new TextBlock - { - Text = line, - FontSize = 12, - Opacity = 0.85, - TextTrimming = TextTrimming.CharacterEllipsis - }); + if (string.IsNullOrWhiteSpace(value)) + return; + + target.Children.Add(DetailRows.Reading(label, value)); + tooltip.Add($"{label}: {value}"); } - Row(App.Loc["TextureInfo"], Consist.Base(texture.Skinfile)); - var meta = texture.Meta; - if (meta != null) + Reading(App.Loc["TextureInfo"], Consist.Base(texture.Skinfile)); + + if (texture.Meta is { } meta) { - Row(App.Loc["TexOperator"], meta.Operator); - Row(App.Loc["TexStation"], meta.Depot); - Row(App.Loc["TexRevision"], meta.RevisionDate); - Row(App.Loc["TexWorks"], meta.RevisionPlace); - Row(App.Loc["TexAuthor"], meta.TextureAuthor); - Row(App.Loc["TexPhoto"], meta.PhotoAuthor); + Reading(App.Loc["TexOperator"], meta.Operator); + Reading(App.Loc["TexStation"], meta.Depot); + Reading(App.Loc["TexRevision"], meta.RevisionDate); + Reading(App.Loc["TexWorks"], meta.RevisionPlace); + Reading(App.Loc["TexAuthor"], meta.TextureAuthor); + Reading(App.Loc["TexPhoto"], meta.PhotoAuthor); } - ToolTip.SetTip(target, string.Join("\n", full)); + + ToolTip.SetTip(target, string.Join("\n", tooltip)); } public List CurrentTextures() diff --git a/StarterNG/Views/Depot/VehicleDetails.cs b/StarterNG/Views/Depot/VehicleDetails.cs index ba89097..e420df5 100644 --- a/StarterNG/Views/Depot/VehicleDetails.cs +++ b/StarterNG/Views/Depot/VehicleDetails.cs @@ -147,10 +147,6 @@ public sealed class VehicleDetails generalPanel.Children.Add(Reading($"{App.Loc["VMax"]} [km/h]", vmax?.ToString("0", inv))); - if (unit) - generalPanel.Children.Add(Reading(App.Loc["VehicleCount"], - item.Cars.Count.ToString(inv))); - generalPanel.Children.Add(Reading(App.Loc["Model"], car.MmdFile)); generalPanel.Children.Add(Reading(App.Loc["Texture"], car.SkinFile)); } @@ -347,39 +343,12 @@ public sealed class VehicleDetails .Any(s => s.Equals("passengers", StringComparison.OrdinalIgnoreCase)); } - /// - /// The one row shape every detail tab uses. The label column negotiates its width - /// through the shared-size group on the host panel, so all rows in a panel line up - /// without anyone naming a pixel count - see the Compact styles in Settings.axaml. - /// Omitting makes it a read-only line; omitting - /// indents a bare widget to the control column. - /// private static SettingRow Row(string? label = null, Control? content = null, string? value = null, string? description = null, - string? tip = null) - { - var row = new SettingRow - { - Label = label, - Description = description, - ValueText = value, - Content = content - }; - row.Classes.Add("Compact"); - if (!string.IsNullOrWhiteSpace(tip)) - ToolTip.SetTip(row, tip); - return row; - } + string? tip = null) => + DetailRows.Row(label, content, value, description, tip); - /// - /// A read-only label/value line. The dash keeps the row from collapsing when a - /// vehicle has no reading for the field. - /// - private static SettingRow Reading(string label, string? value) - { - bool empty = string.IsNullOrWhiteSpace(value); - return Row(label, value: empty ? "\u2013" : value, tip: empty ? null : value); - } + private static SettingRow Reading(string label, string? value) => DetailRows.Reading(label, value); private static SettingsSection Section(string header, Control content, Control? meta = null) { diff --git a/StarterNG/Views/Settings.axaml.cs b/StarterNG/Views/Settings.axaml.cs index bef3f29..b7a6ea2 100644 --- a/StarterNG/Views/Settings.axaml.cs +++ b/StarterNG/Views/Settings.axaml.cs @@ -15,7 +15,6 @@ using Avalonia.Markup.Xaml.Styling; using Avalonia.Media; using StarterNG.Classes; using StarterNG.Controls; -using StarterNG.Infrastructure.Settings; using StarterNG.Infrastructure; using StarterNG.Services; @@ -65,7 +64,6 @@ public partial class Settings : UserControl, ISettingsCapture AppServices.Current.SettingsStore.RegisterCapture(this); - AnnotateConfigKeys(); PopulateProfileCombo(); ApplyToUi(); @@ -162,19 +160,6 @@ public partial class Settings : UserControl, ISettingsCapture public void ReloadFromSettings() => ApplyToUi(); - /// - /// Expands each row's eu07.ini key into "key = default (range)", so the line - /// under a setting says what the file holds rather than paraphrasing what the - /// setting does. - /// - private void AnnotateConfigKeys() - { - // Logical, not visual: the rows exist as soon as the XAML is loaded, - // while categories that are not on screen yet have no visual tree. - foreach (var row in this.GetLogicalDescendants().OfType()) - row.ConfigKey = SettingKeyCatalog.Annotate(row.ConfigKey); - } - private void ApplyToUi() { var s = AppServices.Current.Settings;