diff --git a/.vscode/launch.json b/.vscode/launch.json
index 6a46171..f17707b 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -8,7 +8,7 @@
{
"name": "C#: Launch Startup Project",
"type": "dotnet",
- "request": "launch"
+ "request": "launch",
}
]
}
\ No newline at end of file
diff --git a/StarterNG/App.axaml b/StarterNG/App.axaml
index 25e004a..99f30bc 100644
--- a/StarterNG/App.axaml
+++ b/StarterNG/App.axaml
@@ -10,6 +10,7 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
@@ -80,9 +236,9 @@
-
-
-
+
+
+
@@ -128,7 +284,7 @@
-
+
-
+
+
-
-
+
-
+
@@ -235,6 +384,33 @@
+
+
+
+
+
+
+
@@ -244,8 +420,10 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/StarterNG/Styles/Settings.axaml b/StarterNG/Styles/Settings.axaml
new file mode 100644
index 0000000..49615b5
--- /dev/null
+++ b/StarterNG/Styles/Settings.axaml
@@ -0,0 +1,262 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/StarterNG/Views/Depot.axaml b/StarterNG/Views/Depot.axaml
index a282d65..552ab03 100644
--- a/StarterNG/Views/Depot.axaml
+++ b/StarterNG/Views/Depot.axaml
@@ -3,27 +3,35 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:starterNg="clr-namespace:StarterNG"
+ xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:views="clr-namespace:StarterNG.Views"
x:CompileBindings="True"
mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="700"
x:Class="StarterNG.Views.Depot">
+
+
+
-
-
-
+
+
+
-
+ CornerRadius="2" Padding="6">
-
@@ -34,10 +42,12 @@
+ ToolTip.Tip="{Binding [OpenTextureBase], Source={x:Static starterNg:App.Loc}}">
+
+
-
-
+
+
-
+
-
-
@@ -107,74 +122,107 @@
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+ HorizontalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Disabled"
+ Padding="0,0,0,4">
+
diff --git a/StarterNG/Views/Depot.axaml.cs b/StarterNG/Views/Depot.axaml.cs
index 92f45bd..d527ec0 100644
--- a/StarterNG/Views/Depot.axaml.cs
+++ b/StarterNG/Views/Depot.axaml.cs
@@ -7,6 +7,7 @@ using System.Text;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Input.Platform;
@@ -76,8 +77,11 @@ public partial class Depot : UserControl
_cards = new VehicleCards(consistStack, _db, _consist, _cargo, _minis, _hand,
RebuildConsist, () => _details.Refresh(), _browser.SelectInBrowser, _drag.ArmCardDrag);
+ // Hidden still scrolls - it only drops the bar itself.
+ MiniTextures.Sharp(miniPreview);
+
_details = new VehicleDetails(
- selectedVehiclePanel, generalPanel, consistTexturePanel, couplerPanel,
+ generalPanel, consistTexturePanel, couplerPanel, couplerActions,
brakesPanel, loadsPanel, damagePanel, removeVehicleButton,
_db, _info, _consist, _cargo, _hand,
RebuildConsist, _cards.RefreshMemberChrome, _browser.ShowTextureInfo);
@@ -163,6 +167,9 @@ public partial class Depot : UserControl
_details.Refresh();
}
+ /// Redraws the consist cards, e.g. after the thumbnail size changed.
+ public void RefreshConsistView() => RebuildConsist();
+
private void RebuildConsist()
{
consistStack.Children.Clear();
@@ -183,6 +190,10 @@ public partial class Depot : UserControl
_details.Refresh();
UpdateTrainStats();
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()
@@ -232,9 +243,9 @@ public partial class Depot : UserControl
depotDrivableOnlyCheck.IsChecked = StarterNG.Classes.Settings.Instance.DrivableOnly;
_listScenery = AppState.Instance.CurrentScenery ?? _sceneries.FirstOrDefault();
- mapConsistsPanel.Header = _listScenery != null
- ? $"{App.Loc["MapConsists"]}: {Path.GetFileNameWithoutExtension(_listScenery.Path)}"
- : App.Loc["NoSceneryConsists"];
+ sceneryConsistsTab.Header = App.Loc["SceneryConsists"];
+ ToolTip.SetTip(sceneryConsistsTab,
+ _listScenery != null ? Path.GetFileNameWithoutExtension(_listScenery.Path) : App.Loc["NoSceneryConsists"]);
if (_listScenery != null)
{
@@ -425,6 +436,25 @@ public partial class Depot : UserControl
AddTexture(_browser.Selected);
}
+ /// Keyboard shortcut entry points. See .
+ 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 ToolAddUniqueMmd() => AddUniqueMmdButton_OnClick(null, null!);
diff --git a/StarterNG/Views/Depot/MiniTextures.cs b/StarterNG/Views/Depot/MiniTextures.cs
index a50ae9b..c5cb0fd 100644
--- a/StarterNG/Views/Depot/MiniTextures.cs
+++ b/StarterNG/Views/Depot/MiniTextures.cs
@@ -1,5 +1,7 @@
using System.Collections.Generic;
using System.IO;
+using Avalonia.Controls;
+using Avalonia.Media;
using Avalonia.Media.Imaging;
using StarterNG.Classes;
@@ -10,6 +12,18 @@ public sealed class MiniTextures
{
private readonly Dictionary _cache = new();
+ ///
+ /// 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.)
+ ///
+ public static Image Sharp(Image image)
+ {
+ RenderOptions.SetBitmapInterpolationMode(image, BitmapInterpolationMode.None);
+ return image;
+ }
+
public Bitmap? Get(string? name, int height, bool strict = false)
{
if (string.IsNullOrEmpty(name))
@@ -27,8 +41,17 @@ public sealed class MiniTextures
{
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);
- 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)
{
diff --git a/StarterNG/Views/Depot/VehicleBrowser.cs b/StarterNG/Views/Depot/VehicleBrowser.cs
index a12370c..08ad464 100644
--- a/StarterNG/Views/Depot/VehicleBrowser.cs
+++ b/StarterNG/Views/Depot/VehicleBrowser.cs
@@ -222,13 +222,13 @@ public sealed class VehicleBrowser
};
var bmp = _minis.Get(cls, height);
if (bmp != null)
- cell.Children.Add(new Image
+ cell.Children.Add(MiniTextures.Sharp(new Image
{
Source = bmp, Height = height,
Stretch = Stretch.Uniform,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
- });
+ }));
cell.Children.Add(new TextBlock
{
Text = cls,
@@ -421,7 +421,7 @@ public sealed class VehicleBrowser
target.Children.Add(new TextBlock
{
Text = line,
- FontSize = 10,
+ FontSize = 12,
Opacity = 0.85,
TextTrimming = TextTrimming.CharacterEllipsis
});
diff --git a/StarterNG/Views/Depot/VehicleCardStyle.cs b/StarterNG/Views/Depot/VehicleCardStyle.cs
index c69aa9c..166809c 100644
--- a/StarterNG/Views/Depot/VehicleCardStyle.cs
+++ b/StarterNG/Views/Depot/VehicleCardStyle.cs
@@ -8,6 +8,9 @@ public static class VehicleCardStyle
public static readonly IBrush BorderSelected = new SolidColorBrush(Color.Parse("#AA41C400"));
public static readonly IBrush BackgroundSelected = new SolidColorBrush(Color.Parse("#2241C400"));
+ /// Lit state of the flip button on a reversed vehicle.
+ public static readonly IBrush Flipped = new SolidColorBrush(Color.Parse("#41C400"));
+
public static readonly IBrush Placeholder = new SolidColorBrush(Color.Parse("#22808080"));
public static int ThumbHeight =>
diff --git a/StarterNG/Views/Depot/VehicleCards.cs b/StarterNG/Views/Depot/VehicleCards.cs
index f86b397..023d0f3 100644
--- a/StarterNG/Views/Depot/VehicleCards.cs
+++ b/StarterNG/Views/Depot/VehicleCards.cs
@@ -7,6 +7,8 @@ using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.VisualTree;
+using Material.Icons;
+using Material.Icons.Avalonia;
using StarterNG.Classes;
using StarterNG.Domain;
@@ -75,7 +77,10 @@ public sealed class VehicleCards
?? _minis.Get(car.MiniName, VehicleCardStyle.ThumbHeight);
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)
{
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
{
Orientation = Orientation.Horizontal,
- Spacing = 2,
- HorizontalAlignment = HorizontalAlignment.Center
+ Spacing = 3,
+ 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(SmallButton("⇄", App.Loc["TipFlip"], () => _consist.Flip(item)));
- controls.Children.Add(SmallButton("▶", App.Loc["TipMoveRight"], () => _consist.MoveRight(item)));
+ controls.Children.Add(FlipButton(item));
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 &&
(Consist.HoldsTail(item) || _consist.CanFormUnit(item, _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.FontWeight = FontWeight.Bold;
controls.Children.Add(remove);
- var inner = new StackPanel { Spacing = 4 };
+ var inner = new StackPanel { Spacing = 3 };
inner.Children.Add(minis);
- inner.Children.Add(nameBlock);
inner.Children.Add(controls);
var card = new Border
{
- Margin = new Thickness(2, 0),
- Padding = new Thickness(6),
+ Margin = new Thickness(3, 2, 3, 6),
+ Padding = new Thickness(selected ? 4 : 5),
CornerRadius = new CornerRadius(6),
BorderThickness = new Thickness(selected ? 2 : 1),
BorderBrush = selected ? VehicleCardStyle.BorderSelected : VehicleCardStyle.Border,
Background = selected ? VehicleCardStyle.BackgroundSelected : Brushes.Transparent,
Cursor = _hand,
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) =>
{
@@ -167,6 +165,7 @@ public sealed class VehicleCards
_consist.SyncStartingVehicle();
RefreshCardSelectionChrome();
_refreshDetails();
+ _selectInBrowser(_consist.SelectedCar);
_armDrag(index, e);
};
@@ -178,31 +177,30 @@ public sealed class VehicleCards
TrainsetDisplay.VehicleLabel(car,
head: index == 0 && ReferenceEquals(car, Consist.HeadCar(item)) && TrainsetDisplay.IsPowered(car));
+ ///
+ /// 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.
+ ///
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
{
- Child = stack,
+ Child = visual,
Padding = new Thickness(1),
BorderThickness = new Thickness(0, 2),
BorderBrush = Brushes.Transparent,
Cursor = _hand
};
+ var lines = new List { 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;
frame.PointerPressed += (_, _) => PressedCar = picked;
@@ -233,12 +231,154 @@ public sealed class VehicleCards
if (child is not Border { Tag: ConsistItem ci } border)
continue;
bool sel = ReferenceEquals(ci, _consist.Selected);
+ border.Padding = new Thickness(sel ? 4 : 5);
border.BorderThickness = new Thickness(sel ? 2 : 1);
border.BorderBrush = sel ? VehicleCardStyle.BorderSelected : VehicleCardStyle.Border;
border.Background = sel ? VehicleCardStyle.BackgroundSelected : Brushes.Transparent;
}
}
+ ///
+ /// Selects an item without going through a redraw. picks the
+ /// active member of a multi-vehicle unit; null keeps the current one when possible.
+ ///
+ 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()
+ .FirstOrDefault(b => ReferenceEquals(b.Tag, item));
+
+ ///
+ /// 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.
+ ///
+ 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]);
+ }
+
+ /// Ctrl+N on the selected card, from the window's shortcut table.
+ 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)
{
var num = new NumericUpDown
@@ -276,7 +416,7 @@ public sealed class VehicleCards
};
var coupler = new Button
{
- Padding = new Thickness(2),
+ Padding = new Thickness(4, 6, 4, 6),
MinWidth = 0,
Cursor = _hand,
VerticalAlignment = VerticalAlignment.Center,
@@ -288,7 +428,7 @@ public sealed class VehicleCards
}
};
coupler.Classes.Add("Basic");
- ToolTip.SetTip(coupler, App.Loc["Coupling"]);
+ ToolTip.SetTip(coupler, $"{App.Loc["Coupling"]} — {App.Loc["TipReorder"]}");
return coupler;
}
@@ -302,7 +442,7 @@ public sealed class VehicleCards
var copy = new Button
{
Content = "\u00AB",
- FontSize = 13,
+ FontSize = 12,
Padding = new Thickness(6, 1),
Margin = new Thickness(12, 0, 0, 0),
IsEnabled = unitIdx > 0,
@@ -324,11 +464,20 @@ public sealed class VehicleCards
{
Text = App.Loc["Coupling"],
FontWeight = FontWeight.Bold,
- FontSize = 11,
+ FontSize = 12,
VerticalAlignment = VerticalAlignment.Center
});
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++)
{
int bit = 1 << i;
@@ -336,17 +485,20 @@ public sealed class VehicleCards
{
Content = App.Loc[CouplingBits.BitKeys[i]],
IsChecked = d.Coupling.Has(bit),
- FontSize = 11
+ FontSize = 12
};
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
{
Content = App.Loc["ThermoAmbient"],
IsChecked = d.Coupling.ThermoDynamic,
- FontSize = 11,
+ FontSize = 12,
Margin = new Thickness(0, 2, 0, 0)
};
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 };
}
- private const string TextPresentation = "\uFE0E";
+ ///
+ /// Card buttons draw vector icons rather than glyphs - Windows renders half
+ /// the arrow and link characters as colour emoji, whatever the font asks for.
+ ///
+ 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
{
- Content = glyph + TextPresentation,
- FontSize = 15,
+ FontSize = 13,
Padding = new Thickness(0),
MinWidth = 0,
MinHeight = 0,
- Width = 26,
+ Width = 28,
Height = 22,
HorizontalContentAlignment = HorizontalAlignment.Center,
VerticalContentAlignment = VerticalAlignment.Center,
@@ -378,9 +538,26 @@ public sealed class VehicleCards
return btn;
}
+ ///
+ /// 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.
+ ///
+ 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)
{
- var btn = SmallButton("", App.Loc["TipDriver"], () => _consist.CycleDriver(item));
+ var btn = SmallButton(App.Loc["TipDriver"], () => _consist.CycleDriver(item));
btn.FontWeight = FontWeight.Bold;
_driverChrome.Add((btn, item));
PaintDriverButton(btn, item);
diff --git a/StarterNG/Views/Depot/VehicleDetails.cs b/StarterNG/Views/Depot/VehicleDetails.cs
index d4b1143..5e911d6 100644
--- a/StarterNG/Views/Depot/VehicleDetails.cs
+++ b/StarterNG/Views/Depot/VehicleDetails.cs
@@ -10,16 +10,17 @@ using Avalonia.Layout;
using Avalonia.Media;
using StarterNG.Classes;
+using StarterNG.Controls;
using StarterNG.Domain;
namespace StarterNG.Views;
public sealed class VehicleDetails
{
- private readonly HeaderedContentControl selectedVehiclePanel;
private readonly StackPanel generalPanel;
private readonly StackPanel consistTexturePanel;
private readonly StackPanel couplerPanel;
+ private readonly Panel couplerActions;
private readonly StackPanel brakesPanel;
private readonly StackPanel loadsPanel;
private readonly StackPanel damagePanel;
@@ -36,17 +37,17 @@ public sealed class VehicleDetails
private readonly Action _showTextureInfo;
public VehicleDetails(
- HeaderedContentControl selectedVehiclePanel,
StackPanel generalPanel, StackPanel consistTexturePanel, StackPanel couplerPanel,
+ Panel couplerActions,
StackPanel brakesPanel, StackPanel loadsPanel, StackPanel damagePanel,
Button removeVehicleButton,
VehicleDatabase db, VehicleInfo info, Consist consist, Cargo cargo, Cursor hand,
Action redraw, Action refreshChrome, Action showTextureInfo)
{
- this.selectedVehiclePanel = selectedVehiclePanel;
this.generalPanel = generalPanel;
this.consistTexturePanel = consistTexturePanel;
this.couplerPanel = couplerPanel;
+ this.couplerActions = couplerActions;
this.brakesPanel = brakesPanel;
this.loadsPanel = loadsPanel;
this.damagePanel = damagePanel;
@@ -67,31 +68,27 @@ public sealed class VehicleDetails
generalPanel.Children.Clear();
consistTexturePanel.Children.Clear();
couplerPanel.Children.Clear();
+ couplerActions.Children.Clear();
brakesPanel.Children.Clear();
loadsPanel.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!))
_consist.SelectedCar = _consist.Selected.Cars[0];
_refreshChrome();
- loadsPanel.Children.Add(BuildConsistLoadTools());
-
if (_consist.Selected is null)
{
- generalPanel.Children.Add(DetailHint(App.Loc["SelectVehicleHint"]));
- consistTexturePanel.Children.Add(DetailHint(App.Loc["SelectVehicleHint"]));
- couplerPanel.Children.Add(DetailHint(App.Loc["SelectVehicleHint"]));
- brakesPanel.Children.Add(DetailHint(App.Loc["SelectVehicleHint"]));
- loadsPanel.Children.Add(DetailHint(App.Loc["SelectVehicleHint"]));
- damagePanel.Children.Add(DetailHint(App.Loc["SelectVehicleHint"]));
+ generalPanel.Children.Add(PanelNote(App.Loc["SelectVehicleHint"]));
+ consistTexturePanel.Children.Add(PanelNote(App.Loc["SelectVehicleHint"]));
+ couplerPanel.Children.Add(PanelNote(App.Loc["SelectVehicleHint"]));
+ brakesPanel.Children.Add(PanelNote(App.Loc["SelectVehicleHint"]));
+ loadsPanel.Children.Add(PanelNote(App.Loc["SelectVehicleHint"]));
+ damagePanel.Children.Add(PanelNote(App.Loc["SelectVehicleHint"]));
removeVehicleButton.IsEnabled = false;
+ // The whole-consist tools still apply with nothing selected.
+ AddConsistLoadSection();
return;
}
@@ -100,21 +97,27 @@ public sealed class VehicleDetails
if (_db.TextureForSkin(_consist.ActiveCar(_consist.Selected).SkinFile) is { } tex)
_showTextureInfo(tex, consistTexturePanel);
else
- consistTexturePanel.Children.Add(DetailHint(App.Loc["NoTextureInfo"]));
+ consistTexturePanel.Children.Add(PanelNote(App.Loc["NoTextureInfo"]));
removeVehicleButton.IsEnabled = true;
if (_consist.Selected.Cars.Count > 1)
{
- couplerPanel.Children.Add(UnitScopeHint(App.Loc["CouplingUnitRear"]));
- brakesPanel.Children.Add(UnitScopeHint(App.Loc["AppliesToUnit"]));
+ couplerPanel.Children.Add(PanelNote(App.Loc["CouplingUnitRear"]));
+ brakesPanel.Children.Add(PanelNote(App.Loc["AppliesToUnit"]));
}
BuildCouplerEditor(_consist.Selected);
BuildBrakesEditor(_consist.Selected);
- BuildLoadsEditor(_consist.Selected);
- BuildConfigExtras(_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)
@@ -122,6 +125,9 @@ public sealed class VehicleDetails
var car = _consist.ActiveCar(item);
var inv = CultureInfo.InvariantCulture;
+ if (!string.IsNullOrWhiteSpace(car.Name))
+ generalPanel.Children.Add(Reading(App.Loc["Name"], car.Name));
+
bool unit = item.Cars.Count > 1;
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
: 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)));
- generalPanel.Children.Add(InfoRow($"{App.Loc["Mass"]} [t]:",
+ generalPanel.Children.Add(Reading($"{App.Loc["Mass"]} [t]",
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)));
if (unit)
- generalPanel.Children.Add(InfoRow($"{App.Loc["VehicleCount"]}:",
+ generalPanel.Children.Add(Reading(App.Loc["VehicleCount"],
item.Cars.Count.ToString(inv)));
- generalPanel.Children.Add(InfoRow($"{App.Loc["Model"]}:", car.MmdFile));
- generalPanel.Children.Add(InfoRow($"{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;
+ generalPanel.Children.Add(Reading(App.Loc["Model"], car.MmdFile));
+ generalPanel.Children.Add(Reading(App.Loc["Texture"], car.SkinFile));
}
private void BuildCouplerEditor(ConsistItem item)
@@ -187,12 +163,17 @@ public sealed class VehicleDetails
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,*"),
- RowDefinitions = new RowDefinitions("Auto,Auto,Auto,Auto"),
- RowSpacing = 4
+ ColumnDefinitions = new ColumnDefinitions("*,*"),
+ RowSpacing = 2,
+ 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++)
{
int bit = 1 << i;
@@ -207,17 +188,16 @@ public sealed class VehicleDetails
d.Coupling.Set(bit, check.IsChecked == true);
_redraw();
};
- int half = (CouplingBits.BitKeys.Length + 1) / 2;
- Grid.SetColumn(check, i < half ? 0 : 2);
- Grid.SetRow(check, i % half);
- grid.Children.Add(check);
+ Grid.SetRow(check, i / 2);
+ Grid.SetColumn(check, i % 2);
+ list.Children.Add(check);
}
- couplerPanel.Children.Add(grid);
+ couplerPanel.Children.Add(list);
var copy = new Button
{
Content = App.Loc["CopyCoupler"],
- FontSize = 11,
+ FontSize = 12,
Cursor = _hand,
HorizontalAlignment = HorizontalAlignment.Stretch,
HorizontalContentAlignment = HorizontalAlignment.Center,
@@ -235,7 +215,7 @@ public sealed class VehicleDetails
var auto = new Button
{
Content = App.Loc["AutoCoupler"],
- FontSize = 11,
+ FontSize = 12,
Cursor = _hand,
HorizontalAlignment = HorizontalAlignment.Stretch,
HorizontalContentAlignment = HorizontalAlignment.Center,
@@ -250,19 +230,23 @@ public sealed class VehicleDetails
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)
};
- Grid.SetColumn(copy, 0);
- Grid.SetColumn(auto, 2);
buttons.Children.Add(copy);
buttons.Children.Add(auto);
- couplerPanel.Children.Add(buttons);
+ couplerActions.Children.Add(buttons);
}
- private void BuildConfigExtras(ConsistItem item)
+ ///
+ /// 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.
+ ///
+ private Control BuildVehicleSection(ConsistItem item)
{
var driver = new ComboBox { FontSize = 12, MinWidth = 0 };
foreach (string key in new[] { "DriverHead", "DriverRear", "DriverPassenger", "DriverNobody" })
@@ -296,75 +280,143 @@ public sealed class VehicleDetails
};
driver.MinWidth = 0;
driver.HorizontalAlignment = HorizontalAlignment.Stretch;
- loadsPanel.Children.Insert(0, LabeledRow(App.Loc["CrewLabel"], driver, labelWidth: 70));
- var reversed = new CheckBox
- {
- Content = App.Loc["Reversed"],
- IsChecked = item.Flipped,
- FontSize = 11
- };
- reversed.IsCheckedChanged += (_, _) =>
- {
- if ((reversed.IsChecked == true) != item.Flipped)
- _consist.Flip(item);
- };
+ var rows = new StackPanel();
+ rows.Children.Add(Row(App.Loc["CrewLabel"], driver));
+ 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 thermo = new CheckBox
- {
- Content = App.Loc["ThermoAmbient"],
- IsChecked = d.Coupling.ThermoDynamic,
- FontSize = 11
- };
- thermo.IsCheckedChanged += (_, _) => d.Coupling.ThermoDynamic = thermo.IsChecked == true;
+ bool thermoApplies = _info.PhysicsFor(d)?.IsDieselEngine == true
+ || d.Coupling.ThermoDynamic;
- var switches = new Grid
+ if (thermoApplies)
{
- ColumnDefinitions = new ColumnDefinitions("*,8,*"),
- Margin = new Thickness(0, 4, 0, 0)
- };
- Grid.SetColumn(thermo, 2);
- switches.Children.Add(reversed);
- switches.Children.Add(thermo);
- loadsPanel.Children.Add(switches);
+ var thermo = new CheckBox
+ {
+ Content = App.Loc["ThermoAmbient"],
+ IsChecked = d.Coupling.ThermoDynamic,
+ FontSize = 12
+ };
+ 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));
+ }
+
+ ///
+ /// The one row shape every detail tab uses. The label column negotiates its width
+ /// through the shared-size group on the host panel, so all rows in a panel line up
+ /// without anyone naming a pixel count - see the Compact styles in Settings.axaml.
+ /// Omitting makes it a read-only line; omitting
+ /// indents a bare widget to the control column.
+ ///
+ private static SettingRow Row(string? label = null, Control? content = null,
+ string? value = null, string? description = null,
+ string? tip = null)
+ {
+ var row = new SettingRow
+ {
+ Label = label,
+ Description = description,
+ ValueText = value,
+ Content = content
+ };
+ row.Classes.Add("Compact");
+ if (!string.IsNullOrWhiteSpace(tip))
+ ToolTip.SetTip(row, tip);
+ return row;
+ }
+
+ ///
+ /// A read-only label/value line. The dash keeps the row from collapsing when a
+ /// vehicle has no reading for the field.
+ ///
+ private static SettingRow Reading(string label, string? value)
+ {
+ bool empty = string.IsNullOrWhiteSpace(value);
+ return Row(label, value: empty ? "\u2013" : value, tip: empty ? null : value);
+ }
+
+ private static 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"));
+
+ ///
+ /// 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.
+ ///
+ private static TextBlock PanelNote(string text) => new()
{
Text = text,
- FontSize = 11,
- Opacity = 0.6,
+ FontSize = 12,
+ Foreground = DimBrush,
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)
=> combo.Items.Add(new ComboBoxItem { Content = content, Tag = tag });
@@ -402,9 +454,9 @@ public sealed class VehicleDetails
switchCombo.SelectionChanged += (_, _) => Apply();
loadCombo.SelectionChanged += (_, _) => Apply();
- brakesPanel.Children.Add(LabeledRow(App.Loc["BrakeMode"], modeCombo, labelWidth: 96));
- brakesPanel.Children.Add(LabeledRow(App.Loc["BrakeSwitch"], switchCombo, labelWidth: 96));
- brakesPanel.Children.Add(LabeledRow(App.Loc["BrakeLoad"], loadCombo, labelWidth: 96));
+ brakesPanel.Children.Add(Row(App.Loc["BrakeMode"], modeCombo));
+ brakesPanel.Children.Add(Row(App.Loc["BrakeSwitch"], switchCombo));
+ brakesPanel.Children.Add(Row(App.Loc["BrakeLoad"], loadCombo));
}
private static string BrakeModeLabel(string? code) => code switch
@@ -413,7 +465,6 @@ public sealed class VehicleDetails
"P" => App.Loc["BrakePassenger"],
"R" => App.Loc["BrakeExpress"],
"M" => App.Loc["BrakeExpressMg"],
- "Q" => App.Loc["BrakeNoAir"],
_ => App.Loc["BrakeActingNone"]
};
@@ -427,8 +478,12 @@ public sealed class VehicleDetails
private static string SwitchLabel(string? code) => code switch
{
+ "<>" => App.Loc["SwitchEmergencyTest"],
"0" => App.Loc["SwitchOff"],
"1" => App.Loc["SwitchOff10"],
+ "X" => App.Loc["SwitchAgonal"],
+ "E" => App.Loc["SwitchEmptied"],
+ "Q" => App.Loc["BrakeNoAir"],
_ => App.Loc["SwitchNormal"]
};
@@ -439,7 +494,7 @@ public sealed class VehicleDetails
NumericUpDown Spin(int value, int max) => new()
{
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);
@@ -465,84 +520,100 @@ public sealed class VehicleDetails
flatRand.ValueChanged += (_, _) => Apply();
flatProb.ValueChanged += (_, _) => Apply();
- var grid = new Grid
- {
- ColumnDefinitions = new ColumnDefinitions("*,8,Auto"),
- RowSpacing = 6
- };
- 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);
+ damagePanel.Children.Add(Row(App.Loc["DamageSway"], sway));
+ damagePanel.Children.Add(Row(App.Loc["DamageFlatness"], flat));
+ damagePanel.Children.Add(Row(App.Loc["DamageFlatnessRand"], flatRand));
+ damagePanel.Children.Add(Row(App.Loc["DamageFlatnessProb"], flatProb));
}
- private Control BuildConsistLoadTools()
+ ///
+ /// 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.
+ ///
+ private Control BuildVehicleLoadTools(ConsistItem sel)
{
- var panel = new StackPanel { Spacing = 4, Margin = new Thickness(0, 0, 0, 4) };
- var row = new WrapPanel { Orientation = Orientation.Horizontal };
- row.Children.Add(LoadToolButton(App.Loc["ConsistRandomType"],
- () => { _cargo.RandomTypes(_consist); _redraw(); }));
- row.Children.Add(LoadToolButton(App.Loc["ConsistMaxAmount"],
- () => { _cargo.MaxAmounts(_consist); _redraw(); }));
- row.Children.Add(LoadToolButton(App.Loc["ConsistRandomAmount"],
- () => { _cargo.RandomAmounts(_consist); _redraw(); }));
- panel.Children.Add(row);
- return panel;
+ bool loadable = _cargo.IsLoadable(sel);
+
+ var prev = LoadToolButton(App.Loc["LoadPrevShort"],
+ () => { if (_consist.Selected is { } s) { _cargo.CopyFromPrevious(_consist, s); _redraw(); } });
+ prev.IsEnabled = loadable && _consist.IndexOf(sel) > 0;
+ ToolTip.SetTip(prev, App.Loc["LoadCopyPrev"]);
+
+ var max = LoadToolButton(App.Loc["LoadMaxShort"],
+ () => { if (_consist.Selected is { } s) { _cargo.FillUnit(s); _redraw(); } });
+ 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)
+ ///
+ /// 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.
+ ///
+ 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
{
- Content = text, FontSize = 11, Margin = new Thickness(0, 0, 4, 4),
- Padding = new Thickness(8, 3), Cursor = _hand,
+ Content = text, FontSize = 12,
+ Padding = new Thickness(6, 2), Cursor = _hand,
+ HorizontalAlignment = HorizontalAlignment.Stretch,
+ HorizontalContentAlignment = HorizontalAlignment.Center,
IsEnabled = _consist.Count > 0
};
- b.Classes.Add("Flat");
+ b.Classes.Add(secondary ? "Outline" : "Flat");
b.Click += (_, _) => onClick();
return b;
}
- private void BuildLoadsEditor(ConsistItem item)
+ ///
+ /// 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.
+ ///
+ private Control BuildLoadSection(ConsistItem item)
{
var lead = _consist.ActiveCar(item);
var available = _cargo.AcceptedTypes(lead);
if (available.Count == 0 && string.IsNullOrEmpty(lead.LoadType))
- {
- loadsPanel.Children.Add(DetailHint(App.Loc["LoadNotLoadable"]));
- return;
- }
+ return PanelNote(App.Loc["LoadNotLoadable"]);
if (!string.IsNullOrEmpty(lead.LoadType) &&
!available.Contains(lead.LoadType!, StringComparer.OrdinalIgnoreCase))
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 });
foreach (var name in available)
typeCombo.Items.Add(new ComboBoxItem { Content = LoadWeights.Describe(name), Tag = name });
@@ -557,41 +628,129 @@ public sealed class VehicleDetails
break;
}
+ string? SelectedType() => (typeCombo.SelectedItem as ComboBoxItem)?.Tag as string;
+ bool IsPant() => Dynamic.IsPantStateType(SelectedType());
+
int limit = _cargo.LimitFor(lead);
var countBox = new NumericUpDown
{
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"]);
- pantHint.IsVisible = lead.IsPantState;
+ var pantStateCombo = new ComboBox
+ {
+ 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()
{
string? type = SelectedType();
- int count = (int)(countBox.Value ?? 0);
+ int count = GetCount();
foreach (var c in _consist.MemberTargets(item))
{
-
c.LoadType = string.IsNullOrEmpty(type) ? null : 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 += (_, _) =>
{
- string? type = SelectedType();
- pantHint.IsVisible = Dynamic.IsPantStateType(type);
- countBox.Maximum = _cargo.LimitFor(lead, type);
+ countBox.Maximum = _cargo.LimitFor(lead, SelectedType());
+
+ // 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();
};
- countBox.ValueChanged += (_, _) => Apply();
+ pantStateCombo.SelectionChanged += (_, _) => Apply();
+ countBox.ValueChanged += (_, _) => { Apply(); UpdateVisibility(); };
- loadsPanel.Children.Add(LabeledRow(App.Loc["LoadType"], typeCombo));
- loadsPanel.Children.Add(LabeledRow(App.Loc["LoadCount"], countBox));
- loadsPanel.Children.Add(pantHint);
+ UpdateVisibility();
+
+ return Stack(
+ Row(App.Loc["LoadType"], typeCombo),
+ countRow,
+ BuildVehicleLoadTools(item));
}
}
diff --git a/StarterNG/Views/Scenarios.axaml b/StarterNG/Views/Scenarios.axaml
index ddec1a6..d19b930 100644
--- a/StarterNG/Views/Scenarios.axaml
+++ b/StarterNG/Views/Scenarios.axaml
@@ -15,7 +15,8 @@
-
+
+
@@ -144,9 +145,9 @@
VerticalAlignment="Center" TextAlignment="Right"
FontWeight="SemiBold" />
-
-
@@ -161,10 +162,10 @@
VerticalAlignment="Center" TextAlignment="Right"
FontWeight="SemiBold" />
-
-
@@ -217,9 +218,13 @@
+
+
+
+
-
diff --git a/StarterNG/Views/Scenarios.axaml.cs b/StarterNG/Views/Scenarios.axaml.cs
index 6dd58ef..46d8299 100644
--- a/StarterNG/Views/Scenarios.axaml.cs
+++ b/StarterNG/Views/Scenarios.axaml.cs
@@ -896,13 +896,13 @@ public partial class Scenarios : UserControl
var bmp = LoadMini(miniName, thumbH);
Control visual = bmp is not null
- ? new Image
+ ? MiniTextures.Sharp(new Image
{
Source = bmp,
Height = thumbH,
Stretch = Avalonia.Media.Stretch.Uniform,
Margin = new Thickness(0)
- }
+ })
: MissingMiniBox(train.SkinFile, thumbH);
bool selected = !string.IsNullOrEmpty(train.Name) &&
diff --git a/StarterNG/Views/Settings.axaml b/StarterNG/Views/Settings.axaml
index ec3d655..49cbaa2 100644
--- a/StarterNG/Views/Settings.axaml
+++ b/StarterNG/Views/Settings.axaml
@@ -3,77 +3,484 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:starterNg="clr-namespace:StarterNG"
+ xmlns:ctl="clr-namespace:StarterNG.Controls"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="StarterNG.Views.Settings"
x:CompileBindings="True">
+
+
-
-
-
-
-
+
-
-
-
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
-
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 3840x2160
+ 2560x1440
+ 1920x1080
+ 1600x900
+ 1366x768
+ 1280x720
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -81,477 +488,30 @@
-
-
-
+
+
-
-
+ Increment="1" FormatString="0" />
+
+
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
- 3840x2160
- 2560x1440
- 1920x1080
- 1600x900
- 1366x768
- 1280x720
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
diff --git a/StarterNG/Views/Settings.axaml.cs b/StarterNG/Views/Settings.axaml.cs
index ccddb7d..e26d6c5 100644
--- a/StarterNG/Views/Settings.axaml.cs
+++ b/StarterNG/Views/Settings.axaml.cs
@@ -3,15 +3,19 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
+using System.Text;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
+using Avalonia.LogicalTree;
using Avalonia.Markup.Xaml.Styling;
using Avalonia.Media;
using StarterNG.Classes;
+using StarterNG.Controls;
+using StarterNG.Infrastructure;
using StarterNG.Services;
using KeyBinding = StarterNG.Classes.KeyBinding;
@@ -22,6 +26,15 @@ public partial class Settings : UserControl
{
private bool _loading;
+ ///
+ /// 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.
+ ///
+ public event Action? ThumbnailSizeChanged;
+
+ /// Thumbnail size the other tabs were last drawn at.
+ private bool _drawnThumbs = StarterNG.Classes.Settings.Instance.LargeThumbnails;
+
public Settings()
{
InitializeComponent();
@@ -41,6 +54,7 @@ public partial class Settings : UserControl
App.Loc.LanguageChanged += SelectActiveLanguage;
App.Loc.LanguageChanged += UpdateQualityCaptions;
+ App.Loc.LanguageChanged += UpdateSaveState;
App.Loc.LanguageChanged += () => shaderResolutionSlider_OnValueChanged(null, null);
FeedbackCb.SelectionChanged += FeedbackCb_OnSelectionChanged;
@@ -54,6 +68,10 @@ public partial class Settings : UserControl
KeyboardConfig.Instance.Load();
BuildControlsTab();
AddHandler(KeyDownEvent, OnControlsKeyDown, RoutingStrategies.Tunnel);
+
+ HookDirtyTracking();
+ ApplyFilter();
+ UpdateSaveState();
}
private static int IndexOfTag(ComboBox cb, string? tag)
@@ -95,6 +113,7 @@ public partial class Settings : UserControl
if (!File.Exists(path)) return;
StarterNG.Classes.Settings.Instance.LoadFrom(path);
ApplyToUi();
+ RedrawThumbnailsIfNeeded();
}
private void ProfileSaveAsButton_OnClick(object? sender, RoutedEventArgs e)
@@ -232,10 +251,9 @@ public partial class Settings : UserControl
VehiclesVolumeSlider.Value = s.VehiclesVolume;
PositionalVolumeSlider.Value = s.PositionalVolume;
AmbientVolumeSlider.Value = s.AmbientVolume;
- VolumePausedSlider.Value = s.VolumePaused;
SkipPipelineCb.IsChecked = s.SkipPipeline;
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;
LargeThumbnailsCb.IsChecked = s.LargeThumbnails;
@@ -327,10 +345,9 @@ public partial class Settings : UserControl
s.VehiclesVolume = (int)VehiclesVolumeSlider.Value;
s.PositionalVolume = (int)PositionalVolumeSlider.Value;
s.AmbientVolume = (int)AmbientVolumeSlider.Value;
- s.VolumePaused = (int)VolumePausedSlider.Value;
s.SkipPipeline = IsChecked(SkipPipelineCb);
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.LargeThumbnails = IsChecked(LargeThumbnailsCb);
@@ -344,31 +361,39 @@ public partial class Settings : UserControl
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)
{
ReadFromUi();
StarterNG.Classes.Settings.Instance.Save();
KeyboardConfig.Instance.Save();
+ RedrawThumbnailsIfNeeded();
+ _dirty = false;
+ UpdateSaveState();
if (SaveStatus is not null)
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)
{
StarterNG.Classes.Settings.Instance.Load();
ApplyToUi();
+ RedrawThumbnailsIfNeeded();
CancelCapture();
KeyboardConfig.Instance.Load();
RebuildBindingList();
RebuildKeyboard();
- if (SaveStatus is not null)
- SaveStatus.Text = string.Empty;
+ _dirty = false;
+ UpdateSaveState();
}
private void SelectResolution(int width, int height)
@@ -404,29 +429,29 @@ public partial class Settings : UserControl
private void TextureResolutionSlider_OnValueChanged(object? sender, RangeBaseValueChangedEventArgs e)
{
- if (texResolution is null || textureResolutionSlider is null)
+ if (TexResolutionRow is null || textureResolutionSlider is null)
return;
int resolution = 1 << (int)textureResolutionSlider.Value;
- texResolution.Text = $"{resolution} px";
+ TexResolutionRow.ValueText = $"{resolution} px";
}
private void CabTextureResolutionSlider_OnValueChanged(object? sender, RangeBaseValueChangedEventArgs e)
{
- if (cabTexResolution is null || cabTextureResolutionSlider is null)
+ if (CabTexResolutionRow is null || cabTextureResolutionSlider is null)
return;
int resolution = 1 << (int)cabTextureResolutionSlider.Value;
- cabTexResolution.Text = $"{resolution} px";
+ CabTexResolutionRow.ValueText = $"{resolution} px";
}
private void shaderResolutionSlider_OnValueChanged(object? sender, RangeBaseValueChangedEventArgs e)
{
- if (shaderResolution is null || shaderResolutionSlider is null)
+ if (ShadowResolutionRow is null || shaderResolutionSlider is null)
return;
int resolution = 1 << (int)shaderResolutionSlider.Value;
- shaderResolution.Text = $"{resolution} px";
+ ShadowResolutionRow.ValueText = $"{resolution} px";
ToolTip.SetTip(shaderResolutionSlider, ShadowMapWord(resolution));
}
@@ -464,16 +489,196 @@ public partial class Settings : UserControl
private void FeedbackCb_OnSelectionChanged(object? sender, SelectionChangedEventArgs e) =>
UpdateFeedbackDetailVisibility();
- private void UpdateFeedbackDetailVisibility()
+ ///
+ /// The port and UART rows only apply to some feedback modes. Visibility is
+ /// decided in one place — — so that a search cannot
+ /// reveal a row the selected mode does not use.
+ ///
+ private void UpdateFeedbackDetailVisibility() => ApplyFilter();
+
+ private bool ConditionAllows(SettingRow row)
{
- FeedbackPortRow.IsVisible = FeedbackCb.SelectedIndex == 3;
- ComButton.IsVisible = FeedbackCb.SelectedIndex == 5;
+ if (ReferenceEquals(row, FeedbackPortRow))
+ 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();
+
+ ///
+ /// 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.
+ ///
+ 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())
+ entry.Classes.Set("NoHits", false);
+ return;
+ }
+
+ foreach (var panel in SettingsStack.Children.OfType())
+ {
+ bool anyRow = false;
+
+ foreach (var section in panel.GetLogicalDescendants().OfType())
+ {
+ bool anyInSection = false;
+
+ foreach (var row in section.GetLogicalDescendants().OfType())
+ {
+ 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())
+ 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()
+ .FirstOrDefault(i => string.Equals(i.Tag as string, tag, StringComparison.Ordinal));
+
+ ///
+ /// 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).
+ ///
+ 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())
+ sb.Append(item.Content).Append(' ');
+ break;
+ }
+
+ return sb.ToString();
+ }
+
+ ///
+ /// Subscribes every input in the panel to 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.
+ ///
+ private void HookDirtyTracking()
+ {
+ foreach (var control in this.GetLogicalDescendants().OfType())
+ {
+ 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())
+ 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 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)
{
if (value <= 0) return fallback;
@@ -490,10 +695,15 @@ public partial class Settings : UserControl
return 4;
}
- private TextBox? _controlsSearch;
private StackPanel? _bindingListPanel;
private StackPanel? _keyboardPanel;
+ ///
+ /// 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.
+ ///
+ public bool IsCapturingKey => _capturing is not null;
+
private KeyBinding? _capturing;
private Button? _capturingButton;
@@ -527,10 +737,6 @@ public partial class Settings : UserControl
VerticalAlignment = VerticalAlignment.Center,
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"] };
restoreBtn.Classes.Add("Flat");
restoreBtn.Click += (_, _) =>
@@ -538,6 +744,7 @@ public partial class Settings : UserControl
CancelCapture();
KeyboardConfig.Instance.LoadDefaults();
KeyboardConfig.Instance.Dirty = true;
+ MarkDirty();
RebuildBindingList();
RebuildKeyboard();
};
@@ -576,7 +783,8 @@ public partial class Settings : UserControl
return;
_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();
foreach (var b in KeyboardConfig.Instance.Bindings)
@@ -588,8 +796,8 @@ public partial class Settings : UserControl
}
private static bool MatchesFilter(KeyBinding b, string filter) =>
- b.Command.ToLowerInvariant().Contains(filter) ||
- b.Description.ToLowerInvariant().Contains(filter);
+ TextMatch.Contains(b.Command, filter) ||
+ TextMatch.Contains(b.Description, filter);
private Control BuildBindingRow(KeyBinding b, HashSet conflicts)
{
@@ -604,14 +812,14 @@ public partial class Settings : UserControl
{
Text = CommandLabel(b),
Foreground = FgBrush,
- FontSize = 13,
+ FontSize = 12,
TextTrimming = TextTrimming.CharacterEllipsis
});
label.Children.Add(new TextBlock
{
Text = b.Command,
Foreground = FgDimBrush,
- FontSize = 11,
+ FontSize = 12,
TextTrimming = TextTrimming.CharacterEllipsis
});
Grid.SetColumn(label, 0);
@@ -647,6 +855,7 @@ public partial class Settings : UserControl
b.Shift = b.Ctrl = false;
b.Key = "none";
KeyboardConfig.Instance.Dirty = true;
+ MarkDirty();
RebuildBindingList();
RebuildKeyboard();
};
@@ -710,6 +919,7 @@ public partial class Settings : UserControl
_capturing.Ctrl = e.KeyModifiers.HasFlag(KeyModifiers.Control);
_capturing.Key = token;
KeyboardConfig.Instance.Dirty = true;
+ MarkDirty();
e.Handled = true;
_capturing = null;
@@ -912,7 +1122,7 @@ public partial class Settings : UserControl
{
Text = App.Loc["BindKeyHint"],
Foreground = FgDimBrush,
- FontSize = 11,
+ FontSize = 12,
TextWrapping = TextWrapping.Wrap
});
panel.Children.Add(grid);
@@ -957,6 +1167,7 @@ public partial class Settings : UserControl
}
KeyboardConfig.Instance.Dirty = true;
+ MarkDirty();
}
private static KeyBinding? CommandInSlot(string token, bool shift, bool ctrl) =>
@@ -1074,25 +1285,25 @@ public partial class Settings : UserControl
{
var q = QualityWords;
- if (texFilteringValue != null)
- texFilteringValue.Text = q[Clamp((int)TexFilteringSlider.Value, 1, 5) - 1];
+ if (TexFilteringRow != null)
+ TexFilteringRow.ValueText = q[Clamp((int)TexFilteringSlider.Value, 1, 5) - 1];
string[] four = { q[0], q[1], q[3], q[4] };
- if (trackCurvesValue != null)
- trackCurvesValue.Text = four[Clamp((int)TrackCurvesSlider.Value, 1, 4) - 1];
- if (smokeParticlesValue != null)
- smokeParticlesValue.Text = four[Clamp((int)SmokeParticlesSlider.Value, 1, 4) - 1];
+ if (TrackCurvesRow != null)
+ TrackCurvesRow.ValueText = four[Clamp((int)TrackCurvesSlider.Value, 1, 4) - 1];
+ if (SmokeParticlesRow != null)
+ SmokeParticlesRow.ValueText = four[Clamp((int)SmokeParticlesSlider.Value, 1, 4) - 1];
string[] msaa = { App.Loc["MsaaNone"], "x2", "x4", "x8" };
- if (multisamplingValue != null)
- multisamplingValue.Text = msaa[Clamp((int)MultisamplingSlider.Value, 1, 4) - 1];
+ if (MultisamplingRow != null)
+ MultisamplingRow.ValueText = msaa[Clamp((int)MultisamplingSlider.Value, 1, 4) - 1];
string[] range = { App.Loc["RangeNormal"], App.Loc["RangeHigh"], App.Loc["RangeVeryHigh"] };
- if (renderRangeValue != null)
- renderRangeValue.Text = range[Clamp((int)RenderRangeSlider.Value, 1, 3) - 1];
+ if (RenderRangeRow != null)
+ RenderRangeRow.ValueText = range[Clamp((int)RenderRangeSlider.Value, 1, 3) - 1];
- if (cursorSensitivityValue != null)
- cursorSensitivityValue.Text = (int)CursorSensitivitySlider.Value switch
+ if (CursorSensitivityRow != null)
+ CursorSensitivityRow.ValueText = (int)CursorSensitivitySlider.Value switch
{
<= 1 => q[1],
2 => App.Loc["QualityStandard"],
@@ -1100,25 +1311,25 @@ public partial class Settings : UserControl
_ => q[4]
};
- if (shaderRangeValue != null)
+ if (ShadowRangeRow != null)
{
int metres = (int)shaderRange.Value;
- shaderRangeValue.Text = $"{metres} m";
+ ShadowRangeRow.ValueText = $"{metres} m";
ToolTip.SetTip(shaderRange, ShadowRangeWord(metres));
}
- if (cabShadowRangeValue != null)
+ if (CabShadowRangeRow != null)
{
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));
}
- if (reflectionsFramerateValue != null)
+ if (ReflectionsFramerateRow != null)
{
int fps = (int)reflectionsFramerate.Value;
string word = ReflectionsRefreshWord(fps);
- reflectionsFramerateValue.Text = $"{fps} FPS";
+ ReflectionsFramerateRow.ValueText = $"{fps} FPS";
ToolTip.SetTip(reflectionsFramerate, word.Length == 0 ? null : word);
}
}
diff --git a/StarterNG/Views/ShortcutsWindow.cs b/StarterNG/Views/ShortcutsWindow.cs
new file mode 100644
index 0000000..421284f
--- /dev/null
+++ b/StarterNG/Views/ShortcutsWindow.cs
@@ -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;
+
+///
+/// The cheat sheet, printed straight from so it can never
+/// list a gesture the window does not actually dispatch.
+///
+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;
+ }
+}
diff --git a/StarterNG/Views/TextureBaseWindow.axaml b/StarterNG/Views/TextureBaseWindow.axaml
index a5cdd79..b6bf1d5 100644
--- a/StarterNG/Views/TextureBaseWindow.axaml
+++ b/StarterNG/Views/TextureBaseWindow.axaml
@@ -36,7 +36,7 @@
+ FontSize="12" Opacity="0.75" />
diff --git a/StarterNG/Views/UartWindow.cs b/StarterNG/Views/UartWindow.cs
index f3ddafd..d19f0ce 100644
--- a/StarterNG/Views/UartWindow.cs
+++ b/StarterNG/Views/UartWindow.cs
@@ -45,7 +45,7 @@ public sealed class UartWindow : Window
root.Children.Add(new TextBlock
{
Text = App.Loc["UartHint"],
- FontSize = 11,
+ FontSize = 12,
Opacity = 0.65,
TextWrapping = TextWrapping.Wrap
});
diff --git a/StarterNG/startercfg/lang/cz.xml b/StarterNG/startercfg/lang/cz.xml
index dc1d901..4ca1d41 100644
--- a/StarterNG/startercfg/lang/cz.xml
+++ b/StarterNG/startercfg/lang/cz.xml
@@ -69,7 +69,7 @@
Volitelné prvky
Otevřít složku s texturami
Kopírovat název
- Kopírovat spřáhlo z předchozího
+ Kopírovat z předchozího
Chladivo na okolní teplotě
Textura
Dopravce
@@ -185,6 +185,9 @@
N - brzdič zabržděný
0 - odbržděný, odpuštěný
1 - odbržděný, odp. z 10% pravd.
+ <> - vyp. na zkoušku rychlobrzdy
+ X - dosluhující (20% vyp., náhodně)
+ E - vyprázdněný (potrubí na 0)
Poškození
@@ -194,15 +197,24 @@
Pravděpodobnost plochého místa (%)
Druh nákladu
Množství
+ ks
+ t
+ j.
Pro naložení vozidla zadejte množství větší než 0.
Toto vozidlo nepřijímá žádný náklad.
Zvednutý pantograf na startu
Množství určuje pantograf: 1 — A, 2 — B, 3 — oba.
+ Pantograf
+ Žádný
+ Oba
Číslo vozu
Kopírovat náklad z předchozího vozidla
Maximální možný náklad
+ Z předchozího
+ Na následující
+ Maximální
Náklad celé soupravy
- Náhodný náklad
+ Náhodný druh
Maximální množství
Náhodné množství
Nastavit
@@ -252,6 +264,7 @@
Spřáhlo na konci jednotky
Spojit
Spojit toto vozidlo s následujícím do jedné jednotky
+ Přetáhněte pro změnu pořadí
Přidat
Přidat toto vozidlo do soupravy
Přidat celou jednotku do soupravy
@@ -278,7 +291,7 @@
Celá obrazovka
Pozastavit simulaci při neaktivním okně
Pozastavit po spuštění
- Citlivost kurzoru:
+ Citlivost kurzoru
Převrátit myš vodorovně
Převrátit myš svisle
@@ -309,7 +322,7 @@
Kanál rádia
Ostatní
- Vybrat exe:
+ Vybrat exe
Vybrat automaticky
Režim ladění (testovací režim)
Virtuální posun
@@ -324,12 +337,12 @@
Vypnout vizuální vrstvu
Experimentální
- Rozlišení:
- Rozlišení bufferu:
- Maximální rozlišení textur:
- Maximální rozlišení textur kabiny:
- Kvalita filtrování textur:
- Multisampling:
+ Rozlišení
+ Rozlišení bufferu
+ Maximální rozlišení textur
+ Maximální rozlišení textur kabiny
+ Kvalita filtrování textur
+ Multisampling
Velmi nízká
Nízká
Normální
@@ -346,8 +359,8 @@
Vysoká
Velmi vysoká
Vypnuté
- Dynamické zdroje světla:
- Dohlednost vykreslování:
+ Dynamické zdroje světla
+ Dohlednost vykreslování
Omezit snímkovou frekvenci
Omezení délky stínů
Bez omezení
@@ -362,6 +375,7 @@
Rozmazání pohybem
Další efekty shaderů
Odrazy jako cubemapa
+ SkipPipeline
Vykreslování VBO
Vykreslovat stíny
Frekvence obnovy odrazů
@@ -419,14 +433,14 @@
Přeskočit dekorační soupravy při startu
Starter
- Profil nastavení:
+ Profil nastavení
Použít
Uložit jako…
Název profilu
Automaticky zavřít starter
Velké náhledy
Automaticky rozbalit strom scenérií
- Baterie při spuštění:
+ Baterie při spuštění
Podle scénáře
Vždy vypnutá
Vždy zapnutá
@@ -455,6 +469,7 @@
Uložit
Obnovit
Nastavení uloženo
+ Neuložené změny
Chyba
@@ -482,6 +497,7 @@
Obecné
Model
VMax
+ Název
Textura
@@ -509,9 +525,9 @@
Přidat soupravy ze souboru...
Spustit bez uložení nastavení
Spustit bez vozidla
- Hlasitost při pauze:
+ Hlasitost při pauze
Zobrazovat průběh simulace
- Frekvence obnovení doplňkových obrazovek:
+ Frekvence obnovení doplňkových obrazovek
Standardní
Nízká
Velmi nízká
@@ -531,7 +547,7 @@
Revize:
Revize od:
Revize do:
- Obsazení:
+ Obsazení
Přidat do pravidla
— nové pravidlo —
Odebrat z pravidla
@@ -551,4 +567,18 @@
Deník událostí
Vymazat log
Sestava
+
+
+ Klávesové zkratky
+ Obecné
+ Tab přepíná mezi panely, šipky fungují uvnitř seznamů. V pásu soupravy Tab skočí na vybrané vozidlo.
+ Tento seznam
+ Přejít do vyhledávání vozidel
+ Přidat vybrané vozidlo do soupravy
+ Nastavit číslo vozu
+ Předchozí / další vozidlo
+ První / poslední vozidlo
+ Vybrat člen ve spojené jednotce
+ Posunout vozidlo v soupravě
+ Otevřít nabídku vozidla
diff --git a/StarterNG/startercfg/lang/en.xml b/StarterNG/startercfg/lang/en.xml
index 4afecb2..300d28d 100644
--- a/StarterNG/startercfg/lang/en.xml
+++ b/StarterNG/startercfg/lang/en.xml
@@ -69,7 +69,7 @@
Optional elements
Open texture folder
Copy name
- Copy coupler from previous
+ Copy from previous
Coolant at ambient temperature
Texture
Operator
@@ -185,6 +185,9 @@
N - On
0 - Off, slack
1 - Off, slack (10% chance)
+ <> - off for emergency brake test
+ X - worn out (20% off, random)
+ E - emptied (pipes at 0)
Damage
@@ -194,15 +197,24 @@
Flat-spot probability (%)
Load type
Amount
+ pcs
+ t
+ u.
Set an amount above 0 to load the vehicle.
This vehicle accepts no load.
Pantograph raised at start
The amount selects the pantograph: 1 — A, 2 — B, 3 — both.
+ Pantograph
+ None
+ Both
Wagon number
Copy load from previous vehicle
Maximum possible load
+ From previous
+ To following
+ Maximum
Whole-consist load
- Random load
+ Random type
Maximum amount
Random amount
Set
@@ -252,6 +264,7 @@
Coupler at the rear of the unit
Join
Couple this vehicle with the next one into a single unit
+ Drag to reorder
Add
Add this vehicle to the consist
Add the whole unit to the consist
@@ -278,23 +291,26 @@
Fullscreen
Pause simulation when window is inactive
Pause on start
- Cursor sensitivity:
+ Camera rotation sensitivity (horizontal)
Invert mouse horizontally
Invert mouse vertically
- Communication
+ External devices
- Ignore gamepad input
+ Ignore gamepad control
+ Disables gamepad input even when one is connected.
Feedback
+ 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.
Off
- Caps: CA/SHP; ScrollLock: starting resistances
- Caps: CA; ScrollLock: SHP
- LPT
- PoKeys55
- Serial port (COM)
+ Keyboard: Caps = CA+SHP, Scroll = starting resistances
+ Keyboard: Caps = CA, Scroll = SHP
+ LPT (inpout32)
+ PoKeys55 (USB)
+ COM / serial port
LPT port address
+ LPT port address used by the selected feedback mode.
UART configuration
Enable UART communication
@@ -309,27 +325,34 @@
Radio channel
Other
- Select exe:
+ Select exe
Select automatically
- Debug mode (test mode)
+ Debug mode
+ Unlocks debug menus, extra panels and test shortcuts.
Virtual shunting
+ Enables an AI assistant for coupling, uncoupling and simple shunting moves while the vehicle is stopped.
Log missing vehicle files during export
+ Writes vehicles the starter could not find into the log.
Graphics
Render engine
+ OpenGL rendering path. 'Simplified' and 'Old' use the legacy renderer.
Full
Old
- Simplified shader
+ Simplified (legacy)
Simplified
Disable visual layer
Experimental
- Resolution:
- Buffer resolution:
- Maximum texture resolution:
- Maximum cabin texture resolution:
- Texture filtering quality:
- Multisampling:
+ Resolution
+ Buffer resolution
+ Scales the internal render buffer relative to the window resolution.
+ Maximum texture resolution
+ Maximum cabin texture resolution
+ Texture filtering quality
+ Anisotropic texture filtering level.
+ Multisampling
+ Anti-aliasing of object edges. Higher settings cost more GPU time.
Very low
Low
Normal
@@ -346,15 +369,20 @@
High
Very high
Disabled
- Dynamic light sources:
- Rendering range:
+ Dynamic light sources
+ Maximum number of simultaneous dynamic light sources.
+ Rendering range
+ Maximum draw-distance multiplier; the engine lowers it automatically when the FPS drops.
Limit frame rate
Shadow length limit
+ Shortens the long shadows cast by a low sun. Higher values cut them sooner.
No limit
Shortened
Automatic resolution (borderless fullscreen)
Use ANGLE Vulkan renderer
+ Runs the ANGLE renderer on Vulkan instead of the default backend. Useful when OpenGL misbehaves.
VSync
+ Synchronizes frame rate with the monitor refresh rate.
Smoke display
Smoke particles multiplier
@@ -362,41 +390,61 @@
Motion blur
Additional shader effects
Reflections as cubemap
+ Renders reflections into a cube map, so surfaces mirror the surroundings from every direction.
+ Simplified depth read (compatibility)
+ Alternative depth-buffer read path with MSAA; enable it if your GPU has compatibility issues.
Render VBO
+ Keeps geometry in GPU buffers (faster). Disable only if you have problems with an old GPU.
Render shadows
Reflections refresh rate
Shadows resolution
Shadow range
Cabin shadow sources range
+ Distance at which light sources still cast shadows inside the cab. Zero turns cab shadows off.
Display shadows
+ Sets which objects cast shadows based on material priority. Lower value = fewer shadows, higher performance.
Only important
Limited
All
Reflections detail level
+ Which objects are rendered into the reflection cubemap.
Terrain geometry only
Terrain geometry and static models
Terrain geometry, static models and vehicles
View angle
+ Vertical camera field of view in degrees (15–75).
Render onboard computer screens
+ Enables cab screens driven by Python scripts.
Render screens in a separate thread
Physics
Track curves quality
+ How finely curved track is subdivided. Higher values are smoother but cost more CPU/GPU when loading.
Increased physics accuracy
+ Runs the full physics model with smaller time steps. More faithful, but heavier CPU load on large consists.
Possibility of pantograph breaking
+ Disabling protects the pantograph from breaking on faulty overhead line geometry.
Powered only under overhead line
- Save speedometer tapes
+ When disabled, a raised pantograph can power the vehicle even without touching the wire.
+ Save physics parameters log
+ Writes a detailed trip log to the physicslog/ folder.
Save simulation log
Keep previous logs
- Display simulation
+ Creates a separate log file for each scenery instead of overwriting errors.txt.
+ Render 3D image
+ When disabled, the scenery is not drawn (black screen), but the simulation keeps running.
Collision damage
+ Damages couplers and consists during collisions and sharp jerks.
Friction multiplier
- Brake valve sensitivity
- FV4a valve sensitivity
+ Multiplies wheel-to-rail friction. 1.00 leaves it unchanged.
+ Train brake step size (Num3/Num9)
+ Multiplies the size of a single step of the train brake handle controlled with Num3 and Num9. 1.00 leaves it unchanged.
+ FV4a valve speed
+ Multiplies how fast the locomotive brake handle (FV4a) and the local brake move. 1.00 leaves it unchanged.
Sound
Enable sound
@@ -419,14 +467,15 @@
Skip decorative consists when starting
Starter
- Settings profile:
+ Settings profile
Apply
Save as…
Profile name
Automatically close starter
Large thumbnails
Automatically expand scenery tree
- Battery on launch:
+ Battery on launch
+ Battery state when the simulation starts, unless the scenario decides otherwise.
Scenario default
Always off
Always on
@@ -455,6 +504,7 @@
Save
Reset
Settings saved
+ Unsaved changes
Error
@@ -482,6 +532,7 @@
General
Model
VMax
+ Name
Texture
@@ -509,14 +560,15 @@
Add consists from file...
Start without saving settings
Start with no vehicle
- Volume while paused:
- Show simulation log on screen
- Extra screen refresh rate:
- Standard
- Low
- Very low
- None
- Disabled
+ Volume while paused
+ Show log in console
+ Extra screen update delay
+ Minimum time between Python cab screen updates in milliseconds. Higher value = less frequent updates.
+ Standard (200 ms)
+ Slower (400 ms)
+ Very slow (800 ms)
+ Only when idle (1600 ms)
+ Off
Gross mass
Good
@@ -531,7 +583,7 @@
Overhaul:
Overhaul from:
Overhaul to:
- Crew:
+ Crew
Add to rule
— new rule —
Remove from rule
@@ -551,4 +603,18 @@
Event log
Clear log
Composition
+
+
+ Keyboard shortcuts
+ General
+ Tab moves between panels, arrow keys inside lists. In the consist strip Tab lands on the selected vehicle.
+ This list
+ Jump to the vehicle search box
+ Add the browsed vehicle to the consist
+ Set the wagon number
+ Previous / next vehicle
+ First / last vehicle
+ Pick a car inside a coupled unit
+ Move the vehicle along the consist
+ Open the vehicle menu
diff --git a/StarterNG/startercfg/lang/hu.xml b/StarterNG/startercfg/lang/hu.xml
index 0972b0a..f58fd46 100644
--- a/StarterNG/startercfg/lang/hu.xml
+++ b/StarterNG/startercfg/lang/hu.xml
@@ -69,7 +69,7 @@
Opcionális elemek
Textúramappa megnyitása
Név másolása
- Kapcsolókészülék másolása az előzőről
+ Másolás az előzőről
Hűtőfolyadék környezeti hőmérsékleten
Textúra
Üzemeltető
@@ -185,6 +185,9 @@
N - fék bekapcsolva
0 - kikapcsolva, oldva
1 - kikapcsolva, oldva (10%)
+ <> - kikapcsolva gyorsfékpróbához
+ X - elhasználódott (20% ki, véletlen)
+ E - kiürítve (vezetékek 0-n)
Sérülés
@@ -194,15 +197,24 @@
Laposodás valószínűsége (%)
Rakomány típusa
Mennyiség
+ db
+ t
+ egys.
A jármű megrakásához adjon meg 0-nál nagyobb mennyiséget.
Ez a jármű nem fogad rakományt.
Felemelt áramszedő induláskor
A mennyiség az áramszedőt választja ki: 1 — A, 2 — B, 3 — mindkettő.
+ Áramszedő
+ Egyik sem
+ Mindkettő
Kocsiszám
Rakomány másolása az előző járműről
Legnagyobb lehetséges rakomány
+ Az előzőről
+ A következőkre
+ Legnagyobb
A teljes szerelvény rakománya
- Véletlen rakomány
+ Véletlen típus
Legnagyobb mennyiség
Véletlen mennyiség
Beállítás
@@ -252,6 +264,7 @@
Az egység végén lévő kapcsolókészülék
Összekapcsolás
A jármű összekapcsolása a következővel egy egységgé
+ Húzza az átrendezéshez
Hozzáadás
Jármű hozzáadása a szerelvényhez
A teljes egység hozzáadása a szerelvényhez
@@ -278,7 +291,7 @@
Teljes képernyő
Szimuláció szüneteltetése inaktív ablaknál
Szüneteltetés indításkor
- Kurzorérzékenység:
+ Kurzorérzékenység
Egér vízszintes megfordítása
Egér függőleges megfordítása
@@ -309,7 +322,7 @@
Rádiócsatorna
Egyéb
- Exe kiválasztása:
+ Exe kiválasztása
Automatikus kiválasztás
Hibakeresési mód (teszt mód)
Virtuális tolatás
@@ -324,12 +337,12 @@
Vizuális réteg kikapcsolása
Kísérleti
- Felbontás:
- Puffer felbontása:
- Textúrák legnagyobb felbontása:
- Vezetőállás-textúrák legnagyobb felbontása:
- Textúraszűrés minősége:
- Multisampling:
+ Felbontás
+ Puffer felbontása
+ Textúrák legnagyobb felbontása
+ Vezetőállás-textúrák legnagyobb felbontása
+ Textúraszűrés minősége
+ Multisampling
Nagyon alacsony
Alacsony
Normál
@@ -346,8 +359,8 @@
Magas
Nagyon magas
Kikapcsolva
- Dinamikus fényforrások:
- Megjelenítési távolság:
+ Dinamikus fényforrások
+ Megjelenítési távolság
Képkockaszám korlátozása
Árnyékhossz korlátozása
Nincs korlát
@@ -362,6 +375,7 @@
Mozgási elmosás
További shader-effektek
Tükröződések cubemapként
+ SkipPipeline
VBO megjelenítés
Árnyékok megjelenítése
Tükröződések frissítési gyakorisága
@@ -419,14 +433,14 @@
Dekorációs szerelvények kihagyása indításkor
Starter
- Beállítási profil:
+ Beállítási profil
Alkalmaz
Mentés másként…
Profil neve
A starter automatikus bezárása
Nagy előnézetek
Pályafa automatikus kibontása
- Akkumulátor indításkor:
+ Akkumulátor indításkor
Forgatókönyv szerint
Mindig ki
Mindig be
@@ -455,6 +469,7 @@
Mentés
Visszaállítás
A beállítások elmentve
+ Nem mentett módosítások
Hiba
@@ -482,6 +497,7 @@
Általános
Modell
VMax
+ Név
Textúra
@@ -509,9 +525,9 @@
Szerelvények hozzáadása fájlból...
Indítás a beállítások mentése nélkül
Indítás jármű nélkül
- Hangerő szünet alatt:
+ Hangerő szünet alatt
Szimuláció menetének megjelenítése
- Kiegészítő képernyők frissítési gyakorisága:
+ Kiegészítő képernyők frissítési gyakorisága
Normál
Alacsony
Nagyon alacsony
@@ -531,7 +547,7 @@
Felújítás:
Felújítás ettől:
Felújítás eddig:
- Személyzet:
+ Személyzet
Hozzáadás a szabályhoz
— új szabály —
Eltávolítás a szabályból
@@ -551,4 +567,18 @@
Eseménynapló
Napló törlése
Összeállítás
+
+
+ Billentyűparancsok
+ Általános
+ 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.
+ Ez a lista
+ Ugrás a járműkeresőre
+ A kiválasztott jármű hozzáadása
+ Kocsiszám beállítása
+ Előző / következő jármű
+ Első / utolsó jármű
+ Tag kiválasztása kapcsolt egységen belül
+ Jármű mozgatása a szerelvényben
+ Jármű menüjének megnyitása
diff --git a/StarterNG/startercfg/lang/pl.xml b/StarterNG/startercfg/lang/pl.xml
index 676981d..9e6a098 100644
--- a/StarterNG/startercfg/lang/pl.xml
+++ b/StarterNG/startercfg/lang/pl.xml
@@ -69,7 +69,7 @@
Elementy opcjonalne
Otwórz folder tekstury
Kopiuj nazwę
- Kopiuj sprzęg z poprzedniego
+ Kopiuj z poprzedniego
Chłodziwo w temp. otoczenia
Tekstura
Przewoźnik
@@ -89,7 +89,7 @@
Wybrany pojazd
Pojazd
z obsadą
- obrócony
+ Obrócony
Nie znaleziono pojazdów w dynamic/**/textures.txt.
Dodaj pojazdy z bazy po lewej stronie lub wczytaj istniejący skład z pliku scenerii.
Przesuń w lewo
@@ -185,6 +185,9 @@
N - hamulec włączony
0 - wyłączony, odluźniony
1 - wył., odl. z 10% szansą
+ <> - wył. na próbę hamowania nagłego
+ X - agonalny (20% wył., losowo)
+ E - opróżniony (przewody na 0)
Uszkodzenia
@@ -194,15 +197,24 @@
Prawdop. podkucia (%)
Rodzaj ładunku
Ilość
+ szt.
+ t
+ j.
Ustaw ilość większą od 0, aby załadować pojazd.
Ten pojazd nie przyjmuje ładunku.
Pantograf podniesiony na starcie
Ilość wybiera pantograf: 1 — A, 2 — B, 3 — oba.
+ Pantograf
+ Brak
+ Oba
Numer wagonu
Kopiuj ładunek z poprzedniego pojazdu
Maksymalny możliwy ładunek
- Ładunek całego składu
- Losowy ładunek
+ Z poprzedniego
+ Do następnych
+ Maksymalny
+ Ładunki zestawienia
+ Losowy rodzaj
Maksymalna ilość
Losowa ilość
Ustaw
@@ -252,6 +264,7 @@
Sprzęg na końcu zespołu
Złącz
Połącz ten pojazd z następnym w jeden zespół
+ Przeciągnij, aby zmienić kolejność
Dodaj
Dodaj ten pojazd do składu
Dodaj cały zespół do składu
@@ -278,23 +291,26 @@
Tryb pełnoekranowy
Wstrzymaj symulację, gdy okno nie jest aktywne
Pauza po uruchomieniu
- Czułość kursora:
+ Czułość obrotu kamery (oś pozioma)
Inwersja myszy w poziomie
Inwersja myszy w pionie
- Komunikacja
+ Urządzenia zewnętrzne
- Ignoruj sygnały z gamepada
+ Ignoruj sterowanie z gamepada
+ Wyłącza obsługę gamepada, nawet gdy jest podłączony.
Informacja zwrotna
+ 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.
Wyłączona
- Caps: CA/SHP; ScrollLock: opory rozr.
- Caps: CA; Scrollock: SHP
- LPT
- PoKeys55
- Serialport (COM)
+ Klawiatura: Caps = CA+SHP, Scroll = opory
+ Klawiatura: Caps = CA, Scroll = SHP
+ LPT (inpout32)
+ PoKeys55 (USB)
+ COM / port szeregowy
Adres portu LPT
+ Adres portu LPT używanego przez wybrany tryb informacji zwrotnej.
Konfiguracja UART
Włącz komunikację UART
@@ -309,27 +325,34 @@
Kanał radiotelefonu
Inne
- Wybór exe:
+ Wybór exe
Wybierz automatycznie
- Tryb testowy (debugmode)
+ Tryb testowy
+ Odblokowuje menu debug, dodatkowe panele i skróty testowe.
Wirtualny manewrowy
+ Włącza asystenta AI do sprzęgania, rozprzęgania i prostych manewrów, gdy pojazd stoi.
Zapisuj w logu brakujące pliki pojazdów podczas eksportu
+ Zapisuje w logu pojazdy, których starter nie znalazł.
Grafika
Silnik renderowania
+ Wybór ścieżki renderowania OpenGL. 'Uproszczony' i 'Stary' to starszy renderer.
Pełny
Stary
- Uproszczony shaderowy
+ Uproszczony (legacy)
Uproszczony
Wyłącz warstwę wizualną
Eksperymentalny
- Rozdzielczość:
- Rozdzielczość bufora:
- Maksymalna rozdzielczość tekstur:
- Maksymalna rozdzielczość tekstur kabiny:
- Jakość filtrowania tekstur:
- Multisampling:
+ Rozdzielczość
+ Rozdzielczość bufora
+ Skaluje wewnętrzny bufor renderowania względem rozdzielczości okna.
+ Maksymalna rozdzielczość tekstur
+ Maksymalna rozdzielczość tekstur kabiny
+ Jakość filtrowania tekstur
+ Poziom anizotropowego filtrowania tekstur.
+ Multisampling
+ Wygładzanie krawędzi obiektów.
Bardzo niska
Niska
Normalna
@@ -346,15 +369,20 @@
Wysoki
Bardzo wysoki
Wyłączone
- Dynamiczne źródła światła:
- Zasięg renderowania:
+ Dynamiczne źródła światła
+ Maksymalna liczba jednoczesnych źródeł światła dynamicznego.
+ Zasięg renderowania
+ Maksymalny mnożnik zasięgu rysowania; silnik zmniejsza go automatycznie przy niskim FPS.
Ogranicz liczbę klatek na sekundę
Limit długości cienia
+ Skraca długie cienie rzucane przez nisko stojące słońce. Wyższe wartości ucinają je wcześniej.
Bez ograniczeń
Skrócone
Automatyczna rozdzielczość (pełny ekran bez obramowania)
Użyj renderera ANGLE Vulkan
+ Uruchamia renderer ANGLE na Vulkanie zamiast domyślnego backendu. Przydatne, gdy OpenGL sprawia problemy.
VSync
+ Synchronizacja klatek z odświeżaniem monitora.
Wyświetlanie dymu
Mnożnik ilości cząsteczek dymu
@@ -362,48 +390,68 @@
Rozmycie podczas ruchu
Dodatkowe efekty shaderów
Odbicia jako cubemapa
+ Renderuje odbicia do mapy sześciennej, dzięki czemu powierzchnie odbijają otoczenie ze wszystkich stron.
+ Uproszczony odczyt głębi (zgodność)
+ Alternatywna ścieżka odczytu bufora głębi przy MSAA; włącz przy problemach z kompatybilnością karty graficznej.
Renderowanie VBO
+ Trzyma geometrię w buforach karty graficznej. Wyłącz tylko przy problemach ze starszymi kartami.
Renderowanie cieni
Odświeżanie odbić
Rozdzielczość cieni
Zasięg cieni
Zasięg źródeł cieni w kabinie
+ Odległość, z jakiej źródła światła nadal rzucają cień w kabinie. Zero wyłącza cienie w kabinie.
Wyświetlanie cieni
+ Określa, które obiekty rzucają cień według priorytetu materiału. Niższa wartość = mniej cieni, wyższa wydajność.
Tylko ważne
Ograniczone
Wszystkie
Szczegółowość odbić
+ Jakie obiekty są renderowane w odbiciach cubemap.
Tylko geometria terenu
Geometria terenu i modele statyczne
Geometria terenu, modele statyczne i pojazdy
Kąt widzenia
+ Pionowy kąt widzenia kamery w stopniach (15–75).
Renderowanie ekranów komputerów pokładowych
+ Włącza ekrany kabinowe obsługiwane przez skrypty Python.
Renderowanie ekranów w osobnym wątku
Fizyka
Jakość łuków torów
+ Gęstość podziału łuków torów. Wyższa wartość = gładsze łuki.
Zwiększona dokładność fizyki
+ Uruchamia pełny model fizyki z mniejszym krokiem czasowym.
Możliwość złamania pantografu
+ Wyłączenie chroni pantograf przed uszkodzeniem przy błędnej geometrii sieci.
Zasilanie tylko pod siecią trakcyjna
- Zapisuj taśmy z prędkościomierza
+ Gdy wyłączone, podniesiony pantograf zasila pojazd nawet bez kontaktu z drutem.
+ Zapisuj log parametrów fizycznych
+ Zapisuje szczegółowy log jazdy do folderu physicslog/.
Zapisuj log z symulacji
Zachowuj poprzednie logi
- Wyświetlaj symulację
+ Tworzy osobny plik logu dla każdej scenerii zamiast nadpisywać errors.txt.
+ Renderuj obraz 3D
+ Wyłączenie pomija rysowanie scenerii (czarny ekran), ale symulacja nadal działa.
Uszkodzenia przy zderzeniu
+ Uszkodzenia sprzęgów i składów przy kolizjach i gwałtownym naciągu.
Mnożnik tarcia
- Czułość zaworu hamulcowego
- Czułość zaworu FV4a
+ Mnoży tarcie koła o szynę. 1,00 nie zmienia niczego.
+ Krok nastawy hamulca zespolonego (Num3/Num9)
+ Mnoży wielkość pojedynczego kroku kranu hamulca zespolonego sterowanego klawiszami Num3 i Num9. 1,00 nie zmienia niczego.
+ Szybkość zaworu FV4a
+ Mnoży szybkość przesuwania kranu hamulca lokomotywy (FV4a) i hamulca lokalnego. 1,00 nie zmienia niczego.
- Dżwięk
+ Dźwięk
Włącz dźwięk
Głośność
Głośność radiotelefonu
Głośność pojazdów
- Głośność dżwięków pozycjonowanych
+ Głośność dźwięków pozycjonowanych
Głośność otoczenia
Zaawansowane
@@ -419,14 +467,15 @@
Pomijaj składy dekoracyjne przy starcie
Starter
- Profil ustawień:
+ Profil ustawień
Zastosuj
Zapisz jako…
Nazwa profilu
Zamknij starter automatycznie
Duże miniaturki
Automatycznie rozwijanie drzewka scenerii
- Akumulator przy starcie:
+ Akumulator przy starcie
+ Stan akumulatora przy starcie symulacji, o ile scenariusz nie zdecyduje inaczej.
Domyślnie ze scenerii
Zawsze wyłączony
Zawsze włączony
@@ -455,6 +504,7 @@
Zapisz
Przywróć
Zapisano ustawienia
+ Niezapisane zmiany
Błąd
@@ -482,6 +532,7 @@
Ogólne
Model
VMax
+ Nazwa
Tekstura
@@ -509,13 +560,14 @@
Dodaj składy z pliku...
Uruchom bez zapisu ustawień
Uruchom bez pojazdu
- Głośność podczas pauzy:
- Wyświetlaj przebieg symulacji
- Częstotliwość odświeżania ekranów dodatkowych:
- Standardowa
- Niska
- Bardzo niska
- Brak
+ Głośność podczas pauzy
+ Wyświetlaj log w konsoli
+ Opóźnienie odświeżania ekranów dodatkowych
+ Minimalny czas między aktualizacjami ekranów Python w kabinach (ms). Wyższa wartość = rzadsze odświeżanie.
+ Standardowe (200 ms)
+ Wolniejsze (400 ms)
+ Bardzo wolne (800 ms)
+ Tylko gdy wolne (1600 ms)
Wyłączona
Masa brutto
@@ -525,13 +577,13 @@
Maszynista (kabina A)
Maszynista (kabina B)
Pasażer
- Nikt
+ Brak
Usuń wszystkie składy ze scenerii
Usunąć pojazdy ze wszystkich składów na tej scenerii?
Rewizja:
Rewizja od:
Rewizja do:
- Obsada:
+ Obsada
Dodaj do reguły
— nowa reguła —
Usuń z reguły
@@ -551,4 +603,18 @@
Dziennik zdarzeń
Wyczyść log
Zestawienie
+
+
+ Skróty klawiaturowe
+ Ogólne
+ Tab przechodzi między panelami, strzałki działają wewnątrz list. W pasku składu Tab wchodzi na zaznaczony pojazd.
+ Ta lista
+ Przejdź do wyszukiwarki pojazdów
+ Dodaj wybrany pojazd do składu
+ Ustaw numer wagonu
+ Poprzedni / następny pojazd
+ Pierwszy / ostatni pojazd
+ Wybierz człon w zespolonej jednostce
+ Przesuń pojazd w składzie
+ Otwórz menu pojazdu
diff --git a/StarterNG/startercfg/lang/ru.xml b/StarterNG/startercfg/lang/ru.xml
index d3f0f1a..21d2a8a 100644
--- a/StarterNG/startercfg/lang/ru.xml
+++ b/StarterNG/startercfg/lang/ru.xml
@@ -69,7 +69,7 @@
Дополнительные элементы
Открыть папку с текстурами
Копировать название
- Копировать сцепку с предыдущего
+ Копировать с предыдущего
Охлаждающая жидкость при температуре среды
Текстура
Перевозчик
@@ -185,6 +185,9 @@
N - тормоз включён
0 - выключен, отпущен
1 - выключен, отпущен (10%)
+ <> - выкл. на пробу экстренного торможения
+ X - изношенный (20% выкл., случайно)
+ E - опорожнён (магистрали на 0)
Повреждения
@@ -194,15 +197,24 @@
Вероятность ползуна (%)
Вид груза
Количество
+ шт
+ т
+ ед.
Задайте количество больше 0, чтобы загрузить единицу.
Эта единица не принимает груз.
Поднятый пантограф на старте
Количество выбирает пантограф: 1 — A, 2 — B, 3 — оба.
+ Пантограф
+ Нет
+ Оба
Номер вагона
Копировать груз с предыдущей единицы
Максимально возможный груз
+ С предыдущей
+ На следующие
+ Максимум
Груз всего состава
- Случайный груз
+ Случайный тип
Максимальное количество
Случайное количество
Задать
@@ -252,6 +264,7 @@
Сцепка в конце состава
Соединить
Соединить этот вагон со следующим в один состав
+ Перетащите для изменения порядка
Добавить
Добавить эту единицу в состав
Добавить весь сцеп в состав
@@ -278,7 +291,7 @@
Полный экран
Приостанавливать симуляцию при неактивном окне
Пауза при запуске
- Чувствительность курсора:
+ Чувствительность курсора
Инвертировать мышь по горизонтали
Инвертировать мышь по вертикали
@@ -309,7 +322,7 @@
Канал радио
Прочее
- Выбрать exe:
+ Выбрать exe
Выбрать автоматически
Режим отладки (тестовый режим)
Виртуальный манёвр
@@ -324,12 +337,12 @@
Отключить визуальный слой
Экспериментальный
- Разрешение:
- Разрешение буфера:
- Максимальное разрешение текстур:
- Максимальное разрешение текстур кабины:
- Качество фильтрации текстур:
- Multisampling:
+ Разрешение
+ Разрешение буфера
+ Максимальное разрешение текстур
+ Максимальное разрешение текстур кабины
+ Качество фильтрации текстур
+ Multisampling
Очень низкое
Низкое
Нормальное
@@ -346,8 +359,8 @@
Высокий
Очень высокий
Отключено
- Динамические источники света:
- Дальность отрисовки:
+ Динамические источники света
+ Дальность отрисовки
Ограничить частоту кадров
Ограничение длины теней
Без ограничений
@@ -362,6 +375,7 @@
Размытие в движении
Дополнительные эффекты шейдеров
Отражения как кубическая карта
+ SkipPipeline
Отрисовка VBO
Отрисовывать тени
Частота обновления отражений
@@ -419,14 +433,14 @@
Пропускать декоративные составы при запуске
Starter
- Профиль настроек:
+ Профиль настроек
Применить
Сохранить как…
Название профиля
Автоматически закрывать стартер
Крупные миниатюры
Автоматически разворачивать дерево сценерий
- Батарея при запуске:
+ Батарея при запуске
По сценарию
Всегда выключена
Всегда включена
@@ -455,6 +469,7 @@
Сохранить
Сбросить
Настройки сохранены
+ Несохранённые изменения
Ошибка
@@ -482,6 +497,7 @@
Общие
Модель
VMax
+ Имя
Текстура
@@ -509,9 +525,9 @@
Добавить составы из файла...
Запустить без сохранения настроек
Запустить без транспорта
- Громкость на паузе:
+ Громкость на паузе
Показывать ход симуляции
- Частота обновления дополнительных экранов:
+ Частота обновления дополнительных экранов
Стандартная
Низкая
Очень низкая
@@ -531,7 +547,7 @@
Ревизия:
Ревизия с:
Ревизия по:
- Экипаж:
+ Экипаж
Добавить в правило
— новое правило —
Удалить из правила
@@ -551,4 +567,18 @@
Журнал событий
Очистить журнал
Состав
+
+
+ Сочетания клавиш
+ Общие
+ Tab переключает панели, стрелки работают внутри списков. В полосе состава Tab переходит на выбранный вагон.
+ Этот список
+ Перейти к поиску подвижного состава
+ Добавить выбранный вагон в состав
+ Задать номер вагона
+ Предыдущий / следующий вагон
+ Первый / последний вагон
+ Выбрать секцию в сцепленной единице
+ Переместить вагон в составе
+ Открыть меню вагона