Rework the depot browser layout and confirm a crewless start

The vehicle database panel loses its header bar and packs the search box and
the archival filter into one line, the class combo takes a full-width line of
its own with a miniature (big in the drop-down, one line when closed) and the
bulk / texture-base actions move into the Add button's drop-down. The mini
preview sits beside the texture data and stays hidden until an entry is
picked, so the list keeps the space.

Picking a class or a vehicle now fills both combos in, and a class alone is a
valid filter (it used to leave the list empty without a category). The combos
and the list are rebuilt on the next dispatcher pass, since clearing Items
from inside the SelectionChanged that asked for it throws.

Vehicle removal becomes a per-card tool next to flip / crew, the scenery and
vehicle names move into their panel headers, and the texture randomizer
dialog sizes to its content instead of clipping its last button.

Start no longer hides behind Ctrl: a consist with no crew and no passenger
says so on the button and asks for confirmation before launching.
This commit is contained in:
maj00r
2026-08-25 20:58:25 +02:00
parent a93af4bd44
commit 9e2166065b
8 changed files with 347 additions and 194 deletions

View File

@@ -19,8 +19,6 @@ namespace StarterNG;
public partial class MainWindow : Window public partial class MainWindow : Window
{ {
// Tracks Ctrl for Start bypass (Pascal btnStart.Tag while Ctrl held).
private KeyModifiers _mods;
private DateTime _openedUtc = DateTime.UtcNow; private DateTime _openedUtc = DateTime.UtcNow;
public MainWindow() public MainWindow()
@@ -48,13 +46,6 @@ public partial class MainWindow : Window
UpdateStartButton(); UpdateStartButton();
}; };
Activated += (_, _) => CheckExternalSettings(); Activated += (_, _) => CheckExternalSettings();
AddHandler(InputElement.KeyDownEvent, (_, e) => { _mods = e.KeyModifiers; UpdateStartButton(); }, handledEventsToo: true);
AddHandler(InputElement.KeyUpEvent, (_, e) => { _mods = e.KeyModifiers; UpdateStartButton(); }, handledEventsToo: true);
AddHandler(InputElement.PointerMovedEvent, (_, e) =>
{
var m = e.KeyModifiers;
if (m != _mods) { _mods = m; UpdateStartButton(); }
}, handledEventsToo: true);
} }
// Lets the user drag the window by the top caption strip. // Lets the user drag the window by the top caption strip.
@@ -185,15 +176,14 @@ public partial class MainWindow : Window
} }
} }
// Pascal actStartUpdate: Start needs scenery + trainset + staffed/passenger // Pascal actStartUpdate: Start needs a scenery and a trainset. A consist with
// vehicle. Hold Ctrl to force START (bypass crew check). // nobody aboard still starts, after the confirmation in StartButton_OnClick.
private void UpdateStartButton() private void UpdateStartButton()
{ {
if (startButton is null) return; if (startButton is null) return;
var scenery = AppState.Instance.CurrentScenery; var scenery = AppState.Instance.CurrentScenery;
var trainset = AppState.Instance.CurrentTrainset; var trainset = AppState.Instance.CurrentTrainset;
bool ctrl = _mods.HasFlag(KeyModifiers.Control);
if (scenery is null) if (scenery is null)
{ {
@@ -207,14 +197,11 @@ public partial class MainWindow : Window
startButton.IsEnabled = false; startButton.IsEnabled = false;
return; return;
} }
if (!HasStartableVehicle(trainset) && !ctrl) // No driver and no passenger: still startable, but say so on the button
{ // and ask before launching.
startButton.Content = App.Loc["StartNoStaff"]; bool staffed = HasStartableVehicle(trainset);
startButton.IsEnabled = false; startButton.Content = staffed ? App.Loc["Start"] : App.Loc["StartNoStaff"];
return; ToolTip.SetTip(startButton, staffed ? null : App.Loc["StartNoStaffConfirm"]);
}
startButton.Content = App.Loc["Start"];
startButton.IsEnabled = true; startButton.IsEnabled = true;
} }
@@ -236,7 +223,7 @@ public partial class MainWindow : Window
// ── START: export the (possibly depot-modified) scenery and launch the sim. // ── START: export the (possibly depot-modified) scenery and launch the sim.
// Global on the main window, like the original Starter's bottom START button. // Global on the main window, like the original Starter's bottom START button.
private void StartButton_OnClick(object? sender, RoutedEventArgs e) private async void StartButton_OnClick(object? sender, RoutedEventArgs e)
{ {
var scenery = AppState.Instance.CurrentScenery; var scenery = AppState.Instance.CurrentScenery;
if (scenery is null) if (scenery is null)
@@ -245,7 +232,11 @@ public partial class MainWindow : Window
if (trainset is null) if (trainset is null)
return; return;
if (!HasStartableVehicle(trainset) && !_mods.HasFlag(KeyModifiers.Control)) // Launching a consist with no driver and no passenger is legal (a scenery
// can be watched from the outside), so confirm instead of blocking it.
if (!HasStartableVehicle(trainset) &&
!await MessageBox.Show(this, App.Loc["StartNoStaffConfirm"],
App.Loc["StartNoStaffTitle"], MessageBoxButtons.YesNo))
return; return;
if (trainset.Vehicles.Count > 0) if (trainset.Vehicles.Count > 0)

View File

@@ -13,97 +13,106 @@
<ColumnDefinition /> <ColumnDefinition />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<!-- Consists list and the brakes/loads editor share the upper area <!-- Consists list and the brakes/loads editor share the upper area,
evenly, so the editor has room for all its controls. --> the editor taking a bit more of it. -->
<RowDefinition Height="*" /> <RowDefinition Height="*" />
<RowDefinition Height="*" MinHeight="210" /> <RowDefinition Height="1.25*" MinHeight="250" />
<RowDefinition Height="190" /> <RowDefinition Height="190" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<!-- Vehicle database browser (left, rows 0-1) --> <!-- Vehicle database browser (left, rows 0-1) -->
<HeaderedContentControl Grid.RowSpan="2" Theme="{StaticResource GroupBox}" <Border Grid.RowSpan="2" Background="{StaticResource CarbonPanelBg}"
Header="{Binding [VehicleDatabase], Source={x:Static starterNg:App.Loc}}"> BorderBrush="{StaticResource CarbonBorderLight}" BorderThickness="1"
CornerRadius="2" Padding="8">
<DockPanel LastChildFill="True"> <DockPanel LastChildFill="True">
<Button Name="textureBaseButton" DockPanel.Dock="Top" Classes="Basic" FontSize="11" <DockPanel DockPanel.Dock="Top" Margin="0,0,0,6" LastChildFill="True">
Margin="0,0,0,6" Padding="8,2" Cursor="Hand" <CheckBox Name="hideArchivalCheck" DockPanel.Dock="Right" FontSize="11"
HorizontalAlignment="Left" VerticalAlignment="Center" Margin="8,0,0,0"
Click="TextureBaseButton_OnClick" Content="{Binding [HideArchivalVehicles], Source={x:Static starterNg:App.Loc}}"
Content="{Binding [OpenTextureBase], Source={x:Static starterNg:App.Loc}}" /> IsCheckedChanged="HideArchivalCheck_OnChanged" />
<Grid DockPanel.Dock="Top" ColumnDefinitions="*,*" ColumnSpacing="6" Margin="0,0,0,6"> <TextBox Name="searchBox" VerticalAlignment="Center"
<ComboBox Name="categoryCombo" Grid.Column="0" HorizontalAlignment="Stretch" Watermark="{Binding [Search], Source={x:Static starterNg:App.Loc}}"
TextChanged="SearchBox_OnTextChanged" />
</DockPanel>
<Grid DockPanel.Dock="Top" RowDefinitions="Auto,Auto" RowSpacing="6" Margin="0,0,0,6">
<ComboBox Name="categoryCombo" Grid.Row="0" HorizontalAlignment="Stretch"
PlaceholderText="{Binding [Category], Source={x:Static starterNg:App.Loc}}" PlaceholderText="{Binding [Category], Source={x:Static starterNg:App.Loc}}"
PointerWheelChanged="Combo_OnPointerWheel" PointerWheelChanged="Combo_OnPointerWheel"
SelectionChanged="CategoryCombo_OnSelectionChanged" /> SelectionChanged="CategoryCombo_OnSelectionChanged" />
<ComboBox Name="classCombo" Grid.Column="1" HorizontalAlignment="Stretch" <ComboBox Name="classCombo" Grid.Row="1" HorizontalAlignment="Stretch"
PlaceholderText="{Binding [Class], Source={x:Static starterNg:App.Loc}}" PlaceholderText="{Binding [Class], Source={x:Static starterNg:App.Loc}}"
PointerWheelChanged="Combo_OnPointerWheel" PointerWheelChanged="Combo_OnPointerWheel"
SelectionChanged="ClassCombo_OnSelectionChanged" /> SelectionChanged="ClassCombo_OnSelectionChanged" />
</Grid> </Grid>
<TextBox Name="searchBox" DockPanel.Dock="Top" Margin="0,0,0,6"
Watermark="{Binding [Search], Source={x:Static starterNg:App.Loc}}"
TextChanged="SearchBox_OnTextChanged" />
<CheckBox Name="hideArchivalCheck" DockPanel.Dock="Top" Margin="0,0,0,8"
Content="{Binding [HideArchivalVehicles], Source={x:Static starterNg:App.Loc}}"
IsCheckedChanged="HideArchivalCheck_OnChanged" />
<!-- Mini preview of the selected entry + the single Add button. <!-- The arrow stays enabled with nothing selected - adding a whole
Both are docked at the bottom, under the flat vehicle list. --> category needs no highlighted entry. -->
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" Spacing="4" Margin="0,4,0,0"> <DockPanel DockPanel.Dock="Bottom" Margin="0,8,0,0">
<Button Name="addAllCategoryButton" Classes="Flat" FontSize="11" Padding="6,2" <Button Name="addMoreButton" DockPanel.Dock="Right" Classes="Flat Accent"
Cursor="Hand" Click="AddAllCategoryButton_OnClick" Width="28" Margin="4,0,0,0" Cursor="Hand" Content="&#x25BE;"
Content="{Binding [AddAllCategory], Source={x:Static starterNg:App.Loc}}" HorizontalContentAlignment="Center"
ToolTip.Tip="{Binding [TipAddAllCategory], Source={x:Static starterNg:App.Loc}}" /> ToolTip.Tip="{Binding [AddAllCategory], Source={x:Static starterNg:App.Loc}}">
<Button Name="addUniqueMmdButton" Classes="Flat" FontSize="11" Padding="6,2" <Button.Flyout>
Cursor="Hand" Click="AddUniqueMmdButton_OnClick" <MenuFlyout Placement="TopEdgeAlignedRight">
Content="{Binding [AddUniqueMmd], Source={x:Static starterNg:App.Loc}}" <MenuItem Click="AddAllCategoryButton_OnClick"
ToolTip.Tip="{Binding [TipAddUniqueMmd], Source={x:Static starterNg:App.Loc}}" /> Header="{Binding [AddAllCategory], Source={x:Static starterNg:App.Loc}}"
</StackPanel> ToolTip.Tip="{Binding [TipAddAllCategory], Source={x:Static starterNg:App.Loc}}" />
<Button Name="addVehicleButton" DockPanel.Dock="Bottom" Classes="Flat Accent" <MenuItem Click="AddUniqueMmdButton_OnClick"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center" Header="{Binding [AddUniqueMmd], Source={x:Static starterNg:App.Loc}}"
Margin="0,8,0,0" IsEnabled="False" Cursor="Hand" ToolTip.Tip="{Binding [TipAddUniqueMmd], Source={x:Static starterNg:App.Loc}}" />
Click="AddVehicleButton_OnClick" <Separator />
Content="{Binding [AddVehicle], Source={x:Static starterNg:App.Loc}}" /> <MenuItem Click="TextureBaseButton_OnClick"
<Border Name="miniPreviewPanel" DockPanel.Dock="Bottom" Height="62" Margin="0,6,0,0" Header="{Binding [OpenTextureBase], Source={x:Static starterNg:App.Loc}}" />
Background="{StaticResource CarbonInputBg}" </MenuFlyout>
BorderBrush="{StaticResource CarbonBorderLight}" BorderThickness="1" </Button.Flyout>
CornerRadius="2" Cursor="Hand" </Button>
PointerPressed="MiniPreview_OnPointerPressed"> <Button Name="addVehicleButton" Classes="Flat Accent"
<Image Name="miniPreview" Stretch="Uniform" Margin="6" /> HorizontalAlignment="Stretch" HorizontalContentAlignment="Center"
</Border> IsEnabled="False" Cursor="Hand"
<StackPanel Name="textureInfoPanel" DockPanel.Dock="Bottom" Margin="0,4,0,0" Spacing="1" /> Click="AddVehicleButton_OnClick"
Content="{Binding [AddVehicle], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Grid Name="browserDetails" DockPanel.Dock="Bottom" ColumnDefinitions="130,*"
ColumnSpacing="6" Margin="0,6,0,0" IsVisible="False">
<Border Name="miniPreviewPanel" Grid.Column="0" Height="66"
VerticalAlignment="Top"
Background="{StaticResource CarbonInputBg}"
BorderBrush="{StaticResource CarbonBorderLight}" BorderThickness="1"
CornerRadius="2" Cursor="Hand"
PointerPressed="MiniPreview_OnPointerPressed">
<Image Name="miniPreview" Stretch="Uniform" Margin="6" />
</Border>
<StackPanel Name="textureInfoPanel" Grid.Column="1" Spacing="1"
VerticalAlignment="Center" />
</Grid>
<!-- Flat list of vehicle entries (plain text, no expanders). <!-- Flat list of vehicle entries (plain text, no expanders).
Selecting one shows its mini and enables Add; double-click Selecting one shows its mini and enables Add; double-click
replaces the selected consist vehicle. --> replaces the selected consist vehicle. -->
<ListBox Name="vehicleListBox" Classes="Compact" <ListBox Name="vehicleListBox" Classes="Compact" MinHeight="120"
SelectionChanged="VehicleListBox_OnSelectionChanged" SelectionChanged="VehicleListBox_OnSelectionChanged"
DoubleTapped="VehicleListBox_OnDoubleTapped" DoubleTapped="VehicleListBox_OnDoubleTapped"
PointerPressed="VehicleListBox_OnPointerPressed" /> PointerPressed="VehicleListBox_OnPointerPressed" />
</DockPanel> </DockPanel>
</HeaderedContentControl> </Border>
<!-- Consists present on the scenery selected in the Scenarios tab. <!-- Consists present on the scenery selected in the Scenarios tab.
Selecting one loads it into the editor below. --> Selecting one loads it into the editor below; the header carries the
<HeaderedContentControl Grid.Column="1" Grid.Row="0" Theme="{StaticResource GroupBox}" scenery name (see PopulateSceneryConsists). -->
Header="{Binding [MapConsists], Source={x:Static starterNg:App.Loc}}"> <HeaderedContentControl Name="mapConsistsPanel" Grid.Column="1" Grid.Row="0"
Theme="{StaticResource GroupBox}">
<DockPanel LastChildFill="True"> <DockPanel LastChildFill="True">
<TextBlock Name="mapSceneryLabel" DockPanel.Dock="Top" FontSize="12"
Opacity="0.8" Margin="0,0,0,6" TextTrimming="CharacterEllipsis" />
<ListBox Name="sceneryConsistList" Classes="Compact" <ListBox Name="sceneryConsistList" Classes="Compact"
SelectionChanged="SceneryConsistList_OnSelectionChanged" SelectionChanged="SceneryConsistList_OnSelectionChanged"
KeyDown="SceneryConsistList_OnKeyDown" /> KeyDown="SceneryConsistList_OnKeyDown" />
</DockPanel> </DockPanel>
</HeaderedContentControl> </HeaderedContentControl>
<!-- Brakes / Loads editor (middle-right) --> <!-- Brakes / Loads editor (middle-right). Header carries the vehicle
<HeaderedContentControl Grid.Column="1" Grid.Row="1" Theme="{StaticResource GroupBox}" name (see UpdateDetails). -->
Header="{Binding [SelectedVehicle], Source={x:Static starterNg:App.Loc}}"> <HeaderedContentControl Name="selectedVehiclePanel" Grid.Column="1" Grid.Row="1"
Theme="{StaticResource GroupBox}">
<DockPanel LastChildFill="True"> <DockPanel LastChildFill="True">
<Button Name="removeVehicleButton" DockPanel.Dock="Bottom" Classes="Flat"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center"
Margin="0,8,0,0" IsEnabled="False" Cursor="Hand"
Click="RemoveVehicleButton_OnClick"
Content="{Binding [RemoveSelectedVehicle], Source={x:Static starterNg:App.Loc}}" />
<TabControl> <TabControl>
<TabItem Header="{Binding [Brakes], Source={x:Static starterNg:App.Loc}}"> <TabItem Header="{Binding [Brakes], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto"> <ScrollViewer VerticalScrollBarVisibility="Auto">

View File

@@ -6,6 +6,7 @@ using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Input.Platform; using Avalonia.Input.Platform;
using Avalonia.Layout; using Avalonia.Layout;
@@ -37,6 +38,12 @@ public partial class Depot : UserControl
private int ConsistThumbHeight => private int ConsistThumbHeight =>
StarterNG.Classes.Settings.Instance.LargeThumbnails ? 68 : 34; StarterNG.Classes.Settings.Instance.LargeThumbnails ? 68 : 34;
// Display height of the mini under the vehicle list; the class combo uses
// the same size.
private const int MiniPreviewHeight = 54;
private const int CompactThumbHeight = 24;
private readonly Cursor _hand = new(StandardCursorType.Hand); private readonly Cursor _hand = new(StandardCursorType.Hand);
private int _nameCounter; private int _nameCounter;
@@ -45,6 +52,7 @@ public partial class Depot : UserControl
// suppresses combo SelectionChanged handlers while we populate them // suppresses combo SelectionChanged handlers while we populate them
private bool _suppress; private bool _suppress;
private bool _syncingCombos; // set while the combos mirror a pick
// debounces search input so we don't rebuild on every keystroke // debounces search input so we don't rebuild on every keystroke
private DispatcherTimer? _searchTimer; private DispatcherTimer? _searchTimer;
@@ -166,6 +174,7 @@ public partial class Depot : UserControl
Dispatcher.UIThread.Post(() => Dispatcher.UIThread.Post(() =>
{ {
hideArchivalCheck.IsChecked = StarterNG.Classes.Settings.Instance.HideArchivalVehicles; hideArchivalCheck.IsChecked = StarterNG.Classes.Settings.Instance.HideArchivalVehicles;
InitClassComboTemplates();
PopulateCategoryCombo(); // -> populates class combo -> builds browser PopulateCategoryCombo(); // -> populates class combo -> builds browser
PopulateSceneryConsists(); // -> lists the current scenery's consists PopulateSceneryConsists(); // -> lists the current scenery's consists
RebuildConsist(); RebuildConsist();
@@ -366,12 +375,8 @@ public partial class Depot : UserControl
private void RebuildClassCombo() private void RebuildClassCombo()
{ {
// No "All" entry: an unselected class means "every class in the category".
// Each class row shows its category mini thumbnail (like the Pascal starter).
_suppress = true; _suppress = true;
classCombo.Items.Clear(); FillClassCombo();
foreach (string cls in ClassesForCategory(_categoryFilter))
classCombo.Items.Add(new ComboBoxItem { Content = ClassComboContent(cls), Tag = cls });
classCombo.SelectedIndex = -1; classCombo.SelectedIndex = -1;
_suppress = false; _suppress = false;
@@ -379,27 +384,88 @@ public partial class Depot : UserControl
BuildList(); BuildList();
} }
private Control ClassComboContent(string cls) // The dropdown shows the big stacked miniature, the closed combo a one-line
// row. Plain "=": SelectionBoxItemTemplate coerces to ItemTemplate while
// unset, so "??=" would never assign.
private void InitClassComboTemplates()
{ {
var row = new StackPanel classCombo.ItemTemplate = new FuncDataTemplate<string>(
(cls, _) => ClassComboContent(cls, large: true), false);
classCombo.SelectionBoxItemTemplate = new FuncDataTemplate<string>(
(cls, _) => ClassComboContent(cls, large: false), false);
}
// Fills the class combo for the current category filter; the caller owns the
// selection and _classFilter. Runs under _suppress.
// No "All" entry: an unselected class means "every class in the category".
private void FillClassCombo()
{
classCombo.Items.Clear();
foreach (string cls in ClassesForCategory(_categoryFilter))
classCombo.Items.Add(cls);
}
// Points both combos at the given category/class pair without firing their
// handlers. Returns false when they already matched.
private bool ApplyFilters(string? category, string? cls)
{
var catItem = categoryCombo.Items.OfType<ComboBoxItem>()
.FirstOrDefault(it => it.Tag is Func<string?, bool> f && f(category));
if (catItem != null && ReferenceEquals(categoryCombo.SelectedItem, catItem) &&
string.Equals(_classFilter, cls, StringComparison.OrdinalIgnoreCase))
return false;
_suppress = true;
try
{ {
Orientation = Orientation.Horizontal, if (catItem != null)
Spacing = 8, {
categoryCombo.SelectedItem = catItem;
_categoryFilter = catItem.Tag as Func<string?, bool>;
}
FillClassCombo();
classCombo.SelectedItem = classCombo.Items.OfType<string>()
.FirstOrDefault(s => string.Equals(s, cls, StringComparison.OrdinalIgnoreCase));
_classFilter = classCombo.SelectedItem as string;
}
finally { _suppress = false; }
return true;
}
// Every texture of a class shares its category.
private string? CategoryOfClass(string cls) =>
_db.Textures.FirstOrDefault(t => string.Equals(ClassOf(t), cls, StringComparison.OrdinalIgnoreCase))
is { } t ? CategoryOf(t) : null;
private Control ClassComboContent(string cls, bool large)
{
int height = large ? MiniPreviewHeight : CompactThumbHeight;
var cell = new StackPanel
{
Orientation = large ? Orientation.Vertical : Orientation.Horizontal,
Spacing = large ? 2 : 8,
HorizontalAlignment = large ? HorizontalAlignment.Center : HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Center VerticalAlignment = VerticalAlignment.Center
}; };
var bmp = GetMiniBitmap(cls, 28); var bmp = GetMiniBitmap(cls, height);
if (bmp != null) if (bmp != null)
row.Children.Add(new Image cell.Children.Add(new Image
{ {
Source = bmp, Height = 28, Width = 56, Source = bmp, Height = height,
Stretch = Stretch.Uniform, VerticalAlignment = VerticalAlignment.Center Stretch = Stretch.Uniform,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
}); });
row.Children.Add(new TextBlock cell.Children.Add(new TextBlock
{ {
Text = cls, Text = cls,
VerticalAlignment = VerticalAlignment.Center HorizontalAlignment = large ? HorizontalAlignment.Center : HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Center,
TextAlignment = TextAlignment.Center,
TextTrimming = TextTrimming.CharacterEllipsis
}); });
return row; return cell;
} }
private IEnumerable<string> ClassesForCategory(Func<string?, bool>? category) => private IEnumerable<string> ClassesForCategory(Func<string?, bool>? category) =>
@@ -422,7 +488,14 @@ public partial class Depot : UserControl
private void ClassCombo_OnSelectionChanged(object? sender, SelectionChangedEventArgs e) private void ClassCombo_OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
{ {
if (_suppress) return; if (_suppress) return;
_classFilter = (classCombo.SelectedItem as ComboBoxItem)?.Tag as string; string? cls = classCombo.SelectedItem as string;
_classFilter = cls;
// ApplyFilters refills this very combo, so it cannot run from here.
if (cls != null && _categoryFilter == null &&
PostSyncing(() => { ApplyFilters(CategoryOfClass(cls), cls); BuildList(); }))
return;
BuildList(); BuildList();
} }
@@ -453,22 +526,25 @@ public partial class Depot : UserControl
// ----------------------------------------------------------- vehicle browser // ----------------------------------------------------------- vehicle browser
// Flat list of plain-text vehicle entries (no expanders, like the original // Flat list of plain-text vehicle entries (no expanders, like the original
// Starter). Empty by default: an entry only appears once a category is chosen // Starter). Empty by default: an entry only appears once a category or class
// or a search is typed. Picking one drives the mini preview and Add button. // is chosen or a search is typed. Picking one drives the mini preview and the
// Add button.
private void BuildList() private void BuildList()
{ {
// reset selection-dependent UI // reset selection-dependent UI
_browserSelected = null; _browserSelected = null;
miniPreview.Source = null; miniPreview.Source = null;
addVehicleButton.IsEnabled = false; addVehicleButton.IsEnabled = false;
browserDetails.IsVisible = false;
vehicleListBox.Items.Clear(); vehicleListBox.Items.Clear();
string search = searchBox.Text?.Trim() ?? ""; string search = searchBox.Text?.Trim() ?? "";
bool hasSearch = search.Length > 0; bool hasSearch = search.Length > 0;
// nothing selected and nothing searched -> keep the list empty // no filter and nothing searched -> keep the list empty (a class counts
if (_categoryFilter == null && !hasSearch) // as a filter: it can be picked without a category)
if (_categoryFilter == null && _classFilter == null && !hasSearch)
return; return;
var matched = _db.Textures var matched = _db.Textures
@@ -502,9 +578,11 @@ public partial class Depot : UserControl
if (vehicleListBox.SelectedItem is ListBoxItem { Tag: VehicleTexture texture }) if (vehicleListBox.SelectedItem is ListBoxItem { Tag: VehicleTexture texture })
{ {
_browserSelected = texture; _browserSelected = texture;
miniPreview.Source = GetMiniBitmap(_db.ResolveMiniName(texture), 54); miniPreview.Source = GetMiniBitmap(_db.ResolveMiniName(texture), MiniPreviewHeight);
addVehicleButton.IsEnabled = true; addVehicleButton.IsEnabled = true;
ShowTextureInfo(texture); ShowTextureInfo(texture);
browserDetails.IsVisible = true;
SyncCombosTo(texture);
} }
else else
{ {
@@ -512,6 +590,7 @@ public partial class Depot : UserControl
miniPreview.Source = null; miniPreview.Source = null;
addVehicleButton.IsEnabled = false; addVehicleButton.IsEnabled = false;
textureInfoPanel.Children.Clear(); textureInfoPanel.Children.Clear();
browserDetails.IsVisible = false;
} }
} }
@@ -551,12 +630,16 @@ public partial class Depot : UserControl
private void ShowTextureInfo(VehicleTexture texture) private void ShowTextureInfo(VehicleTexture texture)
{ {
textureInfoPanel.Children.Clear(); textureInfoPanel.Children.Clear();
// The rows are trimmed, so the tooltip carries the full text.
var full = new List<string>();
void Row(string label, string? value) void Row(string label, string? value)
{ {
if (string.IsNullOrWhiteSpace(value)) return; if (string.IsNullOrWhiteSpace(value)) return;
string line = $"{label}: {value}";
full.Add(line);
textureInfoPanel.Children.Add(new TextBlock textureInfoPanel.Children.Add(new TextBlock
{ {
Text = $"{label}: {value}", Text = line,
FontSize = 10, FontSize = 10,
Opacity = 0.85, Opacity = 0.85,
TextTrimming = TextTrimming.CharacterEllipsis TextTrimming = TextTrimming.CharacterEllipsis
@@ -565,12 +648,15 @@ public partial class Depot : UserControl
Row(App.Loc["TextureInfo"], Base(texture.Skinfile)); Row(App.Loc["TextureInfo"], Base(texture.Skinfile));
var meta = texture.Meta; var meta = texture.Meta;
if (meta == null) return; if (meta != null)
Row(App.Loc["TexOperator"], meta.Operator); {
Row(App.Loc["TexStation"], meta.Depot); Row(App.Loc["TexOperator"], meta.Operator);
Row(App.Loc["TexRevision"], meta.RevisionDate); Row(App.Loc["TexStation"], meta.Depot);
Row(App.Loc["TexAuthor"], meta.TextureAuthor); Row(App.Loc["TexRevision"], meta.RevisionDate);
Row(App.Loc["TexPhoto"], meta.PhotoAuthor); Row(App.Loc["TexAuthor"], meta.TextureAuthor);
Row(App.Loc["TexPhoto"], meta.PhotoAuthor);
}
ToolTip.SetTip(textureInfoPanel, string.Join("\n", full));
} }
// Double-click an entry to replace the selected consist vehicle. // Double-click an entry to replace the selected consist vehicle.
@@ -763,8 +849,8 @@ public partial class Depot : UserControl
sceneryConsistList.Items.Clear(); sceneryConsistList.Items.Clear();
_listScenery = AppState.Instance.CurrentScenery ?? _sceneries.FirstOrDefault(); _listScenery = AppState.Instance.CurrentScenery ?? _sceneries.FirstOrDefault();
mapSceneryLabel.Text = _listScenery != null mapConsistsPanel.Header = _listScenery != null
? Path.GetFileNameWithoutExtension(_listScenery.Path) ? $"{App.Loc["MapConsists"]}: {Path.GetFileNameWithoutExtension(_listScenery.Path)}"
: App.Loc["NoSceneryConsists"]; : App.Loc["NoSceneryConsists"];
if (_listScenery != null) if (_listScenery != null)
@@ -1179,6 +1265,11 @@ public partial class Depot : UserControl
controls.Children.Add(SmallButton("⇄", App.Loc["TipFlip"], () => FlipItem(item))); controls.Children.Add(SmallButton("⇄", App.Loc["TipFlip"], () => FlipItem(item)));
controls.Children.Add(SmallButton("▶", App.Loc["TipMoveRight"], () => MoveRight(item))); controls.Children.Add(SmallButton("▶", App.Loc["TipMoveRight"], () => MoveRight(item)));
var remove = SmallButton("✕", App.Loc["RemoveSelectedVehicle"], () => RemoveItem(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 = 4 };
inner.Children.Add(minis); inner.Children.Add(minis);
inner.Children.Add(nameBlock); inner.Children.Add(nameBlock);
@@ -1268,37 +1359,51 @@ public partial class Depot : UserControl
var set = _db.ResolveSet(texture); var set = _db.ResolveSet(texture);
var browserTex = set is { Count: > 0 } ? set[0] : texture; var browserTex = set is { Count: > 0 } ? set[0] : texture;
string? category = CategoryOf(browserTex); WhileSyncing(() =>
var catItem = categoryCombo.Items.OfType<ComboBoxItem>()
.FirstOrDefault(it => it.Tag is Func<string?, bool> f && f(category));
if (catItem != null)
{ {
if (!ReferenceEquals(categoryCombo.SelectedItem, catItem)) ApplyFilters(CategoryOf(browserTex), ClassOf(browserTex));
categoryCombo.SelectedItem = catItem; // -> rebuilds class combo + list
else
RebuildClassCombo();
}
// Select the vehicle's class (type) in the class combo.
string cls = ClassOf(browserTex);
var classItem = classCombo.Items.OfType<ComboBoxItem>()
.FirstOrDefault(it => it.Tag is string s &&
string.Equals(s, cls, StringComparison.OrdinalIgnoreCase));
if (classItem != null)
{
_suppress = true;
classCombo.SelectedItem = classItem;
_classFilter = cls;
_suppress = false;
BuildList(); BuildList();
} SelectListEntry(browserTex);
});
}
// highlight the matching entry (selection drives the mini + Add button) // Mirrors a list pick in the combos, then restores the selection that the
// rebuilt list dropped.
private void SyncCombosTo(VehicleTexture texture)
{
if (_syncingCombos || !ApplyFilters(CategoryOf(texture), ClassOf(texture)))
return;
PostSyncing(() => { BuildList(); SelectListEntry(texture); });
}
// Runs work with the sync flag up, so the selections it makes do not come
// back as another sync.
private void WhileSyncing(Action work)
{
_syncingCombos = true;
try { work(); }
finally { _syncingCombos = false; }
}
// Same, on the next dispatcher pass: clearing Items from inside a
// SelectionChanged throws. False when a sync is already pending.
private bool PostSyncing(Action work)
{
if (_syncingCombos)
return false;
_syncingCombos = true;
Dispatcher.UIThread.Post(() => WhileSyncing(work), DispatcherPriority.Background);
return true;
}
private void SelectListEntry(VehicleTexture texture)
{
foreach (var obj in vehicleListBox.Items) foreach (var obj in vehicleListBox.Items)
if (obj is ListBoxItem { Tag: VehicleTexture t } entry && if (obj is ListBoxItem { Tag: VehicleTexture t } entry &&
string.Equals(t.Skinfile, browserTex.Skinfile, StringComparison.OrdinalIgnoreCase) && string.Equals(t.Skinfile, texture.Skinfile, StringComparison.OrdinalIgnoreCase) &&
string.Equals(t.Directory, browserTex.Directory, StringComparison.OrdinalIgnoreCase)) string.Equals(t.Directory, texture.Directory, StringComparison.OrdinalIgnoreCase))
{ {
vehicleListBox.SelectedItem = entry; vehicleListBox.SelectedItem = entry;
entry.BringIntoView(); entry.BringIntoView();
@@ -1485,7 +1590,11 @@ public partial class Depot : UserControl
brakesPanel.Children.Clear(); brakesPanel.Children.Clear();
loadsPanel.Children.Clear(); loadsPanel.Children.Clear();
damagePanel.Children.Clear(); damagePanel.Children.Clear();
removeVehicleButton.IsEnabled = _selected != null;
selectedVehiclePanel.Header = _selected is null
? App.Loc["SelectedVehicle"]
: $"{App.Loc["SelectedVehicle"]}: " +
(_selected.Grouped ? UnitLabel(_selected) : _selected.Cars[0].Name);
// whole-consist load tools sit at the top of the Loads tab, always available // whole-consist load tools sit at the top of the Loads tab, always available
loadsPanel.Children.Add(BuildConsistLoadTools()); loadsPanel.Children.Add(BuildConsistLoadTools());
@@ -1542,11 +1651,6 @@ public partial class Depot : UserControl
private Control BuildConsistLoadTools() private Control BuildConsistLoadTools()
{ {
var panel = new StackPanel { Spacing = 4, Margin = new Thickness(0, 0, 0, 4) }; var panel = new StackPanel { Spacing = 4, Margin = new Thickness(0, 0, 0, 4) };
panel.Children.Add(new TextBlock
{
Text = App.Loc["ConsistLoadTools"], FontWeight = FontWeight.Bold, FontSize = 12
});
var row = new WrapPanel { Orientation = Orientation.Horizontal }; var row = new WrapPanel { Orientation = Orientation.Horizontal };
row.Children.Add(LoadToolButton(App.Loc["ConsistRandomType"], RandomLoadTypeForConsist)); row.Children.Add(LoadToolButton(App.Loc["ConsistRandomType"], RandomLoadTypeForConsist));
row.Children.Add(LoadToolButton(App.Loc["ConsistMaxAmount"], MaxAmountForConsist)); row.Children.Add(LoadToolButton(App.Loc["ConsistMaxAmount"], MaxAmountForConsist));
@@ -1681,12 +1785,6 @@ public partial class Depot : UserControl
Text = text, Opacity = 0.6, FontSize = 12, TextWrapping = TextWrapping.Wrap Text = text, Opacity = 0.6, FontSize = 12, TextWrapping = TextWrapping.Wrap
}; };
private static TextBlock UnitTitle(ConsistItem item) => new()
{
Text = item.Grouped ? UnitLabel(item) : item.Cars[0].Name,
FontWeight = FontWeight.Bold, FontSize = 12
};
// Row of "<label> <control>". // Row of "<label> <control>".
private static Control LabeledRow(string label, Control control) private static Control LabeledRow(string label, Control control)
{ {
@@ -1745,7 +1843,6 @@ public partial class Depot : UserControl
loadCombo.SelectionChanged += (_, _) => Apply(); loadCombo.SelectionChanged += (_, _) => Apply();
switchCombo.SelectionChanged += (_, _) => Apply(); switchCombo.SelectionChanged += (_, _) => Apply();
brakesPanel.Children.Add(UnitTitle(item));
brakesPanel.Children.Add(LabeledRow(App.Loc["BrakeMode"], modeCombo)); brakesPanel.Children.Add(LabeledRow(App.Loc["BrakeMode"], modeCombo));
brakesPanel.Children.Add(LabeledRow(App.Loc["BrakeLoad"], loadCombo)); brakesPanel.Children.Add(LabeledRow(App.Loc["BrakeLoad"], loadCombo));
brakesPanel.Children.Add(LabeledRow(App.Loc["BrakeSwitch"], switchCombo)); brakesPanel.Children.Add(LabeledRow(App.Loc["BrakeSwitch"], switchCombo));
@@ -1789,7 +1886,7 @@ public partial class Depot : UserControl
NumericUpDown Spin(int value, int max) => new() NumericUpDown Spin(int value, int max) => new()
{ {
FontSize = 12, Minimum = 0, Maximum = max, Increment = 1, FontSize = 12, Minimum = 0, Maximum = max, Increment = 1,
Value = value, MinWidth = 120, FormatString = "0" Value = value, MinWidth = 0, Width = 100, FormatString = "0"
}; };
var sway = Spin(w.Sway, 100); var sway = Spin(w.Sway, 100);
@@ -1815,11 +1912,39 @@ public partial class Depot : UserControl
flatRand.ValueChanged += (_, _) => Apply(); flatRand.ValueChanged += (_, _) => Apply();
flatProb.ValueChanged += (_, _) => Apply(); flatProb.ValueChanged += (_, _) => Apply();
damagePanel.Children.Add(UnitTitle(item)); var grid = new Grid
damagePanel.Children.Add(LabeledRow(App.Loc["DamageSway"], sway)); {
damagePanel.Children.Add(LabeledRow(App.Loc["DamageFlatness"], flat)); ColumnDefinitions = new ColumnDefinitions("Auto,*"),
damagePanel.Children.Add(LabeledRow(App.Loc["DamageFlatnessRand"], flatRand)); RowSpacing = 6,
damagePanel.Children.Add(LabeledRow(App.Loc["DamageFlatnessProb"], flatProb)); ColumnSpacing = 8
};
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,
VerticalAlignment = VerticalAlignment.Center
};
Grid.SetRow(caption, r);
Grid.SetColumn(caption, 0);
control.HorizontalAlignment = HorizontalAlignment.Left;
Grid.SetRow(control, r);
Grid.SetColumn(control, 1);
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);
} }
// Cargo type + amount, written into each car's node::dynamic trailing params. // Cargo type + amount, written into each car's node::dynamic trailing params.
@@ -1884,13 +2009,8 @@ public partial class Depot : UserControl
typeCombo.SelectionChanged += (_, _) => Apply(); typeCombo.SelectionChanged += (_, _) => Apply();
countBox.ValueChanged += (_, _) => Apply(); countBox.ValueChanged += (_, _) => Apply();
loadsPanel.Children.Add(UnitTitle(item));
loadsPanel.Children.Add(LabeledRow(App.Loc["LoadType"], typeCombo)); loadsPanel.Children.Add(LabeledRow(App.Loc["LoadType"], typeCombo));
loadsPanel.Children.Add(LabeledRow(App.Loc["LoadCount"], countBox)); loadsPanel.Children.Add(LabeledRow(App.Loc["LoadCount"], countBox));
loadsPanel.Children.Add(new TextBlock
{
Text = App.Loc["LoadHint"], Opacity = 0.6, FontSize = 11, TextWrapping = TextWrapping.Wrap
});
} }
// Cargo names + per-unit weights parsed once from data/load_weights.txt (the // Cargo names + per-unit weights parsed once from data/load_weights.txt (the
@@ -1992,12 +2112,6 @@ public partial class Depot : UserControl
BuildList(); BuildList();
} }
private void RemoveVehicleButton_OnClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
if (_selected != null)
RemoveItem(_selected);
}
private async void TextureBaseButton_OnClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e) private async void TextureBaseButton_OnClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{ {
if (TopLevel.GetTopLevel(this) is not Window owner) return; if (TopLevel.GetTopLevel(this) is not Window owner) return;
@@ -2032,38 +2146,54 @@ public partial class Depot : UserControl
private Window BuildTextureRandomizerDialog() private Window BuildTextureRandomizerDialog()
{ {
static Control Docked(Control c, Dock side)
{
DockPanel.SetDock(c, side);
c.Margin = new Thickness(8, 0, 0, 0);
c.VerticalAlignment = VerticalAlignment.Center;
return c;
}
static TextBlock Caption(string text) => new()
{
Text = text,
FontSize = 12,
TextWrapping = TextWrapping.Wrap,
VerticalAlignment = VerticalAlignment.Center
};
var revDiff = new CheckBox var revDiff = new CheckBox
{ {
Content = App.Loc["RandomizeTexturesRevDiff"], Content = Caption(App.Loc["RandomizeTexturesRevDiff"]),
IsChecked = true, IsChecked = true,
FontSize = 12 FontSize = 12
}; };
var age = new NumericUpDown var age = new NumericUpDown
{ {
Minimum = 2, Maximum = 8, Value = 3, Width = 56, Minimum = 2, Maximum = 8, Value = 3, Width = 110,
FormatString = "0", ShowButtonSpinner = true FormatString = "0", ShowButtonSpinner = true
}; };
var vsCurrent = new RadioButton var vsCurrent = new RadioButton
{ {
Content = App.Loc["RandomizeTexturesVsCurrent"], Content = Caption(App.Loc["RandomizeTexturesVsCurrent"]),
IsChecked = true, IsChecked = true,
FontSize = 12, FontSize = 12,
GroupName = "revBase" GroupName = "revBase"
}; };
var vsYear = new RadioButton var vsYear = new RadioButton
{ {
Content = App.Loc["RandomizeTexturesVsYear"], Content = Caption(App.Loc["RandomizeTexturesVsYear"]),
FontSize = 12, FontSize = 12,
GroupName = "revBase" GroupName = "revBase"
}; };
var year = new NumericUpDown var year = new NumericUpDown
{ {
Minimum = 1950, Maximum = 2050, Value = DateTime.Now.Year, Width = 72, Minimum = 1950, Maximum = 2050, Value = DateTime.Now.Year, Width = 150,
FormatString = "0", ShowButtonSpinner = true, IsEnabled = false FormatString = "0", ShowButtonSpinner = true, IsEnabled = false
}; };
var noArch = new CheckBox var noArch = new CheckBox
{ {
Content = App.Loc["RandomizeTexturesNoArchival"], Content = Caption(App.Loc["RandomizeTexturesNoArchival"]),
IsChecked = true, IsChecked = true,
FontSize = 12 FontSize = 12
}; };
@@ -2074,8 +2204,8 @@ public partial class Depot : UserControl
var win = new Window var win = new Window
{ {
Title = App.Loc["RandomizeTexturesTitle"], Title = App.Loc["RandomizeTexturesTitle"],
Width = 320, Width = 420,
Height = 300, SizeToContent = SizeToContent.Height,
CanResize = false, CanResize = false,
WindowStartupLocation = WindowStartupLocation.CenterOwner WindowStartupLocation = WindowStartupLocation.CenterOwner
}; };
@@ -2092,7 +2222,7 @@ public partial class Depot : UserControl
var ok = new Button var ok = new Button
{ {
Content = App.Loc["RandomizeTexturesApply"], Content = App.Loc["RandomizeTexturesApply"],
HorizontalAlignment = HorizontalAlignment.Stretch, MinWidth = 90,
Cursor = _hand Cursor = _hand
}; };
ok.Classes.Add("Accent"); ok.Classes.Add("Accent");
@@ -2107,7 +2237,7 @@ public partial class Depot : UserControl
win.Close(true); win.Close(true);
}; };
var cancel = new Button { Content = App.Loc["Cancel"], Cursor = _hand }; var cancel = new Button { Content = App.Loc["Cancel"], MinWidth = 90, Cursor = _hand };
cancel.Classes.Add("Flat"); cancel.Classes.Add("Flat");
cancel.Click += (_, _) => win.Close(false); cancel.Click += (_, _) => win.Close(false);
@@ -2123,23 +2253,36 @@ public partial class Depot : UserControl
FontWeight = FontWeight.Bold, FontWeight = FontWeight.Bold,
HorizontalAlignment = HorizontalAlignment.Center HorizontalAlignment = HorizontalAlignment.Center
}, },
new StackPanel // DockPanel: in a horizontal StackPanel the caption gets infinite
// width, never wraps and shoves the spinner off the edge.
new DockPanel
{ {
Orientation = Orientation.Horizontal, LastChildFill = true,
Spacing = 8, Children =
Children = { revDiff, age } {
Docked(age, Dock.Right),
revDiff
}
}, },
vsCurrent, vsCurrent,
new StackPanel new DockPanel
{ {
Orientation = Orientation.Horizontal, LastChildFill = true,
Spacing = 8, Children =
Children = { vsYear, year } {
Docked(year, Dock.Right),
vsYear
}
}, },
noArch, noArch,
rulesBtn, rulesBtn,
ok, new StackPanel
cancel {
Orientation = Orientation.Horizontal,
HorizontalAlignment = HorizontalAlignment.Right,
Spacing = 8,
Children = { ok, cancel }
}
} }
}; };
return win; return win;
@@ -2161,7 +2304,7 @@ public partial class Depot : UserControl
Text = text, Text = text,
AcceptsReturn = true, AcceptsReturn = true,
TextWrapping = TextWrapping.NoWrap, TextWrapping = TextWrapping.NoWrap,
FontFamily = new FontFamily("Consolas,Courier New,monospace"), FontFamily = StarterNG.Infrastructure.MonoFont.Family,
FontSize = 12 FontSize = 12
}; };
ScrollViewer.SetHorizontalScrollBarVisibility(box, Avalonia.Controls.Primitives.ScrollBarVisibility.Auto); ScrollViewer.SetHorizontalScrollBarVisibility(box, Avalonia.Controls.Primitives.ScrollBarVisibility.Auto);

View File

@@ -56,7 +56,9 @@
<String key="Start">Spustit</String> <String key="Start">Spustit</String>
<String key="StartNoScenery">Select a scenery</String> <String key="StartNoScenery">Select a scenery</String>
<String key="StartNoTrain">Select a consist</String> <String key="StartNoTrain">Select a consist</String>
<String key="StartNoStaff">No crew / passenger (hold Ctrl to start)</String> <String key="StartNoStaff">No crew / passenger</String>
<String key="StartNoStaffTitle">Consist without crew</String>
<String key="StartNoStaffConfirm">This consist has no crew and no passenger — there will be nothing to drive. Start anyway?</String>
<String key="RestoreWeather">Restore weather</String> <String key="RestoreWeather">Restore weather</String>
<String key="ReloadSceneries">Reload</String> <String key="ReloadSceneries">Reload</String>
<String key="OpenSceneryFolder">Open folder</String> <String key="OpenSceneryFolder">Open folder</String>

View File

@@ -56,7 +56,9 @@
<String key="Start">Start</String> <String key="Start">Start</String>
<String key="StartNoScenery">Select a scenery</String> <String key="StartNoScenery">Select a scenery</String>
<String key="StartNoTrain">Select a consist</String> <String key="StartNoTrain">Select a consist</String>
<String key="StartNoStaff">No crew / passenger (hold Ctrl to start)</String> <String key="StartNoStaff">No crew / passenger</String>
<String key="StartNoStaffTitle">Consist without crew</String>
<String key="StartNoStaffConfirm">This consist has no crew and no passenger — there will be nothing to drive. Start anyway?</String>
<String key="RestoreWeather">Restore weather</String> <String key="RestoreWeather">Restore weather</String>
<String key="ReloadSceneries">Reload</String> <String key="ReloadSceneries">Reload</String>
<String key="OpenSceneryFolder">Open folder</String> <String key="OpenSceneryFolder">Open folder</String>

View File

@@ -56,7 +56,9 @@
<String key="Start">Indítás</String> <String key="Start">Indítás</String>
<String key="StartNoScenery">Select a scenery</String> <String key="StartNoScenery">Select a scenery</String>
<String key="StartNoTrain">Select a consist</String> <String key="StartNoTrain">Select a consist</String>
<String key="StartNoStaff">No crew / passenger (hold Ctrl to start)</String> <String key="StartNoStaff">No crew / passenger</String>
<String key="StartNoStaffTitle">Consist without crew</String>
<String key="StartNoStaffConfirm">This consist has no crew and no passenger — there will be nothing to drive. Start anyway?</String>
<String key="RestoreWeather">Restore weather</String> <String key="RestoreWeather">Restore weather</String>
<String key="ReloadSceneries">Reload</String> <String key="ReloadSceneries">Reload</String>
<String key="OpenSceneryFolder">Open folder</String> <String key="OpenSceneryFolder">Open folder</String>

View File

@@ -56,7 +56,9 @@
<String key="Start">Uruchom</String> <String key="Start">Uruchom</String>
<String key="StartNoScenery">Wybierz scenerię</String> <String key="StartNoScenery">Wybierz scenerię</String>
<String key="StartNoTrain">Wybierz skład</String> <String key="StartNoTrain">Wybierz skład</String>
<String key="StartNoStaff">Brak obsady / pasażera (Ctrl = start)</String> <String key="StartNoStaff">Brak obsady / pasażera</String>
<String key="StartNoStaffTitle">Skład bez obsady</String>
<String key="StartNoStaffConfirm">Skład nie ma obsady ani pasażera — nie będzie czym kierować. Uruchomić mimo to?</String>
<String key="RestoreWeather">Przywróć pogodę</String> <String key="RestoreWeather">Przywróć pogodę</String>
<String key="ReloadSceneries">Przeładuj</String> <String key="ReloadSceneries">Przeładuj</String>
<String key="OpenSceneryFolder">Otwórz folder</String> <String key="OpenSceneryFolder">Otwórz folder</String>

View File

@@ -56,7 +56,9 @@
<String key="Start">Запуск</String> <String key="Start">Запуск</String>
<String key="StartNoScenery">Select a scenery</String> <String key="StartNoScenery">Select a scenery</String>
<String key="StartNoTrain">Select a consist</String> <String key="StartNoTrain">Select a consist</String>
<String key="StartNoStaff">No crew / passenger (hold Ctrl to start)</String> <String key="StartNoStaff">No crew / passenger</String>
<String key="StartNoStaffTitle">Consist without crew</String>
<String key="StartNoStaffConfirm">This consist has no crew and no passenger — there will be nothing to drive. Start anyway?</String>
<String key="RestoreWeather">Restore weather</String> <String key="RestoreWeather">Restore weather</String>
<String key="ReloadSceneries">Reload</String> <String key="ReloadSceneries">Reload</String>
<String key="OpenSceneryFolder">Open folder</String> <String key="OpenSceneryFolder">Open folder</String>