WIP: settings sections, shortcuts window, depot tweaks

This commit is contained in:
maj00r
2026-09-04 20:40:47 +02:00
parent bcf266a167
commit aef086e9f0
35 changed files with 2945 additions and 1104 deletions

2
.vscode/launch.json vendored
View File

@@ -8,7 +8,7 @@
{ {
"name": "C#: Launch Startup Project", "name": "C#: Launch Startup Project",
"type": "dotnet", "type": "dotnet",
"request": "launch" "request": "launch",
} }
] ]
} }

View File

@@ -10,6 +10,7 @@
<FluentTheme /> <FluentTheme />
<materialIcons:MaterialIconStyles /> <materialIcons:MaterialIconStyles />
<StyleInclude Source="avares://Starter/Styles/Carbon.axaml" /> <StyleInclude Source="avares://Starter/Styles/Carbon.axaml" />
<StyleInclude Source="avares://Starter/Styles/Settings.axaml" />
<Style Selector="Button[(chrome|WindowDecorationProperties.ElementRole)=FullScreenButton]"> <Style Selector="Button[(chrome|WindowDecorationProperties.ElementRole)=FullScreenButton]">
<Setter Property="IsVisible" Value="False" /> <Setter Property="IsVisible" Value="False" />

View File

@@ -12,12 +12,27 @@ public sealed class Physics
public double VMax; public double VMax;
public double Length; public double Length;
public string LoadAccepted = ""; public string LoadAccepted = "";
public string LoadQ = "";
public int MaxLoad; public int MaxLoad;
public int AllowedFlagA = 3; public int AllowedFlagA = 3;
public int AllowedFlagB = 3; public int AllowedFlagB = 3;
public string ControlTypeA = ""; public string ControlTypeA = "";
public string ControlTypeB = ""; public string ControlTypeB = "";
public string EngineType = "";
/// <summary>
/// True for the engine types the simulator actually runs its diesel heat model for.
/// Mover.cpp calls dizel_Update - and through it dizel_Heat - only for DieselEngine
/// and DieselElectric; LoadFIZ_EngineDecode maps the legacy "DumbDE" onto
/// DieselElectric. On anything else the coolant temperature struct is written and
/// never read.
/// </summary>
public bool IsDieselEngine =>
EngineType.Equals("DieselEngine", StringComparison.OrdinalIgnoreCase)
|| EngineType.Equals("DieselElectric", StringComparison.OrdinalIgnoreCase)
|| EngineType.Equals("DumbDE", StringComparison.OrdinalIgnoreCase);
private static readonly Dictionary<string, Physics?> Cache = new(StringComparer.OrdinalIgnoreCase); private static readonly Dictionary<string, Physics?> Cache = new(StringComparer.OrdinalIgnoreCase);
private static Dictionary<string, string>? _fizIndex; private static Dictionary<string, string>? _fizIndex;
@@ -85,7 +100,8 @@ public sealed class Physics
private static readonly HashSet<string> KnownKeys = new(StringComparer.OrdinalIgnoreCase) private static readonly HashSet<string> KnownKeys = new(StringComparer.OrdinalIgnoreCase)
{ {
"M", "Vmax", "L", "LoadAccepted", "MaxLoad", "AllowedFlag", "ControlType" "M", "Vmax", "L", "LoadAccepted", "LoadQ", "MaxLoad", "AllowedFlag", "ControlType",
"EngineType"
}; };
private static void Parse(Physics p, string path, string dir, string[]? prms, int depth) private static void Parse(Physics p, string path, string dir, string[]? prms, int depth)
@@ -156,8 +172,12 @@ public sealed class Physics
break; break;
case "load": case "load":
if (key.Equals("LoadAccepted", StringComparison.OrdinalIgnoreCase)) p.LoadAccepted = value; if (key.Equals("LoadAccepted", StringComparison.OrdinalIgnoreCase)) p.LoadAccepted = value;
else if (key.Equals("LoadQ", StringComparison.OrdinalIgnoreCase)) p.LoadQ = value;
else if (key.Equals("MaxLoad", StringComparison.OrdinalIgnoreCase)) p.MaxLoad = (int)ParseD(value, p.MaxLoad); else if (key.Equals("MaxLoad", StringComparison.OrdinalIgnoreCase)) p.MaxLoad = (int)ParseD(value, p.MaxLoad);
break; break;
case "engine":
if (key.Equals("EngineType", StringComparison.OrdinalIgnoreCase)) p.EngineType = value;
break;
case "dimensions": case "dimensions":
if (key.Equals("L", StringComparison.OrdinalIgnoreCase)) p.Length = ParseD(value, p.Length); if (key.Equals("L", StringComparison.OrdinalIgnoreCase)) p.Length = ParseD(value, p.Length);
break; break;

View File

@@ -95,9 +95,8 @@ public sealed class Settings
public int VehiclesVolume = 100; public int VehiclesVolume = 100;
public int PositionalVolume = 100; public int PositionalVolume = 100;
public int AmbientVolume = 100; public int AmbientVolume = 100;
public int VolumePaused = 100;
public bool SkipPipeline; public bool SkipPipeline;
public string PyScreenPriority = "normal"; public int PythonScreenUpdateRate = 200;
public bool DebugLogVisible; public bool DebugLogVisible;
private int _debugLogExtraBits; private int _debugLogExtraBits;
@@ -123,6 +122,13 @@ public sealed class Settings
public bool HideArchivalVehicles = true; public bool HideArchivalVehicles = true;
public string LastScenery = ""; public string LastScenery = "";
/// <summary>
/// Starter's own window state, the way the old one kept WindowMaximized in
/// starter.ini. Only the maximised flag is remembered - a normal window
/// always opens at its design size, centred on the screen.
/// </summary>
public bool WindowMaximized;
public static readonly string[] RenderEngines = public static readonly string[] RenderEngines =
{ "full", "legacy", "simpleshader", "simple", "off", "experimental" }; { "full", "legacy", "simpleshader", "simple", "off", "experimental" };
@@ -339,7 +345,6 @@ public sealed class Settings
public void SaveTo(string path) public void SaveTo(string path)
{ {
WriteToConfig(); WriteToConfig();
ApplyFramebufferFidelity();
try try
{ {
@@ -385,21 +390,6 @@ public sealed class Settings
} }
} }
private void ApplyFramebufferFidelity()
{
int fidelity = BufferScalePercent switch
{
<= 75 => 1,
<= 125 => 2,
<= 175 => 3,
_ => 0
};
if (fidelity > 0)
_config.SetInt("gfx.framebuffer.fidelity", fidelity);
else
_config.Remove("gfx.framebuffer.fidelity");
}
public void DumpMissingVehicleLog() public void DumpMissingVehicleLog()
{ {
if (!LogMissingVehicleFiles) return; if (!LogMissingVehicleFiles) return;
@@ -523,9 +513,10 @@ public sealed class Settings
VehiclesVolume = Clamp((int)Math.Round(c.GetDouble("sound.volume.vehicle", 1.0) * 100), 1, 100); VehiclesVolume = Clamp((int)Math.Round(c.GetDouble("sound.volume.vehicle", 1.0) * 100), 1, 100);
PositionalVolume = Clamp((int)Math.Round(c.GetDouble("sound.volume.positional", 1.0) * 100), 1, 100); PositionalVolume = Clamp((int)Math.Round(c.GetDouble("sound.volume.positional", 1.0) * 100), 1, 100);
AmbientVolume = Clamp((int)Math.Round(c.GetDouble("sound.volume.ambient", 1.0) * 100), 1, 100); AmbientVolume = Clamp((int)Math.Round(c.GetDouble("sound.volume.ambient", 1.0) * 100), 1, 100);
VolumePaused = Clamp((int)Math.Round(c.GetDouble("sound.volume.paused", 1.0) * 100), 0, 100);
SkipPipeline = c.GetBool("gfx.skippipeline", false); SkipPipeline = c.GetBool("gfx.skippipeline", false);
PyScreenPriority = c.GetString("pyscreenrendererpriority", "normal"); PythonScreenUpdateRate = c.Has("python.updatetime")
? Clamp(c.GetInt("python.updatetime", 200), 0, 10000)
: MapOldPyScreenPriority(c.GetString("pyscreenrendererpriority", "normal"));
CompressTextures = c.GetBool("compresstex", true); CompressTextures = c.GetBool("compresstex", true);
ScaleSpeculars = c.GetBool("scalespeculars", true); ScaleSpeculars = c.GetBool("scalespeculars", true);
@@ -547,6 +538,7 @@ public sealed class Settings
ShowArchivalSceneries = c.GetBool("starter.show.archival", true); ShowArchivalSceneries = c.GetBool("starter.show.archival", true);
HideArchivalVehicles = c.GetBool("starter.hide.archivalvehicles", true); HideArchivalVehicles = c.GetBool("starter.hide.archivalvehicles", true);
LastScenery = c.GetString("starter.last.scenery", ""); LastScenery = c.GetString("starter.last.scenery", "");
WindowMaximized = c.GetBool("starter.window.maximized", false);
} }
private static readonly string[] ObsoleteKeys = private static readonly string[] ObsoleteKeys =
@@ -597,6 +589,10 @@ public sealed class Settings
c.SetInt("width", Width); c.SetInt("width", Width);
c.SetInt("height", Height); c.SetInt("height", Height);
c.SetInt("starter.bufferscale", BufferScalePercent); c.SetInt("starter.bufferscale", BufferScalePercent);
int fbw = (int)Math.Round(Width * BufferScalePercent / 100.0);
int fbh = (int)Math.Round(Height * BufferScalePercent / 100.0);
c.SetInt("gfx.framebuffer.width", fbw);
c.SetInt("gfx.framebuffer.height", fbh);
c.SetInt("maxtexturesize", MaxTextureSize); c.SetInt("maxtexturesize", MaxTextureSize);
c.SetInt("maxcabtexturesize", MaxCabTextureSize); c.SetInt("maxcabtexturesize", MaxCabTextureSize);
c.SetInt("anisotropicfiltering", TextureFiltering); c.SetInt("anisotropicfiltering", TextureFiltering);
@@ -648,9 +644,8 @@ public sealed class Settings
c.SetDouble("sound.volume.vehicle", VehiclesVolume / 100.0); c.SetDouble("sound.volume.vehicle", VehiclesVolume / 100.0);
c.SetDouble("sound.volume.positional", PositionalVolume / 100.0); c.SetDouble("sound.volume.positional", PositionalVolume / 100.0);
c.SetDouble("sound.volume.ambient", AmbientVolume / 100.0); c.SetDouble("sound.volume.ambient", AmbientVolume / 100.0);
c.SetDouble("sound.volume.paused", VolumePaused / 100.0);
c.SetBool("gfx.skippipeline", SkipPipeline); c.SetBool("gfx.skippipeline", SkipPipeline);
c.Set("pyscreenrendererpriority", PyScreenPriority); c.SetInt("python.updatetime", PythonScreenUpdateRate);
c.SetBool("compresstex", CompressTextures); c.SetBool("compresstex", CompressTextures);
c.SetBool("scalespeculars", ScaleSpeculars); c.SetBool("scalespeculars", ScaleSpeculars);
@@ -675,6 +670,7 @@ public sealed class Settings
c.Set("starter.last.scenery", LastScenery); c.Set("starter.last.scenery", LastScenery);
else else
c.Remove("starter.last.scenery"); c.Remove("starter.last.scenery");
c.SetBool("starter.window.maximized", WindowMaximized);
} }
private static int Clamp(int v, int lo, int hi) => v < lo ? lo : (v > hi ? hi : v); private static int Clamp(int v, int lo, int hi) => v < lo ? lo : (v > hi ? hi : v);
@@ -689,4 +685,14 @@ public sealed class Settings
return i; return i;
return fallback; return fallback;
} }
private static int MapOldPyScreenPriority(string priority) =>
priority.ToLowerInvariant() switch
{
"lower" => 400,
"lowest" => 800,
"idle" => 1600,
"off" => 0,
_ => 200
};
} }

View File

@@ -188,7 +188,17 @@ public class Dynamic
public const int PantStateMax = 3; public const int PantStateMax = 3;
public int MaxLoad = -1; /// <summary>
/// Lives in the coupling token as ".L&lt;n&gt;" - that is where the simulator reads
/// it from (DynObj.cpp, the MoreParams loop). The older space-separated "L&lt;n&gt;"
/// token in front of the load count is still read for legacy files, but never
/// written back: the current scenario parser reads that slot as the load count.
/// </summary>
public int MaxLoad
{
get => Coupling.MaxLoad;
set => Coupling.MaxLoad = value;
}
public bool HasVelocity; public bool HasVelocity;
@@ -211,6 +221,9 @@ public class Dynamic
{ {
int p = 0; int p = 0;
// Legacy form: max load as its own token in front of the load count. The
// current scenario parser reads that slot as an int, so this is read only -
// the setter moves the value into the coupling token, where it is written.
if (p < t.Count && t[p].Length > 0 && t[p][0] == 'L') if (p < t.Count && t[p].Length > 0 && t[p][0] == 'L')
{ {
if (int.TryParse(t[p].AsSpan(1), NumberStyles.Integer, if (int.TryParse(t[p].AsSpan(1), NumberStyles.Integer,
@@ -239,8 +252,6 @@ public class Dynamic
internal string WriteTrailing() internal string WriteTrailing()
{ {
var sb = new StringBuilder(); var sb = new StringBuilder();
if (MaxLoad >= 0)
sb.Append(" L").Append(MaxLoad.ToString(CultureInfo.InvariantCulture));
bool loaded = !string.IsNullOrEmpty(LoadType); bool loaded = !string.IsNullOrEmpty(LoadType);
sb.Append(' ').Append((loaded ? LoadCount : 0).ToString(CultureInfo.InvariantCulture)); sb.Append(' ').Append((loaded ? LoadCount : 0).ToString(CultureInfo.InvariantCulture));
@@ -263,7 +274,6 @@ public class Dynamic
Offset = Offset, Offset = Offset,
DriverType = DriverType, DriverType = DriverType,
Coupling = Coupling.Clone(), Coupling = Coupling.Clone(),
MaxLoad = MaxLoad,
HasVelocity = HasVelocity, HasVelocity = HasVelocity,
Velocity = Velocity, Velocity = Velocity,
LoadCount = LoadCount, LoadCount = LoadCount,
@@ -386,6 +396,28 @@ public sealed class Coupling
Parameters.Add(wheels.ToParameter()); Parameters.Add(wheels.ToParameter());
} }
/// <summary>
/// Max load override, the ".L&lt;n&gt;" sub-parameter. -1 means "not set", so the
/// vehicle keeps the MaxLoad from its .fiz.
/// </summary>
public int MaxLoad
{
get => Parameters.Find(IsMaxLoad) is { } p
&& int.TryParse(p.AsSpan(1), NumberStyles.Integer,
CultureInfo.InvariantCulture, out int v) ? v : -1;
set
{
Parameters.RemoveAll(IsMaxLoad);
if (value >= 0)
Parameters.Add("L" + value.ToString(CultureInfo.InvariantCulture));
}
}
private static bool IsMaxLoad(string param) =>
param.Length > 1
&& char.ToUpperInvariant(param[0]) == 'L'
&& param.Skip(1).All(char.IsDigit);
public bool ThermoDynamic public bool ThermoDynamic
{ {
get => Parameters.Exists(p => p.Equals("TA", StringComparison.OrdinalIgnoreCase)); get => Parameters.Exists(p => p.Equals("TA", StringComparison.OrdinalIgnoreCase));
@@ -414,11 +446,15 @@ public sealed class Coupling
public sealed class BrakeSetting public sealed class BrakeSetting
{ {
public static readonly string[] Modes = { "G", "P", "R", "M", "Q" }; public static readonly string[] Modes = { "G", "P", "R", "M" };
public static readonly string[] Loads = { "T", "H", "F" }; public static readonly string[] Loads = { "T", "H", "F" };
public static readonly string[] Switches = { "0", "1" }; // The simulator tests every letter of the B parameter independently, so the
// "off" states are a separate axis from the G/P/R/M delay position. They stay
// one combo because they contradict each other, but they no longer block a
// delay position from being picked alongside them (e.g. "BRQ").
public static readonly string[] Switches = { "<>", "0", "1", "X", "E", "Q" };
public string? Mode; public string? Mode;
@@ -426,7 +462,9 @@ public sealed class BrakeSetting
public string? Switch; public string? Switch;
public bool IsEmpty => Mode is null; // A brake switch or a load adaptation with no delay position is still a
// parameter worth writing, so all three axes count towards emptiness.
public bool IsEmpty => Mode is null && Load is null && Switch is null;
public static BrakeSetting? FromParameter(string? param) public static BrakeSetting? FromParameter(string? param)
{ {
@@ -436,7 +474,10 @@ public sealed class BrakeSetting
string body = param.Substring(1).ToUpperInvariant(); string body = param.Substring(1).ToUpperInvariant();
var b = new BrakeSetting var b = new BrakeSetting
{ {
Mode = First(body, "M", "G", "P", "R", "Q"), // "M" first: the simulator reads it as R plus the magnetic rail brake,
// so a bare "R" must not swallow it. Switches are matched in the
// declared order too, so "<>" wins over the digits it cannot contain.
Mode = First(body, "M", "G", "P", "R"),
Load = First(body, Loads), Load = First(body, Loads),
Switch = First(body, Switches) Switch = First(body, Switches)
}; };
@@ -457,9 +498,18 @@ public sealed class WheelSettings
public int FlatnessRand; public int FlatnessRand;
public int FlatnessProb; /// <summary>
/// Chance (%) that the flat spot is applied at all. The simulator defaults it
/// to 100 when the P sub-parameter is missing, so that is the default here too
/// - otherwise "0" would be written as "absent" and mean the exact opposite.
/// </summary>
public int FlatnessProb = DefaultFlatnessProb;
public bool IsEmpty => Sway <= 0 && Flatness <= 0 && FlatnessRand <= 0 && FlatnessProb <= 0; public const int DefaultFlatnessProb = 100;
// A probability on its own does nothing: without a flat size there is nothing
// to roll for.
public bool IsEmpty => Sway <= 0 && Flatness <= 0 && FlatnessRand <= 0;
public static WheelSettings? FromParameter(string? param) public static WheelSettings? FromParameter(string? param)
{ {
@@ -492,7 +542,7 @@ public sealed class WheelSettings
if (Sway > 0) sb.Append('H').Append(Sway); if (Sway > 0) sb.Append('H').Append(Sway);
if (Flatness > 0) sb.Append('F').Append(Flatness); if (Flatness > 0) sb.Append('F').Append(Flatness);
if (FlatnessRand > 0) sb.Append('R').Append(FlatnessRand); if (FlatnessRand > 0) sb.Append('R').Append(FlatnessRand);
if (FlatnessProb > 0) sb.Append('P').Append(FlatnessProb); if (FlatnessProb != DefaultFlatnessProb) sb.Append('P').Append(FlatnessProb);
return sb.ToString(); return sb.ToString();
} }
} }

View File

@@ -0,0 +1,71 @@
using Avalonia;
using Avalonia.Controls;
namespace StarterNG.Controls;
/// <summary>
/// One line of the settings panel: label on the left, the control itself in the
/// middle, a value read-out on the right and an optional description underneath.
/// Replaces the hand-rolled StackPanel/DockPanel pattern that used to be repeated
/// for every setting.
/// </summary>
public class SettingRow : ContentControl
{
public static readonly StyledProperty<string?> LabelProperty =
AvaloniaProperty.Register<SettingRow, string?>(nameof(Label));
public static readonly StyledProperty<string?> DescriptionProperty =
AvaloniaProperty.Register<SettingRow, string?>(nameof(Description));
public static readonly StyledProperty<string?> ValueTextProperty =
AvaloniaProperty.Register<SettingRow, string?>(nameof(ValueText));
public string? Label
{
get => GetValue(LabelProperty);
set => SetValue(LabelProperty, value);
}
public string? Description
{
get => GetValue(DescriptionProperty);
set => SetValue(DescriptionProperty, value);
}
public string? ValueText
{
get => GetValue(ValueTextProperty);
set => SetValue(ValueTextProperty, value);
}
public SettingRow()
{
UpdatePseudoClasses();
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == LabelProperty ||
change.Property == DescriptionProperty ||
change.Property == ValueTextProperty ||
change.Property == ContentProperty)
UpdatePseudoClasses();
}
/// <summary>
/// Drives the template: an empty label lets the control span the label column,
/// an empty description or value collapses that part of the row entirely.
/// </summary>
private void UpdatePseudoClasses()
{
PseudoClasses.Set(":no-label", string.IsNullOrEmpty(Label));
PseudoClasses.Set(":no-description", string.IsNullOrEmpty(Description));
PseudoClasses.Set(":no-value", string.IsNullOrEmpty(ValueText));
// A read-only row is a label and a reading with no widget between them. The
// reading then takes the middle column too, so a long path ellipsises instead
// of demanding its full width from an Auto column and getting cut off.
PseudoClasses.Set(":no-content", Content is null);
}
}

View File

@@ -0,0 +1,21 @@
using Avalonia;
using Avalonia.Controls.Primitives;
namespace StarterNG.Controls;
/// <summary>
/// A group of <see cref="SettingRow"/>s under a flat title with an accent rule.
/// <see cref="Toggle"/> holds an optional master switch shown on the right of the
/// title line; the section content gates itself off it.
/// </summary>
public class SettingsSection : HeaderedContentControl
{
public static readonly StyledProperty<object?> ToggleProperty =
AvaloniaProperty.Register<SettingsSection, object?>(nameof(Toggle));
public object? Toggle
{
get => GetValue(ToggleProperty);
set => SetValue(ToggleProperty, value);
}
}

View File

@@ -0,0 +1,102 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Input;
namespace StarterNG.Infrastructure;
public enum ShortcutScope
{
/// <summary>Works anywhere in the window.</summary>
Global,
/// <summary>Only while the depot page is on screen.</summary>
Depot,
/// <summary>Handled by the consist strip itself, once a vehicle card has focus.</summary>
Consist
}
/// <summary>
/// One row of the shortcut table. <see cref="Key"/> is <see cref="Avalonia.Input.Key.None"/>
/// for rows that only document a gesture handled elsewhere (the consist strip reads the
/// arrow keys itself, they are never dispatched from the window).
/// </summary>
public sealed record Shortcut(
string Id,
string Display,
string LabelKey,
ShortcutScope Scope,
Key Key = Key.None,
KeyModifiers Modifiers = KeyModifiers.None)
{
public bool IsDispatched => Key != Key.None;
public bool Matches(Key key, KeyModifiers modifiers) =>
IsDispatched && Key == key && Modifiers == modifiers;
}
/// <summary>
/// The single source of truth for the starter's own shortcuts: the window dispatches from
/// this table and the cheat sheet is printed from it, so the two cannot drift apart. These
/// are deliberately hardcoded - the configurable key list in Settings belongs to the
/// simulator's cab controls and mixing the two only confuses people.
/// </summary>
public static class Shortcuts
{
public const string Start = "start";
public const string PageScenarios = "page.scenarios";
public const string PageDepot = "page.depot";
public const string PageSettings = "page.settings";
public const string ShowHelp = "help";
public const string AddVehicle = "vehicle.add";
public const string RemoveVehicle = "vehicle.remove";
public const string ClearConsist = "consist.clear";
public const string SetNumber = "vehicle.number";
public const string FocusSearch = "search";
public static readonly IReadOnlyList<Shortcut> All = new[]
{
new Shortcut(Start, "Ctrl+Enter", "Start", ShortcutScope.Global,
Key.Enter, KeyModifiers.Control),
new Shortcut(PageScenarios, "Ctrl+1", "NavScenarios", ShortcutScope.Global,
Key.D1, KeyModifiers.Control),
new Shortcut(PageDepot, "Ctrl+2", "NavDepot", ShortcutScope.Global,
Key.D2, KeyModifiers.Control),
new Shortcut(PageSettings, "Ctrl+3", "NavSettings", ShortcutScope.Global,
Key.D3, KeyModifiers.Control),
new Shortcut(ShowHelp, "F1", "ShortcutShowHelp", ShortcutScope.Global,
Key.F1),
new Shortcut(FocusSearch, "Ctrl+F", "ShortcutFocusSearch", ShortcutScope.Depot,
Key.F, KeyModifiers.Control),
new Shortcut(AddVehicle, "Insert", "ShortcutAddVehicle", ShortcutScope.Depot,
Key.Insert),
new Shortcut(RemoveVehicle, "Delete", "RemoveVehicle", ShortcutScope.Depot,
Key.Delete),
new Shortcut(ClearConsist, "Ctrl+Delete", "ConsistRemoveAll", ShortcutScope.Depot,
Key.Delete, KeyModifiers.Control),
new Shortcut(SetNumber, "Ctrl+N", "ShortcutSetNumber", ShortcutScope.Depot,
Key.N, KeyModifiers.Control),
new Shortcut("consist.prevnext", "← →", "ShortcutPrevNext", ShortcutScope.Consist),
new Shortcut("consist.firstlast", "Home / End", "ShortcutFirstLast", ShortcutScope.Consist),
new Shortcut("consist.member", "↑ ↓", "ShortcutMember", ShortcutScope.Consist),
new Shortcut("consist.move", "Ctrl+← Ctrl+→", "ShortcutMoveVehicle", ShortcutScope.Consist),
new Shortcut("consist.flip", "Space", "TipFlip", ShortcutScope.Consist),
new Shortcut("consist.menu", "Enter", "ShortcutCardMenu", ShortcutScope.Consist)
};
public static string? Match(Key key, KeyModifiers modifiers) =>
All.FirstOrDefault(s => s.Matches(key, modifiers))?.Id;
public static IEnumerable<Shortcut> InScope(ShortcutScope scope) =>
All.Where(s => s.Scope == scope);
public static string ScopeLabel(ShortcutScope scope) => scope switch
{
ShortcutScope.Depot => App.Loc["NavDepot"],
ShortcutScope.Consist => App.Loc["Consist"],
_ => App.Loc["ShortcutsGlobal"]
};
}

View File

@@ -10,13 +10,16 @@ public static class StatsBar
public static void Fill(Panel host, IReadOnlyList<(string Label, string Value)> fields) public static void Fill(Panel host, IReadOnlyList<(string Label, string Value)> fields)
{ {
host.Children.Clear(); host.Children.Clear();
foreach (var (label, value) in fields) for (int i = 0; i < fields.Count; i++)
{ {
var (label, value) = fields[i];
var pair = new StackPanel var pair = new StackPanel
{ {
Orientation = Orientation.Horizontal, Orientation = Orientation.Horizontal,
Spacing = 5, Spacing = 5,
Margin = new Avalonia.Thickness(0, 0, 22, 0), // no trailing gap on the last pair - these sit in a panel title,
// flush with its right edge
Margin = new Avalonia.Thickness(0, 0, i == fields.Count - 1 ? 0 : 22, 0),
VerticalAlignment = VerticalAlignment.Center VerticalAlignment = VerticalAlignment.Center
}; };
pair.Children.Add(new TextBlock pair.Children.Add(new TextBlock

View File

@@ -0,0 +1,44 @@
using System;
using System.Globalization;
using System.Text;
namespace StarterNG.Infrastructure;
/// <summary>
/// Case- and diacritic-insensitive substring matching, so that typing
/// "rozdzielczosc" in a search box still finds "Rozdzielczość".
/// </summary>
public static class TextMatch
{
public static bool Contains(string? haystack, string? needle)
{
if (string.IsNullOrEmpty(needle))
return true;
if (string.IsNullOrEmpty(haystack))
return false;
return Fold(haystack).Contains(Fold(needle), StringComparison.Ordinal);
}
/// <summary>
/// Lower-cases and strips combining marks, so "Ż" and "z" compare equal.
/// Polish "ł" has no decomposed form, so it is mapped by hand.
/// </summary>
public static string Fold(string text)
{
if (string.IsNullOrEmpty(text))
return string.Empty;
var decomposed = text.ToLowerInvariant().Normalize(NormalizationForm.FormD);
var sb = new StringBuilder(decomposed.Length);
foreach (char c in decomposed)
{
if (CharUnicodeInfo.GetUnicodeCategory(c) == UnicodeCategory.NonSpacingMark)
continue;
sb.Append(c == 'ł' ? 'l' : c);
}
return sb.ToString().Normalize(NormalizationForm.FormC);
}
}

View File

@@ -10,7 +10,7 @@
xmlns:starterNg="clr-namespace:StarterNG" xmlns:starterNg="clr-namespace:StarterNG"
Title="Starter MaSzyna" Title="Starter MaSzyna"
Icon="/Assets/starter.ico" Icon="/Assets/starter.ico"
Width="1010" Height="920" MinWidth="1010" MinHeight="920" Width="1010" Height="768" MinWidth="1010" MinHeight="666"
WindowStartupLocation="CenterScreen"> WindowStartupLocation="CenterScreen">
<DockPanel LastChildFill="True"> <DockPanel LastChildFill="True">
@@ -20,7 +20,7 @@
BorderBrush="{StaticResource CarbonBorderLight}" BorderBrush="{StaticResource CarbonBorderLight}"
BorderThickness="0,0,0,1" BorderThickness="0,0,0,1"
PointerPressed="TitleBar_OnPointerPressed"> PointerPressed="TitleBar_OnPointerPressed">
<Grid RowDefinitions="50"> <Grid RowDefinitions="44">
<Grid ColumnDefinitions="Auto,*,Auto" Background="Transparent"> <Grid ColumnDefinitions="Auto,*,Auto" Background="Transparent">
@@ -53,9 +53,9 @@
VerticalAlignment="Center" Margin="0,0,3,0" Spacing="0"> VerticalAlignment="Center" Margin="0,0,3,0" Spacing="0">
<StackPanel Orientation="Vertical" VerticalAlignment="Center" Margin="0,0,6,0"> <StackPanel Orientation="Vertical" VerticalAlignment="Center" Margin="0,0,6,0">
<Label Content="{Binding [Version], Source={x:Static starterNg:App.Loc}}" <Label Content="{Binding [Version], Source={x:Static starterNg:App.Loc}}"
FontSize="10" Foreground="{StaticResource CarbonFgDim}" Padding="0" /> FontSize="12" Foreground="{StaticResource CarbonFgDim}" Padding="0" />
<Label Content="{x:Static starterNg:AppInfo.Version}" <Label Content="{x:Static starterNg:AppInfo.Version}"
FontSize="10" Foreground="{StaticResource CarbonFg}" Padding="0" /> FontSize="12" Foreground="{StaticResource CarbonFg}" Padding="0" />
</StackPanel> </StackPanel>
<ComboBox x:Name="LangCombo" Width="88" VerticalAlignment="Center" <ComboBox x:Name="LangCombo" Width="88" VerticalAlignment="Center"
@@ -85,10 +85,11 @@
</Border> </Border>
<Button Name="startButton" DockPanel.Dock="Bottom" Classes="Start" Height="42" Margin="3" <Button Name="startButton" DockPanel.Dock="Bottom" Classes="Start" Height="42" Margin="3"
TabIndex="9"
Click="StartButton_OnClick" Click="StartButton_OnClick"
Content="{Binding [Start], Source={x:Static starterNg:App.Loc}}" /> Content="{Binding [Start], Source={x:Static starterNg:App.Loc}}" />
<Panel Margin="3,3,3,0"> <Panel Margin="3,3,3,0" TabIndex="1">
<views:Scenarios x:Name="ScenariosView" /> <views:Scenarios x:Name="ScenariosView" />
<views:Depot x:Name="DepotView" IsVisible="False" /> <views:Depot x:Name="DepotView" IsVisible="False" />
<views:Settings x:Name="SettingsView" IsVisible="False" /> <views:Settings x:Name="SettingsView" IsVisible="False" />

View File

@@ -10,6 +10,7 @@ using Avalonia.Controls;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Threading; using Avalonia.Threading;
using Avalonia.VisualTree;
using StarterNG.Classes; using StarterNG.Classes;
using StarterNG.Domain; using StarterNG.Domain;
using StarterNG.Infrastructure; using StarterNG.Infrastructure;
@@ -34,6 +35,15 @@ public partial class MainWindow : Window
ExtendClientAreaToDecorationsHint = true; ExtendClientAreaToDecorationsHint = true;
ExtendClientAreaTitleBarHeightHint = TitleBarHeight; ExtendClientAreaTitleBarHeightHint = TitleBarHeight;
if (SettingsModel.Instance.WindowMaximized)
WindowState = WindowState.Maximized;
PropertyChanged += (_, e) =>
{
if (e.Property == WindowStateProperty && WindowState != WindowState.Minimized)
SettingsModel.Instance.WindowMaximized = WindowState == WindowState.Maximized;
};
if (OperatingSystem.IsWindows()) if (OperatingSystem.IsWindows())
TopBar.Padding = new Thickness(0, TitleBarHeight, 0, 0); TopBar.Padding = new Thickness(0, TitleBarHeight, 0, 0);
@@ -43,6 +53,12 @@ public partial class MainWindow : Window
startButton.AddHandler(PointerReleasedEvent, StartButton_OnRightClick, startButton.AddHandler(PointerReleasedEvent, StartButton_OnRightClick,
RoutingStrategies.Tunnel); RoutingStrategies.Tunnel);
SettingsView.ThumbnailSizeChanged += () =>
{
DepotView.RefreshConsistView();
ScenariosView.RefreshConsistView();
};
ScenariosView.RandomizeTexturesRequested += async scenery => ScenariosView.RandomizeTexturesRequested += async scenery =>
{ {
await DepotView.RandomizeSceneryTrainsetsAsync(scenery); await DepotView.RandomizeSceneryTrainsetsAsync(scenery);
@@ -54,6 +70,10 @@ public partial class MainWindow : Window
ApplyInitialNav(); ApplyInitialNav();
// Tunnelling: the shortcuts are checked before any view sees the key, which is
// why OnShortcutKey has to be careful about text entry and key capture.
AddHandler(KeyDownEvent, OnShortcutKey, RoutingStrategies.Tunnel);
AppState.Instance.Changed += UpdateStartButton; AppState.Instance.Changed += UpdateStartButton;
Opened += (_, _) => Opened += (_, _) =>
{ {
@@ -89,6 +109,61 @@ public partial class MainWindow : Window
e.Handled = true; e.Handled = true;
} }
private void OnShortcutKey(object? sender, KeyEventArgs e)
{
// A cab-control binding in Settings is waiting for this very keypress.
if (SettingsView?.IsCapturingKey == true)
return;
var mods = e.KeyModifiers & (KeyModifiers.Control | KeyModifiers.Shift | KeyModifiers.Alt);
if (Shortcuts.Match(e.Key, mods) is not { } id)
return;
// Bare keys belong to whatever is being typed into; Ctrl/Alt combinations do not
// collide with text entry, so those stay live everywhere.
if (mods == KeyModifiers.None && IsTextEntryFocused())
return;
switch (id)
{
case Shortcuts.Start:
_ = LaunchAsync(saveSettings: true, freeFly: false);
break;
case Shortcuts.PageScenarios: NavScenarios.IsChecked = true; break;
case Shortcuts.PageDepot: NavDepot.IsChecked = true; break;
case Shortcuts.PageSettings: NavSettings.IsChecked = true; break;
case Shortcuts.ShowHelp:
Views.ShortcutsWindow.Create().ShowDialog(this);
break;
default:
if (!DepotView.IsVisible)
return;
switch (id)
{
case Shortcuts.FocusSearch: DepotView.KeyFocusSearch(); break;
case Shortcuts.AddVehicle: DepotView.KeyAddVehicle(); break;
case Shortcuts.RemoveVehicle: DepotView.KeyRemoveVehicle(); break;
case Shortcuts.ClearConsist: DepotView.KeyClearConsist(); break;
case Shortcuts.SetNumber: DepotView.KeySetWagonNumber(); break;
default: return;
}
break;
}
e.Handled = true;
}
/// <summary>
/// Anything that swallows plain typing: text boxes, but also the editable inner box
/// of a NumericUpDown or AutoCompleteBox.
/// </summary>
private bool IsTextEntryFocused() =>
FocusManager?.GetFocusedElement() is Visual focused
&& (focused is TextBox || focused.FindAncestorOfType<TextBox>() != null);
private void Nav_OnCheckedChanged(object? sender, RoutedEventArgs e) private void Nav_OnCheckedChanged(object? sender, RoutedEventArgs e)
{ {
if (sender is not RadioButton { IsChecked: true } rb) if (sender is not RadioButton { IsChecked: true } rb)
@@ -164,6 +239,15 @@ public partial class MainWindow : Window
{ {
var menu = new ContextMenu(); var menu = new ContextMenu();
var keys = new MenuItem
{
Header = App.Loc["ShortcutsTitle"],
InputGesture = new KeyGesture(Key.F1)
};
keys.Click += (_, _) => Views.ShortcutsWindow.Create().ShowDialog(this);
menu.Items.Add(keys);
menu.Items.Add(new Separator());
var wiki = new MenuItem { Header = App.Loc["HelpWiki"] }; var wiki = new MenuItem { Header = App.Loc["HelpWiki"] };
wiki.Click += (_, _) => OpenPathOrUrl("https://wiki.eu07.pl/"); wiki.Click += (_, _) => OpenPathOrUrl("https://wiki.eu07.pl/");
menu.Items.Add(wiki); menu.Items.Add(wiki);
@@ -190,6 +274,13 @@ public partial class MainWindow : Window
} }
} }
// "About" belongs with the other informational entries, not next to the
// Save/Reset buttons of the settings panel where it used to sit.
menu.Items.Add(new Separator());
var about = new MenuItem { Header = App.Loc["About"] };
about.Click += (_, _) => Views.AboutWindow.Create().ShowDialog(this);
menu.Items.Add(about);
menu.Open(HelpButton); menu.Open(HelpButton);
} }

View File

@@ -2,7 +2,7 @@
"profiles": { "profiles": {
"StarterNG": { "StarterNG": {
"commandName": "Project", "commandName": "Project",
"workingDirectory": "/home/maj00r/.local/share/Steam/steamapps/common/MaSzyna/" "workingDirectory": "/home/maj00r/.local/share/Steam/steamapps/common/_MaSzyna/"
} }
} }
} }

View File

@@ -23,6 +23,14 @@ public class LocalizationService : INotifyPropertyChanged
private readonly HashSet<string> _reportedMisses = new(StringComparer.Ordinal); private readonly HashSet<string> _reportedMisses = new(StringComparer.Ordinal);
/// <summary>
/// Keys with this suffix hold the optional one-line explanation shown under a
/// setting. They are only authored in en/pl, so a miss yields an empty string
/// (which collapses the description line) instead of the raw key, and is not
/// reported as a translation gap.
/// </summary>
private const string OptionalSuffix = "Desc";
public string this[string key] public string this[string key]
{ {
get get
@@ -30,6 +38,9 @@ public class LocalizationService : INotifyPropertyChanged
if (_strings.TryGetValue(key, out var value)) if (_strings.TryGetValue(key, out var value))
return value; return value;
if (key.EndsWith(OptionalSuffix, StringComparison.Ordinal))
return string.Empty;
lock (_reportedMisses) lock (_reportedMisses)
if (_reportedMisses.Add(key)) if (_reportedMisses.Add(key))
Infrastructure.Diagnostics.Log($"lang/{CurrentLangCode}: missing key \"{key}\""); Infrastructure.Diagnostics.Log($"lang/{CurrentLangCode}: missing key \"{key}\"");
@@ -38,6 +49,13 @@ public class LocalizationService : INotifyPropertyChanged
} }
} }
/// <summary>
/// Looks up a key that may legitimately be absent, without logging a miss.
/// Returns an empty string when the current language does not define it.
/// </summary>
public string Opt(string key) =>
_strings.TryGetValue(key, out var value) ? value : string.Empty;
public readonly record struct LanguageInfo(string Code, string Name, string Path); public readonly record struct LanguageInfo(string Code, string Name, string Path);
public static IReadOnlyList<LanguageInfo> AvailableLanguages() public static IReadOnlyList<LanguageInfo> AvailableLanguages()

View File

@@ -11,6 +11,10 @@
<SolidColorBrush x:Key="CarbonPanelBg" Color="#1E2327" /> <SolidColorBrush x:Key="CarbonPanelBg" Color="#1E2327" />
<SolidColorBrush x:Key="CarbonHeaderBg" Color="#2A3036" /> <SolidColorBrush x:Key="CarbonHeaderBg" Color="#2A3036" />
<SolidColorBrush x:Key="CarbonHoverBg" Color="#2A3036" /> <SolidColorBrush x:Key="CarbonHoverBg" Color="#2A3036" />
<!-- Control metrics live here, not in the views. -->
<x:Double x:Key="CarbonControlHeight">24</x:Double>
<x:Double x:Key="CarbonCheckSize">16</x:Double>
<SolidColorBrush x:Key="CarbonInputBg" Color="#262C31" /> <SolidColorBrush x:Key="CarbonInputBg" Color="#262C31" />
<SolidColorBrush x:Key="CarbonBorderLight" Color="#3A424A" /> <SolidColorBrush x:Key="CarbonBorderLight" Color="#3A424A" />
<SolidColorBrush x:Key="CarbonBorderDark" Color="#0E0F12" /> <SolidColorBrush x:Key="CarbonBorderDark" Color="#0E0F12" />
@@ -32,7 +36,7 @@
Background="{StaticResource CarbonHeaderBg}" Background="{StaticResource CarbonHeaderBg}"
BorderBrush="{StaticResource CarbonBorderLight}" BorderBrush="{StaticResource CarbonBorderLight}"
BorderThickness="0,0,0,1" BorderThickness="0,0,0,1"
Padding="8,4"> Padding="8,3">
<DockPanel LastChildFill="True"> <DockPanel LastChildFill="True">
<ContentPresenter DockPanel.Dock="Right" <ContentPresenter DockPanel.Dock="Right"
Content="{TemplateBinding Tag}" Content="{TemplateBinding Tag}"
@@ -44,19 +48,171 @@
</DockPanel> </DockPanel>
</Border> </Border>
<ContentPresenter Content="{TemplateBinding Content}" <ContentPresenter Content="{TemplateBinding Content}"
Margin="8" /> Margin="6" />
</DockPanel> </DockPanel>
</Border> </Border>
</ControlTemplate> </ControlTemplate>
</Setter> </Setter>
</ControlTheme> </ControlTheme>
<ControlTheme x:Key="{x:Type CheckBox}" TargetType="CheckBox">
<Setter Property="Foreground" Value="{StaticResource CarbonFg}" />
<Setter Property="FontSize" Value="12" />
<Setter Property="MinHeight" Value="0" />
<Setter Property="Padding" Value="8,0,0,0" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="Template">
<ControlTemplate>
<Grid ColumnDefinitions="Auto,*" Background="Transparent">
<Border Name="box"
Width="{StaticResource CarbonCheckSize}"
Height="{StaticResource CarbonCheckSize}"
VerticalAlignment="Center"
Background="{StaticResource CarbonInputBg}"
BorderBrush="{StaticResource CarbonBorderLight}"
BorderThickness="1" CornerRadius="2">
<Path Name="glyph" Margin="3" Stretch="Uniform"
Data="M 0,4 L 3,7 L 9,0"
Stroke="White" StrokeThickness="2"
StrokeLineCap="Round" StrokeJoin="Round"
IsVisible="False" />
</Border>
<ContentPresenter Name="PART_ContentPresenter" Grid.Column="1"
Margin="{TemplateBinding Padding}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
VerticalAlignment="Center" />
</Grid>
</ControlTemplate>
</Setter>
<Style Selector="^:pointerover /template/ Border#box">
<Setter Property="BorderBrush" Value="{StaticResource CarbonAccentHover}" />
</Style>
<Style Selector="^:checked /template/ Border#box">
<Setter Property="Background" Value="{StaticResource CarbonAccent}" />
<Setter Property="BorderBrush" Value="{StaticResource CarbonAccent}" />
</Style>
<Style Selector="^:checked /template/ Path#glyph">
<Setter Property="IsVisible" Value="True" />
</Style>
<Style Selector="^:indeterminate /template/ Border#box">
<Setter Property="Background" Value="{StaticResource CarbonAccent}" />
<Setter Property="BorderBrush" Value="{StaticResource CarbonAccent}" />
</Style>
<Style Selector="^:disabled">
<Setter Property="Opacity" Value="0.45" />
</Style>
</ControlTheme>
<ControlTheme x:Key="SpinnerButton" TargetType="RepeatButton">
<Setter Property="Width" Value="20" />
<Setter Property="MinWidth" Value="0" />
<Setter Property="MinHeight" Value="0" />
<Setter Property="Padding" Value="0" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Template">
<ControlTemplate>
<Border Name="root" Background="Transparent">
<ContentPresenter Content="{TemplateBinding Content}"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
</ControlTemplate>
</Setter>
<Style Selector="^:pointerover /template/ Border#root">
<Setter Property="Background" Value="{StaticResource CarbonBorderLight}" />
</Style>
</ControlTheme>
<!-- The spinner frame, so the field and its two buttons read as one control
the same shape and height as a combo box. -->
<ControlTheme x:Key="{x:Type ButtonSpinner}" TargetType="ButtonSpinner">
<Setter Property="Background" Value="{StaticResource CarbonInputBg}" />
<Setter Property="BorderBrush" Value="{StaticResource CarbonBorderLight}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="2" />
<Setter Property="MinHeight" Value="{StaticResource CarbonControlHeight}" />
<Setter Property="Padding" Value="0" />
<Setter Property="Template">
<ControlTemplate>
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"
MinHeight="{TemplateBinding MinHeight}">
<Grid ColumnDefinitions="*,Auto,Auto">
<ContentPresenter Name="PART_ContentPresenter" Grid.Column="0"
Content="{TemplateBinding Content}"
VerticalContentAlignment="Center"
HorizontalContentAlignment="Stretch" />
<RepeatButton Name="PART_IncreaseButton" Grid.Column="1"
Theme="{StaticResource SpinnerButton}"
IsVisible="{TemplateBinding ShowButtonSpinner}">
<Path Data="M 0,4 L 4,0 L 8,4" Stroke="{StaticResource CarbonFgDim}"
StrokeThickness="1.4" />
</RepeatButton>
<RepeatButton Name="PART_DecreaseButton" Grid.Column="2"
Theme="{StaticResource SpinnerButton}"
IsVisible="{TemplateBinding ShowButtonSpinner}">
<Path Data="M 0,0 L 4,4 L 8,0" Stroke="{StaticResource CarbonFgDim}"
StrokeThickness="1.4" />
</RepeatButton>
</Grid>
</Border>
</ControlTemplate>
</Setter>
</ControlTheme>
<ControlTheme x:Key="{x:Type NumericUpDown}" TargetType="NumericUpDown">
<Setter Property="MinHeight" Value="0" />
<Setter Property="FontSize" Value="12" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Template">
<ControlTemplate>
<DataValidationErrors>
<!-- The spinner draws the frame, the text box sits inside it
borderless - same shape as a combo box. -->
<ButtonSpinner Name="PART_Spinner"
MinHeight="{StaticResource CarbonControlHeight}"
Padding="0"
BorderThickness="1"
BorderBrush="{StaticResource CarbonBorderLight}"
Background="{StaticResource CarbonInputBg}"
CornerRadius="2"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Center"
AllowSpin="{TemplateBinding AllowSpin}"
ShowButtonSpinner="{TemplateBinding ShowButtonSpinner}"
ButtonSpinnerLocation="{TemplateBinding ButtonSpinnerLocation}">
<TextBox Name="PART_TextBox"
MinHeight="0"
Padding="6,0"
BorderThickness="0"
Background="Transparent"
HorizontalAlignment="Stretch"
VerticalContentAlignment="Center"
Text="{TemplateBinding Text, Mode=TwoWay}"
Watermark="{TemplateBinding Watermark}"
IsReadOnly="{TemplateBinding IsReadOnly}"
AcceptsReturn="False"
TextWrapping="NoWrap" />
</ButtonSpinner>
</DataValidationErrors>
</ControlTemplate>
</Setter>
</ControlTheme>
<ControlTheme x:Key="NavTab" TargetType="RadioButton"> <ControlTheme x:Key="NavTab" TargetType="RadioButton">
<Setter Property="Foreground" Value="{StaticResource CarbonFgDim}" /> <Setter Property="Foreground" Value="{StaticResource CarbonFgDim}" />
<Setter Property="FontWeight" Value="SemiBold" /> <Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="FontSize" Value="14" /> <Setter Property="FontSize" Value="14" />
<Setter Property="MinWidth" Value="150" /> <Setter Property="MinWidth" Value="150" />
<Setter Property="MinHeight" Value="50" /> <Setter Property="MinHeight" Value="44" />
<Setter Property="Cursor" Value="Hand" /> <Setter Property="Cursor" Value="Hand" />
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate> <ControlTemplate>
@@ -80,9 +236,9 @@
</Style> </Style>
</ControlTheme> </ControlTheme>
<ControlTheme x:Key="{x:Type TabItem}" TargetType="TabItem"> <ControlTheme x:Key="{x:Type TabItem}" TargetType="TabItem">
<Setter Property="FontSize" Value="13" /> <Setter Property="FontSize" Value="12" />
<Setter Property="MinHeight" Value="26" /> <Setter Property="MinHeight" Value="22" />
<Setter Property="Padding" Value="14,4" /> <Setter Property="Padding" Value="14,2" />
<Setter Property="Margin" Value="0" /> <Setter Property="Margin" Value="0" />
<Setter Property="Foreground" Value="{StaticResource CarbonFgDim}" /> <Setter Property="Foreground" Value="{StaticResource CarbonFgDim}" />
<Setter Property="VerticalContentAlignment" Value="Center" /> <Setter Property="VerticalContentAlignment" Value="Center" />
@@ -128,7 +284,7 @@
<Setter Property="Background" Value="{StaticResource CarbonAppBg}" /> <Setter Property="Background" Value="{StaticResource CarbonAppBg}" />
<Setter Property="Foreground" Value="{StaticResource CarbonFg}" /> <Setter Property="Foreground" Value="{StaticResource CarbonFg}" />
<Setter Property="FontFamily" Value="Tahoma, Segoe UI, Inter" /> <Setter Property="FontFamily" Value="Tahoma, Segoe UI, Inter" />
<Setter Property="FontSize" Value="13" /> <Setter Property="FontSize" Value="12" />
</Style> </Style>
<Style Selector="TabControl"> <Style Selector="TabControl">
@@ -137,50 +293,43 @@
</Style> </Style>
<Style Selector="ComboBox"> <Style Selector="ComboBox">
<Setter Property="MinHeight" Value="24" /> <Setter Property="MinHeight" Value="{StaticResource CarbonControlHeight}" />
<Setter Property="FontSize" Value="13" /> <Setter Property="FontSize" Value="12" />
<Setter Property="Padding" Value="8,1" /> <Setter Property="Padding" Value="8,1" />
<Setter Property="CornerRadius" Value="2" /> <Setter Property="CornerRadius" Value="2" />
</Style> </Style>
<Style Selector="TextBox"> <Style Selector="TextBox">
<Setter Property="MinHeight" Value="24" /> <Setter Property="MinHeight" Value="{StaticResource CarbonControlHeight}" />
<Setter Property="FontSize" Value="13" /> <Setter Property="FontSize" Value="12" />
<Setter Property="Padding" Value="6,1" /> <Setter Property="Padding" Value="6,1" />
<Setter Property="CornerRadius" Value="2" /> <Setter Property="CornerRadius" Value="2" />
</Style> </Style>
<Style Selector="NumericUpDown"> <!-- Fluent gives the spinner and the check controls a 32 px floor of their own,
<Setter Property="MinHeight" Value="26" /> inside the template, so setting MinHeight on the control alone does nothing. -->
<Setter Property="FontSize" Value="13" /> <!-- Height comes from the templated TextBox, like every other input. -->
</Style>
<Style Selector="Button"> <Style Selector="Button">
<Setter Property="MinHeight" Value="25" /> <Setter Property="MinHeight" Value="25" />
<Setter Property="FontSize" Value="13" /> <Setter Property="FontSize" Value="12" />
<Setter Property="Padding" Value="10,2" /> <Setter Property="Padding" Value="10,2" />
<Setter Property="CornerRadius" Value="2" /> <Setter Property="CornerRadius" Value="2" />
</Style> </Style>
<Style Selector="CheckBox"> <!-- Checklist rows only sit tighter; the size comes from the theme above. -->
<Setter Property="MinHeight" Value="17" />
<Setter Property="FontSize" Value="13" />
</Style>
<Style Selector="CheckBox.Checklist"> <Style Selector="CheckBox.Checklist">
<Setter Property="MinHeight" Value="0" />
<Setter Property="Height" Value="26" />
<Setter Property="FontSize" Value="12" />
<Setter Property="Padding" Value="6,0,0,0" /> <Setter Property="Padding" Value="6,0,0,0" />
</Style> <Setter Property="Margin" Value="0,1" />
<Style Selector="CheckBox.Checklist /template/ Grid">
<Setter Property="MinHeight" Value="0" />
</Style> </Style>
<Style Selector="CheckBox /template/ ContentPresenter"> <Style Selector="CheckBox /template/ ContentPresenter">
<Setter Property="TextBlock.TextWrapping" Value="Wrap" /> <Setter Property="TextBlock.TextWrapping" Value="Wrap" />
</Style> </Style>
<Style Selector="RadioButton"> <Style Selector="RadioButton">
<Setter Property="MinHeight" Value="17" /> <Setter Property="MinHeight" Value="0" />
<Setter Property="FontSize" Value="13" /> <Setter Property="FontSize" Value="12" />
</Style>
<Style Selector="RadioButton /template/ Grid:not(#RootGrid)">
<Setter Property="Height" Value="20" />
</Style> </Style>
<Style Selector="Label"> <Style Selector="Label">
<Setter Property="FontSize" Value="13" /> <Setter Property="FontSize" Value="12" />
<Setter Property="Padding" Value="0,2" /> <Setter Property="Padding" Value="0,2" />
</Style> </Style>
@@ -235,6 +384,33 @@
<Setter Property="TextBlock.Foreground" Value="{StaticResource CarbonFg}" /> <Setter Property="TextBlock.Foreground" Value="{StaticResource CarbonFg}" />
</Style> </Style>
<!-- Secondary button: says "this is here, but it is not the main move". Used for
actions whose scope is wider than the thing you have selected. The state rules
have to target the presenter, not the button - Fluent paints PART_ContentPresenter
on hover/press/disable and would otherwise fill an outline button solid. That is
why Accent and Basic are written this way too, while Flat gets away with setting
Background on the control itself. -->
<Style Selector="Button.Outline">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{StaticResource CarbonBorderLight}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="2" />
<Setter Property="Foreground" Value="{StaticResource CarbonFgDim}" />
</Style>
<Style Selector="Button.Outline /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="Transparent" />
</Style>
<Style Selector="Button.Outline:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource CarbonHoverBg}" />
<Setter Property="TextBlock.Foreground" Value="{StaticResource CarbonFg}" />
</Style>
<Style Selector="Button.Outline:pressed /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource CarbonInputBg}" />
</Style>
<Style Selector="Button.Outline:disabled /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="Transparent" />
</Style>
<Style Selector="StackPanel.Field"> <Style Selector="StackPanel.Field">
<Setter Property="Spacing" Value="2" /> <Setter Property="Spacing" Value="2" />
</Style> </Style>
@@ -244,8 +420,10 @@
<Setter Property="TextWrapping" Value="Wrap" /> <Setter Property="TextWrapping" Value="Wrap" />
<Setter Property="VerticalAlignment" Value="Center" /> <Setter Property="VerticalAlignment" Value="Center" />
</Style> </Style>
<!-- Accent is reserved for interactive state (checked box, slider fill,
selected category, primary button). Read-outs are plain foreground. -->
<Style Selector="TextBlock.FieldValue"> <Style Selector="TextBlock.FieldValue">
<Setter Property="Foreground" Value="{StaticResource CarbonAccentBright}" /> <Setter Property="Foreground" Value="{StaticResource CarbonFg}" />
<Setter Property="FontSize" Value="12" /> <Setter Property="FontSize" Value="12" />
<Setter Property="FontWeight" Value="SemiBold" /> <Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="VerticalAlignment" Value="Center" /> <Setter Property="VerticalAlignment" Value="Center" />
@@ -254,7 +432,7 @@
</Style> </Style>
<Style Selector="TextBlock.FieldHint"> <Style Selector="TextBlock.FieldHint">
<Setter Property="Foreground" Value="{StaticResource CarbonFgDim}" /> <Setter Property="Foreground" Value="{StaticResource CarbonFgDim}" />
<Setter Property="FontSize" Value="11" /> <Setter Property="FontSize" Value="12" />
<Setter Property="FontStyle" Value="Italic" /> <Setter Property="FontStyle" Value="Italic" />
</Style> </Style>
<Style Selector="Separator"> <Style Selector="Separator">
@@ -290,15 +468,47 @@
<Style Selector="ListBox.Compact ListBoxItem"> <Style Selector="ListBox.Compact ListBoxItem">
<Setter Property="Padding" Value="4,0" /> <Setter Property="Padding" Value="4,0" />
<Setter Property="MinHeight" Value="17" /> <Setter Property="MinHeight" Value="17" />
<Setter Property="FontSize" Value="13" /> <Setter Property="FontSize" Value="12" />
</Style> </Style>
<Style Selector="TreeView.Compact TreeViewItem"> <Style Selector="TreeView.Compact TreeViewItem">
<Setter Property="MinHeight" Value="19" /> <Setter Property="MinHeight" Value="19" />
<Setter Property="FontSize" Value="13" /> <Setter Property="FontSize" Value="12" />
</Style> </Style>
<Style Selector="TreeView.Compact TreeViewItem /template/ Border#PART_LayoutRoot"> <Style Selector="TreeView.Compact TreeViewItem /template/ Border#PART_LayoutRoot">
<Setter Property="MinHeight" Value="19" /> <Setter Property="MinHeight" Value="19" />
<Setter Property="Padding" Value="3,0" /> <Setter Property="Padding" Value="3,0" />
</Style> </Style>
<!-- Keyboard focus. The default theme leaves the ring almost invisible on this
palette, so everything that can take focus gets the same bright outline; the
consist cards paint it as a box shadow because their border already carries
the selection colour. -->
<Style Selector=":is(Button):focus-visible /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="BorderBrush" Value="{StaticResource CarbonAccentBright}" />
<Setter Property="BorderThickness" Value="1" />
</Style>
<Style Selector="CheckBox:focus-visible /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{StaticResource CarbonAccentBright}" />
</Style>
<Style Selector="TextBox:focus /template/ Border#PART_BorderElement">
<Setter Property="BorderBrush" Value="{StaticResource CarbonAccentBright}" />
</Style>
<Style Selector="ComboBox:focus-visible /template/ Border#Background">
<Setter Property="BorderBrush" Value="{StaticResource CarbonAccentBright}" />
</Style>
<Style Selector="TabItem:focus-visible">
<Setter Property="Foreground" Value="{StaticResource CarbonAccentBright}" />
</Style>
<Style Selector="ListBoxItem:focus-visible /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="BorderBrush" Value="{StaticResource CarbonAccentBright}" />
<Setter Property="BorderThickness" Value="1" />
</Style>
<Style Selector="TreeViewItem:focus-visible /template/ Border#PART_LayoutRoot">
<Setter Property="BorderBrush" Value="{StaticResource CarbonAccentBright}" />
<Setter Property="BorderThickness" Value="1" />
</Style>
<Style Selector="Border.ConsistCard:focus">
<Setter Property="BoxShadow" Value="0 0 0 2 #41c400" />
</Style>
</Styles> </Styles>

View File

@@ -0,0 +1,262 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ctl="clr-namespace:StarterNG.Controls">
<Styles.Resources>
<!-- One setting: [ label | control | value ] with the description on a
second line spanning the whole row. Empty parts collapse, driven by
the pseudo-classes SettingRow sets. -->
<ControlTheme x:Key="{x:Type ctl:SettingRow}" TargetType="ctl:SettingRow">
<Setter Property="Margin" Value="0,0,0,11" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="Template">
<ControlTemplate>
<Grid RowDefinitions="Auto,Auto">
<Grid.ColumnDefinitions>
<!-- Every row is its own Grid, so the label column only
lines up across rows when a shared-size scope makes
them negotiate one width. The group is inert unless
an ancestor declares Grid.IsSharedSizeScope, which is
why this is safe to leave on for the settings page:
there the pin on PART_Label below fixes the width
instead. -->
<ColumnDefinition Width="Auto" SharedSizeGroup="SettingRowLabel" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Name="PART_Label"
Grid.Row="0" Grid.Column="0"
Text="{TemplateBinding Label}"
FontSize="12"
Foreground="{StaticResource CarbonFg}"
TextWrapping="Wrap"
VerticalAlignment="Center"
MinWidth="286" MaxWidth="286"
Margin="0,0,14,0" />
<ContentPresenter Name="PART_Content"
Grid.Row="0" Grid.Column="1"
Content="{TemplateBinding Content}"
HorizontalAlignment="Stretch"
VerticalAlignment="Center" />
<TextBlock Name="PART_Value"
Grid.Row="0" Grid.Column="2"
Text="{TemplateBinding ValueText}"
FontSize="12"
FontWeight="SemiBold"
Foreground="{StaticResource CarbonFg}"
MinWidth="72"
TextAlignment="Right"
TextWrapping="NoWrap"
TextTrimming="CharacterEllipsis"
VerticalAlignment="Center"
Margin="12,0,0,0" />
<TextBlock Name="PART_Description"
Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3"
Text="{TemplateBinding Description}"
FontSize="12"
Foreground="{StaticResource CarbonFgDim}"
TextWrapping="Wrap"
Margin="0,3,0,0" />
</Grid>
</ControlTemplate>
</Setter>
<Style Selector="^:no-description /template/ TextBlock#PART_Description">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="^:no-value /template/ TextBlock#PART_Value">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="^:no-label /template/ TextBlock#PART_Label">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="^:no-content /template/ TextBlock#PART_Value">
<Setter Property="Grid.Column" Value="1" />
<Setter Property="Grid.ColumnSpan" Value="2" />
</Style>
<!-- A checkbox is its own label, so let it take the label column too.
Alignment stays Stretch: a bare slider in a labelless row still
wants the full width. -->
<Style Selector="^:no-label /template/ ContentPresenter#PART_Content">
<Setter Property="Grid.Column" Value="0" />
<Setter Property="Grid.ColumnSpan" Value="2" />
</Style>
</ControlTheme>
<!-- Flat section: title, accent rule, content. No border, no filled header. -->
<ControlTheme x:Key="{x:Type ctl:SettingsSection}" TargetType="ctl:SettingsSection">
<Setter Property="Margin" Value="0,0,0,24" />
<Setter Property="Template">
<ControlTemplate>
<StackPanel>
<DockPanel LastChildFill="True">
<ContentPresenter DockPanel.Dock="Right"
Content="{TemplateBinding Toggle}"
VerticalAlignment="Center" />
<TextBlock Text="{TemplateBinding Header}"
FontSize="12"
FontWeight="SemiBold"
Foreground="{StaticResource CarbonFg}"
VerticalAlignment="Center" />
</DockPanel>
<Grid Name="PART_Rule" ColumnDefinitions="28,*" Height="1" Margin="0,6,0,0">
<Border Grid.Column="0" Background="{StaticResource CarbonAccentBright}" />
<Border Grid.Column="1" Background="{StaticResource CarbonBorderLight}" />
</Grid>
<ContentPresenter Name="PART_Content"
Content="{TemplateBinding Content}"
Margin="0,14,0,0" />
</StackPanel>
</ControlTemplate>
</Setter>
</ControlTheme>
<!-- Category list on the left of the settings panel. -->
<ControlTheme x:Key="SettingsNavItem" TargetType="ListBoxItem">
<Setter Property="Foreground" Value="{StaticResource CarbonFgDim}" />
<Setter Property="FontSize" Value="12" />
<Setter Property="Padding" Value="12,7" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="Template">
<ControlTemplate>
<Border Name="root"
Background="Transparent"
BorderBrush="Transparent"
BorderThickness="3,0,0,0">
<ContentPresenter Content="{TemplateBinding Content}"
Margin="{TemplateBinding Padding}"
VerticalAlignment="Center" />
</Border>
</ControlTemplate>
</Setter>
<Style Selector="^:pointerover /template/ Border#root">
<Setter Property="Background" Value="{StaticResource CarbonHoverBg}" />
</Style>
<Style Selector="^:selected /template/ Border#root">
<Setter Property="Background" Value="{StaticResource CarbonHoverBg}" />
<Setter Property="BorderBrush" Value="{StaticResource CarbonAccentBright}" />
</Style>
<Style Selector="^:selected">
<Setter Property="Foreground" Value="{StaticResource CarbonFg}" />
<Setter Property="FontWeight" Value="SemiBold" />
</Style>
</ControlTheme>
</Styles.Resources>
<!-- Dimmed while a search finds nothing in this category. Still clickable:
selecting it clears the search and opens the category. -->
<Style Selector="ListBoxItem.NoHits">
<Setter Property="Opacity" Value="0.3" />
</Style>
<!-- Names the category above stacked search results. -->
<Style Selector="TextBlock.CategoryCaption">
<Setter Property="Foreground" Value="{StaticResource CarbonAccentBright}" />
<Setter Property="FontSize" Value="12" />
<Setter Property="FontWeight" Value="Bold" />
<Setter Property="Margin" Value="0,0,0,8" />
</Style>
<Style Selector="ListBox.SettingsNav">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="0" />
</Style>
<!-- Controls inside a row: keep them from stretching to absurd widths in the
single-column layout, and give the row a predictable height. -->
<Style Selector="ctl|SettingRow:not(.Compact) > ComboBox">
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="MaxWidth" Value="360" />
</Style>
<Style Selector="ctl|SettingRow:not(.Compact) > NumericUpDown">
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="Width" Value="140" />
</Style>
<Style Selector="ctl|SettingRow:not(.Compact) > Slider">
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<Style Selector="ctl|SettingRow:not(.Compact) > CheckBox">
<Setter Property="HorizontalAlignment" Value="Left" />
</Style>
<Style Selector="ctl|SettingRow:not(.Compact) > Button">
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="MinWidth" Value="180" />
</Style>
<!-- Compact variant. The depot's detail panels are 250-390 px wide, where the
settings page's rigid 300 px label column and 72 px value slot do not fit.
Releasing the pin lets the shared-size group do its job: the column hugs the
longest label in whichever panel it lands in, so no pixel count is picked
here and it follows the translation. -->
<Style Selector="ctl|SettingRow.Compact">
<Setter Property="Margin" Value="0,0,0,5" />
</Style>
<!-- Right-aligned: an Auto column hugs the longest label, so anything shorter
would otherwise hang in space away from its own control. MaxWidth stops one
long translation from eating the panel - the label already wraps. -->
<Style Selector="ctl|SettingRow.Compact /template/ TextBlock#PART_Label">
<Setter Property="MinWidth" Value="96" />
<Setter Property="MaxWidth" Value="170" />
<Setter Property="TextAlignment" Value="Right" />
<Setter Property="Margin" Value="0,0,10,0" />
</Style>
<!-- Reserved, not collapsed: keeping the unit gutter on every row means the
controls share one right edge, and nothing shifts sideways when the unit
disappears in pantograph mode. IsVisible deliberately overrides the theme's
:no-value setter, which a file-level style is allowed to do. -->
<Style Selector="ctl|SettingRow.Compact /template/ TextBlock#PART_Value">
<Setter Property="IsVisible" Value="True" />
<Setter Property="MinWidth" Value="26" />
<Setter Property="Margin" Value="6,0,0,0" />
</Style>
<Style Selector="ctl|SettingRow.Compact /template/ TextBlock#PART_Description">
<Setter Property="Margin" Value="0,2,0,0" />
</Style>
<!-- A checkbox lines up with the other controls here rather than escaping to the
left edge the way it does on the wide settings page: in a narrow column the
label edge reads as a rule, and a stray checkbox breaks it. Keeping the
content out of column 0 also keeps a spanning child away from the
shared-size column, which spares us the question of how the two interact. -->
<Style Selector="ctl|SettingRow.Compact:no-label /template/ ContentPresenter#PART_Content">
<Setter Property="Grid.Column" Value="1" />
<Setter Property="Grid.ColumnSpan" Value="2" />
</Style>
<Style Selector="ctl|SettingRow.Compact > ComboBox">
<Setter Property="HorizontalAlignment" Value="Stretch" />
</Style>
<Style Selector="ctl|SettingRow.Compact > NumericUpDown">
<Setter Property="HorizontalAlignment" Value="Stretch" />
</Style>
<Style Selector="ctl|SettingRow.Compact > CheckBox">
<Setter Property="HorizontalAlignment" Value="Left" />
</Style>
<Style Selector="ctl|SettingRow.Compact > Button">
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="MinWidth" Value="0" />
</Style>
<Style Selector="ctl|SettingRow.Compact > StackPanel">
<Setter Property="HorizontalAlignment" Value="Stretch" />
</Style>
<Style Selector="ctl|SettingRow.Compact > Grid">
<Setter Property="HorizontalAlignment" Value="Stretch" />
</Style>
<Style Selector="ctl|SettingsSection.Compact">
<Setter Property="Margin" Value="0,0,0,9" />
</Style>
<Style Selector="ctl|SettingsSection.Compact /template/ ContentPresenter#PART_Content">
<Setter Property="Margin" Value="0,6,0,0" />
</Style>
<Style Selector="ctl|SettingsSection.Compact /template/ Grid#PART_Rule">
<Setter Property="Margin" Value="0,4,0,0" />
</Style>
</Styles>

View File

@@ -3,27 +3,35 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:starterNg="clr-namespace:StarterNG" xmlns:starterNg="clr-namespace:StarterNG"
xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:views="clr-namespace:StarterNG.Views" xmlns:views="clr-namespace:StarterNG.Views"
x:CompileBindings="True" x:CompileBindings="True"
mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="700" mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="700"
x:Class="StarterNG.Views.Depot"> x:Class="StarterNG.Views.Depot">
<!-- TabIndex follows the workflow - pick a consist, browse, build the train,
then the editors - not the column order the grid happens to have. -->
<Grid Margin="0" ColumnSpacing="3" RowSpacing="3"> <Grid Margin="0" ColumnSpacing="3" RowSpacing="3">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="270" MinWidth="220" /> <ColumnDefinition Width="270" MinWidth="220" />
<ColumnDefinition /> <ColumnDefinition />
<ColumnDefinition Width="340" MinWidth="300" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<!-- The vehicle panel keeps its old height on a roomy window (MaxHeight)
but shrinks with everything else towards the 666 px floor instead of
being clipped. The consist row measures itself, so a whole card fits
whether the thumbnails are the small ones or the large ones. -->
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="*" MinHeight="230" /> <RowDefinition Height="*" MinHeight="150" />
<RowDefinition Height="275" MinHeight="260" /> <RowDefinition Height="1.57*" MinHeight="120" MaxHeight="192" />
<RowDefinition Height="175" MinHeight="165" /> <RowDefinition Height="Auto" MinHeight="140" MaxHeight="230" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Border Grid.Column="0" Grid.Row="0" Background="{StaticResource CarbonPanelBg}" <Border Grid.Column="0" Grid.Row="0" TabIndex="2" Background="{StaticResource CarbonPanelBg}"
BorderBrush="{StaticResource CarbonBorderLight}" BorderThickness="1" BorderBrush="{StaticResource CarbonBorderLight}" BorderThickness="1"
CornerRadius="2" Padding="8"> CornerRadius="2" Padding="6">
<Grid RowDefinitions="Auto,Auto,*,Auto,Auto"> <Grid RowDefinitions="Auto,Auto,*,Auto,Auto">
<DockPanel Grid.Row="0" Margin="0,0,0,6" LastChildFill="True"> <DockPanel Grid.Row="0" Margin="0,0,0,6" LastChildFill="True">
<CheckBox Name="hideArchivalCheck" DockPanel.Dock="Right" FontSize="11" <CheckBox Name="hideArchivalCheck" DockPanel.Dock="Right" FontSize="12"
VerticalAlignment="Center" Margin="8,0,0,0" VerticalAlignment="Center" Margin="8,0,0,0"
Content="{Binding [HideArchivalVehicles], Source={x:Static starterNg:App.Loc}}" Content="{Binding [HideArchivalVehicles], Source={x:Static starterNg:App.Loc}}"
IsCheckedChanged="HideArchivalCheck_OnChanged" /> IsCheckedChanged="HideArchivalCheck_OnChanged" />
@@ -34,10 +42,12 @@
<Grid Grid.Row="1" RowDefinitions="Auto,Auto" RowSpacing="6" Margin="0,0,0,6"> <Grid Grid.Row="1" RowDefinitions="Auto,Auto" RowSpacing="6" Margin="0,0,0,6">
<DockPanel Grid.Row="0" LastChildFill="True"> <DockPanel Grid.Row="0" LastChildFill="True">
<Button Name="textureBaseButton" DockPanel.Dock="Right" Classes="Flat" <Button Name="textureBaseButton" DockPanel.Dock="Right" Classes="Flat"
Width="30" Margin="4,0,0,0" Cursor="Hand" Content="&#x1F50D;" Width="30" Margin="4,0,0,0" Cursor="Hand"
HorizontalContentAlignment="Center" HorizontalContentAlignment="Center"
Click="TextureBaseButton_OnClick" Click="TextureBaseButton_OnClick"
ToolTip.Tip="{Binding [OpenTextureBase], Source={x:Static starterNg:App.Loc}}" /> ToolTip.Tip="{Binding [OpenTextureBase], Source={x:Static starterNg:App.Loc}}">
<materialIcons:MaterialIcon Kind="Magnify" Width="15" Height="15" />
</Button>
<ComboBox Name="categoryCombo" HorizontalAlignment="Stretch" <ComboBox Name="categoryCombo" HorizontalAlignment="Stretch"
PlaceholderText="{Binding [Category], Source={x:Static starterNg:App.Loc}}" PlaceholderText="{Binding [Category], Source={x:Static starterNg:App.Loc}}"
PointerWheelChanged="Combo_OnPointerWheel" PointerWheelChanged="Combo_OnPointerWheel"
@@ -49,7 +59,7 @@
SelectionChanged="ClassCombo_OnSelectionChanged" /> SelectionChanged="ClassCombo_OnSelectionChanged" />
</Grid> </Grid>
<Border Name="miniPreviewPanel" Grid.Row="3" Height="66" <Border Name="miniPreviewPanel" Grid.Row="3" Height="46"
Margin="0,6,0,0" Margin="0,6,0,0"
Background="{StaticResource CarbonInputBg}" Background="{StaticResource CarbonInputBg}"
BorderBrush="{StaticResource CarbonBorderLight}" BorderThickness="1" BorderBrush="{StaticResource CarbonBorderLight}" BorderThickness="1"
@@ -71,10 +81,15 @@
</Grid> </Grid>
</Border> </Border>
<HeaderedContentControl Name="mapConsistsPanel" Grid.Column="1" Grid.Row="0" <!-- No group header here: the tab strip already names the list, and the
Theme="{StaticResource GroupBox}"> scenery it belongs to is written into the tab itself. -->
<Border Grid.Column="1" Grid.Row="0" TabIndex="1"
Background="{StaticResource CarbonPanelBg}"
BorderBrush="{StaticResource CarbonBorderLight}"
BorderThickness="1" CornerRadius="2" Padding="6">
<TabControl> <TabControl>
<TabItem Header="{Binding [SceneryConsists], Source={x:Static starterNg:App.Loc}}"> <TabItem Name="sceneryConsistsTab"
Header="{Binding [SceneryConsists], Source={x:Static starterNg:App.Loc}}">
<DockPanel LastChildFill="True"> <DockPanel LastChildFill="True">
<Button Name="addToWarehouseButton" DockPanel.Dock="Bottom" Classes="Flat" <Button Name="addToWarehouseButton" DockPanel.Dock="Bottom" Classes="Flat"
Margin="0,6,0,0" HorizontalAlignment="Stretch" Cursor="Hand" Margin="0,6,0,0" HorizontalAlignment="Stretch" Cursor="Hand"
@@ -83,10 +98,10 @@
Content="{Binding [AddToWarehouse], Source={x:Static starterNg:App.Loc}}" /> Content="{Binding [AddToWarehouse], Source={x:Static starterNg:App.Loc}}" />
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" Spacing="16" <StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" Spacing="16"
Margin="2,6,0,0"> Margin="2,6,0,0">
<CheckBox Name="depotDrivableOnlyCheck" FontSize="11" <CheckBox Name="depotDrivableOnlyCheck" FontSize="12"
IsCheckedChanged="ConsistFilter_OnChanged" IsCheckedChanged="ConsistFilter_OnChanged"
Content="{Binding [ShowDrivableOnly], Source={x:Static starterNg:App.Loc}}" /> Content="{Binding [ShowDrivableOnly], Source={x:Static starterNg:App.Loc}}" />
<CheckBox Name="depotShowAiCheck" FontSize="11" <CheckBox Name="depotShowAiCheck" FontSize="12"
IsCheckedChanged="ConsistFilter_OnChanged" IsCheckedChanged="ConsistFilter_OnChanged"
Content="{Binding [ShowAiVehicles], Source={x:Static starterNg:App.Loc}}" /> Content="{Binding [ShowAiVehicles], Source={x:Static starterNg:App.Loc}}" />
</StackPanel> </StackPanel>
@@ -107,74 +122,107 @@
</DockPanel> </DockPanel>
</TabItem> </TabItem>
</TabControl> </TabControl>
</HeaderedContentControl> </Border>
<HeaderedContentControl Name="selectedVehiclePanel" Grid.Row="1" Grid.ColumnSpan="2" <!-- The configuration is the one panel here that is actually edited, so it
Theme="{StaticResource GroupBox}"> gets a column of its own spanning both rows - from the top edge down to
<Grid ColumnDefinitions="290*,3,380*,3,280*"> the consist - instead of a third of one squeezed row. -->
<TabControl Grid.Column="0"> <Border Grid.Column="2" Grid.Row="0" Grid.RowSpan="2" TabIndex="6"
<TabItem Header="{Binding [GeneralInfo], Source={x:Static starterNg:App.Loc}}"> Background="{StaticResource CarbonPanelBg}"
<DockPanel LastChildFill="True" Margin="4"> BorderBrush="{StaticResource CarbonBorderLight}"
<Button Name="removeVehicleButton" DockPanel.Dock="Bottom" Classes="Flat" BorderThickness="1" CornerRadius="2" Padding="6">
Margin="0,6,0,0" HorizontalAlignment="Stretch" Cursor="Hand" <DockPanel LastChildFill="True">
HorizontalContentAlignment="Center" <Button Name="removeVehicleButton" DockPanel.Dock="Bottom" Classes="Flat"
Click="RemoveVehicleButton_OnClick" Margin="0,6,0,0" HorizontalAlignment="Stretch" Cursor="Hand"
Content="{Binding [RemoveVehicle], Source={x:Static starterNg:App.Loc}}" /> HorizontalContentAlignment="Center"
<ScrollViewer VerticalScrollBarVisibility="Auto"> Click="RemoveVehicleButton_OnClick"
<StackPanel Name="generalPanel" Spacing="6" /> Content="{Binding [RemoveVehicle], Source={x:Static starterNg:App.Loc}}" />
</ScrollViewer> <TabControl>
</DockPanel>
</TabItem>
<TabItem Header="{Binding [AboutTexture], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Name="consistTexturePanel" Margin="4" Spacing="6" />
</ScrollViewer>
</TabItem>
</TabControl>
<TabControl Grid.Column="2">
<TabItem Header="{Binding [Configuration], Source={x:Static starterNg:App.Loc}}"> <TabItem Header="{Binding [Configuration], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto"> <ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Name="loadsPanel" Margin="4" Spacing="6" /> <StackPanel Name="loadsPanel" Margin="4" Grid.IsSharedSizeScope="True" />
</ScrollViewer>
</TabItem>
<TabItem Header="{Binding [Brakes], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Name="brakesPanel" Margin="4" Spacing="6" />
</ScrollViewer> </ScrollViewer>
</TabItem> </TabItem>
</TabControl> </TabControl>
</DockPanel>
</Border>
<TabControl Grid.Column="4"> <!-- No title band either: the tab strips start straight away, and the
<TabItem Header="{Binding [Coupling], Source={x:Static starterNg:App.Loc}}"> vehicle being edited is the highlighted card in the consist below.
<ScrollViewer VerticalScrollBarVisibility="Auto"> Two cards, not one: the read-only pair (general info / texture) is
<StackPanel Name="couplerPanel" Margin="4" Spacing="6" /> visually separate from the editors (coupling / damage / brakes), and
</ScrollViewer> each lines up with the panel above it. -->
</TabItem> <Border Grid.Row="1" Grid.Column="0" TabIndex="4"
<TabItem Header="{Binding [Damage], Source={x:Static starterNg:App.Loc}}"> Background="{StaticResource CarbonPanelBg}"
<ScrollViewer VerticalScrollBarVisibility="Auto"> BorderBrush="{StaticResource CarbonBorderLight}"
<StackPanel Name="damagePanel" Margin="4" Spacing="6" /> BorderThickness="1" CornerRadius="2" Padding="6">
</ScrollViewer> <TabControl>
</TabItem> <TabItem Header="{Binding [GeneralInfo], Source={x:Static starterNg:App.Loc}}">
</TabControl> <ScrollViewer VerticalScrollBarVisibility="Auto">
</Grid> <StackPanel Name="generalPanel" Margin="4" Grid.IsSharedSizeScope="True" />
</HeaderedContentControl> </ScrollViewer>
</TabItem>
<TabItem Header="{Binding [AboutTexture], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Name="consistTexturePanel" Margin="4" Spacing="3" />
</ScrollViewer>
</TabItem>
</TabControl>
</Border>
<HeaderedContentControl Grid.Row="2" Grid.ColumnSpan="2" Theme="{StaticResource GroupBox}" <Border Grid.Row="1" Grid.Column="1" TabIndex="5"
Background="{StaticResource CarbonPanelBg}"
BorderBrush="{StaticResource CarbonBorderLight}"
BorderThickness="1" CornerRadius="2" Padding="6">
<TabControl>
<TabItem Header="{Binding [Coupling], Source={x:Static starterNg:App.Loc}}">
<!-- The coupling grid now uses the full tab width. The two
actions sit underneath, so they do not steal horizontal
space from the checkbox columns. -->
<DockPanel LastChildFill="True" Margin="4">
<Panel Name="couplerActions" DockPanel.Dock="Bottom" />
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Name="couplerPanel" />
</ScrollViewer>
</DockPanel>
</TabItem>
<TabItem Header="{Binding [Damage], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Name="damagePanel" Margin="4" Grid.IsSharedSizeScope="True" />
</ScrollViewer>
</TabItem>
<TabItem Header="{Binding [Brakes], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<!-- The scope is what makes every row's label column in this
panel settle on one width. -->
<StackPanel Name="brakesPanel" Margin="4"
Grid.IsSharedSizeScope="True" />
</ScrollViewer>
</TabItem>
</TabControl>
</Border>
<HeaderedContentControl Grid.Row="2" Grid.ColumnSpan="3" TabIndex="3" Theme="{StaticResource GroupBox}"
Header="{Binding [Consist], Source={x:Static starterNg:App.Loc}}"> Header="{Binding [Consist], Source={x:Static starterNg:App.Loc}}">
<!-- Mass, length and the rest ride in the title bar instead of taking
a row of their own above the cards. -->
<HeaderedContentControl.Tag>
<StackPanel Name="trainStats" Orientation="Horizontal"
VerticalAlignment="Center" />
</HeaderedContentControl.Tag>
<DockPanel LastChildFill="True"> <DockPanel LastChildFill="True">
<DockPanel DockPanel.Dock="Top" Margin="0,0,0,6">
<StackPanel Name="trainStats" Orientation="Horizontal"
VerticalAlignment="Center" />
</DockPanel>
<Panel Name="consistDropTarget"> <Panel Name="consistDropTarget">
<TextBlock Name="emptyHint" VerticalAlignment="Center" HorizontalAlignment="Center" <TextBlock Name="emptyHint" VerticalAlignment="Center" HorizontalAlignment="Center"
TextWrapping="Wrap" MaxWidth="520" Opacity="0.6" TextAlignment="Center" TextWrapping="Wrap" MaxWidth="520" Opacity="0.6" TextAlignment="Center"
Text="{Binding [ConsistEmpty], Source={x:Static starterNg:App.Loc}}" /> Text="{Binding [ConsistEmpty], Source={x:Static starterNg:App.Loc}}" />
<ScrollViewer Name="consistScroll" <ScrollViewer Name="consistScroll"
HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled"> HorizontalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Disabled"
Padding="0,0,0,4">
<Panel> <Panel>
<!-- Tab reaches the strip once and lands on the selected
card; the arrow keys walk it from there. -->
<StackPanel Name="consistStack" Orientation="Horizontal" <StackPanel Name="consistStack" Orientation="Horizontal"
KeyboardNavigation.TabNavigation="Once"
VerticalAlignment="Center" Spacing="0" /> VerticalAlignment="Center" Spacing="0" />
<Canvas Name="consistOverlay" IsHitTestVisible="False" /> <Canvas Name="consistOverlay" IsHitTestVisible="False" />
</Panel> </Panel>

View File

@@ -7,6 +7,7 @@ using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Input.Platform; using Avalonia.Input.Platform;
@@ -76,8 +77,11 @@ public partial class Depot : UserControl
_cards = new VehicleCards(consistStack, _db, _consist, _cargo, _minis, _hand, _cards = new VehicleCards(consistStack, _db, _consist, _cargo, _minis, _hand,
RebuildConsist, () => _details.Refresh(), _browser.SelectInBrowser, _drag.ArmCardDrag); RebuildConsist, () => _details.Refresh(), _browser.SelectInBrowser, _drag.ArmCardDrag);
// Hidden still scrolls - it only drops the bar itself.
MiniTextures.Sharp(miniPreview);
_details = new VehicleDetails( _details = new VehicleDetails(
selectedVehiclePanel, generalPanel, consistTexturePanel, couplerPanel, generalPanel, consistTexturePanel, couplerPanel, couplerActions,
brakesPanel, loadsPanel, damagePanel, removeVehicleButton, brakesPanel, loadsPanel, damagePanel, removeVehicleButton,
_db, _info, _consist, _cargo, _hand, _db, _info, _consist, _cargo, _hand,
RebuildConsist, _cards.RefreshMemberChrome, _browser.ShowTextureInfo); RebuildConsist, _cards.RefreshMemberChrome, _browser.ShowTextureInfo);
@@ -163,6 +167,9 @@ public partial class Depot : UserControl
_details.Refresh(); _details.Refresh();
} }
/// <summary>Redraws the consist cards, e.g. after the thumbnail size changed.</summary>
public void RefreshConsistView() => RebuildConsist();
private void RebuildConsist() private void RebuildConsist()
{ {
consistStack.Children.Clear(); consistStack.Children.Clear();
@@ -183,6 +190,10 @@ public partial class Depot : UserControl
_details.Refresh(); _details.Refresh();
UpdateTrainStats(); UpdateTrainStats();
WriteBackToScenery(); WriteBackToScenery();
// The cards are thrown away and rebuilt on every change, so a keyboard-driven
// edit has to be handed its card back or focus escapes the strip.
_cards.FocusSelectedCard(onlyIfRequested: true);
} }
private void WriteBackToScenery() private void WriteBackToScenery()
@@ -232,9 +243,9 @@ public partial class Depot : UserControl
depotDrivableOnlyCheck.IsChecked = StarterNG.Classes.Settings.Instance.DrivableOnly; depotDrivableOnlyCheck.IsChecked = StarterNG.Classes.Settings.Instance.DrivableOnly;
_listScenery = AppState.Instance.CurrentScenery ?? _sceneries.FirstOrDefault(); _listScenery = AppState.Instance.CurrentScenery ?? _sceneries.FirstOrDefault();
mapConsistsPanel.Header = _listScenery != null sceneryConsistsTab.Header = App.Loc["SceneryConsists"];
? $"{App.Loc["MapConsists"]}: {Path.GetFileNameWithoutExtension(_listScenery.Path)}" ToolTip.SetTip(sceneryConsistsTab,
: App.Loc["NoSceneryConsists"]; _listScenery != null ? Path.GetFileNameWithoutExtension(_listScenery.Path) : App.Loc["NoSceneryConsists"]);
if (_listScenery != null) if (_listScenery != null)
{ {
@@ -425,6 +436,25 @@ public partial class Depot : UserControl
AddTexture(_browser.Selected); AddTexture(_browser.Selected);
} }
/// <summary>Keyboard shortcut entry points. See <see cref="Infrastructure.Shortcuts"/>.</summary>
public void KeyAddVehicle() => AddVehicleButton_OnClick(null, null!);
public void KeyRemoveVehicle() => RemoveVehicleButton_OnClick(null, null!);
public void KeyClearConsist()
{
if (_consist.EditingTrainset is { } ts && ts.Vehicles.Count > 0)
RemoveAllVehicles(ts);
}
public void KeySetWagonNumber() => _cards.ShowNumberFlyoutForSelection();
public void KeyFocusSearch()
{
searchBox.Focus();
searchBox.SelectAll();
}
public void ToolAddAllCategory() => AddAllCategoryButton_OnClick(null, null!); public void ToolAddAllCategory() => AddAllCategoryButton_OnClick(null, null!);
public void ToolAddUniqueMmd() => AddUniqueMmdButton_OnClick(null, null!); public void ToolAddUniqueMmd() => AddUniqueMmdButton_OnClick(null, null!);

View File

@@ -1,5 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using StarterNG.Classes; using StarterNG.Classes;
@@ -10,6 +12,18 @@ public sealed class MiniTextures
{ {
private readonly Dictionary<string, Bitmap?> _cache = new(); private readonly Dictionary<string, Bitmap?> _cache = new();
/// <summary>
/// Draws a mini without smoothing. These files are about 80x30 and get shown up
/// to 68 px tall, so bilinear just smears the upscale; nearest keeps the edges.
/// (Has to be set in code - Avalonia 12 exposes RenderOptions as a struct with
/// Get/Set helpers, not as a styleable attached property.)
/// </summary>
public static Image Sharp(Image image)
{
RenderOptions.SetBitmapInterpolationMode(image, BitmapInterpolationMode.None);
return image;
}
public Bitmap? Get(string? name, int height, bool strict = false) public Bitmap? Get(string? name, int height, bool strict = false)
{ {
if (string.IsNullOrEmpty(name)) if (string.IsNullOrEmpty(name))
@@ -27,8 +41,17 @@ public sealed class MiniTextures
{ {
try try
{ {
// Decode at native size and let the renderer scale. The minis are
// about 80x30, so DecodeToHeight would bake a 2x upscale into the
// cache - blurry, and five times the memory. Only shrink here.
using var fs = File.OpenRead(path); using var fs = File.OpenRead(path);
bmp = Bitmap.DecodeToHeight(fs, height); bmp = new Bitmap(fs);
if (bmp.PixelSize.Height > height)
{
bmp.Dispose();
fs.Position = 0;
bmp = Bitmap.DecodeToHeight(fs, height);
}
} }
catch (System.Exception ex) catch (System.Exception ex)
{ {

View File

@@ -222,13 +222,13 @@ public sealed class VehicleBrowser
}; };
var bmp = _minis.Get(cls, height); var bmp = _minis.Get(cls, height);
if (bmp != null) if (bmp != null)
cell.Children.Add(new Image cell.Children.Add(MiniTextures.Sharp(new Image
{ {
Source = bmp, Height = height, Source = bmp, Height = height,
Stretch = Stretch.Uniform, Stretch = Stretch.Uniform,
HorizontalAlignment = HorizontalAlignment.Center, HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center VerticalAlignment = VerticalAlignment.Center
}); }));
cell.Children.Add(new TextBlock cell.Children.Add(new TextBlock
{ {
Text = cls, Text = cls,
@@ -421,7 +421,7 @@ public sealed class VehicleBrowser
target.Children.Add(new TextBlock target.Children.Add(new TextBlock
{ {
Text = line, Text = line,
FontSize = 10, FontSize = 12,
Opacity = 0.85, Opacity = 0.85,
TextTrimming = TextTrimming.CharacterEllipsis TextTrimming = TextTrimming.CharacterEllipsis
}); });

View File

@@ -8,6 +8,9 @@ public static class VehicleCardStyle
public static readonly IBrush BorderSelected = new SolidColorBrush(Color.Parse("#AA41C400")); public static readonly IBrush BorderSelected = new SolidColorBrush(Color.Parse("#AA41C400"));
public static readonly IBrush BackgroundSelected = new SolidColorBrush(Color.Parse("#2241C400")); public static readonly IBrush BackgroundSelected = new SolidColorBrush(Color.Parse("#2241C400"));
/// <summary>Lit state of the flip button on a reversed vehicle.</summary>
public static readonly IBrush Flipped = new SolidColorBrush(Color.Parse("#41C400"));
public static readonly IBrush Placeholder = new SolidColorBrush(Color.Parse("#22808080")); public static readonly IBrush Placeholder = new SolidColorBrush(Color.Parse("#22808080"));
public static int ThumbHeight => public static int ThumbHeight =>

View File

@@ -7,6 +7,8 @@ using Avalonia.Input;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.VisualTree; using Avalonia.VisualTree;
using Material.Icons;
using Material.Icons.Avalonia;
using StarterNG.Classes; using StarterNG.Classes;
using StarterNG.Domain; using StarterNG.Domain;
@@ -75,7 +77,10 @@ public sealed class VehicleCards
?? _minis.Get(car.MiniName, VehicleCardStyle.ThumbHeight); ?? _minis.Get(car.MiniName, VehicleCardStyle.ThumbHeight);
if (bmp != null) if (bmp != null)
{ {
var img = new Image { Source = bmp, Height = VehicleCardStyle.ThumbHeight, Stretch = Stretch.Uniform }; var img = MiniTextures.Sharp(new Image
{
Source = bmp, Height = VehicleCardStyle.ThumbHeight, Stretch = Stretch.Uniform
});
if (item.Flipped) if (item.Flipped)
{ {
img.RenderTransform = new ScaleTransform(-1, 1); img.RenderTransform = new ScaleTransform(-1, 1);
@@ -101,57 +106,50 @@ public sealed class VehicleCards
} }
} }
var nameBlock = new TextBlock
{
Text = item.Grouped && item.Cars.Count > 1 ? Consist.UnitLabel(item) : string.Empty,
FontSize = 11,
MaxWidth = Math.Max(120, item.Cars.Count * 96),
TextWrapping = TextWrapping.Wrap,
TextAlignment = TextAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Center,
IsVisible = item.Grouped && item.Cars.Count > 1
};
var controls = new StackPanel var controls = new StackPanel
{ {
Orientation = Orientation.Horizontal, Orientation = Orientation.Horizontal,
Spacing = 2, Spacing = 3,
HorizontalAlignment = HorizontalAlignment.Center HorizontalAlignment = HorizontalAlignment.Center,
Margin = new Thickness(0, 1, 0, 2)
}; };
controls.Children.Add(SmallButton("◀", App.Loc["TipMoveLeft"], () => _consist.MoveLeft(item)));
controls.Children.Add(DriverButton(item)); controls.Children.Add(DriverButton(item));
controls.Children.Add(SmallButton("⇄", App.Loc["TipFlip"], () => _consist.Flip(item))); controls.Children.Add(FlipButton(item));
controls.Children.Add(SmallButton("▶", App.Loc["TipMoveRight"], () => _consist.MoveRight(item)));
if (item.Grouped && item.Cars.Count > 1) if (item.Grouped && item.Cars.Count > 1)
controls.Children.Add(SmallButton("⧉", App.Loc["TipSplit"], () => _consist.Split(item))); controls.Children.Add(SmallButton(MaterialIconKind.CallSplit, App.Loc["TipSplit"], () => _consist.Split(item)));
else if ((index + 1 < _consist.Count && else if ((index + 1 < _consist.Count &&
(Consist.HoldsTail(item) || _consist.CanFormUnit(item, _consist[index + 1]))) || (Consist.HoldsTail(item) || _consist.CanFormUnit(item, _consist[index + 1]))) ||
(index > 0 && Consist.HoldsTail(_consist[index - 1]))) (index > 0 && Consist.HoldsTail(_consist[index - 1])))
controls.Children.Add(SmallButton("⛓", App.Loc["TipJoin"], () => _consist.Join(item))); controls.Children.Add(SmallButton(MaterialIconKind.Link, App.Loc["TipJoin"], () => _consist.Join(item)));
var remove = SmallButton("✕", App.Loc["RemoveSelectedVehicle"], () => _consist.Remove(item)); var remove = SmallButton(MaterialIconKind.Close, App.Loc["RemoveSelectedVehicle"], () => _consist.Remove(item));
remove.Foreground = new SolidColorBrush(Color.Parse("#E05252")); remove.Foreground = new SolidColorBrush(Color.Parse("#E05252"));
remove.FontWeight = FontWeight.Bold; remove.FontWeight = FontWeight.Bold;
controls.Children.Add(remove); controls.Children.Add(remove);
var inner = new StackPanel { Spacing = 4 }; var inner = new StackPanel { Spacing = 3 };
inner.Children.Add(minis); inner.Children.Add(minis);
inner.Children.Add(nameBlock);
inner.Children.Add(controls); inner.Children.Add(controls);
var card = new Border var card = new Border
{ {
Margin = new Thickness(2, 0), Margin = new Thickness(3, 2, 3, 6),
Padding = new Thickness(6), Padding = new Thickness(selected ? 4 : 5),
CornerRadius = new CornerRadius(6), CornerRadius = new CornerRadius(6),
BorderThickness = new Thickness(selected ? 2 : 1), BorderThickness = new Thickness(selected ? 2 : 1),
BorderBrush = selected ? VehicleCardStyle.BorderSelected : VehicleCardStyle.Border, BorderBrush = selected ? VehicleCardStyle.BorderSelected : VehicleCardStyle.Border,
Background = selected ? VehicleCardStyle.BackgroundSelected : Brushes.Transparent, Background = selected ? VehicleCardStyle.BackgroundSelected : Brushes.Transparent,
Cursor = _hand, Cursor = _hand,
Child = inner, Child = inner,
Tag = item Tag = item,
// Focusable so the strip can be reached with Tab and walked with the
// arrow keys; the focus ring itself comes from Carbon.axaml.
Focusable = true
}; };
card.Classes.Add("ConsistCard");
card.GotFocus += (_, _) => Select(item, null);
card.KeyDown += (_, e) => OnCardKey(card, item, e);
card.PointerPressed += (_, e) => card.PointerPressed += (_, e) =>
{ {
@@ -167,6 +165,7 @@ public sealed class VehicleCards
_consist.SyncStartingVehicle(); _consist.SyncStartingVehicle();
RefreshCardSelectionChrome(); RefreshCardSelectionChrome();
_refreshDetails(); _refreshDetails();
_selectInBrowser(_consist.SelectedCar);
_armDrag(index, e); _armDrag(index, e);
}; };
@@ -178,31 +177,30 @@ public sealed class VehicleCards
TrainsetDisplay.VehicleLabel(car, TrainsetDisplay.VehicleLabel(car,
head: index == 0 && ReferenceEquals(car, Consist.HeadCar(item)) && TrainsetDisplay.IsPowered(car)); head: index == 0 && ReferenceEquals(car, Consist.HeadCar(item)) && TrainsetDisplay.IsPowered(car));
/// <summary>
/// The name is not printed under the thumbnail - it costs a row on every card
/// and the tooltip says the same thing when it is actually wanted.
/// </summary>
private Control MemberFrame(Control visual, ConsistItem item, Dynamic car, string caption) private Control MemberFrame(Control visual, ConsistItem item, Dynamic car, string caption)
{ {
var stack = new StackPanel { Spacing = 2 };
stack.Children.Add(visual);
stack.Children.Add(new TextBlock
{
Text = caption,
FontSize = 10,
MaxWidth = 96,
TextWrapping = TextWrapping.Wrap,
TextAlignment = TextAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Center
});
var frame = new Border var frame = new Border
{ {
Child = stack, Child = visual,
Padding = new Thickness(1), Padding = new Thickness(1),
BorderThickness = new Thickness(0, 2), BorderThickness = new Thickness(0, 2),
BorderBrush = Brushes.Transparent, BorderBrush = Brushes.Transparent,
Cursor = _hand Cursor = _hand
}; };
var lines = new List<string> { caption };
if (!string.IsNullOrWhiteSpace(car.Name) && car.Name != caption) if (!string.IsNullOrWhiteSpace(car.Name) && car.Name != caption)
ToolTip.SetTip(frame, car.Name); lines.Add(car.Name);
if (item.Grouped && item.Cars.Count > 1)
lines.Add(Consist.UnitLabel(item));
string tip = string.Join("\n", lines.Where(l => !string.IsNullOrWhiteSpace(l)));
if (tip.Length > 0)
ToolTip.SetTip(frame, tip);
var picked = car; var picked = car;
frame.PointerPressed += (_, _) => PressedCar = picked; frame.PointerPressed += (_, _) => PressedCar = picked;
@@ -233,12 +231,154 @@ public sealed class VehicleCards
if (child is not Border { Tag: ConsistItem ci } border) if (child is not Border { Tag: ConsistItem ci } border)
continue; continue;
bool sel = ReferenceEquals(ci, _consist.Selected); bool sel = ReferenceEquals(ci, _consist.Selected);
border.Padding = new Thickness(sel ? 4 : 5);
border.BorderThickness = new Thickness(sel ? 2 : 1); border.BorderThickness = new Thickness(sel ? 2 : 1);
border.BorderBrush = sel ? VehicleCardStyle.BorderSelected : VehicleCardStyle.Border; border.BorderBrush = sel ? VehicleCardStyle.BorderSelected : VehicleCardStyle.Border;
border.Background = sel ? VehicleCardStyle.BackgroundSelected : Brushes.Transparent; border.Background = sel ? VehicleCardStyle.BackgroundSelected : Brushes.Transparent;
} }
} }
/// <summary>
/// Selects an item without going through a redraw. <paramref name="car"/> picks the
/// active member of a multi-vehicle unit; null keeps the current one when possible.
/// </summary>
private void Select(ConsistItem item, Dynamic? car)
{
var target = car ?? (ReferenceEquals(_consist.Selected, item)
? _consist.ActiveCar(item)
: item.Cars[0]);
if (ReferenceEquals(_consist.Selected, item) && ReferenceEquals(_consist.SelectedCar, target))
return;
_consist.Selected = item;
_consist.SelectedCar = target;
_consist.SyncStartingVehicle();
RefreshCardSelectionChrome();
_refreshDetails();
_selectInBrowser(target);
}
private Border? CardFor(ConsistItem item) =>
consistStack.Children.OfType<Border>()
.FirstOrDefault(b => ReferenceEquals(b.Tag, item));
/// <summary>
/// Set before an action that rebuilds the strip, so focus lands back on the card
/// instead of falling out to whatever comes next in the tab order.
/// </summary>
private bool _refocusAfterRedraw;
public void FocusSelectedCard(bool onlyIfRequested = false)
{
if (onlyIfRequested && !_refocusAfterRedraw)
return;
_refocusAfterRedraw = false;
if (_consist.Selected is { } item && CardFor(item) is { } card)
{
card.Focus();
card.BringIntoView();
}
}
private void Redraw(bool keepFocus)
{
_refocusAfterRedraw = keepFocus;
_redraw();
}
private void OnCardKey(Border card, ConsistItem item, KeyEventArgs e)
{
int index = _consist.IndexOf(item);
if (index < 0)
return;
bool ctrl = e.KeyModifiers.HasFlag(KeyModifiers.Control);
switch (e.Key)
{
case Key.Left when ctrl:
_consist.MoveLeft(item);
Redraw(keepFocus: true);
break;
case Key.Right when ctrl:
_consist.MoveRight(item);
Redraw(keepFocus: true);
break;
case Key.Left:
case Key.Right:
FocusCardAt(index + (e.Key == Key.Left ? -1 : 1));
break;
case Key.Home:
FocusCardAt(0);
break;
case Key.End:
FocusCardAt(_consist.Count - 1);
break;
// Within a coupled unit the arrow keys pick which member the detail
// panels talk about - the same thing clicking a thumbnail does.
case Key.Up:
case Key.Down:
StepMember(item, e.Key == Key.Down ? 1 : -1);
break;
case Key.Space:
_consist.Flip(item);
Redraw(keepFocus: true);
break;
case Key.Enter:
card.ContextMenu?.Open(card);
break;
default:
return;
}
e.Handled = true;
}
private void FocusCardAt(int index)
{
if (index < 0 || index >= _consist.Count)
return;
var item = _consist[index];
Select(item, null);
if (CardFor(item) is { } card)
{
card.Focus();
// Focusing alone does not scroll a plain Border into view.
card.BringIntoView();
}
}
private void StepMember(ConsistItem item, int delta)
{
if (item.Cars.Count < 2)
return;
int at = item.Cars.IndexOf(_consist.ActiveCar(item));
int next = Math.Clamp(at + delta, 0, item.Cars.Count - 1);
if (next != at)
Select(item, item.Cars[next]);
}
/// <summary>Ctrl+N on the selected card, from the window's shortcut table.</summary>
public void ShowNumberFlyoutForSelection()
{
if (_consist.Selected is not { } item || CardFor(item) is not { } card)
return;
ShowWagonNumberFlyout(card, _consist.ActiveCar(item));
}
private void ShowWagonNumberFlyout(Control anchor, Dynamic car) private void ShowWagonNumberFlyout(Control anchor, Dynamic car)
{ {
var num = new NumericUpDown var num = new NumericUpDown
@@ -276,7 +416,7 @@ public sealed class VehicleCards
}; };
var coupler = new Button var coupler = new Button
{ {
Padding = new Thickness(2), Padding = new Thickness(4, 6, 4, 6),
MinWidth = 0, MinWidth = 0,
Cursor = _hand, Cursor = _hand,
VerticalAlignment = VerticalAlignment.Center, VerticalAlignment = VerticalAlignment.Center,
@@ -288,7 +428,7 @@ public sealed class VehicleCards
} }
}; };
coupler.Classes.Add("Basic"); coupler.Classes.Add("Basic");
ToolTip.SetTip(coupler, App.Loc["Coupling"]); ToolTip.SetTip(coupler, $"{App.Loc["Coupling"]} — {App.Loc["TipReorder"]}");
return coupler; return coupler;
} }
@@ -302,7 +442,7 @@ public sealed class VehicleCards
var copy = new Button var copy = new Button
{ {
Content = "\u00AB", Content = "\u00AB",
FontSize = 13, FontSize = 12,
Padding = new Thickness(6, 1), Padding = new Thickness(6, 1),
Margin = new Thickness(12, 0, 0, 0), Margin = new Thickness(12, 0, 0, 0),
IsEnabled = unitIdx > 0, IsEnabled = unitIdx > 0,
@@ -324,11 +464,20 @@ public sealed class VehicleCards
{ {
Text = App.Loc["Coupling"], Text = App.Loc["Coupling"],
FontWeight = FontWeight.Bold, FontWeight = FontWeight.Bold,
FontSize = 11, FontSize = 12,
VerticalAlignment = VerticalAlignment.Center VerticalAlignment = VerticalAlignment.Center
}); });
panel.Children.Add(header); panel.Children.Add(header);
var grid = new Grid
{
ColumnDefinitions = new ColumnDefinitions("*,*"),
RowSpacing = 2,
ColumnSpacing = 12
};
for (int r = 0; r < (CouplingBits.BitKeys.Length + 1) / 2; r++)
grid.RowDefinitions.Add(new RowDefinition(GridLength.Auto));
for (int i = 0; i < CouplingBits.BitKeys.Length; i++) for (int i = 0; i < CouplingBits.BitKeys.Length; i++)
{ {
int bit = 1 << i; int bit = 1 << i;
@@ -336,17 +485,20 @@ public sealed class VehicleCards
{ {
Content = App.Loc[CouplingBits.BitKeys[i]], Content = App.Loc[CouplingBits.BitKeys[i]],
IsChecked = d.Coupling.Has(bit), IsChecked = d.Coupling.Has(bit),
FontSize = 11 FontSize = 12
}; };
check.IsCheckedChanged += (_, _) => d.Coupling.Set(bit, check.IsChecked == true); check.IsCheckedChanged += (_, _) => d.Coupling.Set(bit, check.IsChecked == true);
panel.Children.Add(check); Grid.SetRow(check, i / 2);
Grid.SetColumn(check, i % 2);
grid.Children.Add(check);
} }
panel.Children.Add(grid);
var thermo = new CheckBox var thermo = new CheckBox
{ {
Content = App.Loc["ThermoAmbient"], Content = App.Loc["ThermoAmbient"],
IsChecked = d.Coupling.ThermoDynamic, IsChecked = d.Coupling.ThermoDynamic,
FontSize = 11, FontSize = 12,
Margin = new Thickness(0, 2, 0, 0) Margin = new Thickness(0, 2, 0, 0)
}; };
thermo.IsCheckedChanged += (_, _) => d.Coupling.ThermoDynamic = thermo.IsChecked == true; thermo.IsCheckedChanged += (_, _) => d.Coupling.ThermoDynamic = thermo.IsChecked == true;
@@ -355,18 +507,26 @@ public sealed class VehicleCards
return new Border { Padding = new Thickness(8), Child = panel }; return new Border { Padding = new Thickness(8), Child = panel };
} }
private const string TextPresentation = "\uFE0E"; /// <summary>
/// Card buttons draw vector icons rather than glyphs - Windows renders half
/// the arrow and link characters as colour emoji, whatever the font asks for.
/// </summary>
private Button SmallButton(MaterialIconKind icon, string tip, Action onClick)
{
var btn = SmallButton(tip, onClick);
btn.Content = new MaterialIcon { Kind = icon, Width = 16, Height = 16 };
return btn;
}
private Button SmallButton(string glyph, string tip, Action onClick) private Button SmallButton(string tip, Action onClick)
{ {
var btn = new Button var btn = new Button
{ {
Content = glyph + TextPresentation, FontSize = 13,
FontSize = 15,
Padding = new Thickness(0), Padding = new Thickness(0),
MinWidth = 0, MinWidth = 0,
MinHeight = 0, MinHeight = 0,
Width = 26, Width = 28,
Height = 22, Height = 22,
HorizontalContentAlignment = HorizontalAlignment.Center, HorizontalContentAlignment = HorizontalAlignment.Center,
VerticalContentAlignment = VerticalAlignment.Center, VerticalContentAlignment = VerticalAlignment.Center,
@@ -378,9 +538,26 @@ public sealed class VehicleCards
return btn; return btn;
} }
/// <summary>
/// Orientation is a state, not just an action, so the button carries it: lit means
/// this vehicle sits reversed. Reading it off the card beats a checkbox in a panel
/// two columns away, and the card is rebuilt on every flip so the paint is current.
/// </summary>
private Button FlipButton(ConsistItem item)
{
var btn = SmallButton(MaterialIconKind.SwapHorizontal, App.Loc["TipFlip"],
() => _consist.Flip(item));
if (item.Flipped)
{
btn.Foreground = VehicleCardStyle.Flipped;
ToolTip.SetTip(btn, $"{App.Loc["TipFlip"]} \u2014 {App.Loc["Reversed"]}");
}
return btn;
}
private Button DriverButton(ConsistItem item) private Button DriverButton(ConsistItem item)
{ {
var btn = SmallButton("", App.Loc["TipDriver"], () => _consist.CycleDriver(item)); var btn = SmallButton(App.Loc["TipDriver"], () => _consist.CycleDriver(item));
btn.FontWeight = FontWeight.Bold; btn.FontWeight = FontWeight.Bold;
_driverChrome.Add((btn, item)); _driverChrome.Add((btn, item));
PaintDriverButton(btn, item); PaintDriverButton(btn, item);

View File

@@ -10,16 +10,17 @@ using Avalonia.Layout;
using Avalonia.Media; using Avalonia.Media;
using StarterNG.Classes; using StarterNG.Classes;
using StarterNG.Controls;
using StarterNG.Domain; using StarterNG.Domain;
namespace StarterNG.Views; namespace StarterNG.Views;
public sealed class VehicleDetails public sealed class VehicleDetails
{ {
private readonly HeaderedContentControl selectedVehiclePanel;
private readonly StackPanel generalPanel; private readonly StackPanel generalPanel;
private readonly StackPanel consistTexturePanel; private readonly StackPanel consistTexturePanel;
private readonly StackPanel couplerPanel; private readonly StackPanel couplerPanel;
private readonly Panel couplerActions;
private readonly StackPanel brakesPanel; private readonly StackPanel brakesPanel;
private readonly StackPanel loadsPanel; private readonly StackPanel loadsPanel;
private readonly StackPanel damagePanel; private readonly StackPanel damagePanel;
@@ -36,17 +37,17 @@ public sealed class VehicleDetails
private readonly Action<VehicleTexture, StackPanel> _showTextureInfo; private readonly Action<VehicleTexture, StackPanel> _showTextureInfo;
public VehicleDetails( public VehicleDetails(
HeaderedContentControl selectedVehiclePanel,
StackPanel generalPanel, StackPanel consistTexturePanel, StackPanel couplerPanel, StackPanel generalPanel, StackPanel consistTexturePanel, StackPanel couplerPanel,
Panel couplerActions,
StackPanel brakesPanel, StackPanel loadsPanel, StackPanel damagePanel, StackPanel brakesPanel, StackPanel loadsPanel, StackPanel damagePanel,
Button removeVehicleButton, Button removeVehicleButton,
VehicleDatabase db, VehicleInfo info, Consist consist, Cargo cargo, Cursor hand, VehicleDatabase db, VehicleInfo info, Consist consist, Cargo cargo, Cursor hand,
Action redraw, Action refreshChrome, Action<VehicleTexture, StackPanel> showTextureInfo) Action redraw, Action refreshChrome, Action<VehicleTexture, StackPanel> showTextureInfo)
{ {
this.selectedVehiclePanel = selectedVehiclePanel;
this.generalPanel = generalPanel; this.generalPanel = generalPanel;
this.consistTexturePanel = consistTexturePanel; this.consistTexturePanel = consistTexturePanel;
this.couplerPanel = couplerPanel; this.couplerPanel = couplerPanel;
this.couplerActions = couplerActions;
this.brakesPanel = brakesPanel; this.brakesPanel = brakesPanel;
this.loadsPanel = loadsPanel; this.loadsPanel = loadsPanel;
this.damagePanel = damagePanel; this.damagePanel = damagePanel;
@@ -67,31 +68,27 @@ public sealed class VehicleDetails
generalPanel.Children.Clear(); generalPanel.Children.Clear();
consistTexturePanel.Children.Clear(); consistTexturePanel.Children.Clear();
couplerPanel.Children.Clear(); couplerPanel.Children.Clear();
couplerActions.Children.Clear();
brakesPanel.Children.Clear(); brakesPanel.Children.Clear();
loadsPanel.Children.Clear(); loadsPanel.Children.Clear();
damagePanel.Children.Clear(); damagePanel.Children.Clear();
selectedVehiclePanel.Header = _consist.Selected is null
? App.Loc["SelectedVehicle"]
: $"{App.Loc["SelectedVehicle"]}: " +
(_consist.Selected.Grouped ? Consist.UnitLabel(_consist.Selected) : _consist.Selected.Cars[0].Name);
if (_consist.Selected != null && !_consist.Selected.Cars.Contains(_consist.SelectedCar!)) if (_consist.Selected != null && !_consist.Selected.Cars.Contains(_consist.SelectedCar!))
_consist.SelectedCar = _consist.Selected.Cars[0]; _consist.SelectedCar = _consist.Selected.Cars[0];
_refreshChrome(); _refreshChrome();
loadsPanel.Children.Add(BuildConsistLoadTools());
if (_consist.Selected is null) if (_consist.Selected is null)
{ {
generalPanel.Children.Add(DetailHint(App.Loc["SelectVehicleHint"])); generalPanel.Children.Add(PanelNote(App.Loc["SelectVehicleHint"]));
consistTexturePanel.Children.Add(DetailHint(App.Loc["SelectVehicleHint"])); consistTexturePanel.Children.Add(PanelNote(App.Loc["SelectVehicleHint"]));
couplerPanel.Children.Add(DetailHint(App.Loc["SelectVehicleHint"])); couplerPanel.Children.Add(PanelNote(App.Loc["SelectVehicleHint"]));
brakesPanel.Children.Add(DetailHint(App.Loc["SelectVehicleHint"])); brakesPanel.Children.Add(PanelNote(App.Loc["SelectVehicleHint"]));
loadsPanel.Children.Add(DetailHint(App.Loc["SelectVehicleHint"])); loadsPanel.Children.Add(PanelNote(App.Loc["SelectVehicleHint"]));
damagePanel.Children.Add(DetailHint(App.Loc["SelectVehicleHint"])); damagePanel.Children.Add(PanelNote(App.Loc["SelectVehicleHint"]));
removeVehicleButton.IsEnabled = false; removeVehicleButton.IsEnabled = false;
// The whole-consist tools still apply with nothing selected.
AddConsistLoadSection();
return; return;
} }
@@ -100,21 +97,27 @@ public sealed class VehicleDetails
if (_db.TextureForSkin(_consist.ActiveCar(_consist.Selected).SkinFile) is { } tex) if (_db.TextureForSkin(_consist.ActiveCar(_consist.Selected).SkinFile) is { } tex)
_showTextureInfo(tex, consistTexturePanel); _showTextureInfo(tex, consistTexturePanel);
else else
consistTexturePanel.Children.Add(DetailHint(App.Loc["NoTextureInfo"])); consistTexturePanel.Children.Add(PanelNote(App.Loc["NoTextureInfo"]));
removeVehicleButton.IsEnabled = true; removeVehicleButton.IsEnabled = true;
if (_consist.Selected.Cars.Count > 1) if (_consist.Selected.Cars.Count > 1)
{ {
couplerPanel.Children.Add(UnitScopeHint(App.Loc["CouplingUnitRear"])); couplerPanel.Children.Add(PanelNote(App.Loc["CouplingUnitRear"]));
brakesPanel.Children.Add(UnitScopeHint(App.Loc["AppliesToUnit"])); brakesPanel.Children.Add(PanelNote(App.Loc["AppliesToUnit"]));
} }
BuildCouplerEditor(_consist.Selected); BuildCouplerEditor(_consist.Selected);
BuildBrakesEditor(_consist.Selected); BuildBrakesEditor(_consist.Selected);
BuildLoadsEditor(_consist.Selected);
BuildConfigExtras(_consist.Selected);
BuildDamageEditor(_consist.Selected); BuildDamageEditor(_consist.Selected);
// Configuration tab: what this vehicle is, what it carries, then the actions
// that reach past it - each under a header that says which is which.
loadsPanel.Children.Add(
Section(App.Loc["Vehicle"], BuildVehicleSection(_consist.Selected)));
loadsPanel.Children.Add(
Section(App.Loc["Load"], BuildLoadSection(_consist.Selected)));
AddConsistLoadSection();
} }
private void BuildGeneralInfo(ConsistItem item) private void BuildGeneralInfo(ConsistItem item)
@@ -122,6 +125,9 @@ public sealed class VehicleDetails
var car = _consist.ActiveCar(item); var car = _consist.ActiveCar(item);
var inv = CultureInfo.InvariantCulture; var inv = CultureInfo.InvariantCulture;
if (!string.IsNullOrWhiteSpace(car.Name))
generalPanel.Children.Add(Reading(App.Loc["Name"], car.Name));
bool unit = item.Cars.Count > 1; bool unit = item.Cars.Count > 1;
var members = item.Cars.Select(_info.PhysicsFor).Where(p => p != null).ToList(); var members = item.Cars.Select(_info.PhysicsFor).Where(p => p != null).ToList();
@@ -132,49 +138,19 @@ public sealed class VehicleDetails
double? vmax = members.Count == 0 ? null double? vmax = members.Count == 0 ? null
: unit ? members.Min(p => p!.VMax) : members[0]!.VMax; : unit ? members.Min(p => p!.VMax) : members[0]!.VMax;
generalPanel.Children.Add(InfoRow($"{App.Loc["Length"]} [m]:", generalPanel.Children.Add(Reading($"{App.Loc["Length"]} [m]",
length?.ToString("0.##", inv))); length?.ToString("0.##", inv)));
generalPanel.Children.Add(InfoRow($"{App.Loc["Mass"]} [t]:", generalPanel.Children.Add(Reading($"{App.Loc["Mass"]} [t]",
mass?.ToString("0.#", inv))); mass?.ToString("0.#", inv)));
generalPanel.Children.Add(InfoRow($"{App.Loc["VMax"]} [km/h]:", generalPanel.Children.Add(Reading($"{App.Loc["VMax"]} [km/h]",
vmax?.ToString("0", inv))); vmax?.ToString("0", inv)));
if (unit) if (unit)
generalPanel.Children.Add(InfoRow($"{App.Loc["VehicleCount"]}:", generalPanel.Children.Add(Reading(App.Loc["VehicleCount"],
item.Cars.Count.ToString(inv))); item.Cars.Count.ToString(inv)));
generalPanel.Children.Add(InfoRow($"{App.Loc["Model"]}:", car.MmdFile)); generalPanel.Children.Add(Reading(App.Loc["Model"], car.MmdFile));
generalPanel.Children.Add(InfoRow($"{App.Loc["Texture"]}:", car.SkinFile)); generalPanel.Children.Add(Reading(App.Loc["Texture"], car.SkinFile));
}
private static Control InfoRow(string label, string? value)
{
var grid = new Grid { ColumnDefinitions = new ColumnDefinitions("Auto,10,*") };
var caption = new TextBlock
{
Text = label,
FontSize = 12,
MinWidth = 110,
TextAlignment = TextAlignment.Right,
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Center
};
var reading = new TextBlock
{
Text = string.IsNullOrWhiteSpace(value) ? "–" : value,
FontSize = 12,
TextAlignment = TextAlignment.Right,
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Center,
TextWrapping = TextWrapping.Wrap
};
Grid.SetColumn(reading, 2);
grid.Children.Add(caption);
grid.Children.Add(reading);
return grid;
} }
private void BuildCouplerEditor(ConsistItem item) private void BuildCouplerEditor(ConsistItem item)
@@ -187,12 +163,17 @@ public sealed class VehicleDetails
var d = Consist.TailCar(item); var d = Consist.TailCar(item);
var grid = new Grid // Two columns: the tab is wide enough and the list is short enough
// to avoid scrolling while still reading left-to-right, top-to-bottom.
var list = new Grid
{ {
ColumnDefinitions = new ColumnDefinitions("*,8,*"), ColumnDefinitions = new ColumnDefinitions("*,*"),
RowDefinitions = new RowDefinitions("Auto,Auto,Auto,Auto"), RowSpacing = 2,
RowSpacing = 4 ColumnSpacing = 16
}; };
for (int r = 0; r < (CouplingBits.BitKeys.Length + 1) / 2; r++)
list.RowDefinitions.Add(new RowDefinition(GridLength.Auto));
for (int i = 0; i < CouplingBits.BitKeys.Length; i++) for (int i = 0; i < CouplingBits.BitKeys.Length; i++)
{ {
int bit = 1 << i; int bit = 1 << i;
@@ -207,17 +188,16 @@ public sealed class VehicleDetails
d.Coupling.Set(bit, check.IsChecked == true); d.Coupling.Set(bit, check.IsChecked == true);
_redraw(); _redraw();
}; };
int half = (CouplingBits.BitKeys.Length + 1) / 2; Grid.SetRow(check, i / 2);
Grid.SetColumn(check, i < half ? 0 : 2); Grid.SetColumn(check, i % 2);
Grid.SetRow(check, i % half); list.Children.Add(check);
grid.Children.Add(check);
} }
couplerPanel.Children.Add(grid); couplerPanel.Children.Add(list);
var copy = new Button var copy = new Button
{ {
Content = App.Loc["CopyCoupler"], Content = App.Loc["CopyCoupler"],
FontSize = 11, FontSize = 12,
Cursor = _hand, Cursor = _hand,
HorizontalAlignment = HorizontalAlignment.Stretch, HorizontalAlignment = HorizontalAlignment.Stretch,
HorizontalContentAlignment = HorizontalAlignment.Center, HorizontalContentAlignment = HorizontalAlignment.Center,
@@ -235,7 +215,7 @@ public sealed class VehicleDetails
var auto = new Button var auto = new Button
{ {
Content = App.Loc["AutoCoupler"], Content = App.Loc["AutoCoupler"],
FontSize = 11, FontSize = 12,
Cursor = _hand, Cursor = _hand,
HorizontalAlignment = HorizontalAlignment.Stretch, HorizontalAlignment = HorizontalAlignment.Stretch,
HorizontalContentAlignment = HorizontalAlignment.Center, HorizontalContentAlignment = HorizontalAlignment.Center,
@@ -250,19 +230,23 @@ public sealed class VehicleDetails
Refresh(); Refresh();
}; };
var buttons = new Grid // Laid out horizontally under the checkbox list so the grid can use the full tab width
var buttons = new StackPanel
{ {
ColumnDefinitions = new ColumnDefinitions("125*,4,48*"), Orientation = Orientation.Horizontal,
Spacing = 4,
Margin = new Thickness(0, 6, 0, 0) Margin = new Thickness(0, 6, 0, 0)
}; };
Grid.SetColumn(copy, 0);
Grid.SetColumn(auto, 2);
buttons.Children.Add(copy); buttons.Children.Add(copy);
buttons.Children.Add(auto); buttons.Children.Add(auto);
couplerPanel.Children.Add(buttons); couplerActions.Children.Add(buttons);
} }
private void BuildConfigExtras(ConsistItem item) /// <summary>
/// The vehicle's own settings. Every write path is unchanged: crew and wagon number
/// go through the unit's active car, the coolant flag to its first car only.
/// </summary>
private Control BuildVehicleSection(ConsistItem item)
{ {
var driver = new ComboBox { FontSize = 12, MinWidth = 0 }; var driver = new ComboBox { FontSize = 12, MinWidth = 0 };
foreach (string key in new[] { "DriverHead", "DriverRear", "DriverPassenger", "DriverNobody" }) foreach (string key in new[] { "DriverHead", "DriverRear", "DriverPassenger", "DriverNobody" })
@@ -296,75 +280,143 @@ public sealed class VehicleDetails
}; };
driver.MinWidth = 0; driver.MinWidth = 0;
driver.HorizontalAlignment = HorizontalAlignment.Stretch; driver.HorizontalAlignment = HorizontalAlignment.Stretch;
loadsPanel.Children.Insert(0, LabeledRow(App.Loc["CrewLabel"], driver, labelWidth: 70));
var reversed = new CheckBox var rows = new StackPanel();
{ rows.Children.Add(Row(App.Loc["CrewLabel"], driver));
Content = App.Loc["Reversed"],
IsChecked = item.Flipped,
FontSize = 11
};
reversed.IsCheckedChanged += (_, _) =>
{
if ((reversed.IsChecked == true) != item.Flipped)
_consist.Flip(item);
};
if (IsPassengerVehicle(item))
{
var lead = item.Cars[0];
var numBox = new NumericUpDown
{
FontSize = 12,
Minimum = 0,
Maximum = 99999,
Increment = 1,
Value = Consist.GetWagonNumber(lead),
FormatString = "0"
};
numBox.ValueChanged += (_, _) =>
{
_consist.SetWagonNumber(lead, (int)(numBox.Value ?? 0));
_redraw();
};
rows.Children.Add(Row(App.Loc["WagonNumber"], numBox));
}
// Orientation is not here any more: it reads and toggles off the flip button on
// the vehicle's own card, which lights up when the vehicle sits reversed.
// The flag rides on the unit's first car, so that is the car that has to own a
// diesel engine for it to mean anything: the simulator runs its heat model only
// for DieselEngine and DieselElectric, and on anything else ".TA" is written into
// the scenario and then ignored. Shown anyway when the flag is already set, so an
// inherited one stays visible and removable.
var d = item.Cars[0]; var d = item.Cars[0];
var thermo = new CheckBox bool thermoApplies = _info.PhysicsFor(d)?.IsDieselEngine == true
{ || d.Coupling.ThermoDynamic;
Content = App.Loc["ThermoAmbient"],
IsChecked = d.Coupling.ThermoDynamic,
FontSize = 11
};
thermo.IsCheckedChanged += (_, _) => d.Coupling.ThermoDynamic = thermo.IsChecked == true;
var switches = new Grid if (thermoApplies)
{ {
ColumnDefinitions = new ColumnDefinitions("*,8,*"), var thermo = new CheckBox
Margin = new Thickness(0, 4, 0, 0) {
}; Content = App.Loc["ThermoAmbient"],
Grid.SetColumn(thermo, 2); IsChecked = d.Coupling.ThermoDynamic,
switches.Children.Add(reversed); FontSize = 12
switches.Children.Add(thermo); };
loadsPanel.Children.Add(switches); thermo.IsCheckedChanged += (_, _) => d.Coupling.ThermoDynamic = thermo.IsChecked == true;
// Label-less row, so the box indents to the control column with everything
// else instead of starting at the panel's left edge on its own.
rows.Children.Add(Row(content: thermo));
}
return rows;
} }
private static TextBlock UnitScopeHint(string text) => new() private bool IsPassengerVehicle(ConsistItem item)
{
var lead = _consist.ActiveCar(item);
var phys = _info.PhysicsFor(lead);
if (phys == null)
return false;
return phys.LoadAccepted.Split(',', ';')
.Select(s => s.Trim())
.Where(s => s.Length > 0)
.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;
}
/// <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 SettingsSection Section(string header, Control content, Control? meta = null)
{
var section = new SettingsSection
{
Header = header,
Content = content,
// Passive text only: Toggle sits outside the logical tree, so nothing put
// here can be found later with GetLogicalDescendants.
Toggle = meta
};
section.Classes.Add("Compact");
return section;
}
private static StackPanel Stack(params Control[] children)
{
var panel = new StackPanel { Spacing = 3 };
foreach (var c in children)
panel.Children.Add(c);
return panel;
}
private static readonly IBrush DimBrush = new SolidColorBrush(Color.Parse("#9098A0"));
/// <summary>
/// A note that governs a whole panel or section rather than one row - same look as a
/// row's description line. Replaces the old DetailHint/UnitScopeHint pair, which
/// differed only by a 2 px margin.
/// </summary>
private static TextBlock PanelNote(string text) => new()
{ {
Text = text, Text = text,
FontSize = 11, FontSize = 12,
Opacity = 0.6, Foreground = DimBrush,
TextWrapping = TextWrapping.Wrap, TextWrapping = TextWrapping.Wrap,
Margin = new Thickness(0, 0, 0, 2) Margin = new Thickness(0, 0, 0, 8)
}; };
private static TextBlock DetailHint(string text) => new()
{
Text = text, Opacity = 0.6, FontSize = 12, TextWrapping = TextWrapping.Wrap
};
private static Control LabeledRow(string label, Control control, double labelWidth = 120)
{
var grid = new Grid { ColumnDefinitions = new ColumnDefinitions("Auto,8,*") };
var caption = new TextBlock
{
Text = label,
FontSize = 12,
MinWidth = labelWidth,
TextAlignment = TextAlignment.Right,
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Center
};
control.HorizontalAlignment = HorizontalAlignment.Stretch;
Grid.SetColumn(control, 2);
grid.Children.Add(caption);
grid.Children.Add(control);
return grid;
}
private static void AddOption(ComboBox combo, string content, string? tag) private static void AddOption(ComboBox combo, string content, string? tag)
=> combo.Items.Add(new ComboBoxItem { Content = content, Tag = tag }); => combo.Items.Add(new ComboBoxItem { Content = content, Tag = tag });
@@ -402,9 +454,9 @@ public sealed class VehicleDetails
switchCombo.SelectionChanged += (_, _) => Apply(); switchCombo.SelectionChanged += (_, _) => Apply();
loadCombo.SelectionChanged += (_, _) => Apply(); loadCombo.SelectionChanged += (_, _) => Apply();
brakesPanel.Children.Add(LabeledRow(App.Loc["BrakeMode"], modeCombo, labelWidth: 96)); brakesPanel.Children.Add(Row(App.Loc["BrakeMode"], modeCombo));
brakesPanel.Children.Add(LabeledRow(App.Loc["BrakeSwitch"], switchCombo, labelWidth: 96)); brakesPanel.Children.Add(Row(App.Loc["BrakeSwitch"], switchCombo));
brakesPanel.Children.Add(LabeledRow(App.Loc["BrakeLoad"], loadCombo, labelWidth: 96)); brakesPanel.Children.Add(Row(App.Loc["BrakeLoad"], loadCombo));
} }
private static string BrakeModeLabel(string? code) => code switch private static string BrakeModeLabel(string? code) => code switch
@@ -413,7 +465,6 @@ public sealed class VehicleDetails
"P" => App.Loc["BrakePassenger"], "P" => App.Loc["BrakePassenger"],
"R" => App.Loc["BrakeExpress"], "R" => App.Loc["BrakeExpress"],
"M" => App.Loc["BrakeExpressMg"], "M" => App.Loc["BrakeExpressMg"],
"Q" => App.Loc["BrakeNoAir"],
_ => App.Loc["BrakeActingNone"] _ => App.Loc["BrakeActingNone"]
}; };
@@ -427,8 +478,12 @@ public sealed class VehicleDetails
private static string SwitchLabel(string? code) => code switch private static string SwitchLabel(string? code) => code switch
{ {
"<>" => App.Loc["SwitchEmergencyTest"],
"0" => App.Loc["SwitchOff"], "0" => App.Loc["SwitchOff"],
"1" => App.Loc["SwitchOff10"], "1" => App.Loc["SwitchOff10"],
"X" => App.Loc["SwitchAgonal"],
"E" => App.Loc["SwitchEmptied"],
"Q" => App.Loc["BrakeNoAir"],
_ => App.Loc["SwitchNormal"] _ => App.Loc["SwitchNormal"]
}; };
@@ -439,7 +494,7 @@ public sealed class VehicleDetails
NumericUpDown Spin(int value, int max) => new() NumericUpDown Spin(int value, int max) => new()
{ {
FontSize = 12, Minimum = 0, Maximum = max, Increment = 1, FontSize = 12, Minimum = 0, Maximum = max, Increment = 1,
Value = value, MinWidth = 0, Width = 100, FormatString = "0" Value = value, MinWidth = 0, FormatString = "0"
}; };
var sway = Spin(w.Sway, 100); var sway = Spin(w.Sway, 100);
@@ -465,84 +520,100 @@ public sealed class VehicleDetails
flatRand.ValueChanged += (_, _) => Apply(); flatRand.ValueChanged += (_, _) => Apply();
flatProb.ValueChanged += (_, _) => Apply(); flatProb.ValueChanged += (_, _) => Apply();
var grid = new Grid damagePanel.Children.Add(Row(App.Loc["DamageSway"], sway));
{ damagePanel.Children.Add(Row(App.Loc["DamageFlatness"], flat));
ColumnDefinitions = new ColumnDefinitions("*,8,Auto"), damagePanel.Children.Add(Row(App.Loc["DamageFlatnessRand"], flatRand));
RowSpacing = 6 damagePanel.Children.Add(Row(App.Loc["DamageFlatnessProb"], flatProb));
};
void Row(string label, Control control)
{
int r = grid.RowDefinitions.Count;
grid.RowDefinitions.Add(new RowDefinition(GridLength.Auto));
var caption = new TextBlock
{
Text = label, FontSize = 12,
TextWrapping = TextWrapping.Wrap,
TextAlignment = TextAlignment.Right,
VerticalAlignment = VerticalAlignment.Center
};
Grid.SetRow(caption, r);
Grid.SetColumn(caption, 0);
control.HorizontalAlignment = HorizontalAlignment.Right;
Grid.SetRow(control, r);
Grid.SetColumn(control, 2);
grid.Children.Add(caption);
grid.Children.Add(control);
}
Row(App.Loc["DamageSway"], sway);
Row(App.Loc["DamageFlatness"], flat);
Row(App.Loc["DamageFlatnessRand"], flatRand);
Row(App.Loc["DamageFlatnessProb"], flatProb);
damagePanel.Children.Add(grid);
} }
private Control BuildConsistLoadTools() /// <summary>
/// The three actions that fill the selected vehicle from its neighbours. They keep
/// the primary (filled) look because they act on what you have selected.
/// </summary>
private Control BuildVehicleLoadTools(ConsistItem sel)
{ {
var panel = new StackPanel { Spacing = 4, Margin = new Thickness(0, 0, 0, 4) }; bool loadable = _cargo.IsLoadable(sel);
var row = new WrapPanel { Orientation = Orientation.Horizontal };
row.Children.Add(LoadToolButton(App.Loc["ConsistRandomType"], var prev = LoadToolButton(App.Loc["LoadPrevShort"],
() => { _cargo.RandomTypes(_consist); _redraw(); })); () => { if (_consist.Selected is { } s) { _cargo.CopyFromPrevious(_consist, s); _redraw(); } });
row.Children.Add(LoadToolButton(App.Loc["ConsistMaxAmount"], prev.IsEnabled = loadable && _consist.IndexOf(sel) > 0;
() => { _cargo.MaxAmounts(_consist); _redraw(); })); ToolTip.SetTip(prev, App.Loc["LoadCopyPrev"]);
row.Children.Add(LoadToolButton(App.Loc["ConsistRandomAmount"],
() => { _cargo.RandomAmounts(_consist); _redraw(); })); var max = LoadToolButton(App.Loc["LoadMaxShort"],
panel.Children.Add(row); () => { if (_consist.Selected is { } s) { _cargo.FillUnit(s); _redraw(); } });
return panel; max.IsEnabled = loadable;
ToolTip.SetTip(max, App.Loc["LoadMax"]);
var toAll = LoadToolButton(App.Loc["LoadToAllShort"],
() => { if (_consist.Selected is { } s) { _cargo.CopyToFollowing(_consist, s); _redraw(); } });
toAll.IsEnabled = loadable;
ToolTip.SetTip(toAll, App.Loc["LoadCopyToAll"]);
return Stack(prev, max, toAll);
} }
private Button LoadToolButton(string text, Action onClick) /// <summary>
/// The whole-consist actions. Secondary styling plus a header that names the scope
/// and counts the vehicles, because these reach far past the selected card. No
/// tooltips: theirs used to repeat the button label word for word.
/// </summary>
private void AddConsistLoadSection()
{
if (_consist.Count == 0)
return;
var buttons = Stack(
LoadToolButton(App.Loc["ConsistRandomType"],
() => { _cargo.RandomTypes(_consist); _redraw(); }, secondary: true),
LoadToolButton(App.Loc["ConsistMaxAmount"],
() => { _cargo.MaxAmounts(_consist); _redraw(); }, secondary: true),
LoadToolButton(App.Loc["ConsistRandomAmount"],
() => { _cargo.RandomAmounts(_consist); _redraw(); }, secondary: true));
var count = new TextBlock
{
Text = $"{App.Loc["VehicleCount"]}: {_consist.Count}",
FontSize = 12,
Foreground = DimBrush,
VerticalAlignment = VerticalAlignment.Center
};
loadsPanel.Children.Add(Section(App.Loc["ConsistLoadTools"], buttons, count));
}
private Button LoadToolButton(string text, Action onClick, bool secondary = false)
{ {
var b = new Button var b = new Button
{ {
Content = text, FontSize = 11, Margin = new Thickness(0, 0, 4, 4), Content = text, FontSize = 12,
Padding = new Thickness(8, 3), Cursor = _hand, Padding = new Thickness(6, 2), Cursor = _hand,
HorizontalAlignment = HorizontalAlignment.Stretch,
HorizontalContentAlignment = HorizontalAlignment.Center,
IsEnabled = _consist.Count > 0 IsEnabled = _consist.Count > 0
}; };
b.Classes.Add("Flat"); b.Classes.Add(secondary ? "Outline" : "Flat");
b.Click += (_, _) => onClick(); b.Click += (_, _) => onClick();
return b; return b;
} }
private void BuildLoadsEditor(ConsistItem item) /// <summary>
/// What the selected vehicle carries, and the actions that fill it. A vehicle that
/// takes no cargo gets the note alone - previously the three actions stayed behind,
/// greyed out and useless.
/// </summary>
private Control BuildLoadSection(ConsistItem item)
{ {
var lead = _consist.ActiveCar(item); var lead = _consist.ActiveCar(item);
var available = _cargo.AcceptedTypes(lead); var available = _cargo.AcceptedTypes(lead);
if (available.Count == 0 && string.IsNullOrEmpty(lead.LoadType)) if (available.Count == 0 && string.IsNullOrEmpty(lead.LoadType))
{ return PanelNote(App.Loc["LoadNotLoadable"]);
loadsPanel.Children.Add(DetailHint(App.Loc["LoadNotLoadable"]));
return;
}
if (!string.IsNullOrEmpty(lead.LoadType) && if (!string.IsNullOrEmpty(lead.LoadType) &&
!available.Contains(lead.LoadType!, StringComparer.OrdinalIgnoreCase)) !available.Contains(lead.LoadType!, StringComparer.OrdinalIgnoreCase))
available.Insert(0, lead.LoadType!); available.Insert(0, lead.LoadType!);
var typeCombo = new ComboBox { FontSize = 12, MinWidth = 0 }; var typeCombo = new ComboBox { FontSize = 12, MinWidth = 120 };
typeCombo.Items.Add(new ComboBoxItem { Content = App.Loc["None"], Tag = null }); typeCombo.Items.Add(new ComboBoxItem { Content = App.Loc["None"], Tag = null });
foreach (var name in available) foreach (var name in available)
typeCombo.Items.Add(new ComboBoxItem { Content = LoadWeights.Describe(name), Tag = name }); typeCombo.Items.Add(new ComboBoxItem { Content = LoadWeights.Describe(name), Tag = name });
@@ -557,41 +628,129 @@ public sealed class VehicleDetails
break; break;
} }
string? SelectedType() => (typeCombo.SelectedItem as ComboBoxItem)?.Tag as string;
bool IsPant() => Dynamic.IsPantStateType(SelectedType());
int limit = _cargo.LimitFor(lead); int limit = _cargo.LimitFor(lead);
var countBox = new NumericUpDown var countBox = new NumericUpDown
{ {
FontSize = 12, Minimum = 0, Maximum = limit, Increment = 1, FontSize = 12, Minimum = 0, Maximum = limit, Increment = 1,
Value = Math.Min(lead.LoadCount, limit), MinWidth = 120, FormatString = "0" Value = Math.Min(lead.LoadCount, limit), FormatString = "0",
HorizontalAlignment = HorizontalAlignment.Stretch
}; };
var pantHint = DetailHint(App.Loc["LoadPantStateHint"]); var pantStateCombo = new ComboBox
pantHint.IsVisible = lead.IsPantState; {
FontSize = 12, MinWidth = 0,
HorizontalAlignment = HorizontalAlignment.Stretch
};
pantStateCombo.Items.Add(new ComboBoxItem { Content = App.Loc["PantStateNone"], Tag = 0 });
pantStateCombo.Items.Add(new ComboBoxItem { Content = "A", Tag = 1 });
pantStateCombo.Items.Add(new ComboBoxItem { Content = "B", Tag = 2 });
pantStateCombo.Items.Add(new ComboBoxItem { Content = App.Loc["PantStateBoth"], Tag = 3 });
string? SelectedType() => (typeCombo.SelectedItem as ComboBoxItem)?.Tag as string; var countContainer = new Panel();
countContainer.Children.Add(countBox);
countContainer.Children.Add(pantStateCombo);
string UnitText()
{
var phys = _info.PhysicsFor(lead);
if (phys == null) return App.Loc["LoadUnitGeneric"];
if (string.Equals(phys.LoadQ, "pieces", StringComparison.OrdinalIgnoreCase))
return App.Loc["LoadUnitPieces"];
if (string.Equals(phys.LoadQ, "tonns", StringComparison.OrdinalIgnoreCase))
return App.Loc["LoadUnitTons"];
return App.Loc["LoadUnitGeneric"];
}
var countRow = Row(App.Loc["LoadCount"], countContainer);
void SyncPantStateFromCount()
{
int value = Math.Clamp((int)(countBox.Value ?? 0), 0, Dynamic.PantStateMax);
foreach (var obj in pantStateCombo.Items)
if (obj is ComboBoxItem ci && ci.Tag is int t && t == value)
{
pantStateCombo.SelectedItem = ci;
return;
}
}
void SyncCountFromPantState()
{
int value = pantStateCombo.SelectedItem is ComboBoxItem { Tag: int t } ? t : 0;
countBox.Value = Math.Clamp(value, 0, countBox.Maximum);
}
void UpdateVisibility()
{
bool pant = IsPant();
countBox.IsVisible = !pant;
pantStateCombo.IsVisible = pant;
// The unit slot is reserved by the Compact style even when empty, so the
// spinner does not jump sideways as the unit comes and goes.
countRow.Label = pant ? App.Loc["Pantograph"] : App.Loc["LoadCount"];
countRow.ValueText = pant ? null : UnitText();
// The "set an amount above 0" advice is only true while the amount is 0;
// past that it is a line of noise in a panel with no height to spare.
countRow.Description = pant ? App.Loc["LoadPantStateHint"]
: GetCount() == 0 ? App.Loc["LoadHint"]
: null;
}
int GetCount()
{
if (IsPant())
return pantStateCombo.SelectedItem is ComboBoxItem { Tag: int t } ? t : 0;
return (int)(countBox.Value ?? 0);
}
void Apply() void Apply()
{ {
string? type = SelectedType(); string? type = SelectedType();
int count = (int)(countBox.Value ?? 0); int count = GetCount();
foreach (var c in _consist.MemberTargets(item)) foreach (var c in _consist.MemberTargets(item))
{ {
c.LoadType = string.IsNullOrEmpty(type) ? null : type; c.LoadType = string.IsNullOrEmpty(type) ? null : type;
c.LoadCount = string.IsNullOrEmpty(type) ? 0 : Math.Min(count, _cargo.LimitFor(c, type)); c.LoadCount = string.IsNullOrEmpty(type) ? 0 : Math.Min(count, _cargo.LimitFor(c, type));
} }
} }
// Seed the pantograph combo from the model before any handler is listening.
SyncPantStateFromCount();
bool wasPant = IsPant();
typeCombo.SelectionChanged += (_, _) => typeCombo.SelectionChanged += (_, _) =>
{ {
string? type = SelectedType(); countBox.Maximum = _cargo.LimitFor(lead, SelectedType());
pantHint.IsVisible = Dynamic.IsPantStateType(type);
countBox.Maximum = _cargo.LimitFor(lead, type); // Both widgets edit the same LoadCount, so the value is carried across only
// when the mode actually flips. Doing it unconditionally - as UpdateVisibility
// used to - reset the amount to zero on every rebuild, which is why setting
// the maximum load appeared to do nothing: the model was written, the panel
// refreshed, and the refresh immediately wrote zero back.
bool pant = IsPant();
if (pant != wasPant)
{
if (pant)
SyncPantStateFromCount();
else
SyncCountFromPantState();
wasPant = pant;
}
UpdateVisibility();
Apply(); Apply();
}; };
countBox.ValueChanged += (_, _) => Apply(); pantStateCombo.SelectionChanged += (_, _) => Apply();
countBox.ValueChanged += (_, _) => { Apply(); UpdateVisibility(); };
loadsPanel.Children.Add(LabeledRow(App.Loc["LoadType"], typeCombo)); UpdateVisibility();
loadsPanel.Children.Add(LabeledRow(App.Loc["LoadCount"], countBox));
loadsPanel.Children.Add(pantHint); return Stack(
Row(App.Loc["LoadType"], typeCombo),
countRow,
BuildVehicleLoadTools(item));
} }
} }

View File

@@ -15,7 +15,8 @@
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition /> <RowDefinition />
<RowDefinition Height="150" /> <!-- Measures itself, so a whole card fits at either thumbnail size. -->
<RowDefinition Height="Auto" MinHeight="120" MaxHeight="200" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<HeaderedContentControl Grid.Column="0" Grid.Row="0" Theme="{StaticResource GroupBox}" <HeaderedContentControl Grid.Column="0" Grid.Row="0" Theme="{StaticResource GroupBox}"
@@ -23,7 +24,7 @@
Header="{Binding [Sceneries], Source={x:Static starterNg:App.Loc}}"> Header="{Binding [Sceneries], Source={x:Static starterNg:App.Loc}}">
<DockPanel LastChildFill="True"> <DockPanel LastChildFill="True">
<CheckBox DockPanel.Dock="Bottom" Name="archivalSwitch" <CheckBox DockPanel.Dock="Bottom" Name="archivalSwitch"
Margin="2,4,0,0" FontSize="11" Margin="2,4,0,0" FontSize="12"
IsCheckedChanged="ArchivalSwitch_OnChanged" IsCheckedChanged="ArchivalSwitch_OnChanged"
Content="{Binding [HideArchivalSceneries], Source={x:Static starterNg:App.Loc}}" /> Content="{Binding [HideArchivalSceneries], Source={x:Static starterNg:App.Loc}}" />
<TreeView Name="sceneryList" Classes="Compact" <TreeView Name="sceneryList" Classes="Compact"
@@ -54,7 +55,7 @@
Header="{Binding [SceneryConsists], Source={x:Static starterNg:App.Loc}}"> Header="{Binding [SceneryConsists], Source={x:Static starterNg:App.Loc}}">
<HeaderedContentControl.Tag> <HeaderedContentControl.Tag>
<Button Name="randomTexturesButton" Classes="Flat" Cursor="Hand" <Button Name="randomTexturesButton" Classes="Flat" Cursor="Hand"
FontSize="11" MinHeight="0" Padding="10,1" FontSize="12" MinHeight="0" Padding="10,1"
Click="RandomTexturesButton_OnClick" Click="RandomTexturesButton_OnClick"
Content="{Binding [RandomizeTextures], Source={x:Static starterNg:App.Loc}}" Content="{Binding [RandomizeTextures], Source={x:Static starterNg:App.Loc}}"
ToolTip.Tip="{Binding [TipRandomizeTextures], Source={x:Static starterNg:App.Loc}}" /> ToolTip.Tip="{Binding [TipRandomizeTextures], Source={x:Static starterNg:App.Loc}}" />
@@ -144,9 +145,9 @@
VerticalAlignment="Center" TextAlignment="Right" VerticalAlignment="Center" TextAlignment="Right"
FontWeight="SemiBold" /> FontWeight="SemiBold" />
<DockPanel Grid.Row="5" Grid.Column="2" Margin="0,-6,0,0"> <DockPanel Grid.Row="5" Grid.Column="2" Margin="0,-6,0,0">
<TextBlock DockPanel.Dock="Right" FontSize="11" <TextBlock DockPanel.Dock="Right" FontSize="12"
Foreground="{StaticResource CarbonFgDim}" Text="+50 °C" /> Foreground="{StaticResource CarbonFgDim}" Text="+50 °C" />
<TextBlock FontSize="11" <TextBlock FontSize="12"
Foreground="{StaticResource CarbonFgDim}" Text="-50 °C" /> Foreground="{StaticResource CarbonFgDim}" Text="-50 °C" />
</DockPanel> </DockPanel>
@@ -161,10 +162,10 @@
VerticalAlignment="Center" TextAlignment="Right" VerticalAlignment="Center" TextAlignment="Right"
FontWeight="SemiBold" /> FontWeight="SemiBold" />
<DockPanel Grid.Row="8" Grid.Column="2" Margin="0,-6,0,0"> <DockPanel Grid.Row="8" Grid.Column="2" Margin="0,-6,0,0">
<TextBlock DockPanel.Dock="Right" FontSize="11" <TextBlock DockPanel.Dock="Right" FontSize="12"
Foreground="{StaticResource CarbonFgDim}" Foreground="{StaticResource CarbonFgDim}"
Text="{Binding [VisibilityGood], Source={x:Static starterNg:App.Loc}}" /> Text="{Binding [VisibilityGood], Source={x:Static starterNg:App.Loc}}" />
<TextBlock FontSize="11" Foreground="{StaticResource CarbonFgDim}" <TextBlock FontSize="12" Foreground="{StaticResource CarbonFgDim}"
Text="{Binding [VisibilityBad], Source={x:Static starterNg:App.Loc}}" /> Text="{Binding [VisibilityBad], Source={x:Static starterNg:App.Loc}}" />
</DockPanel> </DockPanel>
@@ -217,9 +218,13 @@
<HeaderedContentControl Grid.Row="1" Grid.ColumnSpan="3" Theme="{StaticResource GroupBox}" <HeaderedContentControl Grid.Row="1" Grid.ColumnSpan="3" Theme="{StaticResource GroupBox}"
Header="{Binding [Consist], Source={x:Static starterNg:App.Loc}}"> Header="{Binding [Consist], Source={x:Static starterNg:App.Loc}}">
<!-- Mass, length and the rest ride in the title bar, same as in the
depot, instead of taking a row of their own above the cards. -->
<HeaderedContentControl.Tag>
<StackPanel Name="trainStats" Orientation="Horizontal"
VerticalAlignment="Center" />
</HeaderedContentControl.Tag>
<DockPanel LastChildFill="True"> <DockPanel LastChildFill="True">
<StackPanel DockPanel.Dock="Top" Name="trainStats" Orientation="Horizontal"
VerticalAlignment="Center" Margin="0,0,0,6" />
<ScrollViewer Name="consistScroll" <ScrollViewer Name="consistScroll"
HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled"> HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
<Panel> <Panel>

View File

@@ -896,13 +896,13 @@ public partial class Scenarios : UserControl
var bmp = LoadMini(miniName, thumbH); var bmp = LoadMini(miniName, thumbH);
Control visual = bmp is not null Control visual = bmp is not null
? new Image ? MiniTextures.Sharp(new Image
{ {
Source = bmp, Source = bmp,
Height = thumbH, Height = thumbH,
Stretch = Avalonia.Media.Stretch.Uniform, Stretch = Avalonia.Media.Stretch.Uniform,
Margin = new Thickness(0) Margin = new Thickness(0)
} })
: MissingMiniBox(train.SkinFile, thumbH); : MissingMiniBox(train.SkinFile, thumbH);
bool selected = !string.IsNullOrEmpty(train.Name) && bool selected = !string.IsNullOrEmpty(train.Name) &&

File diff suppressed because it is too large Load Diff

View File

@@ -3,15 +3,19 @@ using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Markup.Xaml.Styling; using Avalonia.Markup.Xaml.Styling;
using Avalonia.Media; using Avalonia.Media;
using StarterNG.Classes; using StarterNG.Classes;
using StarterNG.Controls;
using StarterNG.Infrastructure;
using StarterNG.Services; using StarterNG.Services;
using KeyBinding = StarterNG.Classes.KeyBinding; using KeyBinding = StarterNG.Classes.KeyBinding;
@@ -22,6 +26,15 @@ public partial class Settings : UserControl
{ {
private bool _loading; private bool _loading;
/// <summary>
/// Raised once a saved (or applied) setting changes something the other tabs
/// have already drawn, so they can redraw instead of waiting for a restart.
/// </summary>
public event Action? ThumbnailSizeChanged;
/// <summary>Thumbnail size the other tabs were last drawn at.</summary>
private bool _drawnThumbs = StarterNG.Classes.Settings.Instance.LargeThumbnails;
public Settings() public Settings()
{ {
InitializeComponent(); InitializeComponent();
@@ -41,6 +54,7 @@ public partial class Settings : UserControl
App.Loc.LanguageChanged += SelectActiveLanguage; App.Loc.LanguageChanged += SelectActiveLanguage;
App.Loc.LanguageChanged += UpdateQualityCaptions; App.Loc.LanguageChanged += UpdateQualityCaptions;
App.Loc.LanguageChanged += UpdateSaveState;
App.Loc.LanguageChanged += () => shaderResolutionSlider_OnValueChanged(null, null); App.Loc.LanguageChanged += () => shaderResolutionSlider_OnValueChanged(null, null);
FeedbackCb.SelectionChanged += FeedbackCb_OnSelectionChanged; FeedbackCb.SelectionChanged += FeedbackCb_OnSelectionChanged;
@@ -54,6 +68,10 @@ public partial class Settings : UserControl
KeyboardConfig.Instance.Load(); KeyboardConfig.Instance.Load();
BuildControlsTab(); BuildControlsTab();
AddHandler(KeyDownEvent, OnControlsKeyDown, RoutingStrategies.Tunnel); AddHandler(KeyDownEvent, OnControlsKeyDown, RoutingStrategies.Tunnel);
HookDirtyTracking();
ApplyFilter();
UpdateSaveState();
} }
private static int IndexOfTag(ComboBox cb, string? tag) private static int IndexOfTag(ComboBox cb, string? tag)
@@ -95,6 +113,7 @@ public partial class Settings : UserControl
if (!File.Exists(path)) return; if (!File.Exists(path)) return;
StarterNG.Classes.Settings.Instance.LoadFrom(path); StarterNG.Classes.Settings.Instance.LoadFrom(path);
ApplyToUi(); ApplyToUi();
RedrawThumbnailsIfNeeded();
} }
private void ProfileSaveAsButton_OnClick(object? sender, RoutedEventArgs e) private void ProfileSaveAsButton_OnClick(object? sender, RoutedEventArgs e)
@@ -232,10 +251,9 @@ public partial class Settings : UserControl
VehiclesVolumeSlider.Value = s.VehiclesVolume; VehiclesVolumeSlider.Value = s.VehiclesVolume;
PositionalVolumeSlider.Value = s.PositionalVolume; PositionalVolumeSlider.Value = s.PositionalVolume;
AmbientVolumeSlider.Value = s.AmbientVolume; AmbientVolumeSlider.Value = s.AmbientVolume;
VolumePausedSlider.Value = s.VolumePaused;
SkipPipelineCb.IsChecked = s.SkipPipeline; SkipPipelineCb.IsChecked = s.SkipPipeline;
DebugLogVisibleCb.IsChecked = s.DebugLogVisible; DebugLogVisibleCb.IsChecked = s.DebugLogVisible;
PyScreenPriorityCb.SelectedIndex = Math.Max(0, IndexOfTag(PyScreenPriorityCb, s.PyScreenPriority)); PyScreenPriorityCb.SelectedIndex = Math.Max(0, IndexOfTag(PyScreenPriorityCb, s.PythonScreenUpdateRate.ToString(CultureInfo.InvariantCulture)));
AutoCloseStarterCb.IsChecked = s.AutoCloseStarter; AutoCloseStarterCb.IsChecked = s.AutoCloseStarter;
LargeThumbnailsCb.IsChecked = s.LargeThumbnails; LargeThumbnailsCb.IsChecked = s.LargeThumbnails;
@@ -327,10 +345,9 @@ public partial class Settings : UserControl
s.VehiclesVolume = (int)VehiclesVolumeSlider.Value; s.VehiclesVolume = (int)VehiclesVolumeSlider.Value;
s.PositionalVolume = (int)PositionalVolumeSlider.Value; s.PositionalVolume = (int)PositionalVolumeSlider.Value;
s.AmbientVolume = (int)AmbientVolumeSlider.Value; s.AmbientVolume = (int)AmbientVolumeSlider.Value;
s.VolumePaused = (int)VolumePausedSlider.Value;
s.SkipPipeline = IsChecked(SkipPipelineCb); s.SkipPipeline = IsChecked(SkipPipelineCb);
s.DebugLogVisible = IsChecked(DebugLogVisibleCb); s.DebugLogVisible = IsChecked(DebugLogVisibleCb);
s.PyScreenPriority = (PyScreenPriorityCb.SelectedItem as ComboBoxItem)?.Tag as string ?? "normal"; s.PythonScreenUpdateRate = ParsePriorityTag((PyScreenPriorityCb.SelectedItem as ComboBoxItem)?.Tag);
s.AutoCloseStarter = IsChecked(AutoCloseStarterCb); s.AutoCloseStarter = IsChecked(AutoCloseStarterCb);
s.LargeThumbnails = IsChecked(LargeThumbnailsCb); s.LargeThumbnails = IsChecked(LargeThumbnailsCb);
@@ -344,31 +361,39 @@ public partial class Settings : UserControl
new UartWindow().ShowDialog(owner); new UartWindow().ShowDialog(owner);
} }
private void AboutButton_OnClick(object? sender, RoutedEventArgs e)
{
if (TopLevel.GetTopLevel(this) is Window owner)
AboutWindow.Create().ShowDialog(owner);
}
private void SaveButton_OnClick(object? sender, RoutedEventArgs e) private void SaveButton_OnClick(object? sender, RoutedEventArgs e)
{ {
ReadFromUi(); ReadFromUi();
StarterNG.Classes.Settings.Instance.Save(); StarterNG.Classes.Settings.Instance.Save();
KeyboardConfig.Instance.Save(); KeyboardConfig.Instance.Save();
RedrawThumbnailsIfNeeded();
_dirty = false;
UpdateSaveState();
if (SaveStatus is not null) if (SaveStatus is not null)
SaveStatus.Text = App.Loc["SettingsSaved"]; SaveStatus.Text = App.Loc["SettingsSaved"];
} }
private void RedrawThumbnailsIfNeeded()
{
bool large = StarterNG.Classes.Settings.Instance.LargeThumbnails;
if (large == _drawnThumbs)
return;
_drawnThumbs = large;
ThumbnailSizeChanged?.Invoke();
}
private void ResetButton_OnClick(object? sender, RoutedEventArgs e) private void ResetButton_OnClick(object? sender, RoutedEventArgs e)
{ {
StarterNG.Classes.Settings.Instance.Load(); StarterNG.Classes.Settings.Instance.Load();
ApplyToUi(); ApplyToUi();
RedrawThumbnailsIfNeeded();
CancelCapture(); CancelCapture();
KeyboardConfig.Instance.Load(); KeyboardConfig.Instance.Load();
RebuildBindingList(); RebuildBindingList();
RebuildKeyboard(); RebuildKeyboard();
if (SaveStatus is not null) _dirty = false;
SaveStatus.Text = string.Empty; UpdateSaveState();
} }
private void SelectResolution(int width, int height) private void SelectResolution(int width, int height)
@@ -404,29 +429,29 @@ public partial class Settings : UserControl
private void TextureResolutionSlider_OnValueChanged(object? sender, RangeBaseValueChangedEventArgs e) private void TextureResolutionSlider_OnValueChanged(object? sender, RangeBaseValueChangedEventArgs e)
{ {
if (texResolution is null || textureResolutionSlider is null) if (TexResolutionRow is null || textureResolutionSlider is null)
return; return;
int resolution = 1 << (int)textureResolutionSlider.Value; int resolution = 1 << (int)textureResolutionSlider.Value;
texResolution.Text = $"{resolution} px"; TexResolutionRow.ValueText = $"{resolution} px";
} }
private void CabTextureResolutionSlider_OnValueChanged(object? sender, RangeBaseValueChangedEventArgs e) private void CabTextureResolutionSlider_OnValueChanged(object? sender, RangeBaseValueChangedEventArgs e)
{ {
if (cabTexResolution is null || cabTextureResolutionSlider is null) if (CabTexResolutionRow is null || cabTextureResolutionSlider is null)
return; return;
int resolution = 1 << (int)cabTextureResolutionSlider.Value; int resolution = 1 << (int)cabTextureResolutionSlider.Value;
cabTexResolution.Text = $"{resolution} px"; CabTexResolutionRow.ValueText = $"{resolution} px";
} }
private void shaderResolutionSlider_OnValueChanged(object? sender, RangeBaseValueChangedEventArgs e) private void shaderResolutionSlider_OnValueChanged(object? sender, RangeBaseValueChangedEventArgs e)
{ {
if (shaderResolution is null || shaderResolutionSlider is null) if (ShadowResolutionRow is null || shaderResolutionSlider is null)
return; return;
int resolution = 1 << (int)shaderResolutionSlider.Value; int resolution = 1 << (int)shaderResolutionSlider.Value;
shaderResolution.Text = $"{resolution} px"; ShadowResolutionRow.ValueText = $"{resolution} px";
ToolTip.SetTip(shaderResolutionSlider, ShadowMapWord(resolution)); ToolTip.SetTip(shaderResolutionSlider, ShadowMapWord(resolution));
} }
@@ -464,16 +489,196 @@ public partial class Settings : UserControl
private void FeedbackCb_OnSelectionChanged(object? sender, SelectionChangedEventArgs e) => private void FeedbackCb_OnSelectionChanged(object? sender, SelectionChangedEventArgs e) =>
UpdateFeedbackDetailVisibility(); UpdateFeedbackDetailVisibility();
private void UpdateFeedbackDetailVisibility() /// <summary>
/// The port and UART rows only apply to some feedback modes. Visibility is
/// decided in one place — <see cref="ApplyFilter"/> — so that a search cannot
/// reveal a row the selected mode does not use.
/// </summary>
private void UpdateFeedbackDetailVisibility() => ApplyFilter();
private bool ConditionAllows(SettingRow row)
{ {
FeedbackPortRow.IsVisible = FeedbackCb.SelectedIndex == 3; if (ReferenceEquals(row, FeedbackPortRow))
ComButton.IsVisible = FeedbackCb.SelectedIndex == 5; return FeedbackCb.SelectedIndex == 3;
if (ReferenceEquals(row, ComButtonRow))
return FeedbackCb.SelectedIndex == 5;
return true;
}
// ── Category navigation, search and unsaved-changes state ────────────────
private bool _dirty;
private void CategoryList_OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
// Picking a category is how you leave a search, so the query goes with it.
if (SettingsSearch is { Text.Length: > 0 })
SettingsSearch.Text = string.Empty; // raises TextChanged -> ApplyFilter
else
ApplyFilter();
}
private void SettingsSearch_OnTextChanged(object? sender, TextChangedEventArgs e) => ApplyFilter();
/// <summary>
/// Single owner of what is on screen. With an empty query it shows the selected
/// category; with a query it stacks every category and keeps only the rows that
/// match, collapsing sections and categories that end up empty.
/// </summary>
private void ApplyFilter()
{
if (SettingsStack is null || CategoryList is null || ControlsHost is null)
return;
string query = SettingsSearch?.Text?.Trim() ?? string.Empty;
bool searching = query.Length > 0;
string category = (CategoryList.SelectedItem as ListBoxItem)?.Tag as string ?? string.Empty;
// Key bindings are their own world: they bring their own scrolling and a
// star-sized row, so they replace the settings scroller rather than living
// in it, and the search box filters them instead of the settings.
bool bindings = category == "controls";
ControlsHost.IsVisible = bindings;
SettingsScroll.IsVisible = !bindings;
if (bindings)
{
RebuildBindingList();
foreach (var entry in CategoryList.Items.OfType<ListBoxItem>())
entry.Classes.Set("NoHits", false);
return;
}
foreach (var panel in SettingsStack.Children.OfType<StackPanel>())
{
bool anyRow = false;
foreach (var section in panel.GetLogicalDescendants().OfType<SettingsSection>())
{
bool anyInSection = false;
foreach (var row in section.GetLogicalDescendants().OfType<SettingRow>())
{
bool visible = ConditionAllows(row) &&
(!searching || TextMatch.Contains(RowSearchText(row), query));
row.IsVisible = visible;
anyInSection |= visible;
}
section.IsVisible = anyInSection;
anyRow |= anyInSection;
}
string tag = panel.Tag as string ?? string.Empty;
panel.IsVisible = anyRow && (searching || tag == category);
// The caption only earns its place when several categories are stacked.
foreach (var caption in panel.Children.OfType<TextBlock>())
caption.IsVisible = searching;
if (CategoryItem(tag) is { } item)
item.Classes.Set("NoHits", searching && !anyRow);
}
if (searching)
SettingsScroll.Offset = new Vector(0, 0);
}
private ListBoxItem? CategoryItem(string tag) =>
CategoryList.Items.OfType<ListBoxItem>()
.FirstOrDefault(i => string.Equals(i.Tag as string, tag, StringComparison.Ordinal));
/// <summary>
/// Everything about a row a user might type: its label, its explanation and
/// the text of the control itself (a checkbox caption, a dropdown's options).
/// </summary>
private static string RowSearchText(SettingRow row)
{
var sb = new StringBuilder();
sb.Append(row.Label).Append(' ').Append(row.Description).Append(' ');
switch (row.Content)
{
case CheckBox cb:
sb.Append(cb.Content);
break;
case Button btn:
sb.Append(btn.Content);
break;
case ComboBox combo:
foreach (var item in combo.Items.OfType<ComboBoxItem>())
sb.Append(item.Content).Append(' ');
break;
}
return sb.ToString();
}
/// <summary>
/// Subscribes every input in the panel to <see cref="MarkDirty"/> once, so the
/// Save button can tell the user there is something to save. Section master
/// switches hang off SettingsSection.Toggle rather than the logical tree, so
/// they are collected separately.
/// </summary>
private void HookDirtyTracking()
{
foreach (var control in this.GetLogicalDescendants().OfType<Control>())
{
switch (control)
{
// Choosing a profile changes nothing until Apply is pressed.
case ComboBox combo when ReferenceEquals(combo, ProfileCb):
break;
case CheckBox cb:
cb.IsCheckedChanged += (_, _) => MarkDirty();
break;
case Slider slider:
slider.ValueChanged += (_, _) => MarkDirty();
break;
case ComboBox combo:
combo.SelectionChanged += (_, _) => MarkDirty();
break;
case NumericUpDown nud:
nud.ValueChanged += (_, _) => MarkDirty();
break;
}
}
foreach (var section in this.GetLogicalDescendants().OfType<SettingsSection>())
if (section.Toggle is CheckBox toggle)
toggle.IsCheckedChanged += (_, _) => MarkDirty();
}
private void MarkDirty()
{
if (_loading || _dirty)
return;
_dirty = true;
UpdateSaveState();
}
private void UpdateSaveState()
{
if (SaveButton is null || SaveStatus is null)
return;
bool dirty = _dirty || KeyboardConfig.Instance.Dirty;
SaveButton.Classes.Set("Accent", dirty);
SaveStatus.Text = dirty ? App.Loc["UnsavedChanges"] : string.Empty;
} }
private static bool IsChecked(CheckBox cb) => cb.IsChecked == true; private static bool IsChecked(CheckBox cb) => cb.IsChecked == true;
private static int Clamp(int v, int lo, int hi) => v < lo ? lo : (v > hi ? hi : v); private static int Clamp(int v, int lo, int hi) => v < lo ? lo : (v > hi ? hi : v);
private static int ParsePriorityTag(object? tag)
{
if (tag is int i) return i;
if (tag is string s && int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out int n)) return n;
return 200;
}
private static int Log2(int value, int fallback) private static int Log2(int value, int fallback)
{ {
if (value <= 0) return fallback; if (value <= 0) return fallback;
@@ -490,10 +695,15 @@ public partial class Settings : UserControl
return 4; return 4;
} }
private TextBox? _controlsSearch;
private StackPanel? _bindingListPanel; private StackPanel? _bindingListPanel;
private StackPanel? _keyboardPanel; private StackPanel? _keyboardPanel;
/// <summary>
/// True while a cab-control binding is waiting for a keypress. The window's own
/// shortcuts stand down for the duration, otherwise Ctrl+N would never reach here.
/// </summary>
public bool IsCapturingKey => _capturing is not null;
private KeyBinding? _capturing; private KeyBinding? _capturing;
private Button? _capturingButton; private Button? _capturingButton;
@@ -527,10 +737,6 @@ public partial class Settings : UserControl
VerticalAlignment = VerticalAlignment.Center, VerticalAlignment = VerticalAlignment.Center,
Foreground = FgDimBrush Foreground = FgDimBrush
}); });
_controlsSearch = new TextBox { Width = 240, PlaceholderText = App.Loc["Search"] };
_controlsSearch.TextChanged += (_, _) => RebuildBindingList();
topBar.Children.Add(_controlsSearch);
var restoreBtn = new Button { Content = App.Loc["RestoreDefaults"] }; var restoreBtn = new Button { Content = App.Loc["RestoreDefaults"] };
restoreBtn.Classes.Add("Flat"); restoreBtn.Classes.Add("Flat");
restoreBtn.Click += (_, _) => restoreBtn.Click += (_, _) =>
@@ -538,6 +744,7 @@ public partial class Settings : UserControl
CancelCapture(); CancelCapture();
KeyboardConfig.Instance.LoadDefaults(); KeyboardConfig.Instance.LoadDefaults();
KeyboardConfig.Instance.Dirty = true; KeyboardConfig.Instance.Dirty = true;
MarkDirty();
RebuildBindingList(); RebuildBindingList();
RebuildKeyboard(); RebuildKeyboard();
}; };
@@ -576,7 +783,8 @@ public partial class Settings : UserControl
return; return;
_bindingListPanel.Children.Clear(); _bindingListPanel.Children.Clear();
string filter = _controlsSearch?.Text?.Trim().ToLowerInvariant() ?? string.Empty; // The sidebar search box serves this list too - see ApplyFilter.
string filter = SettingsSearch?.Text?.Trim() ?? string.Empty;
var conflicts = ComputeConflicts(); var conflicts = ComputeConflicts();
foreach (var b in KeyboardConfig.Instance.Bindings) foreach (var b in KeyboardConfig.Instance.Bindings)
@@ -588,8 +796,8 @@ public partial class Settings : UserControl
} }
private static bool MatchesFilter(KeyBinding b, string filter) => private static bool MatchesFilter(KeyBinding b, string filter) =>
b.Command.ToLowerInvariant().Contains(filter) || TextMatch.Contains(b.Command, filter) ||
b.Description.ToLowerInvariant().Contains(filter); TextMatch.Contains(b.Description, filter);
private Control BuildBindingRow(KeyBinding b, HashSet<string> conflicts) private Control BuildBindingRow(KeyBinding b, HashSet<string> conflicts)
{ {
@@ -604,14 +812,14 @@ public partial class Settings : UserControl
{ {
Text = CommandLabel(b), Text = CommandLabel(b),
Foreground = FgBrush, Foreground = FgBrush,
FontSize = 13, FontSize = 12,
TextTrimming = TextTrimming.CharacterEllipsis TextTrimming = TextTrimming.CharacterEllipsis
}); });
label.Children.Add(new TextBlock label.Children.Add(new TextBlock
{ {
Text = b.Command, Text = b.Command,
Foreground = FgDimBrush, Foreground = FgDimBrush,
FontSize = 11, FontSize = 12,
TextTrimming = TextTrimming.CharacterEllipsis TextTrimming = TextTrimming.CharacterEllipsis
}); });
Grid.SetColumn(label, 0); Grid.SetColumn(label, 0);
@@ -647,6 +855,7 @@ public partial class Settings : UserControl
b.Shift = b.Ctrl = false; b.Shift = b.Ctrl = false;
b.Key = "none"; b.Key = "none";
KeyboardConfig.Instance.Dirty = true; KeyboardConfig.Instance.Dirty = true;
MarkDirty();
RebuildBindingList(); RebuildBindingList();
RebuildKeyboard(); RebuildKeyboard();
}; };
@@ -710,6 +919,7 @@ public partial class Settings : UserControl
_capturing.Ctrl = e.KeyModifiers.HasFlag(KeyModifiers.Control); _capturing.Ctrl = e.KeyModifiers.HasFlag(KeyModifiers.Control);
_capturing.Key = token; _capturing.Key = token;
KeyboardConfig.Instance.Dirty = true; KeyboardConfig.Instance.Dirty = true;
MarkDirty();
e.Handled = true; e.Handled = true;
_capturing = null; _capturing = null;
@@ -912,7 +1122,7 @@ public partial class Settings : UserControl
{ {
Text = App.Loc["BindKeyHint"], Text = App.Loc["BindKeyHint"],
Foreground = FgDimBrush, Foreground = FgDimBrush,
FontSize = 11, FontSize = 12,
TextWrapping = TextWrapping.Wrap TextWrapping = TextWrapping.Wrap
}); });
panel.Children.Add(grid); panel.Children.Add(grid);
@@ -957,6 +1167,7 @@ public partial class Settings : UserControl
} }
KeyboardConfig.Instance.Dirty = true; KeyboardConfig.Instance.Dirty = true;
MarkDirty();
} }
private static KeyBinding? CommandInSlot(string token, bool shift, bool ctrl) => private static KeyBinding? CommandInSlot(string token, bool shift, bool ctrl) =>
@@ -1074,25 +1285,25 @@ public partial class Settings : UserControl
{ {
var q = QualityWords; var q = QualityWords;
if (texFilteringValue != null) if (TexFilteringRow != null)
texFilteringValue.Text = q[Clamp((int)TexFilteringSlider.Value, 1, 5) - 1]; TexFilteringRow.ValueText = q[Clamp((int)TexFilteringSlider.Value, 1, 5) - 1];
string[] four = { q[0], q[1], q[3], q[4] }; string[] four = { q[0], q[1], q[3], q[4] };
if (trackCurvesValue != null) if (TrackCurvesRow != null)
trackCurvesValue.Text = four[Clamp((int)TrackCurvesSlider.Value, 1, 4) - 1]; TrackCurvesRow.ValueText = four[Clamp((int)TrackCurvesSlider.Value, 1, 4) - 1];
if (smokeParticlesValue != null) if (SmokeParticlesRow != null)
smokeParticlesValue.Text = four[Clamp((int)SmokeParticlesSlider.Value, 1, 4) - 1]; SmokeParticlesRow.ValueText = four[Clamp((int)SmokeParticlesSlider.Value, 1, 4) - 1];
string[] msaa = { App.Loc["MsaaNone"], "x2", "x4", "x8" }; string[] msaa = { App.Loc["MsaaNone"], "x2", "x4", "x8" };
if (multisamplingValue != null) if (MultisamplingRow != null)
multisamplingValue.Text = msaa[Clamp((int)MultisamplingSlider.Value, 1, 4) - 1]; MultisamplingRow.ValueText = msaa[Clamp((int)MultisamplingSlider.Value, 1, 4) - 1];
string[] range = { App.Loc["RangeNormal"], App.Loc["RangeHigh"], App.Loc["RangeVeryHigh"] }; string[] range = { App.Loc["RangeNormal"], App.Loc["RangeHigh"], App.Loc["RangeVeryHigh"] };
if (renderRangeValue != null) if (RenderRangeRow != null)
renderRangeValue.Text = range[Clamp((int)RenderRangeSlider.Value, 1, 3) - 1]; RenderRangeRow.ValueText = range[Clamp((int)RenderRangeSlider.Value, 1, 3) - 1];
if (cursorSensitivityValue != null) if (CursorSensitivityRow != null)
cursorSensitivityValue.Text = (int)CursorSensitivitySlider.Value switch CursorSensitivityRow.ValueText = (int)CursorSensitivitySlider.Value switch
{ {
<= 1 => q[1], <= 1 => q[1],
2 => App.Loc["QualityStandard"], 2 => App.Loc["QualityStandard"],
@@ -1100,25 +1311,25 @@ public partial class Settings : UserControl
_ => q[4] _ => q[4]
}; };
if (shaderRangeValue != null) if (ShadowRangeRow != null)
{ {
int metres = (int)shaderRange.Value; int metres = (int)shaderRange.Value;
shaderRangeValue.Text = $"{metres} m"; ShadowRangeRow.ValueText = $"{metres} m";
ToolTip.SetTip(shaderRange, ShadowRangeWord(metres)); ToolTip.SetTip(shaderRange, ShadowRangeWord(metres));
} }
if (cabShadowRangeValue != null) if (CabShadowRangeRow != null)
{ {
int metres = (int)cabShaderSourceRange.Value; int metres = (int)cabShaderSourceRange.Value;
cabShadowRangeValue.Text = metres <= 0 ? App.Loc["RangeDisabled"] : $"{metres} m"; CabShadowRangeRow.ValueText = metres <= 0 ? App.Loc["RangeDisabled"] : $"{metres} m";
ToolTip.SetTip(cabShaderSourceRange, CabShadowRangeWord(metres)); ToolTip.SetTip(cabShaderSourceRange, CabShadowRangeWord(metres));
} }
if (reflectionsFramerateValue != null) if (ReflectionsFramerateRow != null)
{ {
int fps = (int)reflectionsFramerate.Value; int fps = (int)reflectionsFramerate.Value;
string word = ReflectionsRefreshWord(fps); string word = ReflectionsRefreshWord(fps);
reflectionsFramerateValue.Text = $"{fps} FPS"; ReflectionsFramerateRow.ValueText = $"{fps} FPS";
ToolTip.SetTip(reflectionsFramerate, word.Length == 0 ? null : word); ToolTip.SetTip(reflectionsFramerate, word.Length == 0 ? null : word);
} }
} }

View File

@@ -0,0 +1,103 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Media;
using StarterNG.Infrastructure;
namespace StarterNG.Views;
/// <summary>
/// The cheat sheet, printed straight from <see cref="Shortcuts.All"/> so it can never
/// list a gesture the window does not actually dispatch.
/// </summary>
public static class ShortcutsWindow
{
public static Window Create()
{
var list = new StackPanel { Spacing = 2, Margin = new Thickness(20, 12, 20, 20) };
foreach (var scope in new[] { ShortcutScope.Global, ShortcutScope.Depot, ShortcutScope.Consist })
{
list.Children.Add(new TextBlock
{
Text = Shortcuts.ScopeLabel(scope),
FontWeight = FontWeight.Bold,
Margin = new Thickness(0, 12, 0, 4)
});
foreach (var s in Shortcuts.InScope(scope))
list.Children.Add(Row(s.Display, App.Loc[s.LabelKey]));
}
list.Children.Add(new TextBlock
{
Text = App.Loc["ShortcutsTabHint"],
Opacity = 0.7,
TextWrapping = TextWrapping.Wrap,
Margin = new Thickness(0, 16, 0, 0)
});
var win = new Window
{
Title = App.Loc["ShortcutsTitle"],
Width = 460,
Height = 560,
MinWidth = 380,
MinHeight = 320,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
Content = new ScrollViewer
{
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
Content = list
}
};
win.KeyDown += (_, e) =>
{
if (e.Key is Key.Escape or Key.F1)
{
win.Close();
e.Handled = true;
}
};
return win;
}
private static Control Row(string gesture, string label)
{
var grid = new Grid { ColumnDefinitions = new ColumnDefinitions("130,12,*") };
var keys = new Border
{
Background = new SolidColorBrush(Color.Parse("#2A3037")),
BorderBrush = new SolidColorBrush(Color.Parse("#3A424A")),
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(3),
Padding = new Thickness(6, 1),
HorizontalAlignment = HorizontalAlignment.Left,
Child = new TextBlock
{
Text = gesture,
FontFamily = new FontFamily("Consolas, Menlo, monospace"),
FontSize = 12
}
};
var text = new TextBlock
{
Text = label,
FontSize = 12,
VerticalAlignment = VerticalAlignment.Center,
TextWrapping = TextWrapping.Wrap
};
Grid.SetColumn(text, 2);
grid.Children.Add(keys);
grid.Children.Add(text);
grid.Margin = new Thickness(0, 2);
return grid;
}
}

View File

@@ -36,7 +36,7 @@
</Grid> </Grid>
<TextBlock Name="statusText" DockPanel.Dock="Bottom" Margin="0,6,0,0" <TextBlock Name="statusText" DockPanel.Dock="Bottom" Margin="0,6,0,0"
FontSize="11" Opacity="0.75" /> FontSize="12" Opacity="0.75" />
<Grid DockPanel.Dock="Left" Width="260" RowDefinitions="*,4,*"> <Grid DockPanel.Dock="Left" Width="260" RowDefinitions="*,4,*">
<ListBox Name="typeList" Grid.Row="0" Classes="Compact" /> <ListBox Name="typeList" Grid.Row="0" Classes="Compact" />

View File

@@ -45,7 +45,7 @@ public sealed class UartWindow : Window
root.Children.Add(new TextBlock root.Children.Add(new TextBlock
{ {
Text = App.Loc["UartHint"], Text = App.Loc["UartHint"],
FontSize = 11, FontSize = 12,
Opacity = 0.65, Opacity = 0.65,
TextWrapping = TextWrapping.Wrap TextWrapping = TextWrapping.Wrap
}); });

View File

@@ -69,7 +69,7 @@
<String key="OptionalIncludes">Volitelné prvky</String> <String key="OptionalIncludes">Volitelné prvky</String>
<String key="OpenTextureFolder">Otevřít složku s texturami</String> <String key="OpenTextureFolder">Otevřít složku s texturami</String>
<String key="CopyTextureName">Kopírovat název</String> <String key="CopyTextureName">Kopírovat název</String>
<String key="CopyCoupler">Kopírovat spřáhlo z předchozího</String> <String key="CopyCoupler">Kopírovat z předchozího</String>
<String key="ThermoAmbient">Chladivo na okolní teplotě</String> <String key="ThermoAmbient">Chladivo na okolní teplotě</String>
<String key="TextureInfo">Textura</String> <String key="TextureInfo">Textura</String>
<String key="TexOperator">Dopravce</String> <String key="TexOperator">Dopravce</String>
@@ -185,6 +185,9 @@
<String key="SwitchNormal">N - brzdič zabržděný</String> <String key="SwitchNormal">N - brzdič zabržděný</String>
<String key="SwitchOff">0 - odbržděný, odpuštěný</String> <String key="SwitchOff">0 - odbržděný, odpuštěný</String>
<String key="SwitchOff10">1 - odbržděný, odp. z 10% pravd.</String> <String key="SwitchOff10">1 - odbržděný, odp. z 10% pravd.</String>
<String key="SwitchEmergencyTest">&lt;&gt; - vyp. na zkoušku rychlobrzdy</String>
<String key="SwitchAgonal">X - dosluhující (20% vyp., náhodně)</String>
<String key="SwitchEmptied">E - vyprázdněný (potrubí na 0)</String>
<!-- Damage (wheel parameters) --> <!-- Damage (wheel parameters) -->
<String key="Damage">Poškození</String> <String key="Damage">Poškození</String>
@@ -194,15 +197,24 @@
<String key="DamageFlatnessProb">Pravděpodobnost plochého místa (%)</String> <String key="DamageFlatnessProb">Pravděpodobnost plochého místa (%)</String>
<String key="LoadType">Druh nákladu</String> <String key="LoadType">Druh nákladu</String>
<String key="LoadCount">Množství</String> <String key="LoadCount">Množství</String>
<String key="LoadUnitPieces">ks</String>
<String key="LoadUnitTons">t</String>
<String key="LoadUnitGeneric">j.</String>
<String key="LoadHint">Pro naložení vozidla zadejte množství větší než 0.</String> <String key="LoadHint">Pro naložení vozidla zadejte množství větší než 0.</String>
<String key="LoadNotLoadable">Toto vozidlo nepřijímá žádný náklad.</String> <String key="LoadNotLoadable">Toto vozidlo nepřijímá žádný náklad.</String>
<String key="LoadPantState">Zvednutý pantograf na startu</String> <String key="LoadPantState">Zvednutý pantograf na startu</String>
<String key="LoadPantStateHint">Množství určuje pantograf: 1 — A, 2 — B, 3 — oba.</String> <String key="LoadPantStateHint">Množství určuje pantograf: 1 — A, 2 — B, 3 — oba.</String>
<String key="Pantograph">Pantograf</String>
<String key="PantStateNone">Žádný</String>
<String key="PantStateBoth">Oba</String>
<String key="WagonNumber">Číslo vozu</String> <String key="WagonNumber">Číslo vozu</String>
<String key="LoadCopyPrev">Kopírovat náklad z předchozího vozidla</String> <String key="LoadCopyPrev">Kopírovat náklad z předchozího vozidla</String>
<String key="LoadMax">Maximální možný náklad</String> <String key="LoadMax">Maximální možný náklad</String>
<String key="LoadPrevShort">Z předchozího</String>
<String key="LoadToAllShort">Na následující</String>
<String key="LoadMaxShort">Maximální</String>
<String key="ConsistLoadTools">Náklad celé soupravy</String> <String key="ConsistLoadTools">Náklad celé soupravy</String>
<String key="ConsistRandomType">Náhodný náklad</String> <String key="ConsistRandomType">Náhodný druh</String>
<String key="ConsistMaxAmount">Maximální množství</String> <String key="ConsistMaxAmount">Maximální množství</String>
<String key="ConsistRandomAmount">Náhodné množství</String> <String key="ConsistRandomAmount">Náhodné množství</String>
<String key="Set">Nastavit</String> <String key="Set">Nastavit</String>
@@ -252,6 +264,7 @@
<String key="CouplingUnitRear">Spřáhlo na konci jednotky</String> <String key="CouplingUnitRear">Spřáhlo na konci jednotky</String>
<String key="Join">Spojit</String> <String key="Join">Spojit</String>
<String key="TipJoin">Spojit toto vozidlo s následujícím do jedné jednotky</String> <String key="TipJoin">Spojit toto vozidlo s následujícím do jedné jednotky</String>
<String key="TipReorder">Přetáhněte pro změnu pořadí</String>
<String key="AddVehicle">Přidat</String> <String key="AddVehicle">Přidat</String>
<String key="TipAddVehicle">Přidat toto vozidlo do soupravy</String> <String key="TipAddVehicle">Přidat toto vozidlo do soupravy</String>
<String key="TipAddUnit">Přidat celou jednotku do soupravy</String> <String key="TipAddUnit">Přidat celou jednotku do soupravy</String>
@@ -278,7 +291,7 @@
<String key="Fullscreen">Celá obrazovka</String> <String key="Fullscreen">Celá obrazovka</String>
<String key="PauseSimInactive">Pozastavit simulaci při neaktivním okně</String> <String key="PauseSimInactive">Pozastavit simulaci při neaktivním okně</String>
<String key="PauseSimStart">Pozastavit po spuštění</String> <String key="PauseSimStart">Pozastavit po spuštění</String>
<String key="CursorSensitivity">Citlivost kurzoru:</String> <String key="CursorSensitivity">Citlivost kurzoru</String>
<String key="MouseHorInvert">Převrátit myš vodorovně</String> <String key="MouseHorInvert">Převrátit myš vodorovně</String>
<String key="MouseVertInvert">Převrátit myš svisle</String> <String key="MouseVertInvert">Převrátit myš svisle</String>
@@ -309,7 +322,7 @@
<String key="UartRadioChannel">Kanál rádia</String> <String key="UartRadioChannel">Kanál rádia</String>
<String key="Others">Ostatní</String> <String key="Others">Ostatní</String>
<String key="SelectEXE">Vybrat exe:</String> <String key="SelectEXE">Vybrat exe</String>
<String key="SelectEXEAuto">Vybrat automaticky</String> <String key="SelectEXEAuto">Vybrat automaticky</String>
<String key="DebugMode">Režim ladění (testovací režim)</String> <String key="DebugMode">Režim ladění (testovací režim)</String>
<String key="VirtualShunting">Virtuální posun</String> <String key="VirtualShunting">Virtuální posun</String>
@@ -324,12 +337,12 @@
<String key="REOff">Vypnout vizuální vrstvu</String> <String key="REOff">Vypnout vizuální vrstvu</String>
<String key="REExperimental">Experimentální</String> <String key="REExperimental">Experimentální</String>
<String key="Resolution">Rozlišení:</String> <String key="Resolution">Rozlišení</String>
<String key="BufforResolution">Rozlišení bufferu:</String> <String key="BufforResolution">Rozlišení bufferu</String>
<String key="MaxTexResolution">Maximální rozlišení textur:</String> <String key="MaxTexResolution">Maximální rozlišení textur</String>
<String key="MaxTexCabResolution">Maximální rozlišení textur kabiny:</String> <String key="MaxTexCabResolution">Maximální rozlišení textur kabiny</String>
<String key="TexFilteringQuality">Kvalita filtrování textur:</String> <String key="TexFilteringQuality">Kvalita filtrování textur</String>
<String key="Multisampling">Multisampling:</String> <String key="Multisampling">Multisampling</String>
<String key="QualityVeryLow">Velmi nízká</String> <String key="QualityVeryLow">Velmi nízká</String>
<String key="QualityLow">Nízká</String> <String key="QualityLow">Nízká</String>
<String key="QualityNormal">Normální</String> <String key="QualityNormal">Normální</String>
@@ -346,8 +359,8 @@
<String key="RangeHigh">Vysoká</String> <String key="RangeHigh">Vysoká</String>
<String key="RangeVeryHigh">Velmi vysoká</String> <String key="RangeVeryHigh">Velmi vysoká</String>
<String key="RangeDisabled">Vypnuté</String> <String key="RangeDisabled">Vypnuté</String>
<String key="DynamicLights">Dynamické zdroje světla:</String> <String key="DynamicLights">Dynamické zdroje světla</String>
<String key="RenderRange">Dohlednost vykreslování:</String> <String key="RenderRange">Dohlednost vykreslování</String>
<String key="FpsLimitEnable">Omezit snímkovou frekvenci</String> <String key="FpsLimitEnable">Omezit snímkovou frekvenci</String>
<String key="ShadowAngleLimit">Omezení délky stínů</String> <String key="ShadowAngleLimit">Omezení délky stínů</String>
<String key="ShadowLimitNone">Bez omezení</String> <String key="ShadowLimitNone">Bez omezení</String>
@@ -362,6 +375,7 @@
<String key="MotionBlur">Rozmazání pohybem</String> <String key="MotionBlur">Rozmazání pohybem</String>
<String key="AdditionalShadersEffects">Další efekty shaderů</String> <String key="AdditionalShadersEffects">Další efekty shaderů</String>
<String key="ReflectionsCubeMap">Odrazy jako cubemapa</String> <String key="ReflectionsCubeMap">Odrazy jako cubemapa</String>
<String key="SkipPipeline">SkipPipeline</String>
<String key="RenderVBO">Vykreslování VBO</String> <String key="RenderVBO">Vykreslování VBO</String>
<String key="RenderShadows">Vykreslovat stíny</String> <String key="RenderShadows">Vykreslovat stíny</String>
<String key="ReflectionsFramerate">Frekvence obnovy odrazů</String> <String key="ReflectionsFramerate">Frekvence obnovy odrazů</String>
@@ -419,14 +433,14 @@
<String key="IgnoreIrrelevantTrains">Přeskočit dekorační soupravy při startu</String> <String key="IgnoreIrrelevantTrains">Přeskočit dekorační soupravy při startu</String>
<String key="Starter">Starter</String> <String key="Starter">Starter</String>
<String key="SettingsProfile">Profil nastavení:</String> <String key="SettingsProfile">Profil nastavení</String>
<String key="ProfileApply">Použít</String> <String key="ProfileApply">Použít</String>
<String key="ProfileSaveAs">Uložit jako…</String> <String key="ProfileSaveAs">Uložit jako…</String>
<String key="ProfileNamePrompt">Název profilu</String> <String key="ProfileNamePrompt">Název profilu</String>
<String key="AutoCloseStarter">Automaticky zavřít starter</String> <String key="AutoCloseStarter">Automaticky zavřít starter</String>
<String key="LargeThumbnails">Velké náhledy</String> <String key="LargeThumbnails">Velké náhledy</String>
<String key="AutoExpandSceneryTree">Automaticky rozbalit strom scenérií</String> <String key="AutoExpandSceneryTree">Automaticky rozbalit strom scenérií</String>
<String key="BatteryDefault">Baterie při spuštění:</String> <String key="BatteryDefault">Baterie při spuštění</String>
<String key="BatteryDefaultScenario">Podle scénáře</String> <String key="BatteryDefaultScenario">Podle scénáře</String>
<String key="BatteryDefaultOff">Vždy vypnutá</String> <String key="BatteryDefaultOff">Vždy vypnutá</String>
<String key="BatteryDefaultOn">Vždy zapnutá</String> <String key="BatteryDefaultOn">Vždy zapnutá</String>
@@ -455,6 +469,7 @@
<String key="Save">Uložit</String> <String key="Save">Uložit</String>
<String key="Reset">Obnovit</String> <String key="Reset">Obnovit</String>
<String key="SettingsSaved">Nastavení uloženo</String> <String key="SettingsSaved">Nastavení uloženo</String>
<String key="UnsavedChanges">Neuložené změny</String>
<!-- Errors / diagnostics --> <!-- Errors / diagnostics -->
<String key="FaultTitle">Chyba</String> <String key="FaultTitle">Chyba</String>
@@ -482,6 +497,7 @@
<String key="GeneralInfo">Obecné</String> <String key="GeneralInfo">Obecné</String>
<String key="Model">Model</String> <String key="Model">Model</String>
<String key="VMax">VMax</String> <String key="VMax">VMax</String>
<String key="Name">Název</String>
<String key="Texture">Textura</String> <String key="Texture">Textura</String>
<!-- Scenery tab --> <!-- Scenery tab -->
@@ -509,9 +525,9 @@
<String key="WarehouseImportFile">Přidat soupravy ze souboru...</String> <String key="WarehouseImportFile">Přidat soupravy ze souboru...</String>
<String key="StartWithoutSaving">Spustit bez uložení nastavení</String> <String key="StartWithoutSaving">Spustit bez uložení nastavení</String>
<String key="StartFreeFly">Spustit bez vozidla</String> <String key="StartFreeFly">Spustit bez vozidla</String>
<String key="VolumePaused">Hlasitost při pauze:</String> <String key="VolumePaused">Hlasitost při pauze</String>
<String key="DebugLogVisible">Zobrazovat průběh simulace</String> <String key="DebugLogVisible">Zobrazovat průběh simulace</String>
<String key="PyScreenPriority">Frekvence obnovení doplňkových obrazovek:</String> <String key="PyScreenPriority">Frekvence obnovení doplňkových obrazovek</String>
<String key="PyPriorityNormal">Standardní</String> <String key="PyPriorityNormal">Standardní</String>
<String key="PyPriorityLower">Nízká</String> <String key="PyPriorityLower">Nízká</String>
<String key="PyPriorityLowest">Velmi nízká</String> <String key="PyPriorityLowest">Velmi nízká</String>
@@ -531,7 +547,7 @@
<String key="TexWorks">Revize:</String> <String key="TexWorks">Revize:</String>
<String key="RevisionFrom">Revize od:</String> <String key="RevisionFrom">Revize od:</String>
<String key="RevisionTo">Revize do:</String> <String key="RevisionTo">Revize do:</String>
<String key="CrewLabel">Obsazení:</String> <String key="CrewLabel">Obsazení</String>
<String key="RuleAdd">Přidat do pravidla</String> <String key="RuleAdd">Přidat do pravidla</String>
<String key="RuleNew">— nové pravidlo —</String> <String key="RuleNew">— nové pravidlo —</String>
<String key="RuleRemove">Odebrat z pravidla</String> <String key="RuleRemove">Odebrat z pravidla</String>
@@ -551,4 +567,18 @@
<String key="AdvLogPage">Deník událostí</String> <String key="AdvLogPage">Deník událostí</String>
<String key="AdvClearLog">Vymazat log</String> <String key="AdvClearLog">Vymazat log</String>
<String key="ConsistSchedule">Sestava</String> <String key="ConsistSchedule">Sestava</String>
<!-- Klávesové zkratky -->
<String key="ShortcutsTitle">Klávesové zkratky</String>
<String key="ShortcutsGlobal">Obecné</String>
<String key="ShortcutsTabHint">Tab přepíná mezi panely, šipky fungují uvnitř seznamů. V pásu soupravy Tab skočí na vybrané vozidlo.</String>
<String key="ShortcutShowHelp">Tento seznam</String>
<String key="ShortcutFocusSearch">Přejít do vyhledávání vozidel</String>
<String key="ShortcutAddVehicle">Přidat vybrané vozidlo do soupravy</String>
<String key="ShortcutSetNumber">Nastavit číslo vozu</String>
<String key="ShortcutPrevNext">Předchozí / další vozidlo</String>
<String key="ShortcutFirstLast">První / poslední vozidlo</String>
<String key="ShortcutMember">Vybrat člen ve spojené jednotce</String>
<String key="ShortcutMoveVehicle">Posunout vozidlo v soupravě</String>
<String key="ShortcutCardMenu">Otevřít nabídku vozidla</String>
</Language> </Language>

View File

@@ -69,7 +69,7 @@
<String key="OptionalIncludes">Optional elements</String> <String key="OptionalIncludes">Optional elements</String>
<String key="OpenTextureFolder">Open texture folder</String> <String key="OpenTextureFolder">Open texture folder</String>
<String key="CopyTextureName">Copy name</String> <String key="CopyTextureName">Copy name</String>
<String key="CopyCoupler">Copy coupler from previous</String> <String key="CopyCoupler">Copy from previous</String>
<String key="ThermoAmbient">Coolant at ambient temperature</String> <String key="ThermoAmbient">Coolant at ambient temperature</String>
<String key="TextureInfo">Texture</String> <String key="TextureInfo">Texture</String>
<String key="TexOperator">Operator</String> <String key="TexOperator">Operator</String>
@@ -185,6 +185,9 @@
<String key="SwitchNormal">N - On</String> <String key="SwitchNormal">N - On</String>
<String key="SwitchOff">0 - Off, slack</String> <String key="SwitchOff">0 - Off, slack</String>
<String key="SwitchOff10">1 - Off, slack (10% chance)</String> <String key="SwitchOff10">1 - Off, slack (10% chance)</String>
<String key="SwitchEmergencyTest">&lt;&gt; - off for emergency brake test</String>
<String key="SwitchAgonal">X - worn out (20% off, random)</String>
<String key="SwitchEmptied">E - emptied (pipes at 0)</String>
<!-- Damage (wheel parameters) --> <!-- Damage (wheel parameters) -->
<String key="Damage">Damage</String> <String key="Damage">Damage</String>
@@ -194,15 +197,24 @@
<String key="DamageFlatnessProb">Flat-spot probability (%)</String> <String key="DamageFlatnessProb">Flat-spot probability (%)</String>
<String key="LoadType">Load type</String> <String key="LoadType">Load type</String>
<String key="LoadCount">Amount</String> <String key="LoadCount">Amount</String>
<String key="LoadUnitPieces">pcs</String>
<String key="LoadUnitTons">t</String>
<String key="LoadUnitGeneric">u.</String>
<String key="LoadHint">Set an amount above 0 to load the vehicle.</String> <String key="LoadHint">Set an amount above 0 to load the vehicle.</String>
<String key="LoadNotLoadable">This vehicle accepts no load.</String> <String key="LoadNotLoadable">This vehicle accepts no load.</String>
<String key="LoadPantState">Pantograph raised at start</String> <String key="LoadPantState">Pantograph raised at start</String>
<String key="LoadPantStateHint">The amount selects the pantograph: 1 — A, 2 — B, 3 — both.</String> <String key="LoadPantStateHint">The amount selects the pantograph: 1 — A, 2 — B, 3 — both.</String>
<String key="Pantograph">Pantograph</String>
<String key="PantStateNone">None</String>
<String key="PantStateBoth">Both</String>
<String key="WagonNumber">Wagon number</String> <String key="WagonNumber">Wagon number</String>
<String key="LoadCopyPrev">Copy load from previous vehicle</String> <String key="LoadCopyPrev">Copy load from previous vehicle</String>
<String key="LoadMax">Maximum possible load</String> <String key="LoadMax">Maximum possible load</String>
<String key="LoadPrevShort">From previous</String>
<String key="LoadToAllShort">To following</String>
<String key="LoadMaxShort">Maximum</String>
<String key="ConsistLoadTools">Whole-consist load</String> <String key="ConsistLoadTools">Whole-consist load</String>
<String key="ConsistRandomType">Random load</String> <String key="ConsistRandomType">Random type</String>
<String key="ConsistMaxAmount">Maximum amount</String> <String key="ConsistMaxAmount">Maximum amount</String>
<String key="ConsistRandomAmount">Random amount</String> <String key="ConsistRandomAmount">Random amount</String>
<String key="Set">Set</String> <String key="Set">Set</String>
@@ -252,6 +264,7 @@
<String key="CouplingUnitRear">Coupler at the rear of the unit</String> <String key="CouplingUnitRear">Coupler at the rear of the unit</String>
<String key="Join">Join</String> <String key="Join">Join</String>
<String key="TipJoin">Couple this vehicle with the next one into a single unit</String> <String key="TipJoin">Couple this vehicle with the next one into a single unit</String>
<String key="TipReorder">Drag to reorder</String>
<String key="AddVehicle">Add</String> <String key="AddVehicle">Add</String>
<String key="TipAddVehicle">Add this vehicle to the consist</String> <String key="TipAddVehicle">Add this vehicle to the consist</String>
<String key="TipAddUnit">Add the whole unit to the consist</String> <String key="TipAddUnit">Add the whole unit to the consist</String>
@@ -278,23 +291,26 @@
<String key="Fullscreen">Fullscreen</String> <String key="Fullscreen">Fullscreen</String>
<String key="PauseSimInactive">Pause simulation when window is inactive</String> <String key="PauseSimInactive">Pause simulation when window is inactive</String>
<String key="PauseSimStart">Pause on start</String> <String key="PauseSimStart">Pause on start</String>
<String key="CursorSensitivity">Cursor sensitivity:</String> <String key="CursorSensitivity">Camera rotation sensitivity (horizontal)</String>
<String key="MouseHorInvert">Invert mouse horizontally</String> <String key="MouseHorInvert">Invert mouse horizontally</String>
<String key="MouseVertInvert">Invert mouse vertically</String> <String key="MouseVertInvert">Invert mouse vertically</String>
<String key="Communication">Communication</String> <String key="Communication">External devices</String>
<String key="GamepadSignalIgnore">Ignore gamepad input</String> <String key="GamepadSignalIgnore">Ignore gamepad control</String>
<String key="GamepadSignalIgnoreDesc">Disables gamepad input even when one is connected.</String>
<String key="Feedback">Feedback</String> <String key="Feedback">Feedback</String>
<String key="FeedbackDesc">How cab feedback signals are delivered to an external panel. Keyboard modes flash LEDs: (1) Caps Lock = CA or SHP, Scroll Lock = starting resistances; (2) Caps Lock = CA, Scroll Lock = SHP. Other modes require an LPT, PoKeys55 or serial interface.</String>
<String key="FeedbackOff">Off</String> <String key="FeedbackOff">Off</String>
<String key="FeedbackCASHP">Caps: CA/SHP; ScrollLock: starting resistances</String> <String key="FeedbackCASHP">Keyboard: Caps = CA+SHP, Scroll = starting resistances</String>
<String key="FeedbackCA">Caps: CA; ScrollLock: SHP</String> <String key="FeedbackCA">Keyboard: Caps = CA, Scroll = SHP</String>
<String key="FeedbackLPT">LPT</String> <String key="FeedbackLPT">LPT (inpout32)</String>
<String key="FeedbackPoKeys">PoKeys55</String> <String key="FeedbackPoKeys">PoKeys55 (USB)</String>
<String key="FeedbackSP">Serial port (COM)</String> <String key="FeedbackSP">COM / serial port</String>
<String key="FeedbackPort">LPT port address</String> <String key="FeedbackPort">LPT port address</String>
<String key="FeedbackPortDesc">LPT port address used by the selected feedback mode.</String>
<String key="UartSection">UART configuration</String> <String key="UartSection">UART configuration</String>
<String key="UartEnable">Enable UART communication</String> <String key="UartEnable">Enable UART communication</String>
@@ -309,27 +325,34 @@
<String key="UartRadioChannel">Radio channel</String> <String key="UartRadioChannel">Radio channel</String>
<String key="Others">Other</String> <String key="Others">Other</String>
<String key="SelectEXE">Select exe:</String> <String key="SelectEXE">Select exe</String>
<String key="SelectEXEAuto">Select automatically</String> <String key="SelectEXEAuto">Select automatically</String>
<String key="DebugMode">Debug mode (test mode)</String> <String key="DebugMode">Debug mode</String>
<String key="DebugModeDesc">Unlocks debug menus, extra panels and test shortcuts.</String>
<String key="VirtualShunting">Virtual shunting</String> <String key="VirtualShunting">Virtual shunting</String>
<String key="VirtualShuntingDesc">Enables an AI assistant for coupling, uncoupling and simple shunting moves while the vehicle is stopped.</String>
<String key="LogMissingVehicleFiles">Log missing vehicle files during export</String> <String key="LogMissingVehicleFiles">Log missing vehicle files during export</String>
<String key="LogMissingVehicleFilesDesc">Writes vehicles the starter could not find into the log.</String>
<String key="Graphic">Graphics</String> <String key="Graphic">Graphics</String>
<String key="RenderEngine">Render engine</String> <String key="RenderEngine">Render engine</String>
<String key="RenderEngineDesc">OpenGL rendering path. 'Simplified' and 'Old' use the legacy renderer.</String>
<String key="REFull">Full</String> <String key="REFull">Full</String>
<String key="REOld">Old</String> <String key="REOld">Old</String>
<String key="RESimpShader">Simplified shader</String> <String key="RESimpShader">Simplified (legacy)</String>
<String key="RESimp">Simplified</String> <String key="RESimp">Simplified</String>
<String key="REOff">Disable visual layer</String> <String key="REOff">Disable visual layer</String>
<String key="REExperimental">Experimental</String> <String key="REExperimental">Experimental</String>
<String key="Resolution">Resolution:</String> <String key="Resolution">Resolution</String>
<String key="BufforResolution">Buffer resolution:</String> <String key="BufforResolution">Buffer resolution</String>
<String key="MaxTexResolution">Maximum texture resolution:</String> <String key="BufforResolutionDesc">Scales the internal render buffer relative to the window resolution.</String>
<String key="MaxTexCabResolution">Maximum cabin texture resolution:</String> <String key="MaxTexResolution">Maximum texture resolution</String>
<String key="TexFilteringQuality">Texture filtering quality:</String> <String key="MaxTexCabResolution">Maximum cabin texture resolution</String>
<String key="Multisampling">Multisampling:</String> <String key="TexFilteringQuality">Texture filtering quality</String>
<String key="TexFilteringQualityDesc">Anisotropic texture filtering level.</String>
<String key="Multisampling">Multisampling</String>
<String key="MultisamplingDesc">Anti-aliasing of object edges. Higher settings cost more GPU time.</String>
<String key="QualityVeryLow">Very low</String> <String key="QualityVeryLow">Very low</String>
<String key="QualityLow">Low</String> <String key="QualityLow">Low</String>
<String key="QualityNormal">Normal</String> <String key="QualityNormal">Normal</String>
@@ -346,15 +369,20 @@
<String key="RangeHigh">High</String> <String key="RangeHigh">High</String>
<String key="RangeVeryHigh">Very high</String> <String key="RangeVeryHigh">Very high</String>
<String key="RangeDisabled">Disabled</String> <String key="RangeDisabled">Disabled</String>
<String key="DynamicLights">Dynamic light sources:</String> <String key="DynamicLights">Dynamic light sources</String>
<String key="RenderRange">Rendering range:</String> <String key="DynamicLightsDesc">Maximum number of simultaneous dynamic light sources.</String>
<String key="RenderRange">Rendering range</String>
<String key="RenderRangeDesc">Maximum draw-distance multiplier; the engine lowers it automatically when the FPS drops.</String>
<String key="FpsLimitEnable">Limit frame rate</String> <String key="FpsLimitEnable">Limit frame rate</String>
<String key="ShadowAngleLimit">Shadow length limit</String> <String key="ShadowAngleLimit">Shadow length limit</String>
<String key="ShadowAngleLimitDesc">Shortens the long shadows cast by a low sun. Higher values cut them sooner.</String>
<String key="ShadowLimitNone">No limit</String> <String key="ShadowLimitNone">No limit</String>
<String key="ShadowLimitShort">Shortened</String> <String key="ShadowLimitShort">Shortened</String>
<String key="FullscreenWindowed">Automatic resolution (borderless fullscreen)</String> <String key="FullscreenWindowed">Automatic resolution (borderless fullscreen)</String>
<String key="RenderAngleVulkan">Use ANGLE Vulkan renderer</String> <String key="RenderAngleVulkan">Use ANGLE Vulkan renderer</String>
<String key="RenderAngleVulkanDesc">Runs the ANGLE renderer on Vulkan instead of the default backend. Useful when OpenGL misbehaves.</String>
<String key="VSync">VSync</String> <String key="VSync">VSync</String>
<String key="VSyncDesc">Synchronizes frame rate with the monitor refresh rate.</String>
<String key="SmokeDisplay">Smoke display</String> <String key="SmokeDisplay">Smoke display</String>
<String key="SmokeParticlesMultiplier">Smoke particles multiplier</String> <String key="SmokeParticlesMultiplier">Smoke particles multiplier</String>
@@ -362,41 +390,61 @@
<String key="MotionBlur">Motion blur</String> <String key="MotionBlur">Motion blur</String>
<String key="AdditionalShadersEffects">Additional shader effects</String> <String key="AdditionalShadersEffects">Additional shader effects</String>
<String key="ReflectionsCubeMap">Reflections as cubemap</String> <String key="ReflectionsCubeMap">Reflections as cubemap</String>
<String key="ReflectionsCubeMapDesc">Renders reflections into a cube map, so surfaces mirror the surroundings from every direction.</String>
<String key="SkipPipeline">Simplified depth read (compatibility)</String>
<String key="SkipPipelineDesc">Alternative depth-buffer read path with MSAA; enable it if your GPU has compatibility issues.</String>
<String key="RenderVBO">Render VBO</String> <String key="RenderVBO">Render VBO</String>
<String key="RenderVBODesc">Keeps geometry in GPU buffers (faster). Disable only if you have problems with an old GPU.</String>
<String key="RenderShadows">Render shadows</String> <String key="RenderShadows">Render shadows</String>
<String key="ReflectionsFramerate">Reflections refresh rate</String> <String key="ReflectionsFramerate">Reflections refresh rate</String>
<String key="ShadowsResolution">Shadows resolution</String> <String key="ShadowsResolution">Shadows resolution</String>
<String key="ShaderRange">Shadow range</String> <String key="ShaderRange">Shadow range</String>
<String key="CabShadowsRange">Cabin shadow sources range</String> <String key="CabShadowsRange">Cabin shadow sources range</String>
<String key="CabShadowsRangeDesc">Distance at which light sources still cast shadows inside the cab. Zero turns cab shadows off.</String>
<String key="ShadowDisplay">Display shadows</String> <String key="ShadowDisplay">Display shadows</String>
<String key="ShadowDisplayDesc">Sets which objects cast shadows based on material priority. Lower value = fewer shadows, higher performance.</String>
<String key="CSROnlyImportant">Only important</String> <String key="CSROnlyImportant">Only important</String>
<String key="CSRLimited">Limited</String> <String key="CSRLimited">Limited</String>
<String key="CSRAll">All</String> <String key="CSRAll">All</String>
<String key="ReflectionsDetails">Reflections detail level</String> <String key="ReflectionsDetails">Reflections detail level</String>
<String key="ReflectionsDetailsDesc">Which objects are rendered into the reflection cubemap.</String>
<String key="RDTerrain">Terrain geometry only</String> <String key="RDTerrain">Terrain geometry only</String>
<String key="RDTerrainModels">Terrain geometry and static models</String> <String key="RDTerrainModels">Terrain geometry and static models</String>
<String key="RDTerrainModelsVehicles">Terrain geometry, static models and vehicles</String> <String key="RDTerrainModelsVehicles">Terrain geometry, static models and vehicles</String>
<String key="FieldOfView">View angle</String> <String key="FieldOfView">View angle</String>
<String key="FieldOfViewDesc">Vertical camera field of view in degrees (15–75).</String>
<String key="RenderScreens">Render onboard computer screens</String> <String key="RenderScreens">Render onboard computer screens</String>
<String key="RenderScreensDesc">Enables cab screens driven by Python scripts.</String>
<String key="RenderScreensThread">Render screens in a separate thread</String> <String key="RenderScreensThread">Render screens in a separate thread</String>
<String key="Physics">Physics</String> <String key="Physics">Physics</String>
<String key="TrackCurvesQuality">Track curves quality</String> <String key="TrackCurvesQuality">Track curves quality</String>
<String key="TrackCurvesQualityDesc">How finely curved track is subdivided. Higher values are smoother but cost more CPU/GPU when loading.</String>
<String key="PhysicsAccuracyIncreased">Increased physics accuracy</String> <String key="PhysicsAccuracyIncreased">Increased physics accuracy</String>
<String key="PhysicsAccuracyIncreasedDesc">Runs the full physics model with smaller time steps. More faithful, but heavier CPU load on large consists.</String>
<String key="BreakingPantographPossibility">Possibility of pantograph breaking</String> <String key="BreakingPantographPossibility">Possibility of pantograph breaking</String>
<String key="BreakingPantographPossibilityDesc">Disabling protects the pantograph from breaking on faulty overhead line geometry.</String>
<String key="PowerOverheadLineOnly">Powered only under overhead line</String> <String key="PowerOverheadLineOnly">Powered only under overhead line</String>
<String key="SaveSpeedometerTapes">Save speedometer tapes</String> <String key="PowerOverheadLineOnlyDesc">When disabled, a raised pantograph can power the vehicle even without touching the wire.</String>
<String key="SaveSpeedometerTapes">Save physics parameters log</String>
<String key="SaveSpeedometerTapesDesc">Writes a detailed trip log to the physicslog/ folder.</String>
<String key="SaveSimLogs">Save simulation log</String> <String key="SaveSimLogs">Save simulation log</String>
<String key="KeepPrevLogs">Keep previous logs</String> <String key="KeepPrevLogs">Keep previous logs</String>
<String key="DisplaySimulation">Display simulation</String> <String key="KeepPrevLogsDesc">Creates a separate log file for each scenery instead of overwriting errors.txt.</String>
<String key="DisplaySimulation">Render 3D image</String>
<String key="DisplaySimulationDesc">When disabled, the scenery is not drawn (black screen), but the simulation keeps running.</String>
<String key="CrashDamage">Collision damage</String> <String key="CrashDamage">Collision damage</String>
<String key="CrashDamageDesc">Damages couplers and consists during collisions and sharp jerks.</String>
<String key="Friction">Friction multiplier</String> <String key="Friction">Friction multiplier</String>
<String key="BrakeStep">Brake valve sensitivity</String> <String key="FrictionDesc">Multiplies wheel-to-rail friction. 1.00 leaves it unchanged.</String>
<String key="BrakeSpeed">FV4a valve sensitivity</String> <String key="BrakeStep">Train brake step size (Num3/Num9)</String>
<String key="BrakeStepDesc">Multiplies the size of a single step of the train brake handle controlled with Num3 and Num9. 1.00 leaves it unchanged.</String>
<String key="BrakeSpeed">FV4a valve speed</String>
<String key="BrakeSpeedDesc">Multiplies how fast the locomotive brake handle (FV4a) and the local brake move. 1.00 leaves it unchanged.</String>
<String key="Sounds">Sound</String> <String key="Sounds">Sound</String>
<String key="EnableSounds">Enable sound</String> <String key="EnableSounds">Enable sound</String>
@@ -419,14 +467,15 @@
<String key="IgnoreIrrelevantTrains">Skip decorative consists when starting</String> <String key="IgnoreIrrelevantTrains">Skip decorative consists when starting</String>
<String key="Starter">Starter</String> <String key="Starter">Starter</String>
<String key="SettingsProfile">Settings profile:</String> <String key="SettingsProfile">Settings profile</String>
<String key="ProfileApply">Apply</String> <String key="ProfileApply">Apply</String>
<String key="ProfileSaveAs">Save as…</String> <String key="ProfileSaveAs">Save as…</String>
<String key="ProfileNamePrompt">Profile name</String> <String key="ProfileNamePrompt">Profile name</String>
<String key="AutoCloseStarter">Automatically close starter</String> <String key="AutoCloseStarter">Automatically close starter</String>
<String key="LargeThumbnails">Large thumbnails</String> <String key="LargeThumbnails">Large thumbnails</String>
<String key="AutoExpandSceneryTree">Automatically expand scenery tree</String> <String key="AutoExpandSceneryTree">Automatically expand scenery tree</String>
<String key="BatteryDefault">Battery on launch:</String> <String key="BatteryDefault">Battery on launch</String>
<String key="BatteryDefaultDesc">Battery state when the simulation starts, unless the scenario decides otherwise.</String>
<String key="BatteryDefaultScenario">Scenario default</String> <String key="BatteryDefaultScenario">Scenario default</String>
<String key="BatteryDefaultOff">Always off</String> <String key="BatteryDefaultOff">Always off</String>
<String key="BatteryDefaultOn">Always on</String> <String key="BatteryDefaultOn">Always on</String>
@@ -455,6 +504,7 @@
<String key="Save">Save</String> <String key="Save">Save</String>
<String key="Reset">Reset</String> <String key="Reset">Reset</String>
<String key="SettingsSaved">Settings saved</String> <String key="SettingsSaved">Settings saved</String>
<String key="UnsavedChanges">Unsaved changes</String>
<!-- Errors / diagnostics --> <!-- Errors / diagnostics -->
<String key="FaultTitle">Error</String> <String key="FaultTitle">Error</String>
@@ -482,6 +532,7 @@
<String key="GeneralInfo">General</String> <String key="GeneralInfo">General</String>
<String key="Model">Model</String> <String key="Model">Model</String>
<String key="VMax">VMax</String> <String key="VMax">VMax</String>
<String key="Name">Name</String>
<String key="Texture">Texture</String> <String key="Texture">Texture</String>
<!-- Scenery tab --> <!-- Scenery tab -->
@@ -509,14 +560,15 @@
<String key="WarehouseImportFile">Add consists from file...</String> <String key="WarehouseImportFile">Add consists from file...</String>
<String key="StartWithoutSaving">Start without saving settings</String> <String key="StartWithoutSaving">Start without saving settings</String>
<String key="StartFreeFly">Start with no vehicle</String> <String key="StartFreeFly">Start with no vehicle</String>
<String key="VolumePaused">Volume while paused:</String> <String key="VolumePaused">Volume while paused</String>
<String key="DebugLogVisible">Show simulation log on screen</String> <String key="DebugLogVisible">Show log in console</String>
<String key="PyScreenPriority">Extra screen refresh rate:</String> <String key="PyScreenPriority">Extra screen update delay</String>
<String key="PyPriorityNormal">Standard</String> <String key="PyScreenPriorityDesc">Minimum time between Python cab screen updates in milliseconds. Higher value = less frequent updates.</String>
<String key="PyPriorityLower">Low</String> <String key="PyPriorityNormal">Standard (200 ms)</String>
<String key="PyPriorityLowest">Very low</String> <String key="PyPriorityLower">Slower (400 ms)</String>
<String key="PyPriorityIdle">None</String> <String key="PyPriorityLowest">Very slow (800 ms)</String>
<String key="PyPriorityOff">Disabled</String> <String key="PyPriorityIdle">Only when idle (1600 ms)</String>
<String key="PyPriorityOff">Off</String>
<String key="MassBrutto">Gross mass</String> <String key="MassBrutto">Gross mass</String>
<String key="VisibilityGood">Good</String> <String key="VisibilityGood">Good</String>
@@ -531,7 +583,7 @@
<String key="TexWorks">Overhaul:</String> <String key="TexWorks">Overhaul:</String>
<String key="RevisionFrom">Overhaul from:</String> <String key="RevisionFrom">Overhaul from:</String>
<String key="RevisionTo">Overhaul to:</String> <String key="RevisionTo">Overhaul to:</String>
<String key="CrewLabel">Crew:</String> <String key="CrewLabel">Crew</String>
<String key="RuleAdd">Add to rule</String> <String key="RuleAdd">Add to rule</String>
<String key="RuleNew">— new rule —</String> <String key="RuleNew">— new rule —</String>
<String key="RuleRemove">Remove from rule</String> <String key="RuleRemove">Remove from rule</String>
@@ -551,4 +603,18 @@
<String key="AdvLogPage">Event log</String> <String key="AdvLogPage">Event log</String>
<String key="AdvClearLog">Clear log</String> <String key="AdvClearLog">Clear log</String>
<String key="ConsistSchedule">Composition</String> <String key="ConsistSchedule">Composition</String>
<!-- Keyboard shortcuts -->
<String key="ShortcutsTitle">Keyboard shortcuts</String>
<String key="ShortcutsGlobal">General</String>
<String key="ShortcutsTabHint">Tab moves between panels, arrow keys inside lists. In the consist strip Tab lands on the selected vehicle.</String>
<String key="ShortcutShowHelp">This list</String>
<String key="ShortcutFocusSearch">Jump to the vehicle search box</String>
<String key="ShortcutAddVehicle">Add the browsed vehicle to the consist</String>
<String key="ShortcutSetNumber">Set the wagon number</String>
<String key="ShortcutPrevNext">Previous / next vehicle</String>
<String key="ShortcutFirstLast">First / last vehicle</String>
<String key="ShortcutMember">Pick a car inside a coupled unit</String>
<String key="ShortcutMoveVehicle">Move the vehicle along the consist</String>
<String key="ShortcutCardMenu">Open the vehicle menu</String>
</Language> </Language>

View File

@@ -69,7 +69,7 @@
<String key="OptionalIncludes">Opcionális elemek</String> <String key="OptionalIncludes">Opcionális elemek</String>
<String key="OpenTextureFolder">Textúramappa megnyitása</String> <String key="OpenTextureFolder">Textúramappa megnyitása</String>
<String key="CopyTextureName">Név másolása</String> <String key="CopyTextureName">Név másolása</String>
<String key="CopyCoupler">Kapcsolókészülék másolása az előzőről</String> <String key="CopyCoupler">Másolás az előzőről</String>
<String key="ThermoAmbient">Hűtőfolyadék környezeti hőmérsékleten</String> <String key="ThermoAmbient">Hűtőfolyadék környezeti hőmérsékleten</String>
<String key="TextureInfo">Textúra</String> <String key="TextureInfo">Textúra</String>
<String key="TexOperator">Üzemeltető</String> <String key="TexOperator">Üzemeltető</String>
@@ -185,6 +185,9 @@
<String key="SwitchNormal">N - fék bekapcsolva</String> <String key="SwitchNormal">N - fék bekapcsolva</String>
<String key="SwitchOff">0 - kikapcsolva, oldva</String> <String key="SwitchOff">0 - kikapcsolva, oldva</String>
<String key="SwitchOff10">1 - kikapcsolva, oldva (10%)</String> <String key="SwitchOff10">1 - kikapcsolva, oldva (10%)</String>
<String key="SwitchEmergencyTest">&lt;&gt; - kikapcsolva gyorsfékpróbához</String>
<String key="SwitchAgonal">X - elhasználódott (20% ki, véletlen)</String>
<String key="SwitchEmptied">E - kiürítve (vezetékek 0-n)</String>
<!-- Damage (wheel parameters) --> <!-- Damage (wheel parameters) -->
<String key="Damage">Sérülés</String> <String key="Damage">Sérülés</String>
@@ -194,15 +197,24 @@
<String key="DamageFlatnessProb">Laposodás valószínűsége (%)</String> <String key="DamageFlatnessProb">Laposodás valószínűsége (%)</String>
<String key="LoadType">Rakomány típusa</String> <String key="LoadType">Rakomány típusa</String>
<String key="LoadCount">Mennyiség</String> <String key="LoadCount">Mennyiség</String>
<String key="LoadUnitPieces">db</String>
<String key="LoadUnitTons">t</String>
<String key="LoadUnitGeneric">egys.</String>
<String key="LoadHint">A jármű megrakásához adjon meg 0-nál nagyobb mennyiséget.</String> <String key="LoadHint">A jármű megrakásához adjon meg 0-nál nagyobb mennyiséget.</String>
<String key="LoadNotLoadable">Ez a jármű nem fogad rakományt.</String> <String key="LoadNotLoadable">Ez a jármű nem fogad rakományt.</String>
<String key="LoadPantState">Felemelt áramszedő induláskor</String> <String key="LoadPantState">Felemelt áramszedő induláskor</String>
<String key="LoadPantStateHint">A mennyiség az áramszedőt választja ki: 1 — A, 2 — B, 3 — mindkettő.</String> <String key="LoadPantStateHint">A mennyiség az áramszedőt választja ki: 1 — A, 2 — B, 3 — mindkettő.</String>
<String key="Pantograph">Áramszedő</String>
<String key="PantStateNone">Egyik sem</String>
<String key="PantStateBoth">Mindkettő</String>
<String key="WagonNumber">Kocsiszám</String> <String key="WagonNumber">Kocsiszám</String>
<String key="LoadCopyPrev">Rakomány másolása az előző járműről</String> <String key="LoadCopyPrev">Rakomány másolása az előző járműről</String>
<String key="LoadMax">Legnagyobb lehetséges rakomány</String> <String key="LoadMax">Legnagyobb lehetséges rakomány</String>
<String key="LoadPrevShort">Az előzőről</String>
<String key="LoadToAllShort">A következőkre</String>
<String key="LoadMaxShort">Legnagyobb</String>
<String key="ConsistLoadTools">A teljes szerelvény rakománya</String> <String key="ConsistLoadTools">A teljes szerelvény rakománya</String>
<String key="ConsistRandomType">Véletlen rakomány</String> <String key="ConsistRandomType">Véletlen típus</String>
<String key="ConsistMaxAmount">Legnagyobb mennyiség</String> <String key="ConsistMaxAmount">Legnagyobb mennyiség</String>
<String key="ConsistRandomAmount">Véletlen mennyiség</String> <String key="ConsistRandomAmount">Véletlen mennyiség</String>
<String key="Set">Beállítás</String> <String key="Set">Beállítás</String>
@@ -252,6 +264,7 @@
<String key="CouplingUnitRear">Az egység végén lévő kapcsolókészülék</String> <String key="CouplingUnitRear">Az egység végén lévő kapcsolókészülék</String>
<String key="Join">Összekapcsolás</String> <String key="Join">Összekapcsolás</String>
<String key="TipJoin">A jármű összekapcsolása a következővel egy egységgé</String> <String key="TipJoin">A jármű összekapcsolása a következővel egy egységgé</String>
<String key="TipReorder">Húzza az átrendezéshez</String>
<String key="AddVehicle">Hozzáadás</String> <String key="AddVehicle">Hozzáadás</String>
<String key="TipAddVehicle">Jármű hozzáadása a szerelvényhez</String> <String key="TipAddVehicle">Jármű hozzáadása a szerelvényhez</String>
<String key="TipAddUnit">A teljes egység hozzáadása a szerelvényhez</String> <String key="TipAddUnit">A teljes egység hozzáadása a szerelvényhez</String>
@@ -278,7 +291,7 @@
<String key="Fullscreen">Teljes képernyő</String> <String key="Fullscreen">Teljes képernyő</String>
<String key="PauseSimInactive">Szimuláció szüneteltetése inaktív ablaknál</String> <String key="PauseSimInactive">Szimuláció szüneteltetése inaktív ablaknál</String>
<String key="PauseSimStart">Szüneteltetés indításkor</String> <String key="PauseSimStart">Szüneteltetés indításkor</String>
<String key="CursorSensitivity">Kurzorérzékenység:</String> <String key="CursorSensitivity">Kurzorérzékenység</String>
<String key="MouseHorInvert">Egér vízszintes megfordítása</String> <String key="MouseHorInvert">Egér vízszintes megfordítása</String>
<String key="MouseVertInvert">Egér függőleges megfordítása</String> <String key="MouseVertInvert">Egér függőleges megfordítása</String>
@@ -309,7 +322,7 @@
<String key="UartRadioChannel">Rádiócsatorna</String> <String key="UartRadioChannel">Rádiócsatorna</String>
<String key="Others">Egyéb</String> <String key="Others">Egyéb</String>
<String key="SelectEXE">Exe kiválasztása:</String> <String key="SelectEXE">Exe kiválasztása</String>
<String key="SelectEXEAuto">Automatikus kiválasztás</String> <String key="SelectEXEAuto">Automatikus kiválasztás</String>
<String key="DebugMode">Hibakeresési mód (teszt mód)</String> <String key="DebugMode">Hibakeresési mód (teszt mód)</String>
<String key="VirtualShunting">Virtuális tolatás</String> <String key="VirtualShunting">Virtuális tolatás</String>
@@ -324,12 +337,12 @@
<String key="REOff">Vizuális réteg kikapcsolása</String> <String key="REOff">Vizuális réteg kikapcsolása</String>
<String key="REExperimental">Kísérleti</String> <String key="REExperimental">Kísérleti</String>
<String key="Resolution">Felbontás:</String> <String key="Resolution">Felbontás</String>
<String key="BufforResolution">Puffer felbontása:</String> <String key="BufforResolution">Puffer felbontása</String>
<String key="MaxTexResolution">Textúrák legnagyobb felbontása:</String> <String key="MaxTexResolution">Textúrák legnagyobb felbontása</String>
<String key="MaxTexCabResolution">Vezetőállás-textúrák legnagyobb felbontása:</String> <String key="MaxTexCabResolution">Vezetőállás-textúrák legnagyobb felbontása</String>
<String key="TexFilteringQuality">Textúraszűrés minősége:</String> <String key="TexFilteringQuality">Textúraszűrés minősége</String>
<String key="Multisampling">Multisampling:</String> <String key="Multisampling">Multisampling</String>
<String key="QualityVeryLow">Nagyon alacsony</String> <String key="QualityVeryLow">Nagyon alacsony</String>
<String key="QualityLow">Alacsony</String> <String key="QualityLow">Alacsony</String>
<String key="QualityNormal">Normál</String> <String key="QualityNormal">Normál</String>
@@ -346,8 +359,8 @@
<String key="RangeHigh">Magas</String> <String key="RangeHigh">Magas</String>
<String key="RangeVeryHigh">Nagyon magas</String> <String key="RangeVeryHigh">Nagyon magas</String>
<String key="RangeDisabled">Kikapcsolva</String> <String key="RangeDisabled">Kikapcsolva</String>
<String key="DynamicLights">Dinamikus fényforrások:</String> <String key="DynamicLights">Dinamikus fényforrások</String>
<String key="RenderRange">Megjelenítési távolság:</String> <String key="RenderRange">Megjelenítési távolság</String>
<String key="FpsLimitEnable">Képkockaszám korlátozása</String> <String key="FpsLimitEnable">Képkockaszám korlátozása</String>
<String key="ShadowAngleLimit">Árnyékhossz korlátozása</String> <String key="ShadowAngleLimit">Árnyékhossz korlátozása</String>
<String key="ShadowLimitNone">Nincs korlát</String> <String key="ShadowLimitNone">Nincs korlát</String>
@@ -362,6 +375,7 @@
<String key="MotionBlur">Mozgási elmosás</String> <String key="MotionBlur">Mozgási elmosás</String>
<String key="AdditionalShadersEffects">További shader-effektek</String> <String key="AdditionalShadersEffects">További shader-effektek</String>
<String key="ReflectionsCubeMap">Tükröződések cubemapként</String> <String key="ReflectionsCubeMap">Tükröződések cubemapként</String>
<String key="SkipPipeline">SkipPipeline</String>
<String key="RenderVBO">VBO megjelenítés</String> <String key="RenderVBO">VBO megjelenítés</String>
<String key="RenderShadows">Árnyékok megjelenítése</String> <String key="RenderShadows">Árnyékok megjelenítése</String>
<String key="ReflectionsFramerate">Tükröződések frissítési gyakorisága</String> <String key="ReflectionsFramerate">Tükröződések frissítési gyakorisága</String>
@@ -419,14 +433,14 @@
<String key="IgnoreIrrelevantTrains">Dekorációs szerelvények kihagyása indításkor</String> <String key="IgnoreIrrelevantTrains">Dekorációs szerelvények kihagyása indításkor</String>
<String key="Starter">Starter</String> <String key="Starter">Starter</String>
<String key="SettingsProfile">Beállítási profil:</String> <String key="SettingsProfile">Beállítási profil</String>
<String key="ProfileApply">Alkalmaz</String> <String key="ProfileApply">Alkalmaz</String>
<String key="ProfileSaveAs">Mentés másként…</String> <String key="ProfileSaveAs">Mentés másként…</String>
<String key="ProfileNamePrompt">Profil neve</String> <String key="ProfileNamePrompt">Profil neve</String>
<String key="AutoCloseStarter">A starter automatikus bezárása</String> <String key="AutoCloseStarter">A starter automatikus bezárása</String>
<String key="LargeThumbnails">Nagy előnézetek</String> <String key="LargeThumbnails">Nagy előnézetek</String>
<String key="AutoExpandSceneryTree">Pályafa automatikus kibontása</String> <String key="AutoExpandSceneryTree">Pályafa automatikus kibontása</String>
<String key="BatteryDefault">Akkumulátor indításkor:</String> <String key="BatteryDefault">Akkumulátor indításkor</String>
<String key="BatteryDefaultScenario">Forgatókönyv szerint</String> <String key="BatteryDefaultScenario">Forgatókönyv szerint</String>
<String key="BatteryDefaultOff">Mindig ki</String> <String key="BatteryDefaultOff">Mindig ki</String>
<String key="BatteryDefaultOn">Mindig be</String> <String key="BatteryDefaultOn">Mindig be</String>
@@ -455,6 +469,7 @@
<String key="Save">Mentés</String> <String key="Save">Mentés</String>
<String key="Reset">Visszaállítás</String> <String key="Reset">Visszaállítás</String>
<String key="SettingsSaved">A beállítások elmentve</String> <String key="SettingsSaved">A beállítások elmentve</String>
<String key="UnsavedChanges">Nem mentett módosítások</String>
<!-- Errors / diagnostics --> <!-- Errors / diagnostics -->
<String key="FaultTitle">Hiba</String> <String key="FaultTitle">Hiba</String>
@@ -482,6 +497,7 @@
<String key="GeneralInfo">Általános</String> <String key="GeneralInfo">Általános</String>
<String key="Model">Modell</String> <String key="Model">Modell</String>
<String key="VMax">VMax</String> <String key="VMax">VMax</String>
<String key="Name">Név</String>
<String key="Texture">Textúra</String> <String key="Texture">Textúra</String>
<!-- Scenery tab --> <!-- Scenery tab -->
@@ -509,9 +525,9 @@
<String key="WarehouseImportFile">Szerelvények hozzáadása fájlból...</String> <String key="WarehouseImportFile">Szerelvények hozzáadása fájlból...</String>
<String key="StartWithoutSaving">Indítás a beállítások mentése nélkül</String> <String key="StartWithoutSaving">Indítás a beállítások mentése nélkül</String>
<String key="StartFreeFly">Indítás jármű nélkül</String> <String key="StartFreeFly">Indítás jármű nélkül</String>
<String key="VolumePaused">Hangerő szünet alatt:</String> <String key="VolumePaused">Hangerő szünet alatt</String>
<String key="DebugLogVisible">Szimuláció menetének megjelenítése</String> <String key="DebugLogVisible">Szimuláció menetének megjelenítése</String>
<String key="PyScreenPriority">Kiegészítő képernyők frissítési gyakorisága:</String> <String key="PyScreenPriority">Kiegészítő képernyők frissítési gyakorisága</String>
<String key="PyPriorityNormal">Normál</String> <String key="PyPriorityNormal">Normál</String>
<String key="PyPriorityLower">Alacsony</String> <String key="PyPriorityLower">Alacsony</String>
<String key="PyPriorityLowest">Nagyon alacsony</String> <String key="PyPriorityLowest">Nagyon alacsony</String>
@@ -531,7 +547,7 @@
<String key="TexWorks">Felújítás:</String> <String key="TexWorks">Felújítás:</String>
<String key="RevisionFrom">Felújítás ettől:</String> <String key="RevisionFrom">Felújítás ettől:</String>
<String key="RevisionTo">Felújítás eddig:</String> <String key="RevisionTo">Felújítás eddig:</String>
<String key="CrewLabel">Személyzet:</String> <String key="CrewLabel">Személyzet</String>
<String key="RuleAdd">Hozzáadás a szabályhoz</String> <String key="RuleAdd">Hozzáadás a szabályhoz</String>
<String key="RuleNew">— új szabály —</String> <String key="RuleNew">— új szabály —</String>
<String key="RuleRemove">Eltávolítás a szabályból</String> <String key="RuleRemove">Eltávolítás a szabályból</String>
@@ -551,4 +567,18 @@
<String key="AdvLogPage">Eseménynapló</String> <String key="AdvLogPage">Eseménynapló</String>
<String key="AdvClearLog">Napló törlése</String> <String key="AdvClearLog">Napló törlése</String>
<String key="ConsistSchedule">Összeállítás</String> <String key="ConsistSchedule">Összeállítás</String>
<!-- Billentyűparancsok -->
<String key="ShortcutsTitle">Billentyűparancsok</String>
<String key="ShortcutsGlobal">Általános</String>
<String key="ShortcutsTabHint">A Tab a panelek között lép, a nyilak a listákon belül. A szerelvénysávban a Tab a kijelölt járműre ugrik.</String>
<String key="ShortcutShowHelp">Ez a lista</String>
<String key="ShortcutFocusSearch">Ugrás a járműkeresőre</String>
<String key="ShortcutAddVehicle">A kiválasztott jármű hozzáadása</String>
<String key="ShortcutSetNumber">Kocsiszám beállítása</String>
<String key="ShortcutPrevNext">Előző / következő jármű</String>
<String key="ShortcutFirstLast">Első / utolsó jármű</String>
<String key="ShortcutMember">Tag kiválasztása kapcsolt egységen belül</String>
<String key="ShortcutMoveVehicle">Jármű mozgatása a szerelvényben</String>
<String key="ShortcutCardMenu">Jármű menüjének megnyitása</String>
</Language> </Language>

View File

@@ -69,7 +69,7 @@
<String key="OptionalIncludes">Elementy opcjonalne</String> <String key="OptionalIncludes">Elementy opcjonalne</String>
<String key="OpenTextureFolder">Otwórz folder tekstury</String> <String key="OpenTextureFolder">Otwórz folder tekstury</String>
<String key="CopyTextureName">Kopiuj nazwę</String> <String key="CopyTextureName">Kopiuj nazwę</String>
<String key="CopyCoupler">Kopiuj sprzęg z poprzedniego</String> <String key="CopyCoupler">Kopiuj z poprzedniego</String>
<String key="ThermoAmbient">Chłodziwo w temp. otoczenia</String> <String key="ThermoAmbient">Chłodziwo w temp. otoczenia</String>
<String key="TextureInfo">Tekstura</String> <String key="TextureInfo">Tekstura</String>
<String key="TexOperator">Przewoźnik</String> <String key="TexOperator">Przewoźnik</String>
@@ -89,7 +89,7 @@
<String key="SelectedVehicle">Wybrany pojazd</String> <String key="SelectedVehicle">Wybrany pojazd</String>
<String key="Vehicle">Pojazd</String> <String key="Vehicle">Pojazd</String>
<String key="Crew">z obsadą</String> <String key="Crew">z obsadą</String>
<String key="Reversed">obrócony</String> <String key="Reversed">Obrócony</String>
<String key="NoVehicles">Nie znaleziono pojazdów w dynamic/**/textures.txt.</String> <String key="NoVehicles">Nie znaleziono pojazdów w dynamic/**/textures.txt.</String>
<String key="ConsistEmpty">Dodaj pojazdy z bazy po lewej stronie lub wczytaj istniejący skład z pliku scenerii.</String> <String key="ConsistEmpty">Dodaj pojazdy z bazy po lewej stronie lub wczytaj istniejący skład z pliku scenerii.</String>
<String key="TipMoveLeft">Przesuń w lewo</String> <String key="TipMoveLeft">Przesuń w lewo</String>
@@ -185,6 +185,9 @@
<String key="SwitchNormal">N - hamulec włączony</String> <String key="SwitchNormal">N - hamulec włączony</String>
<String key="SwitchOff">0 - wyłączony, odluźniony</String> <String key="SwitchOff">0 - wyłączony, odluźniony</String>
<String key="SwitchOff10">1 - wył., odl. z 10% szansą</String> <String key="SwitchOff10">1 - wył., odl. z 10% szansą</String>
<String key="SwitchEmergencyTest">&lt;&gt; - wył. na próbę hamowania nagłego</String>
<String key="SwitchAgonal">X - agonalny (20% wył., losowo)</String>
<String key="SwitchEmptied">E - opróżniony (przewody na 0)</String>
<!-- Uszkodzenia (parametry kół) --> <!-- Uszkodzenia (parametry kół) -->
<String key="Damage">Uszkodzenia</String> <String key="Damage">Uszkodzenia</String>
@@ -194,15 +197,24 @@
<String key="DamageFlatnessProb">Prawdop. podkucia (%)</String> <String key="DamageFlatnessProb">Prawdop. podkucia (%)</String>
<String key="LoadType">Rodzaj ładunku</String> <String key="LoadType">Rodzaj ładunku</String>
<String key="LoadCount">Ilość</String> <String key="LoadCount">Ilość</String>
<String key="LoadUnitPieces">szt.</String>
<String key="LoadUnitTons">t</String>
<String key="LoadUnitGeneric">j.</String>
<String key="LoadHint">Ustaw ilość większą od 0, aby załadować pojazd.</String> <String key="LoadHint">Ustaw ilość większą od 0, aby załadować pojazd.</String>
<String key="LoadNotLoadable">Ten pojazd nie przyjmuje ładunku.</String> <String key="LoadNotLoadable">Ten pojazd nie przyjmuje ładunku.</String>
<String key="LoadPantState">Pantograf podniesiony na starcie</String> <String key="LoadPantState">Pantograf podniesiony na starcie</String>
<String key="LoadPantStateHint">Ilość wybiera pantograf: 1 — A, 2 — B, 3 — oba.</String> <String key="LoadPantStateHint">Ilość wybiera pantograf: 1 — A, 2 — B, 3 — oba.</String>
<String key="Pantograph">Pantograf</String>
<String key="PantStateNone">Brak</String>
<String key="PantStateBoth">Oba</String>
<String key="WagonNumber">Numer wagonu</String> <String key="WagonNumber">Numer wagonu</String>
<String key="LoadCopyPrev">Kopiuj ładunek z poprzedniego pojazdu</String> <String key="LoadCopyPrev">Kopiuj ładunek z poprzedniego pojazdu</String>
<String key="LoadMax">Maksymalny możliwy ładunek</String> <String key="LoadMax">Maksymalny możliwy ładunek</String>
<String key="ConsistLoadTools">Ładunek całego składu</String> <String key="LoadPrevShort">Z poprzedniego</String>
<String key="ConsistRandomType">Losowy ładunek</String> <String key="LoadToAllShort">Do następnych</String>
<String key="LoadMaxShort">Maksymalny</String>
<String key="ConsistLoadTools">Ładunki zestawienia</String>
<String key="ConsistRandomType">Losowy rodzaj</String>
<String key="ConsistMaxAmount">Maksymalna ilość</String> <String key="ConsistMaxAmount">Maksymalna ilość</String>
<String key="ConsistRandomAmount">Losowa ilość</String> <String key="ConsistRandomAmount">Losowa ilość</String>
<String key="Set">Ustaw</String> <String key="Set">Ustaw</String>
@@ -252,6 +264,7 @@
<String key="CouplingUnitRear">Sprzęg na końcu zespołu</String> <String key="CouplingUnitRear">Sprzęg na końcu zespołu</String>
<String key="Join">Złącz</String> <String key="Join">Złącz</String>
<String key="TipJoin">Połącz ten pojazd z następnym w jeden zespół</String> <String key="TipJoin">Połącz ten pojazd z następnym w jeden zespół</String>
<String key="TipReorder">Przeciągnij, aby zmienić kolejność</String>
<String key="AddVehicle">Dodaj</String> <String key="AddVehicle">Dodaj</String>
<String key="TipAddVehicle">Dodaj ten pojazd do składu</String> <String key="TipAddVehicle">Dodaj ten pojazd do składu</String>
<String key="TipAddUnit">Dodaj cały zespół do składu</String> <String key="TipAddUnit">Dodaj cały zespół do składu</String>
@@ -278,23 +291,26 @@
<String key="Fullscreen">Tryb pełnoekranowy</String> <String key="Fullscreen">Tryb pełnoekranowy</String>
<String key="PauseSimInactive">Wstrzymaj symulację, gdy okno nie jest aktywne</String> <String key="PauseSimInactive">Wstrzymaj symulację, gdy okno nie jest aktywne</String>
<String key="PauseSimStart">Pauza po uruchomieniu</String> <String key="PauseSimStart">Pauza po uruchomieniu</String>
<String key="CursorSensitivity">Czułość kursora:</String> <String key="CursorSensitivity">Czułość obrotu kamery (oś pozioma)</String>
<String key="MouseHorInvert">Inwersja myszy w poziomie</String> <String key="MouseHorInvert">Inwersja myszy w poziomie</String>
<String key="MouseVertInvert">Inwersja myszy w pionie</String> <String key="MouseVertInvert">Inwersja myszy w pionie</String>
<String key="Communication">Komunikacja</String> <String key="Communication">Urządzenia zewnętrzne</String>
<String key="GamepadSignalIgnore">Ignoruj sygnały z gamepada</String> <String key="GamepadSignalIgnore">Ignoruj sterowanie z gamepada</String>
<String key="GamepadSignalIgnoreDesc">Wyłącza obsługę gamepada, nawet gdy jest podłączony.</String>
<String key="Feedback">Informacja zwrotna</String> <String key="Feedback">Informacja zwrotna</String>
<String key="FeedbackDesc">Sposób przekazywania sygnałów kabinowych na zewnętrzny pulpit. Tryby klawiatury migają diodami: (1) Caps Lock = CA lub SHP, Scroll Lock = jazda na oporach; (2) Caps Lock = CA, Scroll Lock = SHP. Pozostałe tryby wymagają interfejsu LPT, PoKeys55 lub portu szeregowego.</String>
<String key="FeedbackOff">Wyłączona</String> <String key="FeedbackOff">Wyłączona</String>
<String key="FeedbackCASHP">Caps: CA/SHP; ScrollLock: opory rozr.</String> <String key="FeedbackCASHP">Klawiatura: Caps = CA+SHP, Scroll = opory</String>
<String key="FeedbackCA">Caps: CA; Scrollock: SHP</String> <String key="FeedbackCA">Klawiatura: Caps = CA, Scroll = SHP</String>
<String key="FeedbackLPT">LPT</String> <String key="FeedbackLPT">LPT (inpout32)</String>
<String key="FeedbackPoKeys">PoKeys55</String> <String key="FeedbackPoKeys">PoKeys55 (USB)</String>
<String key="FeedbackSP">Serialport (COM)</String> <String key="FeedbackSP">COM / port szeregowy</String>
<String key="FeedbackPort">Adres portu LPT</String> <String key="FeedbackPort">Adres portu LPT</String>
<String key="FeedbackPortDesc">Adres portu LPT używanego przez wybrany tryb informacji zwrotnej.</String>
<String key="UartSection">Konfiguracja UART</String> <String key="UartSection">Konfiguracja UART</String>
<String key="UartEnable">Włącz komunikację UART</String> <String key="UartEnable">Włącz komunikację UART</String>
@@ -309,27 +325,34 @@
<String key="UartRadioChannel">Kanał radiotelefonu</String> <String key="UartRadioChannel">Kanał radiotelefonu</String>
<String key="Others">Inne</String> <String key="Others">Inne</String>
<String key="SelectEXE">Wybór exe:</String> <String key="SelectEXE">Wybór exe</String>
<String key="SelectEXEAuto">Wybierz automatycznie</String> <String key="SelectEXEAuto">Wybierz automatycznie</String>
<String key="DebugMode">Tryb testowy (debugmode)</String> <String key="DebugMode">Tryb testowy</String>
<String key="DebugModeDesc">Odblokowuje menu debug, dodatkowe panele i skróty testowe.</String>
<String key="VirtualShunting">Wirtualny manewrowy</String> <String key="VirtualShunting">Wirtualny manewrowy</String>
<String key="VirtualShuntingDesc">Włącza asystenta AI do sprzęgania, rozprzęgania i prostych manewrów, gdy pojazd stoi.</String>
<String key="LogMissingVehicleFiles">Zapisuj w logu brakujące pliki pojazdów podczas eksportu</String> <String key="LogMissingVehicleFiles">Zapisuj w logu brakujące pliki pojazdów podczas eksportu</String>
<String key="LogMissingVehicleFilesDesc">Zapisuje w logu pojazdy, których starter nie znalazł.</String>
<String key="Graphic">Grafika</String> <String key="Graphic">Grafika</String>
<String key="RenderEngine">Silnik renderowania</String> <String key="RenderEngine">Silnik renderowania</String>
<String key="RenderEngineDesc">Wybór ścieżki renderowania OpenGL. 'Uproszczony' i 'Stary' to starszy renderer.</String>
<String key="REFull">Pełny</String> <String key="REFull">Pełny</String>
<String key="REOld">Stary</String> <String key="REOld">Stary</String>
<String key="RESimpShader">Uproszczony shaderowy</String> <String key="RESimpShader">Uproszczony (legacy)</String>
<String key="RESimp">Uproszczony</String> <String key="RESimp">Uproszczony</String>
<String key="REOff">Wyłącz warstwę wizualną</String> <String key="REOff">Wyłącz warstwę wizualną</String>
<String key="REExperimental">Eksperymentalny</String> <String key="REExperimental">Eksperymentalny</String>
<String key="Resolution">Rozdzielczość:</String> <String key="Resolution">Rozdzielczość</String>
<String key="BufforResolution">Rozdzielczość bufora:</String> <String key="BufforResolution">Rozdzielczość bufora</String>
<String key="MaxTexResolution">Maksymalna rozdzielczość tekstur:</String> <String key="BufforResolutionDesc">Skaluje wewnętrzny bufor renderowania względem rozdzielczości okna.</String>
<String key="MaxTexCabResolution">Maksymalna rozdzielczość tekstur kabiny:</String> <String key="MaxTexResolution">Maksymalna rozdzielczość tekstur</String>
<String key="TexFilteringQuality">Jakość filtrowania tekstur:</String> <String key="MaxTexCabResolution">Maksymalna rozdzielczość tekstur kabiny</String>
<String key="Multisampling">Multisampling:</String> <String key="TexFilteringQuality">Jakość filtrowania tekstur</String>
<String key="TexFilteringQualityDesc">Poziom anizotropowego filtrowania tekstur.</String>
<String key="Multisampling">Multisampling</String>
<String key="MultisamplingDesc">Wygładzanie krawędzi obiektów.</String>
<String key="QualityVeryLow">Bardzo niska</String> <String key="QualityVeryLow">Bardzo niska</String>
<String key="QualityLow">Niska</String> <String key="QualityLow">Niska</String>
<String key="QualityNormal">Normalna</String> <String key="QualityNormal">Normalna</String>
@@ -346,15 +369,20 @@
<String key="RangeHigh">Wysoki</String> <String key="RangeHigh">Wysoki</String>
<String key="RangeVeryHigh">Bardzo wysoki</String> <String key="RangeVeryHigh">Bardzo wysoki</String>
<String key="RangeDisabled">Wyłączone</String> <String key="RangeDisabled">Wyłączone</String>
<String key="DynamicLights">Dynamiczne źródła światła:</String> <String key="DynamicLights">Dynamiczne źródła światła</String>
<String key="RenderRange">Zasięg renderowania:</String> <String key="DynamicLightsDesc">Maksymalna liczba jednoczesnych źródeł światła dynamicznego.</String>
<String key="RenderRange">Zasięg renderowania</String>
<String key="RenderRangeDesc">Maksymalny mnożnik zasięgu rysowania; silnik zmniejsza go automatycznie przy niskim FPS.</String>
<String key="FpsLimitEnable">Ogranicz liczbę klatek na sekundę</String> <String key="FpsLimitEnable">Ogranicz liczbę klatek na sekundę</String>
<String key="ShadowAngleLimit">Limit długości cienia</String> <String key="ShadowAngleLimit">Limit długości cienia</String>
<String key="ShadowAngleLimitDesc">Skraca długie cienie rzucane przez nisko stojące słońce. Wyższe wartości ucinają je wcześniej.</String>
<String key="ShadowLimitNone">Bez ograniczeń</String> <String key="ShadowLimitNone">Bez ograniczeń</String>
<String key="ShadowLimitShort">Skrócone</String> <String key="ShadowLimitShort">Skrócone</String>
<String key="FullscreenWindowed">Automatyczna rozdzielczość (pełny ekran bez obramowania)</String> <String key="FullscreenWindowed">Automatyczna rozdzielczość (pełny ekran bez obramowania)</String>
<String key="RenderAngleVulkan">Użyj renderera ANGLE Vulkan</String> <String key="RenderAngleVulkan">Użyj renderera ANGLE Vulkan</String>
<String key="RenderAngleVulkanDesc">Uruchamia renderer ANGLE na Vulkanie zamiast domyślnego backendu. Przydatne, gdy OpenGL sprawia problemy.</String>
<String key="VSync">VSync</String> <String key="VSync">VSync</String>
<String key="VSyncDesc">Synchronizacja klatek z odświeżaniem monitora.</String>
<String key="SmokeDisplay">Wyświetlanie dymu</String> <String key="SmokeDisplay">Wyświetlanie dymu</String>
<String key="SmokeParticlesMultiplier">Mnożnik ilości cząsteczek dymu</String> <String key="SmokeParticlesMultiplier">Mnożnik ilości cząsteczek dymu</String>
@@ -362,48 +390,68 @@
<String key="MotionBlur">Rozmycie podczas ruchu</String> <String key="MotionBlur">Rozmycie podczas ruchu</String>
<String key="AdditionalShadersEffects">Dodatkowe efekty shaderów</String> <String key="AdditionalShadersEffects">Dodatkowe efekty shaderów</String>
<String key="ReflectionsCubeMap">Odbicia jako cubemapa</String> <String key="ReflectionsCubeMap">Odbicia jako cubemapa</String>
<String key="ReflectionsCubeMapDesc">Renderuje odbicia do mapy sześciennej, dzięki czemu powierzchnie odbijają otoczenie ze wszystkich stron.</String>
<String key="SkipPipeline">Uproszczony odczyt głębi (zgodność)</String>
<String key="SkipPipelineDesc">Alternatywna ścieżka odczytu bufora głębi przy MSAA; włącz przy problemach z kompatybilnością karty graficznej.</String>
<String key="RenderVBO">Renderowanie VBO</String> <String key="RenderVBO">Renderowanie VBO</String>
<String key="RenderVBODesc">Trzyma geometrię w buforach karty graficznej. Wyłącz tylko przy problemach ze starszymi kartami.</String>
<String key="RenderShadows">Renderowanie cieni</String> <String key="RenderShadows">Renderowanie cieni</String>
<String key="ReflectionsFramerate">Odświeżanie odbić</String> <String key="ReflectionsFramerate">Odświeżanie odbić</String>
<String key="ShadowsResolution">Rozdzielczość cieni</String> <String key="ShadowsResolution">Rozdzielczość cieni</String>
<String key="ShaderRange">Zasięg cieni</String> <String key="ShaderRange">Zasięg cieni</String>
<String key="CabShadowsRange">Zasięg źródeł cieni w kabinie</String> <String key="CabShadowsRange">Zasięg źródeł cieni w kabinie</String>
<String key="CabShadowsRangeDesc">Odległość, z jakiej źródła światła nadal rzucają cień w kabinie. Zero wyłącza cienie w kabinie.</String>
<String key="ShadowDisplay">Wyświetlanie cieni</String> <String key="ShadowDisplay">Wyświetlanie cieni</String>
<String key="ShadowDisplayDesc">Określa, które obiekty rzucają cień według priorytetu materiału. Niższa wartość = mniej cieni, wyższa wydajność.</String>
<String key="CSROnlyImportant">Tylko ważne</String> <String key="CSROnlyImportant">Tylko ważne</String>
<String key="CSRLimited">Ograniczone</String> <String key="CSRLimited">Ograniczone</String>
<String key="CSRAll">Wszystkie</String> <String key="CSRAll">Wszystkie</String>
<String key="ReflectionsDetails">Szczegółowość odbić</String> <String key="ReflectionsDetails">Szczegółowość odbić</String>
<String key="ReflectionsDetailsDesc">Jakie obiekty są renderowane w odbiciach cubemap.</String>
<String key="RDTerrain">Tylko geometria terenu</String> <String key="RDTerrain">Tylko geometria terenu</String>
<String key="RDTerrainModels">Geometria terenu i modele statyczne</String> <String key="RDTerrainModels">Geometria terenu i modele statyczne</String>
<String key="RDTerrainModelsVehicles">Geometria terenu, modele statyczne i pojazdy</String> <String key="RDTerrainModelsVehicles">Geometria terenu, modele statyczne i pojazdy</String>
<String key="FieldOfView">Kąt widzenia</String> <String key="FieldOfView">Kąt widzenia</String>
<String key="FieldOfViewDesc">Pionowy kąt widzenia kamery w stopniach (15–75).</String>
<String key="RenderScreens">Renderowanie ekranów komputerów pokładowych</String> <String key="RenderScreens">Renderowanie ekranów komputerów pokładowych</String>
<String key="RenderScreensDesc">Włącza ekrany kabinowe obsługiwane przez skrypty Python.</String>
<String key="RenderScreensThread">Renderowanie ekranów w osobnym wątku</String> <String key="RenderScreensThread">Renderowanie ekranów w osobnym wątku</String>
<String key="Physics">Fizyka</String> <String key="Physics">Fizyka</String>
<String key="TrackCurvesQuality">Jakość łuków torów</String> <String key="TrackCurvesQuality">Jakość łuków torów</String>
<String key="TrackCurvesQualityDesc">Gęstość podziału łuków torów. Wyższa wartość = gładsze łuki.</String>
<String key="PhysicsAccuracyIncreased">Zwiększona dokładność fizyki</String> <String key="PhysicsAccuracyIncreased">Zwiększona dokładność fizyki</String>
<String key="PhysicsAccuracyIncreasedDesc">Uruchamia pełny model fizyki z mniejszym krokiem czasowym.</String>
<String key="BreakingPantographPossibility">Możliwość złamania pantografu</String> <String key="BreakingPantographPossibility">Możliwość złamania pantografu</String>
<String key="BreakingPantographPossibilityDesc">Wyłączenie chroni pantograf przed uszkodzeniem przy błędnej geometrii sieci.</String>
<String key="PowerOverheadLineOnly">Zasilanie tylko pod siecią trakcyjna</String> <String key="PowerOverheadLineOnly">Zasilanie tylko pod siecią trakcyjna</String>
<String key="SaveSpeedometerTapes">Zapisuj taśmy z prędkościomierza</String> <String key="PowerOverheadLineOnlyDesc">Gdy wyłączone, podniesiony pantograf zasila pojazd nawet bez kontaktu z drutem.</String>
<String key="SaveSpeedometerTapes">Zapisuj log parametrów fizycznych</String>
<String key="SaveSpeedometerTapesDesc">Zapisuje szczegółowy log jazdy do folderu physicslog/.</String>
<String key="SaveSimLogs">Zapisuj log z symulacji</String> <String key="SaveSimLogs">Zapisuj log z symulacji</String>
<String key="KeepPrevLogs">Zachowuj poprzednie logi</String> <String key="KeepPrevLogs">Zachowuj poprzednie logi</String>
<String key="DisplaySimulation">Wyświetlaj symulację</String> <String key="KeepPrevLogsDesc">Tworzy osobny plik logu dla każdej scenerii zamiast nadpisywać errors.txt.</String>
<String key="DisplaySimulation">Renderuj obraz 3D</String>
<String key="DisplaySimulationDesc">Wyłączenie pomija rysowanie scenerii (czarny ekran), ale symulacja nadal działa.</String>
<String key="CrashDamage">Uszkodzenia przy zderzeniu</String> <String key="CrashDamage">Uszkodzenia przy zderzeniu</String>
<String key="CrashDamageDesc">Uszkodzenia sprzęgów i składów przy kolizjach i gwałtownym naciągu.</String>
<String key="Friction">Mnożnik tarcia</String> <String key="Friction">Mnożnik tarcia</String>
<String key="BrakeStep">Czułość zaworu hamulcowego</String> <String key="FrictionDesc">Mnoży tarcie koła o szynę. 1,00 nie zmienia niczego.</String>
<String key="BrakeSpeed">Czułość zaworu FV4a</String> <String key="BrakeStep">Krok nastawy hamulca zespolonego (Num3/Num9)</String>
<String key="BrakeStepDesc">Mnoży wielkość pojedynczego kroku kranu hamulca zespolonego sterowanego klawiszami Num3 i Num9. 1,00 nie zmienia niczego.</String>
<String key="BrakeSpeed">Szybkość zaworu FV4a</String>
<String key="BrakeSpeedDesc">Mnoży szybkość przesuwania kranu hamulca lokomotywy (FV4a) i hamulca lokalnego. 1,00 nie zmienia niczego.</String>
<String key="Sounds">Dżwięk</String> <String key="Sounds">Dźwięk</String>
<String key="EnableSounds">Włącz dźwięk</String> <String key="EnableSounds">Włącz dźwięk</String>
<String key="Volume">Głośność</String> <String key="Volume">Głośność</String>
<String key="RadiophoneVolume">Głośność radiotelefonu</String> <String key="RadiophoneVolume">Głośność radiotelefonu</String>
<String key="VehiclesVolume">Głośność pojazdów</String> <String key="VehiclesVolume">Głośność pojazdów</String>
<String key="PositionedSoundsVolume">Głośność dżwięków pozycjonowanych</String> <String key="PositionedSoundsVolume">Głośność dźwięków pozycjonowanych</String>
<String key="AmbientVolume">Głośność otoczenia</String> <String key="AmbientVolume">Głośność otoczenia</String>
<String key="Advanced">Zaawansowane</String> <String key="Advanced">Zaawansowane</String>
@@ -419,14 +467,15 @@
<String key="IgnoreIrrelevantTrains">Pomijaj składy dekoracyjne przy starcie</String> <String key="IgnoreIrrelevantTrains">Pomijaj składy dekoracyjne przy starcie</String>
<String key="Starter">Starter</String> <String key="Starter">Starter</String>
<String key="SettingsProfile">Profil ustawień:</String> <String key="SettingsProfile">Profil ustawień</String>
<String key="ProfileApply">Zastosuj</String> <String key="ProfileApply">Zastosuj</String>
<String key="ProfileSaveAs">Zapisz jako…</String> <String key="ProfileSaveAs">Zapisz jako…</String>
<String key="ProfileNamePrompt">Nazwa profilu</String> <String key="ProfileNamePrompt">Nazwa profilu</String>
<String key="AutoCloseStarter">Zamknij starter automatycznie</String> <String key="AutoCloseStarter">Zamknij starter automatycznie</String>
<String key="LargeThumbnails">Duże miniaturki</String> <String key="LargeThumbnails">Duże miniaturki</String>
<String key="AutoExpandSceneryTree">Automatycznie rozwijanie drzewka scenerii</String> <String key="AutoExpandSceneryTree">Automatycznie rozwijanie drzewka scenerii</String>
<String key="BatteryDefault">Akumulator przy starcie:</String> <String key="BatteryDefault">Akumulator przy starcie</String>
<String key="BatteryDefaultDesc">Stan akumulatora przy starcie symulacji, o ile scenariusz nie zdecyduje inaczej.</String>
<String key="BatteryDefaultScenario">Domyślnie ze scenerii</String> <String key="BatteryDefaultScenario">Domyślnie ze scenerii</String>
<String key="BatteryDefaultOff">Zawsze wyłączony</String> <String key="BatteryDefaultOff">Zawsze wyłączony</String>
<String key="BatteryDefaultOn">Zawsze włączony</String> <String key="BatteryDefaultOn">Zawsze włączony</String>
@@ -455,6 +504,7 @@
<String key="Save">Zapisz</String> <String key="Save">Zapisz</String>
<String key="Reset">Przywróć</String> <String key="Reset">Przywróć</String>
<String key="SettingsSaved">Zapisano ustawienia</String> <String key="SettingsSaved">Zapisano ustawienia</String>
<String key="UnsavedChanges">Niezapisane zmiany</String>
<!-- Errors / diagnostics --> <!-- Errors / diagnostics -->
<String key="FaultTitle">Błąd</String> <String key="FaultTitle">Błąd</String>
@@ -482,6 +532,7 @@
<String key="GeneralInfo">Ogólne</String> <String key="GeneralInfo">Ogólne</String>
<String key="Model">Model</String> <String key="Model">Model</String>
<String key="VMax">VMax</String> <String key="VMax">VMax</String>
<String key="Name">Nazwa</String>
<String key="Texture">Tekstura</String> <String key="Texture">Tekstura</String>
<!-- Scenery tab --> <!-- Scenery tab -->
@@ -509,13 +560,14 @@
<String key="WarehouseImportFile">Dodaj składy z pliku...</String> <String key="WarehouseImportFile">Dodaj składy z pliku...</String>
<String key="StartWithoutSaving">Uruchom bez zapisu ustawień</String> <String key="StartWithoutSaving">Uruchom bez zapisu ustawień</String>
<String key="StartFreeFly">Uruchom bez pojazdu</String> <String key="StartFreeFly">Uruchom bez pojazdu</String>
<String key="VolumePaused">Głośność podczas pauzy:</String> <String key="VolumePaused">Głośność podczas pauzy</String>
<String key="DebugLogVisible">Wyświetlaj przebieg symulacji</String> <String key="DebugLogVisible">Wyświetlaj log w konsoli</String>
<String key="PyScreenPriority">Częstotliwość odświeżania ekranów dodatkowych:</String> <String key="PyScreenPriority">Opóźnienie odświeżania ekranów dodatkowych</String>
<String key="PyPriorityNormal">Standardowa</String> <String key="PyScreenPriorityDesc">Minimalny czas między aktualizacjami ekranów Python w kabinach (ms). Wyższa wartość = rzadsze odświeżanie.</String>
<String key="PyPriorityLower">Niska</String> <String key="PyPriorityNormal">Standardowe (200 ms)</String>
<String key="PyPriorityLowest">Bardzo niska</String> <String key="PyPriorityLower">Wolniejsze (400 ms)</String>
<String key="PyPriorityIdle">Brak</String> <String key="PyPriorityLowest">Bardzo wolne (800 ms)</String>
<String key="PyPriorityIdle">Tylko gdy wolne (1600 ms)</String>
<String key="PyPriorityOff">Wyłączona</String> <String key="PyPriorityOff">Wyłączona</String>
<String key="MassBrutto">Masa brutto</String> <String key="MassBrutto">Masa brutto</String>
@@ -525,13 +577,13 @@
<String key="DriverHead">Maszynista (kabina A)</String> <String key="DriverHead">Maszynista (kabina A)</String>
<String key="DriverRear">Maszynista (kabina B)</String> <String key="DriverRear">Maszynista (kabina B)</String>
<String key="DriverPassenger">Pasażer</String> <String key="DriverPassenger">Pasażer</String>
<String key="DriverNobody">Nikt</String> <String key="DriverNobody">Brak</String>
<String key="RemoveAllTrains">Usuń wszystkie składy ze scenerii</String> <String key="RemoveAllTrains">Usuń wszystkie składy ze scenerii</String>
<String key="RemoveAllTrainsConfirm">Usunąć pojazdy ze wszystkich składów na tej scenerii?</String> <String key="RemoveAllTrainsConfirm">Usunąć pojazdy ze wszystkich składów na tej scenerii?</String>
<String key="TexWorks">Rewizja:</String> <String key="TexWorks">Rewizja:</String>
<String key="RevisionFrom">Rewizja od:</String> <String key="RevisionFrom">Rewizja od:</String>
<String key="RevisionTo">Rewizja do:</String> <String key="RevisionTo">Rewizja do:</String>
<String key="CrewLabel">Obsada:</String> <String key="CrewLabel">Obsada</String>
<String key="RuleAdd">Dodaj do reguły</String> <String key="RuleAdd">Dodaj do reguły</String>
<String key="RuleNew">— nowa reguła —</String> <String key="RuleNew">— nowa reguła —</String>
<String key="RuleRemove">Usuń z reguły</String> <String key="RuleRemove">Usuń z reguły</String>
@@ -551,4 +603,18 @@
<String key="AdvLogPage">Dziennik zdarzeń</String> <String key="AdvLogPage">Dziennik zdarzeń</String>
<String key="AdvClearLog">Wyczyść log</String> <String key="AdvClearLog">Wyczyść log</String>
<String key="ConsistSchedule">Zestawienie</String> <String key="ConsistSchedule">Zestawienie</String>
<!-- Skróty klawiaturowe -->
<String key="ShortcutsTitle">Skróty klawiaturowe</String>
<String key="ShortcutsGlobal">Ogólne</String>
<String key="ShortcutsTabHint">Tab przechodzi między panelami, strzałki działają wewnątrz list. W pasku składu Tab wchodzi na zaznaczony pojazd.</String>
<String key="ShortcutShowHelp">Ta lista</String>
<String key="ShortcutFocusSearch">Przejdź do wyszukiwarki pojazdów</String>
<String key="ShortcutAddVehicle">Dodaj wybrany pojazd do składu</String>
<String key="ShortcutSetNumber">Ustaw numer wagonu</String>
<String key="ShortcutPrevNext">Poprzedni / następny pojazd</String>
<String key="ShortcutFirstLast">Pierwszy / ostatni pojazd</String>
<String key="ShortcutMember">Wybierz człon w zespolonej jednostce</String>
<String key="ShortcutMoveVehicle">Przesuń pojazd w składzie</String>
<String key="ShortcutCardMenu">Otwórz menu pojazdu</String>
</Language> </Language>

View File

@@ -69,7 +69,7 @@
<String key="OptionalIncludes">Дополнительные элементы</String> <String key="OptionalIncludes">Дополнительные элементы</String>
<String key="OpenTextureFolder">Открыть папку с текстурами</String> <String key="OpenTextureFolder">Открыть папку с текстурами</String>
<String key="CopyTextureName">Копировать название</String> <String key="CopyTextureName">Копировать название</String>
<String key="CopyCoupler">Копировать сцепку с предыдущего</String> <String key="CopyCoupler">Копировать с предыдущего</String>
<String key="ThermoAmbient">Охлаждающая жидкость при температуре среды</String> <String key="ThermoAmbient">Охлаждающая жидкость при температуре среды</String>
<String key="TextureInfo">Текстура</String> <String key="TextureInfo">Текстура</String>
<String key="TexOperator">Перевозчик</String> <String key="TexOperator">Перевозчик</String>
@@ -185,6 +185,9 @@
<String key="SwitchNormal">N - тормоз включён</String> <String key="SwitchNormal">N - тормоз включён</String>
<String key="SwitchOff">0 - выключен, отпущен</String> <String key="SwitchOff">0 - выключен, отпущен</String>
<String key="SwitchOff10">1 - выключен, отпущен (10%)</String> <String key="SwitchOff10">1 - выключен, отпущен (10%)</String>
<String key="SwitchEmergencyTest">&lt;&gt; - выкл. на пробу экстренного торможения</String>
<String key="SwitchAgonal">X - изношенный (20% выкл., случайно)</String>
<String key="SwitchEmptied">E - опорожнён (магистрали на 0)</String>
<!-- Damage (wheel parameters) --> <!-- Damage (wheel parameters) -->
<String key="Damage">Повреждения</String> <String key="Damage">Повреждения</String>
@@ -194,15 +197,24 @@
<String key="DamageFlatnessProb">Вероятность ползуна (%)</String> <String key="DamageFlatnessProb">Вероятность ползуна (%)</String>
<String key="LoadType">Вид груза</String> <String key="LoadType">Вид груза</String>
<String key="LoadCount">Количество</String> <String key="LoadCount">Количество</String>
<String key="LoadUnitPieces">шт</String>
<String key="LoadUnitTons">т</String>
<String key="LoadUnitGeneric">ед.</String>
<String key="LoadHint">Задайте количество больше 0, чтобы загрузить единицу.</String> <String key="LoadHint">Задайте количество больше 0, чтобы загрузить единицу.</String>
<String key="LoadNotLoadable">Эта единица не принимает груз.</String> <String key="LoadNotLoadable">Эта единица не принимает груз.</String>
<String key="LoadPantState">Поднятый пантограф на старте</String> <String key="LoadPantState">Поднятый пантограф на старте</String>
<String key="LoadPantStateHint">Количество выбирает пантограф: 1 — A, 2 — B, 3 — оба.</String> <String key="LoadPantStateHint">Количество выбирает пантограф: 1 — A, 2 — B, 3 — оба.</String>
<String key="Pantograph">Пантограф</String>
<String key="PantStateNone">Нет</String>
<String key="PantStateBoth">Оба</String>
<String key="WagonNumber">Номер вагона</String> <String key="WagonNumber">Номер вагона</String>
<String key="LoadCopyPrev">Копировать груз с предыдущей единицы</String> <String key="LoadCopyPrev">Копировать груз с предыдущей единицы</String>
<String key="LoadMax">Максимально возможный груз</String> <String key="LoadMax">Максимально возможный груз</String>
<String key="LoadPrevShort">С предыдущей</String>
<String key="LoadToAllShort">На следующие</String>
<String key="LoadMaxShort">Максимум</String>
<String key="ConsistLoadTools">Груз всего состава</String> <String key="ConsistLoadTools">Груз всего состава</String>
<String key="ConsistRandomType">Случайный груз</String> <String key="ConsistRandomType">Случайный тип</String>
<String key="ConsistMaxAmount">Максимальное количество</String> <String key="ConsistMaxAmount">Максимальное количество</String>
<String key="ConsistRandomAmount">Случайное количество</String> <String key="ConsistRandomAmount">Случайное количество</String>
<String key="Set">Задать</String> <String key="Set">Задать</String>
@@ -252,6 +264,7 @@
<String key="CouplingUnitRear">Сцепка в конце состава</String> <String key="CouplingUnitRear">Сцепка в конце состава</String>
<String key="Join">Соединить</String> <String key="Join">Соединить</String>
<String key="TipJoin">Соединить этот вагон со следующим в один состав</String> <String key="TipJoin">Соединить этот вагон со следующим в один состав</String>
<String key="TipReorder">Перетащите для изменения порядка</String>
<String key="AddVehicle">Добавить</String> <String key="AddVehicle">Добавить</String>
<String key="TipAddVehicle">Добавить эту единицу в состав</String> <String key="TipAddVehicle">Добавить эту единицу в состав</String>
<String key="TipAddUnit">Добавить весь сцеп в состав</String> <String key="TipAddUnit">Добавить весь сцеп в состав</String>
@@ -278,7 +291,7 @@
<String key="Fullscreen">Полный экран</String> <String key="Fullscreen">Полный экран</String>
<String key="PauseSimInactive">Приостанавливать симуляцию при неактивном окне</String> <String key="PauseSimInactive">Приостанавливать симуляцию при неактивном окне</String>
<String key="PauseSimStart">Пауза при запуске</String> <String key="PauseSimStart">Пауза при запуске</String>
<String key="CursorSensitivity">Чувствительность курсора:</String> <String key="CursorSensitivity">Чувствительность курсора</String>
<String key="MouseHorInvert">Инвертировать мышь по горизонтали</String> <String key="MouseHorInvert">Инвертировать мышь по горизонтали</String>
<String key="MouseVertInvert">Инвертировать мышь по вертикали</String> <String key="MouseVertInvert">Инвертировать мышь по вертикали</String>
@@ -309,7 +322,7 @@
<String key="UartRadioChannel">Канал радио</String> <String key="UartRadioChannel">Канал радио</String>
<String key="Others">Прочее</String> <String key="Others">Прочее</String>
<String key="SelectEXE">Выбрать exe:</String> <String key="SelectEXE">Выбрать exe</String>
<String key="SelectEXEAuto">Выбрать автоматически</String> <String key="SelectEXEAuto">Выбрать автоматически</String>
<String key="DebugMode">Режим отладки (тестовый режим)</String> <String key="DebugMode">Режим отладки (тестовый режим)</String>
<String key="VirtualShunting">Виртуальный манёвр</String> <String key="VirtualShunting">Виртуальный манёвр</String>
@@ -324,12 +337,12 @@
<String key="REOff">Отключить визуальный слой</String> <String key="REOff">Отключить визуальный слой</String>
<String key="REExperimental">Экспериментальный</String> <String key="REExperimental">Экспериментальный</String>
<String key="Resolution">Разрешение:</String> <String key="Resolution">Разрешение</String>
<String key="BufforResolution">Разрешение буфера:</String> <String key="BufforResolution">Разрешение буфера</String>
<String key="MaxTexResolution">Максимальное разрешение текстур:</String> <String key="MaxTexResolution">Максимальное разрешение текстур</String>
<String key="MaxTexCabResolution">Максимальное разрешение текстур кабины:</String> <String key="MaxTexCabResolution">Максимальное разрешение текстур кабины</String>
<String key="TexFilteringQuality">Качество фильтрации текстур:</String> <String key="TexFilteringQuality">Качество фильтрации текстур</String>
<String key="Multisampling">Multisampling:</String> <String key="Multisampling">Multisampling</String>
<String key="QualityVeryLow">Очень низкое</String> <String key="QualityVeryLow">Очень низкое</String>
<String key="QualityLow">Низкое</String> <String key="QualityLow">Низкое</String>
<String key="QualityNormal">Нормальное</String> <String key="QualityNormal">Нормальное</String>
@@ -346,8 +359,8 @@
<String key="RangeHigh">Высокий</String> <String key="RangeHigh">Высокий</String>
<String key="RangeVeryHigh">Очень высокий</String> <String key="RangeVeryHigh">Очень высокий</String>
<String key="RangeDisabled">Отключено</String> <String key="RangeDisabled">Отключено</String>
<String key="DynamicLights">Динамические источники света:</String> <String key="DynamicLights">Динамические источники света</String>
<String key="RenderRange">Дальность отрисовки:</String> <String key="RenderRange">Дальность отрисовки</String>
<String key="FpsLimitEnable">Ограничить частоту кадров</String> <String key="FpsLimitEnable">Ограничить частоту кадров</String>
<String key="ShadowAngleLimit">Ограничение длины теней</String> <String key="ShadowAngleLimit">Ограничение длины теней</String>
<String key="ShadowLimitNone">Без ограничений</String> <String key="ShadowLimitNone">Без ограничений</String>
@@ -362,6 +375,7 @@
<String key="MotionBlur">Размытие в движении</String> <String key="MotionBlur">Размытие в движении</String>
<String key="AdditionalShadersEffects">Дополнительные эффекты шейдеров</String> <String key="AdditionalShadersEffects">Дополнительные эффекты шейдеров</String>
<String key="ReflectionsCubeMap">Отражения как кубическая карта</String> <String key="ReflectionsCubeMap">Отражения как кубическая карта</String>
<String key="SkipPipeline">SkipPipeline</String>
<String key="RenderVBO">Отрисовка VBO</String> <String key="RenderVBO">Отрисовка VBO</String>
<String key="RenderShadows">Отрисовывать тени</String> <String key="RenderShadows">Отрисовывать тени</String>
<String key="ReflectionsFramerate">Частота обновления отражений</String> <String key="ReflectionsFramerate">Частота обновления отражений</String>
@@ -419,14 +433,14 @@
<String key="IgnoreIrrelevantTrains">Пропускать декоративные составы при запуске</String> <String key="IgnoreIrrelevantTrains">Пропускать декоративные составы при запуске</String>
<String key="Starter">Starter</String> <String key="Starter">Starter</String>
<String key="SettingsProfile">Профиль настроек:</String> <String key="SettingsProfile">Профиль настроек</String>
<String key="ProfileApply">Применить</String> <String key="ProfileApply">Применить</String>
<String key="ProfileSaveAs">Сохранить как…</String> <String key="ProfileSaveAs">Сохранить как…</String>
<String key="ProfileNamePrompt">Название профиля</String> <String key="ProfileNamePrompt">Название профиля</String>
<String key="AutoCloseStarter">Автоматически закрывать стартер</String> <String key="AutoCloseStarter">Автоматически закрывать стартер</String>
<String key="LargeThumbnails">Крупные миниатюры</String> <String key="LargeThumbnails">Крупные миниатюры</String>
<String key="AutoExpandSceneryTree">Автоматически разворачивать дерево сценерий</String> <String key="AutoExpandSceneryTree">Автоматически разворачивать дерево сценерий</String>
<String key="BatteryDefault">Батарея при запуске:</String> <String key="BatteryDefault">Батарея при запуске</String>
<String key="BatteryDefaultScenario">По сценарию</String> <String key="BatteryDefaultScenario">По сценарию</String>
<String key="BatteryDefaultOff">Всегда выключена</String> <String key="BatteryDefaultOff">Всегда выключена</String>
<String key="BatteryDefaultOn">Всегда включена</String> <String key="BatteryDefaultOn">Всегда включена</String>
@@ -455,6 +469,7 @@
<String key="Save">Сохранить</String> <String key="Save">Сохранить</String>
<String key="Reset">Сбросить</String> <String key="Reset">Сбросить</String>
<String key="SettingsSaved">Настройки сохранены</String> <String key="SettingsSaved">Настройки сохранены</String>
<String key="UnsavedChanges">Несохранённые изменения</String>
<!-- Errors / diagnostics --> <!-- Errors / diagnostics -->
<String key="FaultTitle">Ошибка</String> <String key="FaultTitle">Ошибка</String>
@@ -482,6 +497,7 @@
<String key="GeneralInfo">Общие</String> <String key="GeneralInfo">Общие</String>
<String key="Model">Модель</String> <String key="Model">Модель</String>
<String key="VMax">VMax</String> <String key="VMax">VMax</String>
<String key="Name">Имя</String>
<String key="Texture">Текстура</String> <String key="Texture">Текстура</String>
<!-- Scenery tab --> <!-- Scenery tab -->
@@ -509,9 +525,9 @@
<String key="WarehouseImportFile">Добавить составы из файла...</String> <String key="WarehouseImportFile">Добавить составы из файла...</String>
<String key="StartWithoutSaving">Запустить без сохранения настроек</String> <String key="StartWithoutSaving">Запустить без сохранения настроек</String>
<String key="StartFreeFly">Запустить без транспорта</String> <String key="StartFreeFly">Запустить без транспорта</String>
<String key="VolumePaused">Громкость на паузе:</String> <String key="VolumePaused">Громкость на паузе</String>
<String key="DebugLogVisible">Показывать ход симуляции</String> <String key="DebugLogVisible">Показывать ход симуляции</String>
<String key="PyScreenPriority">Частота обновления дополнительных экранов:</String> <String key="PyScreenPriority">Частота обновления дополнительных экранов</String>
<String key="PyPriorityNormal">Стандартная</String> <String key="PyPriorityNormal">Стандартная</String>
<String key="PyPriorityLower">Низкая</String> <String key="PyPriorityLower">Низкая</String>
<String key="PyPriorityLowest">Очень низкая</String> <String key="PyPriorityLowest">Очень низкая</String>
@@ -531,7 +547,7 @@
<String key="TexWorks">Ревизия:</String> <String key="TexWorks">Ревизия:</String>
<String key="RevisionFrom">Ревизия с:</String> <String key="RevisionFrom">Ревизия с:</String>
<String key="RevisionTo">Ревизия по:</String> <String key="RevisionTo">Ревизия по:</String>
<String key="CrewLabel">Экипаж:</String> <String key="CrewLabel">Экипаж</String>
<String key="RuleAdd">Добавить в правило</String> <String key="RuleAdd">Добавить в правило</String>
<String key="RuleNew">— новое правило —</String> <String key="RuleNew">— новое правило —</String>
<String key="RuleRemove">Удалить из правила</String> <String key="RuleRemove">Удалить из правила</String>
@@ -551,4 +567,18 @@
<String key="AdvLogPage">Журнал событий</String> <String key="AdvLogPage">Журнал событий</String>
<String key="AdvClearLog">Очистить журнал</String> <String key="AdvClearLog">Очистить журнал</String>
<String key="ConsistSchedule">Состав</String> <String key="ConsistSchedule">Состав</String>
<!-- Сочетания клавиш -->
<String key="ShortcutsTitle">Сочетания клавиш</String>
<String key="ShortcutsGlobal">Общие</String>
<String key="ShortcutsTabHint">Tab переключает панели, стрелки работают внутри списков. В полосе состава Tab переходит на выбранный вагон.</String>
<String key="ShortcutShowHelp">Этот список</String>
<String key="ShortcutFocusSearch">Перейти к поиску подвижного состава</String>
<String key="ShortcutAddVehicle">Добавить выбранный вагон в состав</String>
<String key="ShortcutSetNumber">Задать номер вагона</String>
<String key="ShortcutPrevNext">Предыдущий / следующий вагон</String>
<String key="ShortcutFirstLast">Первый / последний вагон</String>
<String key="ShortcutMember">Выбрать секцию в сцепленной единице</String>
<String key="ShortcutMoveVehicle">Переместить вагон в составе</String>
<String key="ShortcutCardMenu">Открыть меню вагона</String>
</Language> </Language>