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;