mirror of
https://github.com/MaSzyna-EU07/StarterNG.git
synced 2026-10-16 23:49:19 +02:00
Show bare keys in settings, fix the depot detail tabs
Drops the vehicle counter from the General tab, and the texture tab stops gluing "label: value" into single lines and uses the same rows as General.
This commit is contained in:
@@ -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"));
|
||||
}
|
||||
}
|
||||
56
StarterNG/Controls/DetailRows.cs
Normal file
56
StarterNG/Controls/DetailRows.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace StarterNG.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// The read-only label/value lines the depot's detail tabs are built from.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
public static class DetailRows
|
||||
{
|
||||
/// <summary>Shown in place of a reading a vehicle does not have.</summary>
|
||||
public const string NoValue = "–";
|
||||
|
||||
/// <summary>
|
||||
/// 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 <paramref name="content"/> makes it a
|
||||
/// read-only line; omitting <paramref name="label"/> indents a bare widget to
|
||||
/// the control column.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public static SettingRow Reading(string label, string? value)
|
||||
{
|
||||
bool empty = string.IsNullOrWhiteSpace(value);
|
||||
return Row(label, value: empty ? NoValue : value, tip: empty ? null : value);
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace StarterNG.Infrastructure.Settings;
|
||||
|
||||
/// <summary>What the settings screen can state about a key without describing it.</summary>
|
||||
/// <param name="Key">The eu07.ini key itself.</param>
|
||||
/// <param name="Default">The value the simulator assumes when the key is absent.</param>
|
||||
/// <param name="Range">"lo-hi" when the starter clamps the value, otherwise null.</param>
|
||||
public readonly record struct SettingKeyInfo(string Key, string? Default, string? Range);
|
||||
|
||||
/// <summary>
|
||||
/// The default and the accepted range of every eu07.ini key the starter writes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Read straight off <see cref="SettingsSerializer"/> - 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 <c>lang</c> means the starter follows the operating system, and an
|
||||
/// absent <c>starter.exe.path</c> means it goes looking for the binary itself.
|
||||
/// Printing "lang = en" beside the language picker would state something untrue.
|
||||
/// </remarks>
|
||||
public static class SettingKeyCatalog
|
||||
{
|
||||
private static readonly Dictionary<string, SettingKeyInfo> 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),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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})";
|
||||
}
|
||||
}
|
||||
@@ -55,15 +55,15 @@
|
||||
|
||||
<!-- Second line: the eu07.ini key this row writes, then the
|
||||
explanation. The key is monospaced and selectable so it
|
||||
can be matched against the file itself. -->
|
||||
can be copied straight into the file, and dimmed so it
|
||||
stays out of the way of the setting itself. -->
|
||||
<StackPanel Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3"
|
||||
Orientation="Horizontal" Spacing="8" Margin="0,3,0,0">
|
||||
<SelectableTextBlock Name="PART_ConfigKey"
|
||||
Text="{TemplateBinding ConfigKey}"
|
||||
FontFamily="Consolas, DejaVu Sans Mono, Liberation Mono, Menlo, Courier New"
|
||||
FontSize="11"
|
||||
Foreground="{StaticResource CarbonAccentBright}"
|
||||
Opacity="0.75"
|
||||
Foreground="{StaticResource CarbonFgDim}"
|
||||
VerticalAlignment="Top" />
|
||||
<TextBlock Name="PART_Description"
|
||||
Text="{TemplateBinding Description}"
|
||||
|
||||
@@ -164,7 +164,7 @@
|
||||
</TabItem>
|
||||
<TabItem Header="{Binding [AboutTexture], Source={x:Static starterNg:App.Loc}}">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Name="consistTexturePanel" Margin="4" Spacing="3" />
|
||||
<StackPanel Name="consistTexturePanel" Margin="4" Grid.IsSharedSizeScope="True" />
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fills a detail panel with what is known about a livery: the skin itself,
|
||||
/// then whatever its credit line carries.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Built from the same <see cref="DetailRows"/> 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.
|
||||
/// </remarks>
|
||||
public void ShowTextureInfo(VehicleTexture texture, StackPanel target)
|
||||
{
|
||||
target.Children.Clear();
|
||||
|
||||
var full = new List<string>();
|
||||
void Row(string label, string? value)
|
||||
var tooltip = new List<string>();
|
||||
|
||||
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<VehicleTexture> CurrentTextures()
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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 <paramref name="content"/> makes it a read-only line; omitting
|
||||
/// <paramref name="label"/> indents a bare widget to the control column.
|
||||
/// </summary>
|
||||
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);
|
||||
|
||||
/// <summary>
|
||||
/// A read-only label/value line. The dash keeps the row from collapsing when a
|
||||
/// vehicle has no reading for the field.
|
||||
/// </summary>
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<SettingRow>())
|
||||
row.ConfigKey = SettingKeyCatalog.Annotate(row.ConfigKey);
|
||||
}
|
||||
|
||||
private void ApplyToUi()
|
||||
{
|
||||
var s = AppServices.Current.Settings;
|
||||
|
||||
Reference in New Issue
Block a user