wider migration from old starter

This commit is contained in:
maj00r
2026-08-30 22:29:45 +02:00
parent c7853f6588
commit 563a937eb9
44 changed files with 6076 additions and 1457 deletions

View File

@@ -0,0 +1,143 @@
using System;
using System.Diagnostics;
using System.IO;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Layout;
using Avalonia.Media;
using StarterNG.Infrastructure;
namespace StarterNG.Views;
public static class AboutWindow
{
private const string RepositoryUrl = "https://github.com/maj00r/StarterNG";
private static readonly string[] ChangelogNames =
{
"changelog.txt", "starter/changelog.txt", "CHANGELOG.md"
};
public static Window Create()
{
var win = new Window
{
Title = App.Loc["AboutTitle"],
Width = 640,
Height = 460,
MinWidth = 480,
MinHeight = 320,
WindowStartupLocation = WindowStartupLocation.CenterOwner
};
var header = new StackPanel { Spacing = 2, Margin = new Avalonia.Thickness(20, 18, 20, 12) };
header.Children.Add(new TextBlock
{
Text = "Starter MaSzyna",
FontSize = 22,
FontWeight = FontWeight.Bold
});
header.Children.Add(new TextBlock
{
Text = $"{App.Loc["Version"]} {AppInfo.Version}",
Opacity = 0.8
});
header.Children.Add(new TextBlock
{
Text = App.Loc["AboutForSimulator"],
Opacity = 0.8
});
var repo = new Button
{
Content = RepositoryUrl,
Tag = RepositoryUrl,
Padding = new Avalonia.Thickness(0),
HorizontalAlignment = HorizontalAlignment.Left,
Margin = new Avalonia.Thickness(0, 6, 0, 10),
Cursor = new Avalonia.Input.Cursor(Avalonia.Input.StandardCursorType.Hand)
};
repo.Classes.Add("Basic");
repo.Click += (_, _) => OpenUrl(RepositoryUrl);
header.Children.Add(repo);
var changelog = new TextBox
{
Text = ReadChangelog(),
IsReadOnly = true,
AcceptsReturn = true,
TextWrapping = TextWrapping.NoWrap,
FontFamily = MonoFont.Family,
FontSize = 12,
Margin = new Avalonia.Thickness(20, 0, 20, 12)
};
ScrollViewer.SetHorizontalScrollBarVisibility(changelog, ScrollBarVisibility.Auto);
ScrollViewer.SetVerticalScrollBarVisibility(changelog, ScrollBarVisibility.Auto);
var close = new Button
{
Content = App.Loc["Close"],
MinWidth = 90,
Cursor = new Avalonia.Input.Cursor(Avalonia.Input.StandardCursorType.Hand)
};
close.Classes.Add("Accent");
close.Click += (_, _) => win.Close();
var footer = new StackPanel
{
Orientation = Orientation.Horizontal,
HorizontalAlignment = HorizontalAlignment.Right,
Margin = new Avalonia.Thickness(20, 0, 20, 16),
Children = { close }
};
var root = new DockPanel();
DockPanel.SetDock(header, Dock.Top);
DockPanel.SetDock(footer, Dock.Bottom);
root.Children.Add(header);
root.Children.Add(footer);
var logLabel = new TextBlock
{
Text = App.Loc["AboutChangelog"],
Margin = new Avalonia.Thickness(20, 0, 20, 4),
Opacity = 0.8
};
DockPanel.SetDock(logLabel, Dock.Top);
root.Children.Add(logLabel);
root.Children.Add(changelog);
win.Content = root;
return win;
}
private static void OpenUrl(string url)
{
try
{
Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
}
catch (Exception ex)
{
Infrastructure.Diagnostics.Log($"Open {url}", ex);
}
}
private static string ReadChangelog()
{
foreach (string name in ChangelogNames)
{
string path = Path.Combine(Directory.GetCurrentDirectory(), name);
if (!File.Exists(path)) continue;
try
{
return File.ReadAllText(path);
}
catch (Exception ex)
{
Diagnostics.Log($"Changelog {path}", ex);
}
}
return App.Loc["AboutNoChangelog"];
}
}

View File

@@ -0,0 +1,187 @@
using System;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Layout;
using Avalonia.Media;
using StarterNG.Infrastructure;
using SettingsModel = StarterNG.Classes.Settings;
namespace StarterNG.Views;
public sealed class AdvancedSettingsWindow : Window
{
private readonly TextBox _log = new()
{
IsReadOnly = true,
AcceptsReturn = true,
TextWrapping = TextWrapping.NoWrap,
FontFamily = MonoFont.Family,
FontSize = 12
};
private readonly CheckBox _compressTex = new();
private readonly CheckBox _mipmaps = new();
private readonly CheckBox _scaleSpeculars = new();
private readonly CheckBox _shaderGamma = new();
private readonly CheckBox _useGles = new();
private readonly CheckBox _convertModels = new();
private readonly CheckBox _resourceMove = new();
private readonly CheckBox _resourceSweep = new();
private readonly CheckBox _ignoreIrrelevant = new();
private readonly Slider _threads = new() { Minimum = 0, Maximum = 16, TickFrequency = 1, IsSnapToTickEnabled = true };
private readonly TextBlock _threadsValue = new() { VerticalAlignment = VerticalAlignment.Center, MinWidth = 24 };
public event Action<string>? ToolRequested;
public AdvancedSettingsWindow()
{
Title = App.Loc["AdvancedSettings"];
Width = 620;
Height = 470;
MinWidth = 480;
MinHeight = 360;
WindowStartupLocation = WindowStartupLocation.CenterOwner;
_compressTex.Content = App.Loc["CompressTextures"];
_mipmaps.Content = App.Loc["ScreenMipmaps"];
_scaleSpeculars.Content = App.Loc["ScaleSpeculars"];
_shaderGamma.Content = App.Loc["ShaderGamma"];
_useGles.Content = App.Loc["UseGLES"];
_convertModels.Content = App.Loc["ExtendedModelConversion"];
_resourceMove.Content = App.Loc["GfxResourceMove"];
_resourceSweep.Content = App.Loc["GfxResourceSweep"];
_ignoreIrrelevant.Content = App.Loc["IgnoreIrrelevantTrains"];
_threads.PropertyChanged += (_, e) =>
{
if (e.Property == RangeBase.ValueProperty)
_threadsValue.Text = ((int)_threads.Value).ToString();
};
Load();
var tabs = new TabControl();
tabs.Items.Add(new TabItem { Header = App.Loc["AdvLogPage"], Content = BuildLogPage() });
tabs.Items.Add(new TabItem { Header = App.Loc["Advanced"], Content = BuildOptionsPage() });
tabs.Items.Add(new TabItem { Header = App.Loc["Tools"], Content = BuildToolsPage() });
var close = new Button
{
Content = App.Loc["Close"],
MinWidth = 100,
HorizontalAlignment = HorizontalAlignment.Right,
Margin = new Avalonia.Thickness(0, 8, 0, 0),
Cursor = new Avalonia.Input.Cursor(Avalonia.Input.StandardCursorType.Hand)
};
close.Classes.Add("Accent");
close.Click += (_, _) => { Store(); Close(); };
var root = new DockPanel { Margin = new Avalonia.Thickness(10) };
DockPanel.SetDock(close, Dock.Bottom);
root.Children.Add(close);
root.Children.Add(tabs);
Content = root;
}
private Control BuildLogPage()
{
_log.Text = Diagnostics.Text;
var clear = new Button
{
Content = App.Loc["AdvClearLog"],
Margin = new Avalonia.Thickness(0, 8, 0, 0),
HorizontalAlignment = HorizontalAlignment.Left,
Cursor = new Avalonia.Input.Cursor(Avalonia.Input.StandardCursorType.Hand)
};
clear.Classes.Add("Flat");
clear.Click += (_, _) => { Diagnostics.Clear(); _log.Text = ""; };
ScrollViewer.SetHorizontalScrollBarVisibility(_log, ScrollBarVisibility.Auto);
ScrollViewer.SetVerticalScrollBarVisibility(_log, ScrollBarVisibility.Auto);
var panel = new DockPanel { Margin = new Avalonia.Thickness(8) };
DockPanel.SetDock(clear, Dock.Bottom);
panel.Children.Add(clear);
panel.Children.Add(_log);
return panel;
}
private Control BuildOptionsPage()
{
var sp = new StackPanel { Margin = new Avalonia.Thickness(10), Spacing = 8 };
foreach (var c in new Control[]
{
_compressTex, _scaleSpeculars, _useGles, _shaderGamma, _mipmaps,
_convertModels, _resourceMove, _resourceSweep, _ignoreIrrelevant
})
sp.Children.Add(c);
var row = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8, Margin = new Avalonia.Thickness(0, 6, 0, 0) };
row.Children.Add(new TextBlock
{
Text = App.Loc["TrainPhysicsThreads"],
VerticalAlignment = VerticalAlignment.Center
});
_threads.Width = 220;
row.Children.Add(_threads);
row.Children.Add(_threadsValue);
sp.Children.Add(row);
return new ScrollViewer { Content = sp, VerticalScrollBarVisibility = ScrollBarVisibility.Auto };
}
private Control BuildToolsPage()
{
var sp = new StackPanel { Margin = new Avalonia.Thickness(10), Spacing = 8 };
Button Tool(string key, string tag)
{
var b = new Button
{
Content = App.Loc[key],
HorizontalAlignment = HorizontalAlignment.Stretch,
HorizontalContentAlignment = HorizontalAlignment.Center,
Cursor = new Avalonia.Input.Cursor(Avalonia.Input.StandardCursorType.Hand)
};
b.Classes.Add("Flat");
b.Click += (_, _) => ToolRequested?.Invoke(tag);
return b;
}
sp.Children.Add(Tool("AddAllCategory", "category"));
sp.Children.Add(Tool("AddUniqueMmd", "mmd"));
sp.Children.Add(Tool("RemoveAllTrains", "removeall"));
return sp;
}
private void Load()
{
var s = SettingsModel.Instance;
_compressTex.IsChecked = s.CompressTextures;
_mipmaps.IsChecked = s.ScreenMipmaps;
_scaleSpeculars.IsChecked = s.ScaleSpeculars;
_shaderGamma.IsChecked = s.ShaderGamma;
_useGles.IsChecked = s.UseGLES;
_convertModels.IsChecked = s.ExtendedModelConversion;
_resourceMove.IsChecked = s.GfxResourceMove;
_resourceSweep.IsChecked = s.GfxResourceSweep;
_ignoreIrrelevant.IsChecked = s.IgnoreIrrelevantTrains;
_threads.Value = s.TrainPhysicsThreads;
_threadsValue.Text = s.TrainPhysicsThreads.ToString();
}
private void Store()
{
var s = SettingsModel.Instance;
s.CompressTextures = _compressTex.IsChecked == true;
s.ScreenMipmaps = _mipmaps.IsChecked == true;
s.ScaleSpeculars = _scaleSpeculars.IsChecked == true;
s.ShaderGamma = _shaderGamma.IsChecked == true;
s.UseGLES = _useGles.IsChecked == true;
s.ExtendedModelConversion = _convertModels.IsChecked == true;
s.GfxResourceMove = _resourceMove.IsChecked == true;
s.GfxResourceSweep = _resourceSweep.IsChecked == true;
s.IgnoreIrrelevantTrains = _ignoreIrrelevant.IsChecked == true;
s.TrainPhysicsThreads = (int)_threads.Value;
}
}

View File

@@ -18,11 +18,43 @@ public static class ConsistContextMenu
Control owner,
Func<Trainset?> getTarget,
Action<List<Dynamic>> applyVehicles,
Action? removeAll = null)
Action? removeAll = null,
Func<Dynamic?>? getVehicle = null,
Action<Dynamic>? removeVehicle = null,
Action? randomOrder = null,
Action? randomTurn = null)
{
var menu = new ContextMenu();
menu.Opening += (_, _) => Populate(menu, owner, getTarget, applyVehicles, removeAll);
menu.Opening += (_, _) =>
{
Populate(menu, owner, getTarget, applyVehicles, removeAll);
if (randomOrder != null || randomTurn != null)
{
var schedule = new MenuItem { Header = App.Loc["ConsistSchedule"] };
if (randomOrder != null)
{
var mo = new MenuItem { Header = App.Loc["RandomizeOrder"] };
mo.Click += (_, _) => randomOrder();
schedule.Items.Add(mo);
}
if (randomTurn != null)
{
var mt = new MenuItem { Header = App.Loc["RandomizeOrientation"] };
mt.Click += (_, _) => randomTurn();
schedule.Items.Add(mt);
}
menu.Items.Add(schedule);
}
if (removeVehicle == null || getVehicle?.Invoke() is not { } v) return;
menu.Items.Add(new Separator());
var mi = new MenuItem { Header = App.Loc["RemoveVehicle"] };
mi.Click += (_, _) => removeVehicle(v);
menu.Items.Add(mi);
};
return menu;
}
@@ -50,6 +82,31 @@ public static class ConsistContextMenu
}
menu.Items.Add(load);
var append = new MenuItem { Header = App.Loc["WarehouseAppend"] };
if (presets.Count == 0)
{
append.Items.Add(new MenuItem { Header = App.Loc["PresetEmpty"], IsEnabled = false });
}
else
{
foreach (var preset in presets)
{
var entry = preset;
var mi = new MenuItem { Header = entry.Name };
mi.Click += (_, _) =>
{
var added = ConsistText.VehiclesFrom(entry.Entry);
if (added is null || added.Count == 0) return;
var merged = target is null
? added
: new List<Dynamic>(target.Vehicles).Concat(added).ToList();
applyVehicles(merged);
};
append.Items.Add(mi);
}
}
menu.Items.Add(append);
var sort = new MenuItem { Header = App.Loc["PresetSort"] };
var sortName = new MenuItem
{
@@ -148,7 +205,7 @@ public static class ConsistContextMenu
var nameBox = new TextBox
{
Watermark = App.Loc["PresetNamePrompt"],
PlaceholderText = App.Loc["PresetNamePrompt"],
Text = DefaultName(target),
MinWidth = 220
};

View File

@@ -7,162 +7,177 @@
x:CompileBindings="True"
mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="700"
x:Class="StarterNG.Views.Depot">
<Grid Margin="0" ColumnSpacing="8" RowSpacing="8">
<Grid Margin="0" ColumnSpacing="3" RowSpacing="3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="340" />
<ColumnDefinition Width="270" MinWidth="220" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<!-- Consists list and the brakes/loads editor share the upper area,
the editor taking a bit more of it. -->
<RowDefinition Height="*" />
<RowDefinition Height="1.25*" MinHeight="250" />
<RowDefinition Height="190" />
<RowDefinition Height="*" MinHeight="230" />
<RowDefinition Height="275" MinHeight="260" />
<RowDefinition Height="175" MinHeight="165" />
</Grid.RowDefinitions>
<!-- Vehicle database browser (left, rows 0-1) -->
<Border Grid.RowSpan="2" Background="{StaticResource CarbonPanelBg}"
<Border Grid.Column="0" Grid.Row="0" Background="{StaticResource CarbonPanelBg}"
BorderBrush="{StaticResource CarbonBorderLight}" BorderThickness="1"
CornerRadius="2" Padding="8">
<DockPanel LastChildFill="True">
<DockPanel DockPanel.Dock="Top" Margin="0,0,0,6" LastChildFill="True">
<Grid RowDefinitions="Auto,Auto,*,Auto,Auto">
<DockPanel Grid.Row="0" Margin="0,0,0,6" LastChildFill="True">
<CheckBox Name="hideArchivalCheck" DockPanel.Dock="Right" FontSize="11"
VerticalAlignment="Center" Margin="8,0,0,0"
Content="{Binding [HideArchivalVehicles], Source={x:Static starterNg:App.Loc}}"
IsCheckedChanged="HideArchivalCheck_OnChanged" />
<TextBox Name="searchBox" VerticalAlignment="Center"
Watermark="{Binding [Search], Source={x:Static starterNg:App.Loc}}"
PlaceholderText="{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}}"
PointerWheelChanged="Combo_OnPointerWheel"
SelectionChanged="CategoryCombo_OnSelectionChanged" />
<Grid Grid.Row="1" RowDefinitions="Auto,Auto" RowSpacing="6" Margin="0,0,0,6">
<DockPanel Grid.Row="0" LastChildFill="True">
<Button Name="textureBaseButton" DockPanel.Dock="Right" Classes="Flat"
Width="30" Margin="4,0,0,0" Cursor="Hand" Content="&#x1F50D;"
HorizontalContentAlignment="Center"
Click="TextureBaseButton_OnClick"
ToolTip.Tip="{Binding [OpenTextureBase], Source={x:Static starterNg:App.Loc}}" />
<ComboBox Name="categoryCombo" HorizontalAlignment="Stretch"
PlaceholderText="{Binding [Category], Source={x:Static starterNg:App.Loc}}"
PointerWheelChanged="Combo_OnPointerWheel"
SelectionChanged="CategoryCombo_OnSelectionChanged" />
</DockPanel>
<ComboBox Name="classCombo" Grid.Row="1" HorizontalAlignment="Stretch"
PlaceholderText="{Binding [Class], Source={x:Static starterNg:App.Loc}}"
PointerWheelChanged="Combo_OnPointerWheel"
SelectionChanged="ClassCombo_OnSelectionChanged" />
</Grid>
<!-- The arrow stays enabled with nothing selected - adding a whole
category needs no highlighted entry. -->
<DockPanel DockPanel.Dock="Bottom" Margin="0,8,0,0">
<Button Name="addMoreButton" DockPanel.Dock="Right" Classes="Flat Accent"
Width="28" Margin="4,0,0,0" Cursor="Hand" Content="&#x25BE;"
HorizontalContentAlignment="Center"
ToolTip.Tip="{Binding [AddAllCategory], Source={x:Static starterNg:App.Loc}}">
<Button.Flyout>
<MenuFlyout Placement="TopEdgeAlignedRight">
<MenuItem Click="AddAllCategoryButton_OnClick"
Header="{Binding [AddAllCategory], Source={x:Static starterNg:App.Loc}}"
ToolTip.Tip="{Binding [TipAddAllCategory], Source={x:Static starterNg:App.Loc}}" />
<MenuItem Click="AddUniqueMmdButton_OnClick"
Header="{Binding [AddUniqueMmd], Source={x:Static starterNg:App.Loc}}"
ToolTip.Tip="{Binding [TipAddUniqueMmd], Source={x:Static starterNg:App.Loc}}" />
<Separator />
<MenuItem Click="TextureBaseButton_OnClick"
Header="{Binding [OpenTextureBase], Source={x:Static starterNg:App.Loc}}" />
</MenuFlyout>
</Button.Flyout>
</Button>
<Button Name="addVehicleButton" Classes="Flat Accent"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center"
IsEnabled="False" Cursor="Hand"
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>
<Border Name="miniPreviewPanel" Grid.Row="3" Height="66"
Margin="0,6,0,0"
Background="{StaticResource CarbonInputBg}"
BorderBrush="{StaticResource CarbonBorderLight}" BorderThickness="1"
CornerRadius="2" Cursor="Hand"
PointerPressed="MiniPreview_OnPointerPressed">
<Image Name="miniPreview" Stretch="Uniform" Margin="6" />
</Border>
<!-- Flat list of vehicle entries (plain text, no expanders).
Selecting one shows its mini and enables Add; double-click
replaces the selected consist vehicle. -->
<ListBox Name="vehicleListBox" Classes="Compact" MinHeight="120"
<Button Name="addVehicleButton" Grid.Row="4" Classes="Flat Accent"
Margin="0,8,0,0" Cursor="Hand" IsEnabled="False"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center"
Click="AddVehicleButton_OnClick"
Content="{Binding [AddVehicle], Source={x:Static starterNg:App.Loc}}" />
<ListBox Name="vehicleListBox" Grid.Row="2" Classes="Compact"
SelectionChanged="VehicleListBox_OnSelectionChanged"
DoubleTapped="VehicleListBox_OnDoubleTapped"
PointerPressed="VehicleListBox_OnPointerPressed" />
</DockPanel>
</Grid>
</Border>
<!-- Consists present on the scenery selected in the Scenarios tab.
Selecting one loads it into the editor below; the header carries the
scenery name (see PopulateSceneryConsists). -->
<HeaderedContentControl Name="mapConsistsPanel" Grid.Column="1" Grid.Row="0"
Theme="{StaticResource GroupBox}">
<DockPanel LastChildFill="True">
<ListBox Name="sceneryConsistList" Classes="Compact"
SelectionChanged="SceneryConsistList_OnSelectionChanged"
KeyDown="SceneryConsistList_OnKeyDown" />
</DockPanel>
<TabControl>
<TabItem Header="{Binding [SceneryConsists], Source={x:Static starterNg:App.Loc}}">
<DockPanel LastChildFill="True">
<Button Name="addToWarehouseButton" DockPanel.Dock="Bottom" Classes="Flat"
Margin="0,6,0,0" HorizontalAlignment="Stretch" Cursor="Hand"
HorizontalContentAlignment="Center"
Click="AddToWarehouseButton_OnClick"
Content="{Binding [AddToWarehouse], Source={x:Static starterNg:App.Loc}}" />
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" Spacing="16"
Margin="2,6,0,0">
<CheckBox Name="depotDrivableOnlyCheck" FontSize="11"
IsCheckedChanged="ConsistFilter_OnChanged"
Content="{Binding [ShowDrivableOnly], Source={x:Static starterNg:App.Loc}}" />
<CheckBox Name="depotShowAiCheck" FontSize="11"
IsCheckedChanged="ConsistFilter_OnChanged"
Content="{Binding [ShowAiVehicles], Source={x:Static starterNg:App.Loc}}" />
</StackPanel>
<ListBox Name="sceneryConsistList" Classes="Compact"
SelectionChanged="SceneryConsistList_OnSelectionChanged"
KeyDown="SceneryConsistList_OnKeyDown" />
</DockPanel>
</TabItem>
<TabItem Header="{Binding [WarehouseConsists], Source={x:Static starterNg:App.Loc}}">
<DockPanel LastChildFill="True">
<Button Name="removeFromWarehouseButton" DockPanel.Dock="Bottom" Classes="Flat"
Margin="0,6,0,0" HorizontalAlignment="Stretch" Cursor="Hand"
HorizontalContentAlignment="Center"
Click="RemoveFromWarehouseButton_OnClick"
Content="{Binding [RemoveFromWarehouse], Source={x:Static starterNg:App.Loc}}" />
<ListBox Name="warehouseList" Classes="Compact"
DoubleTapped="WarehouseList_OnDoubleTapped" />
</DockPanel>
</TabItem>
</TabControl>
</HeaderedContentControl>
<!-- Brakes / Loads editor (middle-right). Header carries the vehicle
name (see UpdateDetails). -->
<HeaderedContentControl Name="selectedVehiclePanel" Grid.Column="1" Grid.Row="1"
<HeaderedContentControl Name="selectedVehiclePanel" Grid.Row="1" Grid.ColumnSpan="2"
Theme="{StaticResource GroupBox}">
<DockPanel LastChildFill="True">
<TabControl>
<TabItem Header="{Binding [Brakes], Source={x:Static starterNg:App.Loc}}">
<Grid ColumnDefinitions="290*,3,380*,3,280*">
<TabControl Grid.Column="0">
<TabItem Header="{Binding [GeneralInfo], Source={x:Static starterNg:App.Loc}}">
<DockPanel LastChildFill="True" Margin="4">
<Button Name="removeVehicleButton" DockPanel.Dock="Bottom" Classes="Flat"
Margin="0,6,0,0" HorizontalAlignment="Stretch" Cursor="Hand"
HorizontalContentAlignment="Center"
Click="RemoveVehicleButton_OnClick"
Content="{Binding [RemoveVehicle], Source={x:Static starterNg:App.Loc}}" />
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Name="generalPanel" Spacing="6" />
</ScrollViewer>
</DockPanel>
</TabItem>
<TabItem Header="{Binding [AboutTexture], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Name="brakesPanel" Margin="4" Spacing="8" />
<StackPanel Name="consistTexturePanel" Margin="4" Spacing="6" />
</ScrollViewer>
</TabItem>
<TabItem Header="{Binding [Loads], Source={x:Static starterNg:App.Loc}}">
</TabControl>
<TabControl Grid.Column="2">
<TabItem Header="{Binding [Configuration], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Name="loadsPanel" Margin="4" Spacing="8" />
<StackPanel Name="loadsPanel" Margin="4" Spacing="6" />
</ScrollViewer>
</TabItem>
<TabItem Header="{Binding [Brakes], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Name="brakesPanel" Margin="4" Spacing="6" />
</ScrollViewer>
</TabItem>
</TabControl>
<TabControl Grid.Column="4">
<TabItem Header="{Binding [Coupling], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Name="couplerPanel" Margin="4" Spacing="6" />
</ScrollViewer>
</TabItem>
<TabItem Header="{Binding [Damage], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Name="damagePanel" Margin="4" Spacing="8" />
<StackPanel Name="damagePanel" Margin="4" Spacing="6" />
</ScrollViewer>
</TabItem>
</TabControl>
</DockPanel>
</Grid>
</HeaderedContentControl>
<!-- Consist (full-width bottom, horizontally scrollable) -->
<HeaderedContentControl Grid.Row="2" Grid.ColumnSpan="2" Theme="{StaticResource GroupBox}"
Header="{Binding [Consist], Source={x:Static starterNg:App.Loc}}">
<DockPanel LastChildFill="True">
<!-- Train totals + randomize tools -->
<DockPanel DockPanel.Dock="Top" Margin="0,0,0,6">
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal" Spacing="4">
<Button Name="randomTexturesButton" Classes="Basic" FontSize="11"
Padding="8,2" Cursor="Hand" Click="RandomTexturesButton_OnClick"
Content="{Binding [RandomizeTextures], Source={x:Static starterNg:App.Loc}}"
ToolTip.Tip="{Binding [TipRandomizeTextures], Source={x:Static starterNg:App.Loc}}" />
<Button Name="randomOrderButton" Classes="Basic" FontSize="11"
Padding="8,2" Cursor="Hand" Click="RandomOrderButton_OnClick"
Content="{Binding [RandomizeOrder], Source={x:Static starterNg:App.Loc}}"
ToolTip.Tip="{Binding [TipRandomizeOrder], Source={x:Static starterNg:App.Loc}}" />
<Button Name="randomOrientButton" Classes="Basic" FontSize="11"
Padding="8,2" Cursor="Hand" Click="RandomOrientButton_OnClick"
Content="{Binding [RandomizeOrientation], Source={x:Static starterNg:App.Loc}}"
ToolTip.Tip="{Binding [TipRandomizeOrientation], Source={x:Static starterNg:App.Loc}}" />
</StackPanel>
<TextBlock Name="trainStats" FontSize="12" Opacity="0.85"
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
<StackPanel Name="trainStats" Orientation="Horizontal"
VerticalAlignment="Center" />
</DockPanel>
<Panel Name="consistDropTarget">
<TextBlock Name="emptyHint" VerticalAlignment="Center" HorizontalAlignment="Center"
TextWrapping="Wrap" MaxWidth="520" Opacity="0.6" TextAlignment="Center"
Text="{Binding [ConsistEmpty], Source={x:Static starterNg:App.Loc}}" />
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
<StackPanel Name="consistStack" Orientation="Horizontal"
VerticalAlignment="Center" Spacing="0" />
<ScrollViewer Name="consistScroll"
HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
<Panel>
<StackPanel Name="consistStack" Orientation="Horizontal"
VerticalAlignment="Center" Spacing="0" />
<Canvas Name="consistOverlay" IsHitTestVisible="False" />
</Panel>
</ScrollViewer>
</Panel>
</DockPanel>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,262 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Layout;
using Avalonia.Media;
using StarterNG.Classes;
using StarterNG.Domain;
using StarterNG.Infrastructure;
namespace StarterNG.Views;
public sealed class RulesWindow : Window
{
private readonly List<List<string>> _rules = new();
private readonly ListBox _ruleList = new() { Classes = { "Compact" } };
private readonly WrapPanel _members = new() { Orientation = Orientation.Horizontal };
private readonly ComboBox _category = new();
private readonly ComboBox _model = new();
private readonly VehicleDatabase _db = GameData.Instance.Vehicles;
public RulesWindow()
{
Title = App.Loc["RandomizeTexturesRules"];
Width = 720;
Height = 460;
MinWidth = 560;
MinHeight = 360;
WindowStartupLocation = WindowStartupLocation.CenterOwner;
LoadRules();
BuildCategories();
_ruleList.SelectionChanged += (_, _) => ShowMembers();
var add = new Button
{
Content = App.Loc["RuleAdd"],
Cursor = new Avalonia.Input.Cursor(Avalonia.Input.StandardCursorType.Hand)
};
add.Classes.Add("Flat");
add.Click += (_, _) => AddToRule();
_category.SelectionChanged += (_, _) => FillModels();
var addRow = new Grid
{
Margin = new Avalonia.Thickness(0, 8, 0, 0),
ColumnDefinitions = new ColumnDefinitions("*,6,*,6,Auto")
};
_category.MinWidth = 0;
_model.MinWidth = 0;
_category.HorizontalAlignment = HorizontalAlignment.Stretch;
_model.HorizontalAlignment = HorizontalAlignment.Stretch;
Grid.SetColumn(_category, 0);
Grid.SetColumn(_model, 2);
Grid.SetColumn(add, 4);
addRow.Children.Add(_category);
addRow.Children.Add(_model);
addRow.Children.Add(add);
var save = new Button
{
Content = App.Loc["RandomizeTexturesRulesSave"],
MinWidth = 110,
Cursor = new Avalonia.Input.Cursor(Avalonia.Input.StandardCursorType.Hand)
};
save.Classes.Add("Accent");
save.Click += async (_, _) => { if (await SaveRules()) Close(); };
var cancel = new Button
{
Content = App.Loc["Cancel"],
MinWidth = 90,
Cursor = new Avalonia.Input.Cursor(Avalonia.Input.StandardCursorType.Hand)
};
cancel.Classes.Add("Flat");
cancel.Click += (_, _) => Close();
var footer = new StackPanel
{
Orientation = Orientation.Horizontal,
HorizontalAlignment = HorizontalAlignment.Right,
Spacing = 8,
Margin = new Avalonia.Thickness(0, 10, 0, 0),
Children = { cancel, save }
};
var right = new DockPanel { LastChildFill = true };
DockPanel.SetDock(addRow, Dock.Bottom);
DockPanel.SetDock(footer, Dock.Bottom);
right.Children.Add(footer);
right.Children.Add(addRow);
right.Children.Add(new ScrollViewer
{
Content = _members,
VerticalScrollBarVisibility = ScrollBarVisibility.Auto
});
var grid = new Grid
{
Margin = new Avalonia.Thickness(10),
ColumnDefinitions = new ColumnDefinitions("260,10,*")
};
Grid.SetColumn(_ruleList, 0);
Grid.SetColumn(right, 2);
grid.Children.Add(_ruleList);
grid.Children.Add(right);
Content = grid;
RefreshList();
}
private void LoadRules()
{
string path = TexRandomizer.RulesPath;
try
{
if (File.Exists(path))
foreach (string line in File.ReadAllLines(path))
{
var members = line.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(x => x.Trim())
.Where(x => x.Length > 0)
.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)
.ToList();
if (members.Count > 0)
_rules.Add(members);
}
}
catch (Exception ex)
{
Diagnostics.Log($"reguly.txt {path}", ex);
}
_rules.Add(new List<string>());
}
private async System.Threading.Tasks.Task<bool> SaveRules()
{
string path = TexRandomizer.RulesPath;
try
{
string? dir = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(dir))
Directory.CreateDirectory(dir);
File.WriteAllLines(path,
_rules.Where(r => r.Count > 0).Select(r => string.Join(",", r)));
return true;
}
catch (Exception ex)
{
await Diagnostics.ReportAsync(path, ex);
return false;
}
}
private void RefreshList()
{
int keep = _ruleList.SelectedIndex;
_ruleList.Items.Clear();
foreach (var rule in _rules)
_ruleList.Items.Add(new ListBoxItem
{
Content = new TextBlock
{
Text = rule.Count == 0 ? App.Loc["RuleNew"] : string.Join(" ", rule),
TextTrimming = TextTrimming.CharacterEllipsis,
Opacity = rule.Count == 0 ? 0.6 : 1
}
});
_ruleList.SelectedIndex = keep >= 0 && keep < _rules.Count ? keep : 0;
ShowMembers();
}
private void ShowMembers()
{
_members.Children.Clear();
int idx = _ruleList.SelectedIndex;
if (idx < 0 || idx >= _rules.Count) return;
foreach (string member in _rules[idx])
{
string name = member;
var btn = new Button
{
Content = name + " ✕",
FontSize = 12,
Margin = new Avalonia.Thickness(0, 0, 4, 4),
Cursor = new Avalonia.Input.Cursor(Avalonia.Input.StandardCursorType.Hand)
};
btn.Classes.Add("Flat");
ToolTip.SetTip(btn, App.Loc["RuleRemove"]);
btn.Click += (_, _) =>
{
_rules[idx].Remove(name);
RefreshList();
};
_members.Children.Add(btn);
}
}
private void BuildCategories()
{
var cats = _db.Textures
.Select(t => t.ResolvedCategory)
.Where(c => !string.IsNullOrEmpty(c))
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(c => c, StringComparer.OrdinalIgnoreCase)
.ToList();
foreach (string? c in cats)
_category.Items.Add(new ComboBoxItem { Content = c, Tag = c });
if (_category.Items.Count > 0)
_category.SelectedIndex = 0;
FillModels();
}
private void FillModels()
{
_model.Items.Clear();
string? cat = (_category.SelectedItem as ComboBoxItem)?.Tag as string;
var models = _db.Textures
.Where(t => cat == null ||
string.Equals(t.ResolvedCategory, cat, StringComparison.OrdinalIgnoreCase))
.Select(t => t.Model ?? "")
.Where(m => m.Length > 0)
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(m => m, StringComparer.OrdinalIgnoreCase);
foreach (string m in models)
_model.Items.Add(m);
if (_model.Items.Count > 0)
_model.SelectedIndex = 0;
}
private void AddToRule()
{
int idx = _ruleList.SelectedIndex;
if (idx < 0 || idx >= _rules.Count) return;
if (_model.SelectedItem is not string model || model.Length == 0) return;
if (_rules[idx].Contains(model, StringComparer.OrdinalIgnoreCase))
return;
_rules[idx].Add(model);
_rules[idx].Sort(StringComparer.OrdinalIgnoreCase);
if (idx == _rules.Count - 1)
_rules.Add(new List<string>());
RefreshList();
}
}

View File

@@ -7,103 +7,104 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="StarterNG.Views.Scenarios"
x:CompileBindings="True">
<Grid Margin="0" ColumnSpacing="8" RowSpacing="8">
<Grid Margin="0" ColumnSpacing="0" RowSpacing="3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="280" />
<ColumnDefinition />
<ColumnDefinition Width="280" />
<ColumnDefinition Width="*" MinWidth="200" MaxWidth="460" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="3.2*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="150" />
</Grid.RowDefinitions>
<!-- Scenery tree (left) -->
<HeaderedContentControl Grid.Column="0" Grid.Row="0" Theme="{StaticResource GroupBox}"
Classes="Compact"
Header="{Binding [Sceneries], Source={x:Static starterNg:App.Loc}}">
<DockPanel LastChildFill="True">
<ToggleSwitch DockPanel.Dock="Bottom" Name="archivalSwitch"
Margin="2,4,0,0" FontSize="11"
IsCheckedChanged="ArchivalSwitch_OnChanged"
OnContent="{Binding [ShowArchival], Source={x:Static starterNg:App.Loc}}"
OffContent="{Binding [ShowArchival], Source={x:Static starterNg:App.Loc}}" />
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" Spacing="8" Margin="2,4,0,0">
<Button x:Name="reloadButton" Classes="Flat" FontSize="11" Padding="8,2" Cursor="Hand"
Click="ReloadSceneries_OnClick"
Content="{Binding [ReloadSceneries], Source={x:Static starterNg:App.Loc}}" />
<Button Classes="Flat" FontSize="11" Padding="8,2" Cursor="Hand"
Click="OpenSceneryDir_OnClick"
Content="{Binding [OpenSceneryFolder], Source={x:Static starterNg:App.Loc}}" />
</StackPanel>
<CheckBox DockPanel.Dock="Bottom" Name="archivalSwitch"
Margin="2,4,0,0" FontSize="11"
IsCheckedChanged="ArchivalSwitch_OnChanged"
Content="{Binding [HideArchivalSceneries], Source={x:Static starterNg:App.Loc}}" />
<TreeView Name="sceneryList" Classes="Compact"
SelectionChanged="SceneryList_OnSelectionChanged" />
SelectionChanged="SceneryList_OnSelectionChanged">
<TreeView.ContextMenu>
<ContextMenu>
<MenuItem Click="ReloadSceneries_OnClick"
Header="{Binding [ReloadSceneries], Source={x:Static starterNg:App.Loc}}" />
<MenuItem Click="OpenSceneryDir_OnClick"
Header="{Binding [OpenSceneryFolder], Source={x:Static starterNg:App.Loc}}" />
</ContextMenu>
</TreeView.ContextMenu>
</TreeView>
</DockPanel>
</HeaderedContentControl>
<!-- Centre: trainsets list (compact, fills) + description/weather/timetable tabs -->
<Grid Grid.Column="1" Grid.Row="0" RowSpacing="8">
<GridSplitter Grid.Column="1" Grid.Row="0" Width="6"
Background="Transparent" ResizeDirection="Columns" />
<Grid Grid.Column="2" Grid.Row="0">
<Grid.RowDefinitions>
<RowDefinition Height="2*" />
<RowDefinition Height="3*" />
<RowDefinition Height="2*" MinHeight="90" />
<RowDefinition Height="4" />
<RowDefinition Height="3*" MinHeight="140" />
</Grid.RowDefinitions>
<HeaderedContentControl Grid.Row="0" Theme="{StaticResource GroupBox}"
Header="{Binding [Vehicles], Source={x:Static starterNg:App.Loc}}">
<DockPanel LastChildFill="True">
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" Spacing="16"
Margin="2,4,0,0">
<CheckBox Name="showAiCheck" FontSize="11"
IsCheckedChanged="VehicleFilter_OnChanged"
Content="{Binding [ShowAiVehicles], Source={x:Static starterNg:App.Loc}}" />
<CheckBox Name="drivableOnlyCheck" FontSize="11"
IsCheckedChanged="VehicleFilter_OnChanged"
Content="{Binding [ShowDrivableOnly], Source={x:Static starterNg:App.Loc}}" />
</StackPanel>
<ListBox Name="vehicleList" Classes="Compact"
SelectionChanged="VehicleList_OnSelectionChanged" />
</DockPanel>
Header="{Binding [SceneryConsists], Source={x:Static starterNg:App.Loc}}">
<HeaderedContentControl.Tag>
<Button Name="randomTexturesButton" Classes="Flat" Cursor="Hand"
FontSize="11" MinHeight="0" Padding="10,1"
Click="RandomTexturesButton_OnClick"
Content="{Binding [RandomizeTextures], Source={x:Static starterNg:App.Loc}}"
ToolTip.Tip="{Binding [TipRandomizeTextures], Source={x:Static starterNg:App.Loc}}" />
</HeaderedContentControl.Tag>
<ListBox Name="vehicleList" Classes="Compact"
SelectionChanged="VehicleList_OnSelectionChanged" />
</HeaderedContentControl>
<TabControl Grid.Row="1">
<GridSplitter Grid.Row="1" Height="4" HorizontalAlignment="Stretch"
Background="Transparent" ResizeDirection="Rows" />
<TabControl Grid.Row="2">
<TabItem Header="{Binding [TabDescription], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<TextBlock TextWrapping="Wrap" Name="missionDescription" Margin="4" />
</ScrollViewer>
<Grid RowDefinitions="2*,6,*" Margin="4">
<Grid Grid.Row="0" ColumnDefinitions="*,3,225">
<HeaderedContentControl Grid.Column="0" Theme="{StaticResource GroupBox}"
Header="{Binding [SceneryDescription], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<TextBlock TextWrapping="Wrap" Name="sceneryDescription" />
</ScrollViewer>
</HeaderedContentControl>
<Border Grid.Column="2" ClipToBounds="True"
Background="{StaticResource CarbonPanelBg}"
BorderBrush="{StaticResource CarbonBorderLight}" BorderThickness="1">
<Image Name="sceneryImage" Stretch="Uniform" Margin="2" />
</Border>
</Grid>
<ScrollViewer Grid.Row="2" VerticalScrollBarVisibility="Auto">
<TextBlock TextWrapping="Wrap" Name="missionDescription" />
</ScrollViewer>
</Grid>
</TabItem>
<TabItem Header="{Binding [Weather], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Name="weatherForm" Margin="8" Spacing="8" IsEnabled="False">
<Grid ColumnDefinitions="130,*" RowSpacing="8">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel Name="weatherForm" Margin="8,6" Spacing="8" IsEnabled="False">
<Grid ColumnDefinitions="Auto,12,380,8,70"
RowDefinitions="Auto,6,Auto,14,Auto,Auto,8,Auto,Auto,14,Auto,14,Auto">
<TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center"
Text="{Binding [Time], Source={x:Static starterNg:App.Loc}}" />
<StackPanel Grid.Row="0" Grid.Column="1" Orientation="Horizontal" Spacing="8">
<TimePicker Name="weatherTime" ClockIdentifier="24HourClock"
MinuteIncrement="1" VerticalAlignment="Center"
SelectedTimeChanged="WeatherTime_OnChanged" />
<Button Name="weatherTimeNow" Classes="Flat" Cursor="Hand"
VerticalAlignment="Center" Padding="10,4"
Click="WeatherTimeNow_OnClick"
Content="{Binding [CurrentTime], Source={x:Static starterNg:App.Loc}}" />
<Button Name="weatherRestore" Classes="Flat" Cursor="Hand"
VerticalAlignment="Center" Padding="10,4"
Click="WeatherRestore_OnClick"
Content="{Binding [RestoreWeather], Source={x:Static starterNg:App.Loc}}" />
</StackPanel>
<TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center"
Text="{Binding [Season], Source={x:Static starterNg:App.Loc}}" />
<ComboBox Grid.Row="1" Grid.Column="1" Name="weatherSeason" Width="180"
HorizontalAlignment="Left"
<TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center"
Foreground="{StaticResource CarbonFgDim}"
Text="{Binding [Date], Source={x:Static starterNg:App.Loc}}" />
<Grid Grid.Row="0" Grid.Column="2" ColumnDefinitions="150,10,150,10,*">
<DatePicker Grid.Column="0" Name="weatherDate"
YearVisible="False" MinWidth="0"
VerticalAlignment="Center" HorizontalAlignment="Stretch"
SelectedDateChanged="WeatherDate_OnChanged" />
<ComboBox Grid.Column="2" Name="weatherSeason"
HorizontalAlignment="Stretch" VerticalAlignment="Center"
SelectionChanged="WeatherSeason_OnChanged">
<ComboBoxItem Tag="0" Content="{Binding [SeasonNow], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="113" Content="{Binding [SeasonSpring], Source={x:Static starterNg:App.Loc}}" />
@@ -111,103 +112,121 @@
<ComboBoxItem Tag="297" Content="{Binding [SeasonAutumn], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="15" Content="{Binding [SeasonWinter], Source={x:Static starterNg:App.Loc}}" />
</ComboBox>
<TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center"
Text="{Binding [Day], Source={x:Static starterNg:App.Loc}}" />
<NumericUpDown Grid.Row="2" Grid.Column="1" Name="weatherDay" Width="140"
HorizontalAlignment="Left" Minimum="0" Maximum="366"
Increment="1" FormatString="0"
ValueChanged="WeatherDay_OnChanged" />
<TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center"
Text="{Binding [Temperature], Source={x:Static starterNg:App.Loc}}" />
<StackPanel Grid.Row="3" Grid.Column="1" Orientation="Horizontal" Spacing="8">
<Slider Name="weatherTemp" Minimum="-50" Maximum="50" Width="220"
TickFrequency="1" IsSnapToTickEnabled="True"
ValueChanged="Weather_OnSliderChanged" />
<TextBlock Name="weatherTempLabel" VerticalAlignment="Center" MinWidth="48" />
</StackPanel>
<TextBlock Grid.Row="4" Grid.Column="0" VerticalAlignment="Center"
Text="{Binding [Visibility], Source={x:Static starterNg:App.Loc}}" />
<StackPanel Grid.Row="4" Grid.Column="1" Orientation="Horizontal" Spacing="8">
<Slider Name="weatherFog" Minimum="0" Maximum="10000" Width="220"
TickFrequency="250" IsSnapToTickEnabled="True"
ValueChanged="Weather_OnSliderChanged" />
<TextBlock Name="weatherFogLabel" VerticalAlignment="Center" MinWidth="56" />
</StackPanel>
<TextBlock Grid.Row="5" Grid.Column="0" VerticalAlignment="Center"
Text="{Binding [Overcast], Source={x:Static starterNg:App.Loc}}" />
<ComboBox Grid.Row="5" Grid.Column="1" Name="weatherOvercast" Width="220"
HorizontalAlignment="Left"
SelectionChanged="Weather_OnSelectionChanged">
<ComboBoxItem Tag="-1.5" Content="{Binding [OvercastClear], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="0" Content="{Binding [OvercastFair], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="0.3" Content="{Binding [OvercastLight], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="0.7" Content="{Binding [OvercastModerate], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="1" Content="{Binding [OvercastHeavy], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="1.1" Content="{Binding [OvercastCloudy], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="1.5" Content="{Binding [OvercastStorm], Source={x:Static starterNg:App.Loc}}" />
</ComboBox>
<TextBlock Grid.Column="4" Name="weatherDayDate"
VerticalAlignment="Center"
Foreground="{StaticResource CarbonFgDim}" />
</Grid>
<TextBlock Name="weatherHint" Opacity="0.6" FontSize="11" TextWrapping="Wrap"
Text="{Binding [WeatherHint], Source={x:Static starterNg:App.Loc}}" />
</StackPanel>
</ScrollViewer>
<TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center"
Foreground="{StaticResource CarbonFgDim}"
Text="{Binding [Time], Source={x:Static starterNg:App.Loc}}" />
<Grid Grid.Row="2" Grid.Column="2" ColumnDefinitions="150,8,*">
<TimePicker Grid.Column="0" Name="weatherTime"
ClockIdentifier="24HourClock" MinuteIncrement="1"
MinWidth="0" VerticalAlignment="Center"
HorizontalAlignment="Stretch"
SelectedTimeChanged="WeatherTime_OnChanged" />
<Button Grid.Column="2" Name="weatherTimeNow"
Classes="Flat" Cursor="Hand" VerticalAlignment="Center"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center"
Click="WeatherTimeNow_OnClick"
Content="{Binding [CurrentTime], Source={x:Static starterNg:App.Loc}}" />
</Grid>
<TextBlock Grid.Row="4" Grid.Column="0" VerticalAlignment="Center"
Foreground="{StaticResource CarbonFgDim}"
Text="{Binding [Temperature], Source={x:Static starterNg:App.Loc}}" />
<Slider Grid.Row="4" Grid.Column="2" Name="weatherTemp"
Minimum="-50" Maximum="50"
TickFrequency="1" IsSnapToTickEnabled="True"
ValueChanged="Weather_OnSliderChanged" />
<TextBlock Grid.Row="4" Grid.Column="4" Name="weatherTempLabel"
VerticalAlignment="Center" TextAlignment="Right"
FontWeight="SemiBold" />
<DockPanel Grid.Row="5" Grid.Column="2" Margin="0,-6,0,0">
<TextBlock DockPanel.Dock="Right" FontSize="11"
Foreground="{StaticResource CarbonFgDim}" Text="+50 °C" />
<TextBlock FontSize="11"
Foreground="{StaticResource CarbonFgDim}" Text="-50 °C" />
</DockPanel>
<TextBlock Grid.Row="7" Grid.Column="0" VerticalAlignment="Center"
Foreground="{StaticResource CarbonFgDim}"
Text="{Binding [Visibility], Source={x:Static starterNg:App.Loc}}" />
<Slider Grid.Row="7" Grid.Column="2" Name="weatherFog"
Minimum="0" Maximum="1000"
TickFrequency="1" IsSnapToTickEnabled="True"
ValueChanged="Weather_OnSliderChanged" />
<TextBlock Grid.Row="7" Grid.Column="4" Name="weatherFogLabel"
VerticalAlignment="Center" TextAlignment="Right"
FontWeight="SemiBold" />
<DockPanel Grid.Row="8" Grid.Column="2" Margin="0,-6,0,0">
<TextBlock DockPanel.Dock="Right" FontSize="11"
Foreground="{StaticResource CarbonFgDim}"
Text="{Binding [VisibilityGood], Source={x:Static starterNg:App.Loc}}" />
<TextBlock FontSize="11" Foreground="{StaticResource CarbonFgDim}"
Text="{Binding [VisibilityBad], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<TextBlock Grid.Row="10" Grid.Column="0" VerticalAlignment="Center"
Foreground="{StaticResource CarbonFgDim}"
Text="{Binding [Overcast], Source={x:Static starterNg:App.Loc}}" />
<ComboBox Grid.Row="10" Grid.Column="2" Name="weatherOvercast"
HorizontalAlignment="Stretch"
SelectionChanged="Weather_OnSelectionChanged">
<ComboBoxItem Tag="random" Content="{Binding [OvercastRandom], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="-1.5" Content="{Binding [OvercastClear], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="0" Content="{Binding [OvercastFair], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="0.3" Content="{Binding [OvercastLight], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="0.7" Content="{Binding [OvercastModerate], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="1" Content="{Binding [OvercastHeavy], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="1.1" Content="{Binding [OvercastCloudy], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="1.5" Content="{Binding [OvercastStorm], Source={x:Static starterNg:App.Loc}}" />
</ComboBox>
<Border Grid.Row="11" Grid.Column="0" Grid.ColumnSpan="5"
Height="1" VerticalAlignment="Center"
Background="{StaticResource CarbonBorderLight}" />
<Button Grid.Row="12" Grid.Column="2" Grid.ColumnSpan="3"
Name="weatherRestore" Classes="Flat" Cursor="Hand"
HorizontalAlignment="Right" MinWidth="180"
HorizontalContentAlignment="Center"
Click="WeatherRestore_OnClick"
Content="{Binding [RestoreWeather], Source={x:Static starterNg:App.Loc}}" />
</Grid>
</StackPanel>
</TabItem>
<TabItem Header="{Binding [Timetable], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto">
<TextBlock Name="timetableContent" Margin="4"
FontFamily="{x:Static infra:MonoFont.Family}"
FontSize="12" TextWrapping="NoWrap" />
</ScrollViewer>
</TabItem>
<TabItem Name="infoTab" Header="{Binding [TabInfo], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<TextBox Name="faultListBox" Margin="4" IsReadOnly="True"
AcceptsReturn="True" TextWrapping="Wrap"
FontFamily="{x:Static infra:MonoFont.Family}" FontSize="12" />
</ScrollViewer>
<TabItem Name="timetableTab" ToolTip.ShowOnDisabled="True"
Header="{Binding [Timetable], Source={x:Static starterNg:App.Loc}}">
<DockPanel LastChildFill="True" Margin="4">
<ScrollViewer DockPanel.Dock="Bottom" HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Disabled" MaxHeight="72">
<StackPanel Name="attachmentsPanel" Orientation="Horizontal" Spacing="6" />
</ScrollViewer>
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto">
<TextBlock Name="timetableContent"
FontFamily="{x:Static infra:MonoFont.Family}"
FontSize="12" TextWrapping="NoWrap" />
</ScrollViewer>
</DockPanel>
</TabItem>
</TabControl>
</Grid>
<!-- Right: scenery image (//$i) + scenery description (//$d) -->
<Grid Grid.Column="2" Grid.Row="0" RowSpacing="8">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<Border Grid.Row="0" ClipToBounds="True" VerticalAlignment="Top"
Background="{StaticResource CarbonPanelBg}"
BorderBrush="{StaticResource CarbonBorderLight}" BorderThickness="1"
Height="{Binding $self.Bounds.Width}">
<Image Name="sceneryImage" Stretch="Uniform" Margin="2" />
</Border>
<HeaderedContentControl Grid.Row="1" Theme="{StaticResource GroupBox}"
Header="{Binding [SceneryDescription], Source={x:Static starterNg:App.Loc}}">
<DockPanel LastChildFill="True">
<StackPanel Name="attachmentsPanel" DockPanel.Dock="Bottom" Spacing="2" Margin="0,6,0,0" />
<StackPanel Name="includesPanel" DockPanel.Dock="Bottom" Spacing="2" Margin="0,6,0,0" />
<ScrollViewer VerticalScrollBarVisibility="Auto">
<TextBlock TextWrapping="Wrap" Name="sceneryDescription" />
</ScrollViewer>
</DockPanel>
</HeaderedContentControl>
</Grid>
<!-- Consist preview (full-width bottom, horizontally scrollable) -->
<HeaderedContentControl Grid.Row="1" Grid.ColumnSpan="3" Theme="{StaticResource GroupBox}"
Header="{Binding [Consist], Source={x:Static starterNg:App.Loc}}">
<DockPanel LastChildFill="True">
<TextBlock Name="trainStats" DockPanel.Dock="Top" FontSize="12"
Opacity="0.85" Margin="0,0,0,6" TextTrimming="CharacterEllipsis" />
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Name="consistStack" />
<StackPanel DockPanel.Dock="Top" Name="trainStats" Orientation="Horizontal"
VerticalAlignment="Center" Margin="0,0,0,6" />
<ScrollViewer Name="consistScroll"
HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
<Panel>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center"
Name="consistStack" />
<Canvas Name="consistOverlay" IsHitTestVisible="False" />
</Panel>
</ScrollViewer>
</DockPanel>
</HeaderedContentControl>

View File

@@ -8,7 +8,9 @@ using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Animation;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Markup.Xaml;
@@ -37,9 +39,7 @@ public partial class Scenarios : UserControl
Sceneries = GameData.Instance.Sceneries;
_loadingFilters = true;
showAiCheck.IsChecked = StarterNG.Classes.Settings.Instance.ShowAiVehicles;
drivableOnlyCheck.IsChecked = StarterNG.Classes.Settings.Instance.DrivableOnly;
archivalSwitch.IsChecked = StarterNG.Classes.Settings.Instance.ShowArchivalSceneries;
archivalSwitch.IsChecked = !StarterNG.Classes.Settings.Instance.ShowArchivalSceneries;
_loadingFilters = false;
BuildSceneryTree();
@@ -63,7 +63,7 @@ public partial class Scenarios : UserControl
{
sceneryList.Items.Clear();
bool showArchival = archivalSwitch.IsChecked ?? true;
bool showArchival = archivalSwitch.IsChecked != true;
var groupNodes = new Dictionary<string, TreeViewItem>();
var topLevel = new List<TreeViewItem>();
@@ -213,15 +213,15 @@ public partial class Scenarios : UserControl
{
vehicleList.Items.Clear();
bool showAi = showAiCheck.IsChecked ?? true;
bool drivableOnly = drivableOnlyCheck.IsChecked ?? true;
bool showAi = StarterNG.Classes.Settings.Instance.ShowAiVehicles;
bool drivableOnly = StarterNG.Classes.Settings.Instance.DrivableOnly;
for (int i = 0; i < scn.Trainsets.Count; i++)
{
var trainset = scn.Trainsets[i];
if (IsAiTrainset(trainset) && !showAi) continue;
if (trainset.Decor && drivableOnly) continue;
if (drivableOnly && !UData.StaffedTrain(trainset)) continue;
var listItem = new ListBoxItem
{
@@ -244,23 +244,11 @@ public partial class Scenarios : UserControl
!string.IsNullOrEmpty(trainset.Description) &&
trainset.Description.TrimStart().StartsWith("-", StringComparison.Ordinal);
private void VehicleFilter_OnChanged(object? sender, RoutedEventArgs e)
{
if (_loadingFilters) return;
StarterNG.Classes.Settings.Instance.ShowAiVehicles = showAiCheck.IsChecked ?? true;
StarterNG.Classes.Settings.Instance.DrivableOnly = drivableOnlyCheck.IsChecked ?? true;
StarterNG.Classes.Settings.Instance.Save();
if (sceneryList.SelectedItem is TreeViewItem { Tag: int tag } && tag < Sceneries.Count)
PopulateVehicleList(Sceneries[tag]);
}
private void ArchivalSwitch_OnChanged(object? sender, RoutedEventArgs e)
{
if (_loadingFilters) return;
StarterNG.Classes.Settings.Instance.ShowArchivalSceneries = archivalSwitch.IsChecked ?? true;
StarterNG.Classes.Settings.Instance.ShowArchivalSceneries = archivalSwitch.IsChecked != true;
StarterNG.Classes.Settings.Instance.Save();
BuildSceneryTree();
@@ -329,9 +317,10 @@ public partial class Scenarios : UserControl
{
sceneryImage.Source = new Bitmap(imagePath);
}
catch
catch (Exception ex)
{
sceneryImage.Source = null;
StarterNG.Infrastructure.Diagnostics.Log($"Scenery image {imagePath}", ex);
}
}
else
@@ -340,7 +329,6 @@ public partial class Scenarios : UserControl
}
PopulateAttachments(scenery);
PopulateIncludes(scenery);
PopulateFaults(scenery);
}
@@ -368,8 +356,8 @@ public partial class Scenarios : UserControl
lines.Add($"# unknown loose texture: {v.Name} ({v.SkinFile})");
}
faultListBox.Text = string.Join(Environment.NewLine, lines);
infoTab.IsVisible = lines.Count > 0 || scenery.Includes.Any(i => i.Kind != 2);
if (lines.Count > 0)
StarterNG.Infrastructure.Diagnostics.Log(lines);
}
private void PopulateAttachments(Scenery scenery)
@@ -392,38 +380,6 @@ public partial class Scenarios : UserControl
}
}
private void PopulateIncludes(Scenery scenery)
{
includesPanel.Children.Clear();
var opts = scenery.Includes.Where(i => i.Kind != 2).ToList();
if (opts.Count == 0) return;
includesPanel.Children.Add(new TextBlock
{
Text = App.Loc["OptionalIncludes"],
FontWeight = Avalonia.Media.FontWeight.Bold,
FontSize = 12,
Margin = new Thickness(0, 0, 0, 2)
});
foreach (var inc in opts)
{
var check = new CheckBox
{
Content = $"{inc.Desc} ({inc.FilePath})",
IsChecked = inc.Selected,
FontSize = 11,
Tag = inc
};
check.IsCheckedChanged += (_, _) =>
{
if (check.Tag is SceneryInclude s)
s.Selected = check.IsChecked == true;
};
includesPanel.Children.Add(check);
}
}
private static void OpenAttachment(string relativePath)
{
string full = Path.GetFullPath(relativePath);
@@ -453,9 +409,12 @@ public partial class Scenarios : UserControl
RefreshSelectedConsist();
}
private bool _reloading;
private async void ReloadSceneries_OnClick(object? sender, RoutedEventArgs e)
{
reloadButton.IsEnabled = false;
if (_reloading) return;
_reloading = true;
try
{
string? selected = AppState.Instance.CurrentScenery?.Path;
@@ -471,22 +430,94 @@ public partial class Scenarios : UserControl
}
finally
{
reloadButton.IsEnabled = true;
_reloading = false;
}
}
public event Action<Scenery>? RandomizeTexturesRequested;
private void RandomTexturesButton_OnClick(object? sender, RoutedEventArgs e)
{
if (AppState.Instance.CurrentScenery is { } scenery)
RandomizeTexturesRequested?.Invoke(scenery);
}
public void RefreshConsistView()
{
RefreshVehicleLabels();
if (AppState.Instance.CurrentTrainset is { } trainset)
ShowConsist(trainset);
}
private static readonly Avalonia.Media.IBrush SelectedVehicleBrush =
new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.Parse("#41C400"));
private static readonly Avalonia.Media.IBrush MissingMiniBrush =
new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.Parse("#22808080"));
private static Control MissingMiniBox(string? label, int height) => new Border
{
Height = height,
Width = height * 2,
Background = MissingMiniBrush,
CornerRadius = new Avalonia.CornerRadius(4),
Child = new TextBlock
{
Text = label,
FontSize = 10,
TextWrapping = Avalonia.Media.TextWrapping.Wrap,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
TextAlignment = Avalonia.Media.TextAlignment.Center
}
};
private readonly Dictionary<string, Bitmap?> _miniCache = new();
private Bitmap? LoadMini(string? name, int height)
{
string key = $"{name}@{height}";
if (_miniCache.TryGetValue(key, out var cached))
return cached;
Bitmap? bmp = null;
string? path = VehicleDatabase.MiniPath(name);
if (path != null)
{
try
{
using var fs = File.OpenRead(path);
bmp = Bitmap.DecodeToHeight(fs, height);
}
catch (Exception ex)
{
StarterNG.Infrastructure.Diagnostics.Log($"Mini {path}", ex);
}
}
_miniCache[key] = bmp;
return bmp;
}
private void OpenSceneryDir_OnClick(object? sender, RoutedEventArgs e)
{
var scn = AppState.Instance.CurrentScenery;
string dir = scn != null
? (Path.GetDirectoryName(Path.GetFullPath(scn.Path)) ?? "scenery")
: Path.GetFullPath("scenery");
if (!Directory.Exists(dir)) return;
if (!Directory.Exists(dir))
{
StarterNG.Infrastructure.Diagnostics.ReportOnUiThread(string.Format(App.Loc["FaultFileNotFound"], dir));
return;
}
try
{
Process.Start(new ProcessStartInfo(dir) { UseShellExecute = true });
}
catch { }
catch (Exception ex)
{
StarterNG.Infrastructure.Diagnostics.ReportOnUiThread(
$"{dir}{Environment.NewLine}{Environment.NewLine}{App.Loc["FaultDetail"]} {ex.Message}");
}
}
private void VehicleList_OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
@@ -518,11 +549,13 @@ public partial class Scenarios : UserControl
{
weatherForm.IsEnabled = true;
weatherTime.SelectedTime = ParseTime(scenery.ScenarioTimeOverride);
weatherDay.Value = scenery.Day;
weatherDate.SelectedDate = DateOfDayOfYear(scenery.Day);
weatherTemp.Value = scenery.Temperature;
weatherFog.Value = System.Math.Clamp(scenery.FogEnd, 0, 10000);
SelectByTag(weatherSeason, scenery.Day.ToString());
SelectByTag(weatherOvercast, scenery.Overcast.ToString(CultureInfo.InvariantCulture));
weatherFog.Value = SliderFromFog(scenery.FogEnd);
SelectByTag(weatherSeason, scenery.Day.ToString(), excludeFromNearest: "0");
SelectByTag(weatherOvercast, scenery.OvercastRandom
? RandomTag
: scenery.Overcast.ToString(CultureInfo.InvariantCulture));
ApplyTodayPreview(scenery.Day == 0);
UpdateWeatherLabels();
@@ -542,12 +575,16 @@ public partial class Scenarios : UserControl
if (weatherTime.SelectedTime is { } t)
scn.ScenarioTimeOverride = $"{t.Hours:D2}:{t.Minutes:D2}";
scn.Day = _todaySeason ? 0 : (int)(weatherDay.Value ?? 0);
scn.Day = _todaySeason ? 0 : SelectedDayOfYear();
scn.Temperature = weatherTemp.Value;
scn.FogEnd = (int)weatherFog.Value;
if ((weatherOvercast.SelectedItem as ComboBoxItem)?.Tag is string ov &&
double.TryParse(ov, System.Globalization.NumberStyles.Float, CultureInfo.InvariantCulture, out double oc))
scn.Overcast = oc;
scn.FogEnd = FogFromSlider(weatherFog.Value);
if ((weatherOvercast.SelectedItem as ComboBoxItem)?.Tag is string ov)
{
scn.OvercastRandom = ov == RandomTag;
if (!scn.OvercastRandom &&
double.TryParse(ov, NumberStyles.Float, CultureInfo.InvariantCulture, out double oc))
scn.Overcast = oc;
}
scn.WeatherDirty = true;
UpdateWeatherLabels();
@@ -556,18 +593,85 @@ public partial class Scenarios : UserControl
private void UpdateWeatherLabels()
{
weatherTempLabel.Text = $"{(int)weatherTemp.Value} °C";
weatherFogLabel.Text = $"{(int)weatherFog.Value} m";
weatherFogLabel.Text = FormatFog(FogFromSlider(weatherFog.Value));
weatherDayDate.Text = string.Format(LanguageCulture, App.Loc["DayOfYear"], SelectedDayOfYear());
weatherRestore.IsEnabled = _currentScenery?.WeatherChanged ?? false;
}
private static void SelectByTag(ComboBox combo, string tag)
private static int FogFromSlider(double position)
{
double metres = Scenery.FogMin *
System.Math.Pow((double)Scenery.FogMax / Scenery.FogMin, position / 1000.0);
int step = metres < 100 ? 5 : metres < 1000 ? 10 : 100;
return System.Math.Clamp((int)(System.Math.Round(metres / step) * step),
Scenery.FogMin, Scenery.FogMax);
}
private static double SliderFromFog(int metres)
{
double clamped = System.Math.Clamp(metres, Scenery.FogMin, Scenery.FogMax);
return 1000.0 * System.Math.Log(clamped / Scenery.FogMin) /
System.Math.Log((double)Scenery.FogMax / Scenery.FogMin);
}
private static string FormatFog(int metres) =>
metres < 1000
? $"{metres} m"
: string.Format(LanguageCulture, "{0:0.#} km", metres / 1000.0);
private static CultureInfo LanguageCulture
{
get
{
string code = App.Loc.CurrentLangCode;
if (code is "en" or "")
return CultureInfo.InvariantCulture;
try
{
return CultureInfo.GetCultureInfo(code == "cz" ? "cs" : code);
}
catch (CultureNotFoundException)
{
return CultureInfo.InvariantCulture;
}
}
}
private const string RandomTag = "random";
private static void SelectByTag(ComboBox combo, string tag, string? excludeFromNearest = null)
{
ComboBoxItem? first = null;
ComboBoxItem? nearest = null;
double bestDistance = double.MaxValue;
bool haveValue = double.TryParse(tag, NumberStyles.Float, CultureInfo.InvariantCulture,
out double value);
foreach (var obj in combo.Items)
if (obj is ComboBoxItem item && (item.Tag as string) == tag)
{
if (obj is not ComboBoxItem item)
continue;
first ??= item;
if ((item.Tag as string) == tag)
{
combo.SelectedItem = item;
return;
}
combo.SelectedItem = null;
if (haveValue && item.Tag is string itemTag && itemTag != excludeFromNearest &&
double.TryParse(itemTag, NumberStyles.Float, CultureInfo.InvariantCulture,
out double itemValue))
{
double distance = System.Math.Abs(itemValue - value);
if (distance < bestDistance)
{
bestDistance = distance;
nearest = item;
}
}
}
combo.SelectedItem = nearest ?? first;
}
private void WeatherTime_OnChanged(object? sender, TimePickerSelectedValueChangedEventArgs e) => CaptureWeather();
@@ -600,12 +704,11 @@ public partial class Scenarios : UserControl
private void ApplyTodayPreview(bool today)
{
_todaySeason = today;
weatherDay.IsEnabled = !today;
if (today)
{
bool prev = _loadingWeather;
_loadingWeather = true;
weatherDay.Value = DateTime.Now.DayOfYear;
weatherDate.SelectedDate = DateOfDayOfYear(0);
_loadingWeather = prev;
}
}
@@ -614,7 +717,28 @@ public partial class Scenarios : UserControl
private void Weather_OnSliderChanged(object? sender, Avalonia.Controls.Primitives.RangeBaseValueChangedEventArgs e) => CaptureWeather();
private void WeatherDay_OnChanged(object? sender, NumericUpDownValueChangedEventArgs e) => CaptureWeather();
private void WeatherDate_OnChanged(object? sender, DatePickerSelectedValueChangedEventArgs e)
{
if (!_loadingWeather)
{
_todaySeason = false;
_loadingWeather = true;
SelectByTag(weatherSeason, SelectedDayOfYear().ToString(), excludeFromNearest: "0");
_loadingWeather = false;
}
CaptureWeather();
}
private static DateTimeOffset DateOfDayOfYear(int day)
{
int year = DateTime.Now.Year;
int last = DateTime.IsLeapYear(year) ? 366 : 365;
int d = System.Math.Clamp(day <= 0 ? DateTime.Now.DayOfYear : day, 1, last);
return new DateTimeOffset(new DateTime(year, 1, 1).AddDays(d - 1));
}
private int SelectedDayOfYear() =>
weatherDate.SelectedDate is { } date ? date.DayOfYear : DateTime.Now.DayOfYear;
private void WeatherSeason_OnChanged(object? sender, SelectionChangedEventArgs e)
{
@@ -631,7 +755,7 @@ public partial class Scenarios : UserControl
{
ApplyTodayPreview(false);
_loadingWeather = true;
weatherDay.Value = day;
weatherDate.SelectedDate = DateOfDayOfYear(day);
_loadingWeather = false;
}
}
@@ -644,19 +768,33 @@ public partial class Scenarios : UserControl
if (path is null)
{
timetableContent.Text = App.Loc["NoTimetable"];
UpdateTimetableTab(false);
return;
}
try
{
timetableContent.Text = File.ReadAllText(path, Encoding.GetEncoding(1250));
UpdateTimetableTab(true);
}
catch
{
timetableContent.Text = App.Loc["NoTimetable"];
UpdateTimetableTab(false);
}
}
private void UpdateTimetableTab(bool hasTimetable)
{
bool usable = hasTimetable || attachmentsPanel.Children.Count > 0;
timetableTab.IsEnabled = usable;
ToolTip.SetTip(timetableTab, usable ? null : App.Loc["NoTimetable"]);
if (!usable && timetableTab.IsSelected && timetableTab.Parent is TabControl tabs)
tabs.SelectedIndex = 0;
}
private static string? ResolveTimetablePath(Scenery scenery, string? name)
{
if (string.IsNullOrWhiteSpace(name) ||
@@ -677,27 +815,127 @@ public partial class Scenarios : UserControl
return candidates.FirstOrDefault(File.Exists);
}
private const double ConsistDragThreshold = 6;
private Trainset? _stripTrainset;
private int _pressIndex = -1;
private Point _pressPoint;
private bool _dragging;
private bool _stripWired;
private int _dragBlockStart = -1;
private int _dragBlockLength;
private double _dragBlockWidth = 32;
private double _grabOffset;
private Control? _carried;
private readonly List<int> _reducedToVehicle = new();
private readonly List<int> _unitStarts = new();
private readonly Infrastructure.DropGap _dropGap = new();
private readonly Infrastructure.DropIndexTracker _dropTracker = new();
private readonly Infrastructure.EdgeScroller _edgeScroller = new();
private void ShowConsist(Trainset trainset)
{
consistStack.Children.Clear();
_stripTrainset = trainset;
_dropGap.Forget();
if (!_stripWired)
{
_stripWired = true;
consistStack.PointerMoved += Strip_OnPointerMoved;
consistStack.PointerReleased += Strip_OnPointerReleased;
consistStack.PointerCaptureLost += (_, _) => CancelConsistDrag();
}
ContextMenu VehicleMenu(Dynamic vehicle) => ConsistContextMenu.Build(
consistStack,
() => trainset,
vehicles =>
{
trainset.Vehicles = vehicles;
RefreshVehicleLabels();
ShowConsist(trainset);
UpdateTrainStats(trainset);
},
removeAll: null,
getVehicle: () => vehicle,
removeVehicle: v =>
{
int gap = trainset.Vehicles.IndexOf(v);
trainset.Vehicles.Remove(v);
bool wasPicked = !string.IsNullOrEmpty(v.Name) &&
string.Equals(v.Name, AppState.Instance.StartingVehicleName,
StringComparison.OrdinalIgnoreCase);
if (wasPicked && trainset.Vehicles.Count > 0)
{
int next = Math.Clamp(gap, 0, trainset.Vehicles.Count - 1);
AppState.Instance.StartingVehicleName = trainset.Vehicles[next].Name;
AppState.Instance.NotifyChanged();
}
RefreshVehicleLabels();
ShowConsist(trainset);
UpdateTrainStats(trainset);
});
var db = GameData.Instance.Vehicles;
foreach (var train in trainset.Vehicles)
{
string miniName = db.MiniForSkin(train.SkinFile) ?? train.SkinFile;
string? path = VehicleDatabase.MiniPath(miniName);
if (path is null)
continue;
if (!VehicleDatabase.HasMini(miniName) && VehicleDatabase.HasMini(train.SkinFile))
miniName = train.SkinFile;
int thumbH = StarterNG.Classes.Settings.Instance.LargeThumbnails ? 64 : 32;
consistStack.Children.Add(new Image
var bmp = LoadMini(miniName, thumbH);
Control visual = bmp is not null
? 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) &&
string.Equals(train.Name, AppState.Instance.StartingVehicleName,
StringComparison.OrdinalIgnoreCase);
var slot = new Border
{
Source = new Bitmap(path),
Height = thumbH,
Stretch = Avalonia.Media.Stretch.Uniform,
Margin = new Thickness(0)
});
Transitions = new Transitions
{
new DoubleTransition
{
Property = OpacityProperty,
Duration = TimeSpan.FromMilliseconds(120)
}
},
Child = visual,
Padding = new Thickness(0),
BorderThickness = new Thickness(0, 2),
BorderBrush = selected ? SelectedVehicleBrush : Avalonia.Media.Brushes.Transparent,
Cursor = new Avalonia.Input.Cursor(Avalonia.Input.StandardCursorType.Hand),
ContextMenu = VehicleMenu(train)
};
ToolTip.SetTip(slot, train.Name);
int slotIndex = consistStack.Children.Count;
slot.PointerPressed += (_, ev) =>
{
if (!ev.GetCurrentPoint(slot).Properties.IsLeftButtonPressed) return;
_pressIndex = slotIndex;
_pressPoint = ev.GetPosition(consistStack);
_dragging = false;
};
consistStack.Children.Add(slot);
}
string desc = trainset.Description ?? "";
if (desc.TrimStart().StartsWith('-'))
@@ -706,22 +944,256 @@ public partial class Scenarios : UserControl
UpdateTrainStats(trainset);
}
private void Strip_OnPointerMoved(object? sender, PointerEventArgs e)
{
var point = e.GetPosition(consistStack);
if (!_dragging)
{
if (_pressIndex < 0 || !e.GetCurrentPoint(consistStack).Properties.IsLeftButtonPressed)
return;
var delta = point - _pressPoint;
if (Math.Abs(delta.X) < ConsistDragThreshold && Math.Abs(delta.Y) < ConsistDragThreshold)
return;
BeginConsistDrag(e);
if (!_dragging) return;
}
Canvas.SetLeft(_carried!, point.X - _grabOffset);
double pointerX = e.GetPosition(consistScroll).X;
if (_dropTracker.Update(pointerX + consistScroll.Offset.X))
{
int reduced = ReducedTarget();
_dropGap.Show(consistStack,
reduced < _reducedToVehicle.Count
? _reducedToVehicle[reduced]
: consistStack.Children.Count,
_dragBlockWidth);
}
_edgeScroller.Update(consistScroll, pointerX);
}
private void Strip_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (!_dragging)
{
int index = _pressIndex;
_pressIndex = -1;
if (e.InitialPressMouseButton != MouseButton.Left ||
index < 0 || _stripTrainset is not { } clicked ||
index >= clicked.Vehicles.Count)
return;
AppState.Instance.StartingVehicleName = clicked.Vehicles[index].Name;
AppState.Instance.NotifyChanged();
ShowConsist(clicked);
return;
}
int target = _dropTracker.Index >= 0 ? ReducedTarget() : -1;
int start = _dragBlockStart;
int length = _dragBlockLength;
var trainset = _stripTrainset;
EndConsistDrag();
if (trainset != null && start >= 0 && target >= 0)
MoveConsistBlock(trainset, start, length, target);
}
private void BeginConsistDrag(PointerEventArgs e)
{
if (_stripTrainset is not { } trainset || _pressIndex < 0 ||
_pressIndex >= trainset.Vehicles.Count)
return;
_dragBlockStart = SetStart(trainset.Vehicles, _pressIndex);
_dragBlockLength = SetLength(trainset.Vehicles, _dragBlockStart);
if (_dragBlockStart + _dragBlockLength > consistStack.Children.Count)
return;
var first = consistStack.Children[_dragBlockStart];
var blockBounds = first.Bounds;
_dragBlockWidth = 0;
for (int i = _dragBlockStart; i < _dragBlockStart + _dragBlockLength; i++)
_dragBlockWidth += consistStack.Children[i].Bounds.Width;
_dragBlockWidth = Math.Max(_dragBlockWidth, 16);
_grabOffset = Math.Clamp(_pressPoint.X - blockBounds.Left, 0, _dragBlockWidth);
CaptureDropTargets(trainset);
_carried = BuildCarriedVisual(blockBounds.Height);
Canvas.SetTop(_carried, blockBounds.Top);
Canvas.SetLeft(_carried, blockBounds.Left);
consistOverlay.Children.Add(_carried);
for (int i = _dragBlockStart; i < _dragBlockStart + _dragBlockLength; i++)
consistStack.Children[i].IsVisible = false;
_dragging = true;
e.Pointer.Capture(consistStack);
}
private void CaptureDropTargets(Trainset trainset)
{
var vehicles = trainset.Vehicles;
_reducedToVehicle.Clear();
for (int i = 0; i < vehicles.Count && i < consistStack.Children.Count; i++)
if (i < _dragBlockStart || i >= _dragBlockStart + _dragBlockLength)
_reducedToVehicle.Add(i);
_unitStarts.Clear();
var midpoints = new List<double>();
int r = 0;
while (r < _reducedToVehicle.Count)
{
int length = 1;
while (r + length < _reducedToVehicle.Count &&
_reducedToVehicle[r + length] == _reducedToVehicle[r + length - 1] + 1 &&
HoldsNext(vehicles[_reducedToVehicle[r + length - 1]]))
length++;
double left = ChildEdge(_reducedToVehicle[r], right: false);
double right = ChildEdge(_reducedToVehicle[r + length - 1], right: true);
midpoints.Add((left + right) / 2);
_unitStarts.Add(r);
r += length;
}
_dropTracker.Capture(midpoints);
}
private double ChildEdge(int childIndex, bool right)
{
var bounds = consistStack.Children[childIndex].Bounds;
double edge = right ? bounds.Right : bounds.Left;
return childIndex > _dragBlockStart ? edge - _dragBlockWidth : edge;
}
private int ReducedTarget()
{
int unit = _dropTracker.Index;
if (unit < 0) return -1;
return unit < _unitStarts.Count ? _unitStarts[unit] : _reducedToVehicle.Count;
}
private Control BuildCarriedVisual(double height)
{
var panel = new StackPanel { Orientation = Avalonia.Layout.Orientation.Horizontal };
for (int i = _dragBlockStart; i < _dragBlockStart + _dragBlockLength; i++)
{
var source = consistStack.Children[i];
Control copy = source is Border { Child: Image image }
? new Image { Source = image.Source, Height = image.Height, Stretch = image.Stretch }
: new Border
{
Width = source.Bounds.Width,
Height = source.Bounds.Height,
Background = MissingMiniBrush
};
panel.Children.Add(copy);
}
return new Border
{
Child = panel,
Height = height,
Opacity = 0.85,
BorderThickness = new Thickness(0, 2),
BorderBrush = SelectedVehicleBrush
};
}
private void CancelConsistDrag()
{
if (!_dragging) return;
EndConsistDrag();
}
private void EndConsistDrag()
{
for (int i = _dragBlockStart; i >= 0 && i < _dragBlockStart + _dragBlockLength; i++)
if (i < consistStack.Children.Count)
consistStack.Children[i].IsVisible = true;
if (_carried != null)
consistOverlay.Children.Remove(_carried);
_carried = null;
_dropTracker.Reset();
_edgeScroller.Stop();
_dropGap.Hide(consistStack);
_dragBlockStart = -1;
_dragBlockLength = 0;
_pressIndex = -1;
_dragging = false;
}
private void MoveConsistBlock(Trainset trainset, int start, int length, int target)
{
var vehicles = trainset.Vehicles;
if (start < 0 || length <= 0 || start + length > vehicles.Count)
return;
var block = vehicles.GetRange(start, length);
vehicles.RemoveRange(start, length);
target = Math.Clamp(target, 0, vehicles.Count);
if (target == start)
{
vehicles.InsertRange(start, block);
return;
}
vehicles.InsertRange(target, block);
RefreshVehicleLabels();
ShowConsist(trainset);
UpdateTrainStats(trainset);
}
private static bool HoldsNext(Dynamic car) =>
car.Coupling.Has(Coupling.WorkshopLock) || car.Coupling.Locked;
private static int SetStart(List<Dynamic> vehicles, int index)
{
while (index > 0 && HoldsNext(vehicles[index - 1]))
index--;
return index;
}
private static int SetLength(List<Dynamic> vehicles, int start)
{
int length = 1;
while (start + length < vehicles.Count && HoldsNext(vehicles[start + length - 1]))
length++;
return length;
}
private void UpdateTrainStats(Trainset? trainset)
{
if (trainset is null)
{
trainStats.Text = "";
trainStats.Children.Clear();
return;
}
var db = GameData.Instance.Vehicles;
trainStats.Text = TrainsetDisplay.FormatStats(
StarterNG.Infrastructure.StatsBar.Fill(trainStats, TrainsetDisplay.StatsFields(
trainset,
car => Physics.For(car.DataFolder, db.TextureForSkin(car.SkinFile)?.Model)
?? Physics.For(car.DataFolder, car.MmdFile)
?? Physics.For(car.DataFolder, car.SkinFile),
App.Loc["Length"], App.Loc["Mass"], App.Loc["VehicleCount"], App.Loc["Track"],
car => db.TextureForSkin(car.SkinFile)?.ResolvedCategory);
car => db.TextureForSkin(car.SkinFile)?.ResolvedCategory));
}
private void RefreshSelectedConsist()

View File

@@ -7,377 +7,551 @@
x:Class="StarterNG.Views.Settings"
x:CompileBindings="True">
<DockPanel>
<!-- Save / reset bar -->
<Border DockPanel.Dock="Bottom" Padding="16,10" Background="{StaticResource CarbonPanelBg}"
BorderBrush="{StaticResource CarbonBorderLight}" BorderThickness="0,1,0,0">
<StackPanel Orientation="Horizontal" Spacing="12" HorizontalAlignment="Right">
<TextBlock x:Name="SaveStatus" VerticalAlignment="Center" Opacity="0.8" Text="" />
<Button x:Name="ResetButton" Classes="Flat" Click="ResetButton_OnClick"
Content="{Binding [Reset], Source={x:Static starterNg:App.Loc}}" />
<Button x:Name="SaveButton" Classes="Flat Accent" Click="SaveButton_OnClick"
Content="{Binding [Save], Source={x:Static starterNg:App.Loc}}" />
</StackPanel>
<Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
<TextBlock VerticalAlignment="Center" Opacity="0.6" FontSize="11"
Text="{Binding [SettingsPath], Source={x:Static starterNg:App.Loc}}" />
<TextBlock x:Name="SettingsPathText" VerticalAlignment="Center" Opacity="0.8" FontSize="11"
TextTrimming="CharacterEllipsis" />
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="12" HorizontalAlignment="Right">
<TextBlock x:Name="SaveStatus" VerticalAlignment="Center" Opacity="0.8" Text="" />
<Button x:Name="AboutButton" Classes="Flat" Click="AboutButton_OnClick"
Content="{Binding [About], Source={x:Static starterNg:App.Loc}}" />
<Button x:Name="ResetButton" Classes="Flat" Click="ResetButton_OnClick"
Content="{Binding [Reset], Source={x:Static starterNg:App.Loc}}" />
<Button x:Name="SaveButton" Classes="Flat Accent" Click="SaveButton_OnClick"
Content="{Binding [Save], Source={x:Static starterNg:App.Loc}}" />
</StackPanel>
</Grid>
</Border>
<TabControl TabStripPlacement="Left">
<!-- General -->
<TabControl TabStripPlacement="Top">
<TabItem Header="{Binding [General], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="8" Orientation="Vertical" Margin="12">
<StackPanel Spacing="8" Orientation="Horizontal">
<Label Content="{Binding [Language], Source={x:Static starterNg:App.Loc}}" VerticalAlignment="Center" FontSize="14" />
<!-- Filled from the lang/ folder in the constructor. -->
<ComboBox x:Name="ChangeLanguageCb" MinWidth="350" MaxWidth="350"
SelectionChanged="LanguageComboBox_OnSelectionChanged" />
</StackPanel>
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
<Grid ColumnDefinitions="*,10,*,10,200" Margin="10">
<CheckBox x:Name="FullscreenCb" Content="{Binding [Fullscreen], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="PauseInactiveCb" Content="{Binding [PauseSimInactive], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="PauseStartCb" Content="{Binding [PauseSimStart], Source={x:Static starterNg:App.Loc}}" />
<StackPanel Grid.Column="0" Spacing="10">
<HeaderedContentControl Theme="{StaticResource GroupBox}"
Header="{Binding [General], Source={x:Static starterNg:App.Loc}}">
<StackPanel Spacing="8">
<StackPanel Classes="Field">
<TextBlock Classes="FieldLabel"
Text="{Binding [Language], Source={x:Static starterNg:App.Loc}}" />
<ComboBox x:Name="ChangeLanguageCb" HorizontalAlignment="Stretch"
SelectionChanged="LanguageComboBox_OnSelectionChanged" />
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<Label VerticalAlignment="Center" FontSize="14" Content="{Binding [CursorSensitivity], Source={x:Static starterNg:App.Loc}}" />
<Slider x:Name="CursorSensitivitySlider" Minimum="1" Maximum="5" TickFrequency="1" MinWidth="200" MaxWidth="200"
IsSnapToTickEnabled="True" Value="3" />
</StackPanel>
<CheckBox x:Name="FullscreenCb" Content="{Binding [Fullscreen], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="PauseInactiveCb" Content="{Binding [PauseSimInactive], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="PauseStartCb" Content="{Binding [PauseSimStart], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="MouseHorInvertCb" Content="{Binding [MouseHorInvert], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="MouseVertInvertCb" Content="{Binding [MouseVertInvert], Source={x:Static starterNg:App.Loc}}" />
</StackPanel>
</ScrollViewer>
</TabItem>
<StackPanel Classes="Field">
<DockPanel>
<TextBlock x:Name="cursorSensitivityValue" DockPanel.Dock="Right" Classes="FieldValue" />
<TextBlock Classes="FieldLabel"
Text="{Binding [CursorSensitivity], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider x:Name="CursorSensitivitySlider" Minimum="1" Maximum="5" TickFrequency="1"
HorizontalAlignment="Stretch"
IsSnapToTickEnabled="True" Value="3"
ValueChanged="QualitySlider_OnValueChanged" />
</StackPanel>
<!-- Communication -->
<TabItem Header="{Binding [Communication], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="8" Orientation="Vertical" Margin="12">
<CheckBox x:Name="GamepadIgnoreCb" Content="{Binding [GamepadSignalIgnore], Source={x:Static starterNg:App.Loc}}" />
<StackPanel Spacing="8" Orientation="Horizontal">
<Label VerticalAlignment="Center" FontSize="14" Content="{Binding [Feedback], Source={x:Static starterNg:App.Loc}}" />
<ComboBox x:Name="FeedbackCb" MinWidth="350" MaxWidth="350" SelectedIndex="0">
<ComboBoxItem Tag="0" Content="{Binding [FeedbackOff], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="1" Content="{Binding [FeedbackCASHP], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="2" Content="{Binding [FeedbackCA], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="3" Content="{Binding [FeedbackLPT], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="4" Content="{Binding [FeedbackPoKeys], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="5" Content="{Binding [FeedbackSP], Source={x:Static starterNg:App.Loc}}" />
</ComboBox>
</StackPanel>
<StackPanel x:Name="FeedbackPortRow" Spacing="8" Orientation="Horizontal" IsVisible="False">
<Label VerticalAlignment="Center" FontSize="14" Content="{Binding [FeedbackPort], Source={x:Static starterNg:App.Loc}}" />
<NumericUpDown x:Name="FeedbackPortNud" Minimum="0" Maximum="65535" Value="888" Width="120" />
</StackPanel>
<Expander x:Name="UartExpander" IsVisible="False" Header="{Binding [UartSection], Source={x:Static starterNg:App.Loc}}">
<StackPanel Spacing="8" Orientation="Vertical" Margin="12">
<CheckBox x:Name="UartEnableCb" Content="{Binding [UartEnable], Source={x:Static starterNg:App.Loc}}" />
<StackPanel Spacing="8" Orientation="Horizontal">
<Label VerticalAlignment="Center" FontSize="14" Content="{Binding [UartPort], Source={x:Static starterNg:App.Loc}}" />
<TextBox x:Name="UartPortTb" MinWidth="150" PlaceholderText="COM3" />
<CheckBox x:Name="MouseHorInvertCb" Content="{Binding [MouseHorInvert], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="MouseVertInvertCb" Content="{Binding [MouseVertInvert], Source={x:Static starterNg:App.Loc}}" />
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<Label VerticalAlignment="Center" FontSize="14" Content="{Binding [UartTune], Source={x:Static starterNg:App.Loc}}" />
<TextBox x:Name="UartTuneTb" MinWidth="150" />
</HeaderedContentControl>
<HeaderedContentControl Theme="{StaticResource GroupBox}"
Header="{Binding [Communication], Source={x:Static starterNg:App.Loc}}">
<StackPanel Spacing="8">
<CheckBox x:Name="GamepadIgnoreCb" Content="{Binding [GamepadSignalIgnore], Source={x:Static starterNg:App.Loc}}" />
<StackPanel Classes="Field">
<TextBlock Classes="FieldLabel"
Text="{Binding [Feedback], Source={x:Static starterNg:App.Loc}}" />
<ComboBox x:Name="FeedbackCb" HorizontalAlignment="Stretch" SelectedIndex="0">
<ComboBoxItem Tag="0" Content="{Binding [FeedbackOff], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="1" Content="{Binding [FeedbackCASHP], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="2" Content="{Binding [FeedbackCA], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="3" Content="{Binding [FeedbackLPT], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="4" Content="{Binding [FeedbackPoKeys], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="5" Content="{Binding [FeedbackSP], Source={x:Static starterNg:App.Loc}}" />
</ComboBox>
</StackPanel>
<StackPanel x:Name="FeedbackPortRow" Classes="Field" IsVisible="False">
<TextBlock Classes="FieldLabel"
Text="{Binding [FeedbackPort], Source={x:Static starterNg:App.Loc}}" />
<NumericUpDown x:Name="FeedbackPortNud" Minimum="0" Maximum="65535" Value="888"
Increment="1" FormatString="0"
HorizontalAlignment="Left" Width="140" />
</StackPanel>
<Button x:Name="ComButton" Classes="Flat" Cursor="Hand"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center"
IsVisible="False" Click="ComButton_OnClick"
Content="{Binding [UartSection], Source={x:Static starterNg:App.Loc}}" />
</StackPanel>
<CheckBox x:Name="UartDebugCb" Content="{Binding [UartDebug], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="UartMainCb" Content="{Binding [UartMain], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="UartScndCb" Content="{Binding [UartScnd], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="UartTrainCb" Content="{Binding [UartTrain], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="UartLocalCb" Content="{Binding [UartLocal], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="UartRadioVolumeCb" Content="{Binding [UartRadioVolume], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="UartRadioChannelCb" Content="{Binding [UartRadioChannel], Source={x:Static starterNg:App.Loc}}" />
</HeaderedContentControl>
</StackPanel>
<StackPanel Grid.Column="2" Spacing="10">
<HeaderedContentControl Theme="{StaticResource GroupBox}"
Header="{Binding [Others], Source={x:Static starterNg:App.Loc}}">
<StackPanel Spacing="8">
<StackPanel Classes="Field">
<TextBlock Classes="FieldLabel"
Text="{Binding [SelectEXE], Source={x:Static starterNg:App.Loc}}" />
<ComboBox x:Name="SelectExeCb" HorizontalAlignment="Stretch" SelectedIndex="0" />
</StackPanel>
<CheckBox x:Name="DebugModeCb" Content="{Binding [DebugMode], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="VirtualShuntingCb" Content="{Binding [VirtualShunting], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="LogMissingVehicleFilesCb" Content="{Binding [LogMissingVehicleFiles], Source={x:Static starterNg:App.Loc}}" />
</StackPanel>
</HeaderedContentControl>
<HeaderedContentControl Theme="{StaticResource GroupBox}"
Header="{Binding [Starter], Source={x:Static starterNg:App.Loc}}">
<StackPanel Spacing="8">
<CheckBox x:Name="AutoCloseStarterCb" Content="{Binding [AutoCloseStarter], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="LargeThumbnailsCb" Content="{Binding [LargeThumbnails], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="AutoExpandTreeCb" Content="{Binding [AutoExpandSceneryTree], Source={x:Static starterNg:App.Loc}}" />
<StackPanel Classes="Field">
<TextBlock Classes="FieldLabel"
Text="{Binding [BatteryDefault], Source={x:Static starterNg:App.Loc}}" />
<ComboBox x:Name="BatteryDefaultCb" HorizontalAlignment="Stretch" SelectedIndex="0">
<ComboBoxItem Content="{Binding [BatteryDefaultScenario], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Content="{Binding [BatteryDefaultOff], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Content="{Binding [BatteryDefaultOn], Source={x:Static starterNg:App.Loc}}" />
</ComboBox>
</StackPanel>
</StackPanel>
</HeaderedContentControl>
</StackPanel>
<HeaderedContentControl Grid.Column="4" VerticalAlignment="Top"
Theme="{StaticResource GroupBox}"
Header="{Binding [SettingsProfile], Source={x:Static starterNg:App.Loc}}">
<StackPanel Spacing="6">
<ComboBox x:Name="ProfileCb" HorizontalAlignment="Stretch" />
<Button x:Name="ProfileApplyBtn" Classes="Flat" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Content="{Binding [ProfileApply], Source={x:Static starterNg:App.Loc}}"
Click="ProfileApplyButton_OnClick" />
<Button x:Name="ProfileSaveAsBtn" Classes="Flat" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Content="{Binding [ProfileSaveAs], Source={x:Static starterNg:App.Loc}}"
Click="ProfileSaveAsButton_OnClick" />
</StackPanel>
</Expander>
</StackPanel>
</HeaderedContentControl>
</Grid>
</ScrollViewer>
</TabItem>
<!-- Controls / key bindings -->
<TabItem Header="{Binding [Controls], Source={x:Static starterNg:App.Loc}}">
<Grid x:Name="ControlsHost" RowDefinitions="Auto,*,Auto" Margin="12">
<!-- Built in code-behind (BuildControlsTab): a search bar, the
scrollable binding list, and the keyboard visualisation. -->
</Grid>
</TabItem>
<!-- Others -->
<TabItem Header="{Binding [Others], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="8" Orientation="Vertical" Margin="12">
<StackPanel Spacing="8" Orientation="Horizontal">
<Label VerticalAlignment="Center" FontSize="14" Content="{Binding [SelectEXE], Source={x:Static starterNg:App.Loc}}" />
<ComboBox x:Name="SelectExeCb" MinWidth="350" MaxWidth="350" SelectedIndex="0" />
</StackPanel>
<CheckBox x:Name="DebugModeCb" Content="{Binding [DebugMode], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="VirtualShuntingCb" Content="{Binding [VirtualShunting], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="LogMissingVehicleFilesCb" Content="{Binding [LogMissingVehicleFiles], Source={x:Static starterNg:App.Loc}}" />
</StackPanel>
</ScrollViewer>
</TabItem>
<!-- Graphic -->
<TabItem Header="{Binding [Graphic], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="8" Orientation="Vertical" Margin="12">
<StackPanel Spacing="8" Orientation="Horizontal">
<Label VerticalAlignment="Center" FontSize="14" Content="{Binding [RenderEngine], Source={x:Static starterNg:App.Loc}}" />
<ComboBox x:Name="RenderEngineCb" MinWidth="150" MaxWidth="150" SelectedIndex="0">
<ComboBoxItem Content="{Binding [REFull], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Content="{Binding [REOld], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Content="{Binding [RESimpShader], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Content="{Binding [RESimp], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Content="{Binding [REOff], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Content="{Binding [REExperimental], Source={x:Static starterNg:App.Loc}}" />
</ComboBox>
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto">
<Grid Margin="10" ColumnDefinitions="250,10,250,10,250">
<HeaderedContentControl Grid.Column="0" VerticalAlignment="Top"
Theme="{StaticResource GroupBox}"
Header="{Binding [GfxColResolution], Source={x:Static starterNg:App.Loc}}">
<StackPanel Spacing="8">
<StackPanel Classes="Field">
<TextBlock Classes="FieldLabel"
Text="{Binding [Resolution], Source={x:Static starterNg:App.Loc}}" />
<ComboBox x:Name="ResolutionCb" HorizontalAlignment="Stretch" SelectedIndex="0">
<ComboBoxItem>3840x2160</ComboBoxItem>
<ComboBoxItem>2560x1440</ComboBoxItem>
<ComboBoxItem>1920x1080</ComboBoxItem>
<ComboBoxItem>1600x900</ComboBoxItem>
<ComboBoxItem>1366x768</ComboBoxItem>
<ComboBoxItem>1280x720</ComboBoxItem>
</ComboBox>
</StackPanel>
<CheckBox x:Name="FullscreenWindowedCb" Content="{Binding [FullscreenWindowed], Source={x:Static starterNg:App.Loc}}" />
<StackPanel Classes="Field">
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldValue"
Text="{Binding #bufferScale.Value, StringFormat='{}{0:0}%'}" />
<TextBlock Classes="FieldLabel"
Text="{Binding [BufforResolution], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider Name="bufferScale" Minimum="50" Maximum="200" TickFrequency="1" IsSnapToTickEnabled="True" Value="100" HorizontalAlignment="Stretch" />
</StackPanel>
<StackPanel Classes="Field">
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldValue"
Name="texResolution" Text="4096 px" />
<TextBlock Classes="FieldLabel"
Text="{Binding [MaxTexResolution], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider Name="textureResolutionSlider" Minimum="9" Maximum="14" TickFrequency="1" IsSnapToTickEnabled="True" Value="12" ValueChanged="TextureResolutionSlider_OnValueChanged" HorizontalAlignment="Stretch" />
</StackPanel>
<StackPanel Classes="Field">
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldValue"
Name="cabTexResolution" Text="4096 px" />
<TextBlock Classes="FieldLabel"
Text="{Binding [MaxTexCabResolution], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider Name="cabTextureResolutionSlider" Minimum="9" Maximum="14" TickFrequency="1" IsSnapToTickEnabled="True" Value="12" ValueChanged="CabTextureResolutionSlider_OnValueChanged" HorizontalAlignment="Stretch" />
</StackPanel>
<StackPanel Classes="Field">
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldValue" Name="texFilteringValue" />
<TextBlock Classes="FieldLabel"
Text="{Binding [TexFilteringQuality], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider x:Name="TexFilteringSlider" ValueChanged="QualitySlider_OnValueChanged" Minimum="1" Maximum="5" TickFrequency="1" IsSnapToTickEnabled="True" Value="4" HorizontalAlignment="Stretch" />
</StackPanel>
<StackPanel Classes="Field">
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldValue" Name="multisamplingValue" />
<TextBlock Classes="FieldLabel"
Text="{Binding [Multisampling], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider x:Name="MultisamplingSlider" ValueChanged="QualitySlider_OnValueChanged" Minimum="1" Maximum="4" TickFrequency="1" IsSnapToTickEnabled="True" Value="3" HorizontalAlignment="Stretch" />
</StackPanel>
<StackPanel Classes="Field">
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldValue" Name="trackCurvesValue" />
<TextBlock Classes="FieldLabel"
Text="{Binding [TrackCurvesQuality], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider x:Name="TrackCurvesSlider" ValueChanged="QualitySlider_OnValueChanged" Minimum="1" Maximum="4" TickFrequency="1" IsSnapToTickEnabled="True" Value="4" HorizontalAlignment="Stretch" />
</StackPanel>
<StackPanel Classes="Field">
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldValue" Name="renderRangeValue" />
<TextBlock Classes="FieldLabel"
Text="{Binding [RenderRange], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider x:Name="RenderRangeSlider" ValueChanged="QualitySlider_OnValueChanged" Minimum="1" Maximum="3" TickFrequency="1" IsSnapToTickEnabled="True" Value="1" HorizontalAlignment="Stretch" />
</StackPanel>
</StackPanel>
</HeaderedContentControl>
<HeaderedContentControl Grid.Column="2" VerticalAlignment="Top"
Theme="{StaticResource GroupBox}"
Header="{Binding [GfxColRendering], Source={x:Static starterNg:App.Loc}}">
<StackPanel Spacing="8">
<StackPanel Classes="Field">
<TextBlock Classes="FieldLabel"
Text="{Binding [RenderEngine], Source={x:Static starterNg:App.Loc}}" />
<ComboBox x:Name="RenderEngineCb" HorizontalAlignment="Stretch" SelectedIndex="0">
<ComboBoxItem Content="{Binding [REFull], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Content="{Binding [REOld], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Content="{Binding [RESimpShader], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Content="{Binding [RESimp], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Content="{Binding [REOff], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Content="{Binding [REExperimental], Source={x:Static starterNg:App.Loc}}" />
</ComboBox>
</StackPanel>
<CheckBox x:Name="RenderAngleVulkanCb" Content="{Binding [RenderAngleVulkan], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="AdditionalShadersCb" Content="{Binding [AdditionalShadersEffects], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="ChromaticAberrationCb" Content="{Binding [ChromaticAberration], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="MotionBlurCb" Content="{Binding [MotionBlur], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="SkipPipelineCb" Content="SkipPipeline" />
<CheckBox x:Name="RenderVBOCb" Content="{Binding [RenderVBO], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="VSyncCb" Content="{Binding [VSync], Source={x:Static starterNg:App.Loc}}" />
<StackPanel Classes="Field">
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldValue"
Text="{Binding #FpsLimitSlider.Value, StringFormat='{}{0:0} FPS'}" />
<CheckBox x:Name="FpsLimitEnableCb" Content="{Binding [FpsLimitEnable], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider x:Name="FpsLimitSlider" Minimum="1" HorizontalAlignment="Stretch"
Maximum="100" TickFrequency="1" IsSnapToTickEnabled="True" Value="30"
IsEnabled="{Binding #FpsLimitEnableCb.IsChecked}" />
</StackPanel>
<Separator />
<CheckBox x:Name="SmokeDisplayCb" Content="{Binding [SmokeDisplay], Source={x:Static starterNg:App.Loc}}" />
<StackPanel Classes="Field" IsEnabled="{Binding #SmokeDisplayCb.IsChecked}">
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldValue" Name="smokeParticlesValue" />
<TextBlock Classes="FieldLabel"
Text="{Binding [SmokeParticlesMultiplier], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider x:Name="SmokeParticlesSlider" ValueChanged="QualitySlider_OnValueChanged" Minimum="1" Maximum="4" TickFrequency="1" IsSnapToTickEnabled="True" Value="2" HorizontalAlignment="Stretch" />
</StackPanel>
<StackPanel Classes="Field">
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldValue"
Text="{Binding #DynamicLightsSlider.Value, StringFormat='{}{0:0}'}" />
<TextBlock Classes="FieldLabel"
Text="{Binding [DynamicLights], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider x:Name="DynamicLightsSlider" Minimum="0" Maximum="7" TickFrequency="1" IsSnapToTickEnabled="True" Value="7" HorizontalAlignment="Stretch" />
</StackPanel>
<StackPanel Classes="Field">
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldValue"
Text="{Binding #fovSlider.Value, StringFormat='{}{0:0}°'}" />
<TextBlock Classes="FieldLabel"
Text="{Binding [FieldOfView], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider Name="fovSlider" Minimum="5" Maximum="110" TickFrequency="5" IsSnapToTickEnabled="True" Value="30" HorizontalAlignment="Stretch" />
</StackPanel>
</StackPanel>
</HeaderedContentControl>
<StackPanel Grid.Column="4" Spacing="10">
<HeaderedContentControl VerticalAlignment="Top"
Theme="{StaticResource GroupBox}"
Header="{Binding [GfxColShadows], Source={x:Static starterNg:App.Loc}}">
<HeaderedContentControl.Tag>
<CheckBox x:Name="RenderShadowsCb" FontSize="11" MinHeight="0"
Content="{Binding [RenderShadows], Source={x:Static starterNg:App.Loc}}" />
</HeaderedContentControl.Tag>
<StackPanel Spacing="8" IsEnabled="{Binding #RenderShadowsCb.IsChecked}">
<StackPanel Classes="Field">
<TextBlock Classes="FieldLabel"
Text="{Binding [ShadowDisplay], Source={x:Static starterNg:App.Loc}}" />
<ComboBox x:Name="ShadowDisplayCb" HorizontalAlignment="Stretch" SelectedIndex="0">
<ComboBoxItem Content="{Binding [CSROnlyImportant], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Content="{Binding [CSRLimited], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Content="{Binding [CSRAll], Source={x:Static starterNg:App.Loc}}" />
</ComboBox>
</StackPanel>
<StackPanel Classes="Field">
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldValue" Name="shaderResolution" />
<TextBlock Classes="FieldLabel"
Text="{Binding [ShadowsResolution], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider Name="shaderResolutionSlider" Minimum="9" Maximum="14" TickFrequency="1" IsSnapToTickEnabled="True" Value="12" ValueChanged="shaderResolutionSlider_OnValueChanged" HorizontalAlignment="Stretch" />
</StackPanel>
<StackPanel Classes="Field">
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldValue" Name="shaderRangeValue" />
<TextBlock Classes="FieldLabel"
Text="{Binding [ShaderRange], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider Name="shaderRange" Minimum="25" Maximum="400" TickFrequency="5" IsSnapToTickEnabled="True" Value="150"
ValueChanged="QualitySlider_OnValueChanged" HorizontalAlignment="Stretch" />
</StackPanel>
<StackPanel Classes="Field">
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldValue" Name="cabShadowRangeValue" />
<TextBlock Classes="FieldLabel"
Text="{Binding [CabShadowsRange], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider Name="cabShaderSourceRange" Minimum="0" Maximum="100" TickFrequency="10" IsSnapToTickEnabled="True" Value="30"
ValueChanged="QualitySlider_OnValueChanged" HorizontalAlignment="Stretch" />
</StackPanel>
<StackPanel Classes="Field">
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldValue"
Text="{Binding #ShadowAngleLimitSlider.Value, StringFormat='{}{0:0.0}'}" />
<TextBlock Classes="FieldLabel"
Text="{Binding [ShadowAngleLimit], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider x:Name="ShadowAngleLimitSlider" Minimum="0.2" Maximum="1.0" TickFrequency="0.1" Value="0.2" HorizontalAlignment="Stretch" />
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldHint"
Text="{Binding [ShadowLimitShort], Source={x:Static starterNg:App.Loc}}" />
<TextBlock Classes="FieldHint"
Text="{Binding [ShadowLimitNone], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
</StackPanel>
</StackPanel>
</HeaderedContentControl>
<HeaderedContentControl Theme="{StaticResource GroupBox}"
Header="{Binding [GfxColReflections], Source={x:Static starterNg:App.Loc}}">
<StackPanel Spacing="8">
<CheckBox x:Name="ReflectionsCubeMapCb" Content="{Binding [ReflectionsCubeMap], Source={x:Static starterNg:App.Loc}}" />
<StackPanel Classes="Field">
<TextBlock Classes="FieldLabel"
Text="{Binding [ReflectionsDetails], Source={x:Static starterNg:App.Loc}}" />
<ComboBox x:Name="ReflectionsDetailsCb" HorizontalAlignment="Stretch" SelectedIndex="0">
<ComboBoxItem Content="{Binding [RDTerrain], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Content="{Binding [RDTerrainModels], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Content="{Binding [RDTerrainModelsVehicles], Source={x:Static starterNg:App.Loc}}" />
</ComboBox>
</StackPanel>
<StackPanel Classes="Field">
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldValue" Name="reflectionsFramerateValue" />
<TextBlock Classes="FieldLabel"
Text="{Binding [ReflectionsFramerate], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider Name="reflectionsFramerate" Minimum="5" Maximum="120" TickFrequency="5" IsSnapToTickEnabled="True" Value="30"
ValueChanged="QualitySlider_OnValueChanged" HorizontalAlignment="Stretch" />
</StackPanel>
</StackPanel>
</HeaderedContentControl>
<HeaderedContentControl Theme="{StaticResource GroupBox}"
Header="{Binding [GfxColScripts], Source={x:Static starterNg:App.Loc}}">
<StackPanel Spacing="8">
<CheckBox x:Name="RenderScreensCb" Content="{Binding [RenderScreens], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="RenderScreensThreadCb" Content="{Binding [RenderScreensThread], Source={x:Static starterNg:App.Loc}}" />
<StackPanel Classes="Field">
<TextBlock Classes="FieldLabel"
Text="{Binding [PyScreenPriority], Source={x:Static starterNg:App.Loc}}" />
<ComboBox x:Name="PyScreenPriorityCb" HorizontalAlignment="Stretch">
<ComboBoxItem Tag="normal" Content="{Binding [PyPriorityNormal], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="lower" Content="{Binding [PyPriorityLower], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="lowest" Content="{Binding [PyPriorityLowest], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="idle" Content="{Binding [PyPriorityIdle], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Tag="off" Content="{Binding [PyPriorityOff], Source={x:Static starterNg:App.Loc}}" />
</ComboBox>
</StackPanel>
</StackPanel>
</HeaderedContentControl>
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<Label VerticalAlignment="Center" FontSize="14" Content="{Binding [Resolution], Source={x:Static starterNg:App.Loc}}" />
<ComboBox x:Name="ResolutionCb" MinWidth="150" MaxWidth="150" SelectedIndex="0">
<ComboBoxItem>3840x2160</ComboBoxItem>
<ComboBoxItem>2560x1440</ComboBoxItem>
<ComboBoxItem>1920x1080</ComboBoxItem>
<ComboBoxItem>1600x900</ComboBoxItem>
<ComboBoxItem>1366x768</ComboBoxItem>
<ComboBoxItem>1280x720</ComboBoxItem>
</ComboBox>
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [BufforResolution], Source={x:Static starterNg:App.Loc}}" />
<Slider Name="bufferScale" Minimum="50" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="200" TickFrequency="1" IsSnapToTickEnabled="True" Value="100" />
<TextBlock VerticalAlignment="Center" Text="{Binding #bufferScale.Value, StringFormat='{}{0}%'}" />
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [MaxTexResolution], Source={x:Static starterNg:App.Loc}}" />
<Slider Name="textureResolutionSlider" Minimum="9" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="14" ValueChanged="TextureResolutionSlider_OnValueChanged"
TickFrequency="1" IsSnapToTickEnabled="True" Value="12" />
<TextBlock Name="texResolution" VerticalAlignment="Center" Text="4096" />
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [MaxTexCabResolution], Source={x:Static starterNg:App.Loc}}" />
<Slider Name="cabTextureResolutionSlider" Minimum="9" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="14" ValueChanged="CabTextureResolutionSlider_OnValueChanged"
TickFrequency="1" IsSnapToTickEnabled="True" Value="12" />
<TextBlock Name="cabTexResolution" VerticalAlignment="Center" Text="4096" />
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [TexFilteringQuality], Source={x:Static starterNg:App.Loc}}" />
<Slider x:Name="TexFilteringSlider" Minimum="1" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="5" TickFrequency="1" IsSnapToTickEnabled="True" Value="4" />
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [Multisampling], Source={x:Static starterNg:App.Loc}}" />
<Slider x:Name="MultisamplingSlider" Minimum="1" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="4" TickFrequency="1" IsSnapToTickEnabled="True" Value="3" />
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [DynamicLights], Source={x:Static starterNg:App.Loc}}" />
<Slider x:Name="DynamicLightsSlider" Minimum="0" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="7" TickFrequency="1" IsSnapToTickEnabled="True" Value="7" />
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [RenderRange], Source={x:Static starterNg:App.Loc}}" />
<Slider x:Name="RenderRangeSlider" Minimum="1" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="3" Value="1" />
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [TrackCurvesQuality], Source={x:Static starterNg:App.Loc}}" />
<Slider x:Name="TrackCurvesSlider" Minimum="1" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="4" TickFrequency="1" IsSnapToTickEnabled="True" Value="4" />
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<CheckBox x:Name="FpsLimitEnableCb" Content="{Binding [FpsLimitEnable], Source={x:Static starterNg:App.Loc}}" />
<Slider x:Name="FpsLimitSlider" Minimum="1" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="100" TickFrequency="1" IsSnapToTickEnabled="True" Value="30" />
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [ShadowAngleLimit], Source={x:Static starterNg:App.Loc}}" />
<Slider x:Name="ShadowAngleLimitSlider" Minimum="0.2" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="1.0" TickFrequency="0.1" Value="0.2" />
</StackPanel>
<CheckBox x:Name="FullscreenWindowedCb" Content="{Binding [FullscreenWindowed], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="RenderAngleVulkanCb" Content="{Binding [RenderAngleVulkan], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="VSyncCb" Content="{Binding [VSync], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="SmokeDisplayCb" Content="{Binding [SmokeDisplay], Source={x:Static starterNg:App.Loc}}" />
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [SmokeParticlesMultiplier], Source={x:Static starterNg:App.Loc}}" />
<Slider x:Name="SmokeParticlesSlider" Minimum="1" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="4" TickFrequency="1" IsSnapToTickEnabled="True" Value="2" />
</StackPanel>
<CheckBox x:Name="ChromaticAberrationCb" Content="{Binding [ChromaticAberration], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="MotionBlurCb" Content="{Binding [MotionBlur], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="AdditionalShadersCb" Content="{Binding [AdditionalShadersEffects], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="ReflectionsCubeMapCb" Content="{Binding [ReflectionsCubeMap], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="RenderVBOCb" Content="{Binding [RenderVBO], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="RenderShadowsCb" Content="{Binding [RenderShadows], Source={x:Static starterNg:App.Loc}}" />
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [ReflectionsFramerate], Source={x:Static starterNg:App.Loc}}" />
<Slider Name="reflectionsFramerate" Minimum="5" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="120" TickFrequency="5" IsSnapToTickEnabled="True" Value="30" />
<Label VerticalAlignment="Center" FontSize="14" Content="{Binding #reflectionsFramerate.Value, StringFormat='{}{0} FPS'}" />
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [ShadowsResolution], Source={x:Static starterNg:App.Loc}}" />
<Slider Name="shaderResolutionSlider" Minimum="9" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="14" ValueChanged="shaderResolutionSlider_OnValueChanged"
TickFrequency="1" IsSnapToTickEnabled="True" Value="12" />
<TextBlock Name="shaderResolution" VerticalAlignment="Center" Text="4096 px" />
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [ShaderRange], Source={x:Static starterNg:App.Loc}}" />
<Slider Name="shaderRange" Minimum="25" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="400" TickFrequency="5" IsSnapToTickEnabled="True" Value="150" />
<Label VerticalAlignment="Center" FontSize="14" Content="{Binding #shaderRange.Value, StringFormat='{}{0} m'}" />
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [CabShadowsRange], Source={x:Static starterNg:App.Loc}}" />
<Slider Name="cabShaderSourceRange" Minimum="0" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="100" TickFrequency="10" IsSnapToTickEnabled="True" Value="30" />
<Label VerticalAlignment="Center" FontSize="14" Content="{Binding #cabShaderSourceRange.Value, StringFormat='{}{0} m'}" />
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [ShadowDisplay], Source={x:Static starterNg:App.Loc}}" />
<ComboBox x:Name="ShadowDisplayCb" MinWidth="150" MaxWidth="150" SelectedIndex="0">
<ComboBoxItem Content="{Binding [CSROnlyImportant], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Content="{Binding [CSRLimited], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Content="{Binding [CSRAll], Source={x:Static starterNg:App.Loc}}" />
</ComboBox>
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [ReflectionsDetails], Source={x:Static starterNg:App.Loc}}" />
<ComboBox x:Name="ReflectionsDetailsCb" MinWidth="350" MaxWidth="350" SelectedIndex="0">
<ComboBoxItem Content="{Binding [RDTerrain], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Content="{Binding [RDTerrainModels], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Content="{Binding [RDTerrainModelsVehicles], Source={x:Static starterNg:App.Loc}}" />
</ComboBox>
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [FieldOfView], Source={x:Static starterNg:App.Loc}}" />
<Slider Name="fovSlider" Minimum="5" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="110" TickFrequency="5" IsSnapToTickEnabled="True" Value="30" />
<Label VerticalAlignment="Center" FontSize="14" Content="{Binding #fovSlider.Value, StringFormat='{}{0}°'}" />
</StackPanel>
<CheckBox x:Name="RenderScreensCb" Content="{Binding [RenderScreens], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="RenderScreensThreadCb" Content="{Binding [RenderScreensThread], Source={x:Static starterNg:App.Loc}}" />
</StackPanel>
</Grid>
</ScrollViewer>
</TabItem>
<!-- Physics -->
<TabItem Header="{Binding [Physics], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="8" Orientation="Vertical" Margin="12">
<CheckBox x:Name="PhysicsAccuracyCb" Content="{Binding [PhysicsAccuracyIncreased], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="PantographBreakCb" Content="{Binding [BreakingPantographPossibility], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="OverheadOnlyCb" Content="{Binding [PowerOverheadLineOnly], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="SpeedometerTapesCb" Content="{Binding [SaveSpeedometerTapes], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="SimLogsCb" Content="{Binding [SaveSimLogs], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="KeepLogsCb" Content="{Binding [KeepPrevLogs], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="DisplaySimulationCb" Content="{Binding [DisplaySimulation], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="CrashDamageCb" Content="{Binding [CrashDamage], Source={x:Static starterNg:App.Loc}}" />
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [Friction], Source={x:Static starterNg:App.Loc}}" />
<Slider x:Name="FrictionSlider" Minimum="0.1" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="3.0" TickFrequency="0.1" Value="1.0" />
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [BrakeStep], Source={x:Static starterNg:App.Loc}}" />
<Slider x:Name="BrakeStepSlider" Minimum="0.4" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="3.0" TickFrequency="0.1" Value="1.0" />
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [BrakeSpeed], Source={x:Static starterNg:App.Loc}}" />
<Slider x:Name="BrakeSpeedSlider" Minimum="0.4" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="3.0" TickFrequency="0.1" Value="1.0" />
</StackPanel>
</StackPanel>
<TabItem Header="{Binding [SoundPhysics], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto">
<Grid ColumnDefinitions="360,10,360" Margin="10">
<HeaderedContentControl Grid.Column="0" VerticalAlignment="Top"
Theme="{StaticResource GroupBox}"
Header="{Binding [Sounds], Source={x:Static starterNg:App.Loc}}">
<StackPanel Spacing="8">
<CheckBox x:Name="EnableSoundsCb" Content="{Binding [EnableSounds], Source={x:Static starterNg:App.Loc}}" />
<StackPanel Classes="Field">
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldValue"
Text="{Binding #VolumeSlider.Value, StringFormat='{}{0:0}%'}" />
<TextBlock Classes="FieldLabel" Text="{Binding [Volume], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider x:Name="VolumeSlider" Minimum="1" HorizontalAlignment="Stretch"
Maximum="100" TickFrequency="5" IsSnapToTickEnabled="True" Value="100" />
</StackPanel>
<StackPanel Classes="Field">
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldValue"
Text="{Binding #RadioVolumeSlider.Value, StringFormat='{}{0:0}%'}" />
<TextBlock Classes="FieldLabel" Text="{Binding [RadiophoneVolume], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider x:Name="RadioVolumeSlider" Minimum="1" HorizontalAlignment="Stretch"
Maximum="100" TickFrequency="5" IsSnapToTickEnabled="True" Value="100" />
</StackPanel>
<StackPanel Classes="Field">
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldValue"
Text="{Binding #VehiclesVolumeSlider.Value, StringFormat='{}{0:0}%'}" />
<TextBlock Classes="FieldLabel" Text="{Binding [VehiclesVolume], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider x:Name="VehiclesVolumeSlider" Minimum="1" HorizontalAlignment="Stretch"
Maximum="100" TickFrequency="5" IsSnapToTickEnabled="True" Value="100" />
</StackPanel>
<StackPanel Classes="Field">
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldValue"
Text="{Binding #PositionalVolumeSlider.Value, StringFormat='{}{0:0}%'}" />
<TextBlock Classes="FieldLabel" Text="{Binding [PositionedSoundsVolume], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider x:Name="PositionalVolumeSlider" Minimum="1" HorizontalAlignment="Stretch"
Maximum="100" TickFrequency="5" IsSnapToTickEnabled="True" Value="100" />
</StackPanel>
<StackPanel Classes="Field">
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldValue"
Text="{Binding #AmbientVolumeSlider.Value, StringFormat='{}{0:0}%'}" />
<TextBlock Classes="FieldLabel" Text="{Binding [AmbientVolume], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider x:Name="AmbientVolumeSlider" Minimum="1" HorizontalAlignment="Stretch"
Maximum="100" TickFrequency="5" IsSnapToTickEnabled="True" Value="100" />
</StackPanel>
<StackPanel Classes="Field">
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldValue"
Text="{Binding #VolumePausedSlider.Value, StringFormat='{}{0:0}%'}" />
<TextBlock Classes="FieldLabel" Text="{Binding [VolumePaused], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider x:Name="VolumePausedSlider" Minimum="0" HorizontalAlignment="Stretch"
Maximum="100" TickFrequency="5" IsSnapToTickEnabled="True" Value="100" />
</StackPanel>
</StackPanel>
</HeaderedContentControl>
<HeaderedContentControl Grid.Column="2" VerticalAlignment="Top"
Theme="{StaticResource GroupBox}"
Header="{Binding [Physics], Source={x:Static starterNg:App.Loc}}">
<StackPanel Spacing="8">
<CheckBox x:Name="PhysicsAccuracyCb" Content="{Binding [PhysicsAccuracyIncreased], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="PantographBreakCb" Content="{Binding [BreakingPantographPossibility], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="OverheadOnlyCb" Content="{Binding [PowerOverheadLineOnly], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="SpeedometerTapesCb" Content="{Binding [SaveSpeedometerTapes], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="SimLogsCb" Content="{Binding [SaveSimLogs], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="KeepLogsCb" Content="{Binding [KeepPrevLogs], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="DisplaySimulationCb" Content="{Binding [DisplaySimulation], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="DebugLogVisibleCb" Content="{Binding [DebugLogVisible], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="CrashDamageCb" Content="{Binding [CrashDamage], Source={x:Static starterNg:App.Loc}}" />
<Separator />
<StackPanel Classes="Field">
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldValue"
Text="{Binding #FrictionSlider.Value, StringFormat='{}{0:0.00}'}" />
<TextBlock Classes="FieldLabel" Text="{Binding [Friction], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider x:Name="FrictionSlider" Minimum="0.1" HorizontalAlignment="Stretch"
Maximum="3.0" TickFrequency="0.1" Value="1.0" />
</StackPanel>
<StackPanel Classes="Field">
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldValue"
Text="{Binding #BrakeStepSlider.Value, StringFormat='{}{0:0.00}'}" />
<TextBlock Classes="FieldLabel" Text="{Binding [BrakeStep], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider x:Name="BrakeStepSlider" Minimum="0.4" HorizontalAlignment="Stretch"
Maximum="3.0" TickFrequency="0.1" Value="1.0" />
</StackPanel>
<StackPanel Classes="Field">
<DockPanel>
<TextBlock DockPanel.Dock="Right" Classes="FieldValue"
Text="{Binding #BrakeSpeedSlider.Value, StringFormat='{}{0:0.00}'}" />
<TextBlock Classes="FieldLabel" Text="{Binding [BrakeSpeed], Source={x:Static starterNg:App.Loc}}" />
</DockPanel>
<Slider x:Name="BrakeSpeedSlider" Minimum="0.4" HorizontalAlignment="Stretch"
Maximum="3.0" TickFrequency="0.1" Value="1.0" />
</StackPanel>
</StackPanel>
</HeaderedContentControl>
</Grid>
</ScrollViewer>
</TabItem>
<!-- Sounds -->
<TabItem Header="{Binding [Sounds], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="8" Orientation="Vertical" Margin="12">
<CheckBox x:Name="EnableSoundsCb" Content="{Binding [EnableSounds], Source={x:Static starterNg:App.Loc}}" />
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [Volume], Source={x:Static starterNg:App.Loc}}" />
<Slider x:Name="VolumeSlider" Minimum="1" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="100" TickFrequency="5" IsSnapToTickEnabled="True" Value="100" />
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [RadiophoneVolume], Source={x:Static starterNg:App.Loc}}" />
<Slider x:Name="RadioVolumeSlider" Minimum="1" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="100" TickFrequency="5" IsSnapToTickEnabled="True" Value="100" />
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [VehiclesVolume], Source={x:Static starterNg:App.Loc}}" />
<Slider x:Name="VehiclesVolumeSlider" Minimum="1" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="100" TickFrequency="5" IsSnapToTickEnabled="True" Value="100" />
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [PositionedSoundsVolume], Source={x:Static starterNg:App.Loc}}" />
<Slider x:Name="PositionalVolumeSlider" Minimum="1" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="100" TickFrequency="5" IsSnapToTickEnabled="True" Value="100" />
</StackPanel>
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [AmbientVolume], Source={x:Static starterNg:App.Loc}}" />
<Slider x:Name="AmbientVolumeSlider" Minimum="1" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="100" TickFrequency="5" IsSnapToTickEnabled="True" Value="100" />
</StackPanel>
</StackPanel>
</ScrollViewer>
</TabItem>
<TabItem Header="{Binding [Controls], Source={x:Static starterNg:App.Loc}}">
<Grid x:Name="ControlsHost" RowDefinitions="Auto,*,Auto" Margin="10">
</Grid>
</TabItem>
<!-- Advanced -->
<TabItem Header="{Binding [Advanced], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="8" Orientation="Vertical" Margin="12">
<CheckBox x:Name="CompressTexturesCb" Content="{Binding [CompressTextures], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="ScaleSpecularsCb" Content="{Binding [ScaleSpeculars], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="UseGLESCb" Content="{Binding [UseGLES], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="ShaderGammaCb" Content="{Binding [ShaderGamma], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="ScreenMipmapsCb" Content="{Binding [ScreenMipmaps], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="ExtendedModelConversionCb" Content="{Binding [ExtendedModelConversion], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="GfxResourceMoveCb" Content="{Binding [GfxResourceMove], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="GfxResourceSweepCb" Content="{Binding [GfxResourceSweep], Source={x:Static starterNg:App.Loc}}" />
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" FontSize="14" Text="{Binding [TrainPhysicsThreads], Source={x:Static starterNg:App.Loc}}" />
<Slider x:Name="TrainPhysicsThreadsSlider" Minimum="0" MaxWidth="400" HorizontalAlignment="Left"
MinWidth="250" Maximum="0" TickFrequency="1" IsSnapToTickEnabled="True" Value="0" />
<Label VerticalAlignment="Center" FontSize="14" Content="{Binding #TrainPhysicsThreadsSlider.Value}" />
</StackPanel>
<CheckBox x:Name="IgnoreIrrelevantTrainsCb" Content="{Binding [IgnoreIrrelevantTrains], Source={x:Static starterNg:App.Loc}}" />
</StackPanel>
</ScrollViewer>
</TabItem>
<!-- Starter -->
<TabItem Header="{Binding [Starter], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="8" Orientation="Vertical" Margin="12">
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" Text="{Binding [SettingsProfile], Source={x:Static starterNg:App.Loc}}" />
<ComboBox x:Name="ProfileCb" MinWidth="220" />
<Button x:Name="ProfileApplyBtn" Content="{Binding [ProfileApply], Source={x:Static starterNg:App.Loc}}"
Click="ProfileApplyButton_OnClick" />
<Button x:Name="ProfileSaveAsBtn" Content="{Binding [ProfileSaveAs], Source={x:Static starterNg:App.Loc}}"
Click="ProfileSaveAsButton_OnClick" />
</StackPanel>
<CheckBox x:Name="AutoCloseStarterCb" Content="{Binding [AutoCloseStarter], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="LargeThumbnailsCb" Content="{Binding [LargeThumbnails], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="AutoExpandTreeCb" Content="{Binding [AutoExpandSceneryTree], Source={x:Static starterNg:App.Loc}}" />
<CheckBox x:Name="HideArchivalVehiclesCb" Content="{Binding [HideArchivalVehicles], Source={x:Static starterNg:App.Loc}}" />
<StackPanel Spacing="8" Orientation="Horizontal">
<TextBlock VerticalAlignment="Center" Text="{Binding [BatteryDefault], Source={x:Static starterNg:App.Loc}}" />
<ComboBox x:Name="BatteryDefaultCb" MinWidth="220" SelectedIndex="0">
<ComboBoxItem Content="{Binding [BatteryDefaultScenario], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Content="{Binding [BatteryDefaultOff], Source={x:Static starterNg:App.Loc}}" />
<ComboBoxItem Content="{Binding [BatteryDefaultOn], Source={x:Static starterNg:App.Loc}}" />
</ComboBox>
</StackPanel>
</StackPanel>
</ScrollViewer>
</TabItem>
</TabControl>
</DockPanel>
</UserControl>

View File

@@ -26,18 +26,22 @@ public partial class Settings : UserControl
{
InitializeComponent();
TrainPhysicsThreadsSlider.Maximum = Environment.ProcessorCount;
SettingsPathText.Text = StarterNG.Classes.Settings.Instance.LoadedFrom;
ToolTip.SetTip(SettingsPathText, StarterNG.Classes.Settings.Instance.LoadedFrom);
this.AttachedToVisualTree += (_, _) =>
{
TextureResolutionSlider_OnValueChanged(null, null);
CabTextureResolutionSlider_OnValueChanged(null, null);
shaderResolutionSlider_OnValueChanged(null, null);
UpdateQualityCaptions();
};
FillLanguageCombo();
App.Loc.LanguageChanged += SelectActiveLanguage;
App.Loc.LanguageChanged += UpdateQualityCaptions;
App.Loc.LanguageChanged += () => shaderResolutionSlider_OnValueChanged(null, null);
FeedbackCb.SelectionChanged += FeedbackCb_OnSelectionChanged;
FpsLimitEnableCb.IsCheckedChanged += (_, _) => FpsLimitSlider.IsEnabled = IsChecked(FpsLimitEnableCb);
@@ -52,6 +56,15 @@ public partial class Settings : UserControl
AddHandler(KeyDownEvent, OnControlsKeyDown, RoutingStrategies.Tunnel);
}
private static int IndexOfTag(ComboBox cb, string? tag)
{
for (int i = 0; i < cb.Items.Count; i++)
if (cb.Items[i] is ComboBoxItem item &&
string.Equals(item.Tag as string, tag, StringComparison.OrdinalIgnoreCase))
return i;
return -1;
}
private static int FindComboIndexByContent(ComboBox cb, string content)
{
for (int i = 0; i < cb.Items.Count; i++)
@@ -146,16 +159,6 @@ public partial class Settings : UserControl
GamepadIgnoreCb.IsChecked = s.IgnoreGamepad;
FeedbackCb.SelectedIndex = s.FeedbackMode;
FeedbackPortNud.Value = (decimal)s.FeedbackPort;
UartEnableCb.IsChecked = s.UartEnabled;
UartPortTb.Text = s.UartPort;
UartTuneTb.Text = s.UartTune;
UartDebugCb.IsChecked = s.UartDebug;
UartMainCb.IsChecked = s.UartMain;
UartScndCb.IsChecked = s.UartScnd;
UartTrainCb.IsChecked = s.UartTrain;
UartLocalCb.IsChecked = s.UartLocal;
UartRadioVolumeCb.IsChecked = s.UartRadioVolume;
UartRadioChannelCb.IsChecked = s.UartRadioChannel;
UpdateFeedbackDetailVisibility();
if (s.SelectExeAutomatically)
@@ -229,28 +232,22 @@ public partial class Settings : UserControl
VehiclesVolumeSlider.Value = s.VehiclesVolume;
PositionalVolumeSlider.Value = s.PositionalVolume;
AmbientVolumeSlider.Value = s.AmbientVolume;
CompressTexturesCb.IsChecked = s.CompressTextures;
ScaleSpecularsCb.IsChecked = s.ScaleSpeculars;
UseGLESCb.IsChecked = s.UseGLES;
ShaderGammaCb.IsChecked = s.ShaderGamma;
ScreenMipmapsCb.IsChecked = s.ScreenMipmaps;
ExtendedModelConversionCb.IsChecked = s.ExtendedModelConversion;
GfxResourceMoveCb.IsChecked = s.GfxResourceMove;
GfxResourceSweepCb.IsChecked = s.GfxResourceSweep;
TrainPhysicsThreadsSlider.Value = s.TrainPhysicsThreads;
IgnoreIrrelevantTrainsCb.IsChecked = s.IgnoreIrrelevantTrains;
VolumePausedSlider.Value = s.VolumePaused;
SkipPipelineCb.IsChecked = s.SkipPipeline;
DebugLogVisibleCb.IsChecked = s.DebugLogVisible;
PyScreenPriorityCb.SelectedIndex = Math.Max(0, IndexOfTag(PyScreenPriorityCb, s.PyScreenPriority));
AutoCloseStarterCb.IsChecked = s.AutoCloseStarter;
LargeThumbnailsCb.IsChecked = s.LargeThumbnails;
AutoExpandTreeCb.IsChecked = s.AutoExpandSceneryTree;
HideArchivalVehiclesCb.IsChecked = s.HideArchivalVehicles;
BatteryDefaultCb.SelectedIndex = (int)s.BatteryDefault;
}
finally
{
_loading = false;
}
UpdateQualityCaptions();
}
private void ReadFromUi()
@@ -269,16 +266,6 @@ public partial class Settings : UserControl
s.IgnoreGamepad = IsChecked(GamepadIgnoreCb);
s.FeedbackMode = Math.Max(0, FeedbackCb.SelectedIndex);
s.FeedbackPort = (int)(FeedbackPortNud.Value ?? 888);
s.UartEnabled = IsChecked(UartEnableCb);
s.UartPort = UartPortTb.Text ?? "";
s.UartTune = UartTuneTb.Text ?? "";
s.UartDebug = IsChecked(UartDebugCb);
s.UartMain = IsChecked(UartMainCb);
s.UartScnd = IsChecked(UartScndCb);
s.UartTrain = IsChecked(UartTrainCb);
s.UartLocal = IsChecked(UartLocalCb);
s.UartRadioVolume = IsChecked(UartRadioVolumeCb);
s.UartRadioChannel = IsChecked(UartRadioChannelCb);
s.SelectExeAutomatically = SelectExeCb.SelectedIndex == 0;
s.ExecutablePath = s.SelectExeAutomatically ? "eu07.exe"
@@ -340,25 +327,29 @@ public partial class Settings : UserControl
s.VehiclesVolume = (int)VehiclesVolumeSlider.Value;
s.PositionalVolume = (int)PositionalVolumeSlider.Value;
s.AmbientVolume = (int)AmbientVolumeSlider.Value;
s.CompressTextures = IsChecked(CompressTexturesCb);
s.ScaleSpeculars = IsChecked(ScaleSpecularsCb);
s.UseGLES = IsChecked(UseGLESCb);
s.ShaderGamma = IsChecked(ShaderGammaCb);
s.ScreenMipmaps = IsChecked(ScreenMipmapsCb);
s.ExtendedModelConversion = IsChecked(ExtendedModelConversionCb);
s.GfxResourceMove = IsChecked(GfxResourceMoveCb);
s.GfxResourceSweep = IsChecked(GfxResourceSweepCb);
s.TrainPhysicsThreads = (int)TrainPhysicsThreadsSlider.Value;
s.IgnoreIrrelevantTrains = IsChecked(IgnoreIrrelevantTrainsCb);
s.VolumePaused = (int)VolumePausedSlider.Value;
s.SkipPipeline = IsChecked(SkipPipelineCb);
s.DebugLogVisible = IsChecked(DebugLogVisibleCb);
s.PyScreenPriority = (PyScreenPriorityCb.SelectedItem as ComboBoxItem)?.Tag as string ?? "normal";
s.AutoCloseStarter = IsChecked(AutoCloseStarterCb);
s.LargeThumbnails = IsChecked(LargeThumbnailsCb);
s.AutoExpandSceneryTree = IsChecked(AutoExpandTreeCb);
s.HideArchivalVehicles = IsChecked(HideArchivalVehiclesCb);
s.BatteryDefault = (BatteryDefault)Math.Max(0, BatteryDefaultCb.SelectedIndex);
}
private void ComButton_OnClick(object? sender, RoutedEventArgs e)
{
if (TopLevel.GetTopLevel(this) is Window owner)
new UartWindow().ShowDialog(owner);
}
private void AboutButton_OnClick(object? sender, RoutedEventArgs e)
{
if (TopLevel.GetTopLevel(this) is Window owner)
AboutWindow.Create().ShowDialog(owner);
}
private void SaveButton_OnClick(object? sender, RoutedEventArgs e)
{
ReadFromUi();
@@ -436,6 +427,7 @@ public partial class Settings : UserControl
int resolution = 1 << (int)shaderResolutionSlider.Value;
shaderResolution.Text = $"{resolution} px";
ToolTip.SetTip(shaderResolutionSlider, ShadowMapWord(resolution));
}
private void FillLanguageCombo()
@@ -475,7 +467,7 @@ public partial class Settings : UserControl
private void UpdateFeedbackDetailVisibility()
{
FeedbackPortRow.IsVisible = FeedbackCb.SelectedIndex == 3;
UartExpander.IsVisible = FeedbackCb.SelectedIndex == 5;
ComButton.IsVisible = FeedbackCb.SelectedIndex == 5;
}
private static bool IsChecked(CheckBox cb) => cb.IsChecked == true;
@@ -535,7 +527,7 @@ public partial class Settings : UserControl
VerticalAlignment = VerticalAlignment.Center,
Foreground = FgDimBrush
});
_controlsSearch = new TextBox { Width = 240, Watermark = App.Loc["Search"] };
_controlsSearch = new TextBox { Width = 240, PlaceholderText = App.Loc["Search"] };
_controlsSearch.TextChanged += (_, _) => RebuildBindingList();
topBar.Children.Add(_controlsSearch);
@@ -1068,4 +1060,103 @@ public partial class Settings : UserControl
private static string Capitalize(string s) =>
string.IsNullOrEmpty(s) ? s : char.ToUpperInvariant(s[0]) + s.Substring(1);
private static string[] QualityWords => new[]
{
App.Loc["QualityVeryLow"], App.Loc["QualityLow"], App.Loc["QualityNormal"],
App.Loc["QualityHigh"], App.Loc["QualityVeryHigh"]
};
private void QualitySlider_OnValueChanged(object? sender, RangeBaseValueChangedEventArgs e)
=> UpdateQualityCaptions();
private void UpdateQualityCaptions()
{
var q = QualityWords;
if (texFilteringValue != null)
texFilteringValue.Text = 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];
string[] msaa = { App.Loc["MsaaNone"], "x2", "x4", "x8" };
if (multisamplingValue != null)
multisamplingValue.Text = 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 (cursorSensitivityValue != null)
cursorSensitivityValue.Text = (int)CursorSensitivitySlider.Value switch
{
<= 1 => q[1],
2 => App.Loc["QualityStandard"],
3 => q[3],
_ => q[4]
};
if (shaderRangeValue != null)
{
int metres = (int)shaderRange.Value;
shaderRangeValue.Text = $"{metres} m";
ToolTip.SetTip(shaderRange, ShadowRangeWord(metres));
}
if (cabShadowRangeValue != null)
{
int metres = (int)cabShaderSourceRange.Value;
cabShadowRangeValue.Text = metres <= 0 ? App.Loc["RangeDisabled"] : $"{metres} m";
ToolTip.SetTip(cabShaderSourceRange, CabShadowRangeWord(metres));
}
if (reflectionsFramerateValue != null)
{
int fps = (int)reflectionsFramerate.Value;
string word = ReflectionsRefreshWord(fps);
reflectionsFramerateValue.Text = $"{fps} FPS";
ToolTip.SetTip(reflectionsFramerate, word.Length == 0 ? null : word);
}
}
private static string ShadowMapWord(int pixels) => pixels switch
{
<= 512 => App.Loc["QualityVeryLow"],
<= 1024 => App.Loc["QualityLow"],
<= 2048 => App.Loc["QualityModerate"],
<= 4096 => App.Loc["QualityHigh"],
_ => App.Loc["QualityVeryHigh"]
};
private static string ShadowRangeWord(int metres) => metres switch
{
<= 25 => App.Loc["RangeVeryLow"],
<= 50 => App.Loc["RangeLow"],
<= 150 => App.Loc["RangeModerate"],
<= 250 => App.Loc["RangeHigh"],
_ => App.Loc["RangeVeryHigh"]
};
private static string CabShadowRangeWord(int metres) => metres switch
{
<= 10 => App.Loc["RangeVeryLow"],
<= 20 => App.Loc["RangeLow"],
<= 30 => App.Loc["RangeStandard"],
<= 50 => App.Loc["RangeHigh"],
_ => App.Loc["RangeVeryHigh"]
};
private static string ReflectionsRefreshWord(int fps) => fps switch
{
<= 5 => App.Loc["QualityVeryLow"],
<= 10 => App.Loc["QualityLow"],
<= 25 => App.Loc["QualityHigh"],
<= 60 => App.Loc["QualityVeryHigh"],
_ => string.Empty
};
}

View File

@@ -8,10 +8,10 @@
WindowStartupLocation="CenterOwner"
MinWidth="720" MinHeight="400">
<DockPanel Margin="10">
<Grid DockPanel.Dock="Top" ColumnDefinitions="*,*,*,*" RowDefinitions="Auto,Auto,Auto"
<Grid DockPanel.Dock="Top" ColumnDefinitions="*,*,*,*" RowDefinitions="Auto,Auto,Auto,Auto,Auto"
ColumnSpacing="8" RowSpacing="6" Margin="0,0,0,8">
<CheckBox Name="fNameOn" Grid.Row="0" Grid.Column="0" Content="{Binding [TextureInfo], Source={x:Static starterNg:App.Loc}}" />
<TextBox Name="fName" Grid.Row="0" Grid.Column="1" Watermark="{Binding [Search], Source={x:Static starterNg:App.Loc}}" />
<TextBox Name="fName" Grid.Row="0" Grid.Column="1" PlaceholderText="{Binding [Search], Source={x:Static starterNg:App.Loc}}" />
<CheckBox Name="fOpOn" Grid.Row="0" Grid.Column="2" Content="{Binding [TexOperator], Source={x:Static starterNg:App.Loc}}" />
<TextBox Name="fOp" Grid.Row="0" Grid.Column="3" />
@@ -24,15 +24,44 @@
<TextBox Name="fStation" Grid.Row="2" Grid.Column="1" />
<CheckBox Name="fPhotoOn" Grid.Row="2" Grid.Column="2" Content="{Binding [TexPhoto], Source={x:Static starterNg:App.Loc}}" />
<TextBox Name="fPhoto" Grid.Row="2" Grid.Column="3" />
<CheckBox Name="fModelOn" Grid.Row="3" Grid.Column="0" Content="{Binding [Model], Source={x:Static starterNg:App.Loc}}" />
<TextBox Name="fModel" Grid.Row="3" Grid.Column="1" />
<CheckBox Name="fRevFromOn" Grid.Row="4" Grid.Column="0" Content="{Binding [RevisionFrom], Source={x:Static starterNg:App.Loc}}" />
<TextBox Name="fRevFrom" Grid.Row="4" Grid.Column="1" PlaceholderText="RRRR-MM-DD" />
<CheckBox Name="fRevToOn" Grid.Row="4" Grid.Column="2" Content="{Binding [RevisionTo], Source={x:Static starterNg:App.Loc}}" />
<TextBox Name="fRevTo" Grid.Row="4" Grid.Column="3" PlaceholderText="RRRR-MM-DD" />
</Grid>
<TextBlock Name="statusText" DockPanel.Dock="Bottom" Margin="0,6,0,0"
FontSize="11" Opacity="0.75" />
<Grid DockPanel.Dock="Left" Width="260" RowDefinitions="*,4,*">
<ListBox Name="typeList" Grid.Row="0" Classes="Compact" />
<GridSplitter Grid.Row="1" Height="4" HorizontalAlignment="Stretch"
Background="Transparent" ResizeDirection="Rows" />
<ListBox Name="modelList" Grid.Row="2" Classes="Compact">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid ColumnDefinitions="28,90,*" ColumnSpacing="4">
<TextBlock Grid.Column="0" Text="{Binding No}" Opacity="0.6" />
<TextBlock Grid.Column="1" Text="{Binding Klasa}" TextTrimming="CharacterEllipsis" />
<TextBlock Grid.Column="2" Text="{Binding Sciezka}" Opacity="0.75"
TextTrimming="CharacterEllipsis" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
<GridSplitter Name="spSplitter" DockPanel.Dock="Left" Width="4"
Background="Transparent" ResizeDirection="Columns" />
<ListBox Name="textureList" DoubleTapped="TextureList_OnDoubleTapped">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid ColumnDefinitions="220,*,160,100,140" ColumnSpacing="8">
<Grid ColumnDefinitions="1.4*,0.9*,1.2*,90,*" ColumnSpacing="8">
<TextBlock Grid.Column="0" Text="{Binding Skin}" TextTrimming="CharacterEllipsis" />
<TextBlock Grid.Column="1" Text="{Binding Mini}" Opacity="0.85" TextTrimming="CharacterEllipsis" />
<TextBlock Grid.Column="2" Text="{Binding Operator}" Opacity="0.85" TextTrimming="CharacterEllipsis" />

View File

@@ -28,21 +28,65 @@ public partial class TextureBaseWindow : Window
_all.Add(new Row(t, _db));
}
foreach (var box in new Control[] { fName, fOp, fMini, fAuthor, fStation, fPhoto })
foreach (var box in new Control[] { fName, fOp, fMini, fAuthor, fStation, fPhoto, fModel })
{
if (box is TextBox tb)
tb.TextChanged += (_, _) => Refresh();
}
foreach (var box in new Control[] { fNameOn, fOpOn, fMiniOn, fAuthorOn, fStationOn, fPhotoOn })
foreach (var box in new Control[] { fNameOn, fOpOn, fMiniOn, fAuthorOn, fStationOn, fPhotoOn,
fModelOn, fRevFromOn, fRevToOn })
{
if (box is CheckBox cb)
cb.IsCheckedChanged += (_, _) => Refresh();
}
fRevFrom.TextChanged += (_, _) => Refresh();
fRevTo.TextChanged += (_, _) => Refresh();
BuildTypeList();
typeList.SelectionChanged += (_, _) => { BuildModelList(); Refresh(); };
modelList.SelectionChanged += (_, _) => Refresh();
textureList.ContextRequested += TextureList_OnContextRequested;
Refresh();
}
private sealed record ModelRow(string No, string Klasa, string Sciezka);
private void BuildModelList()
{
modelList.ItemsSource = null;
string? cat = (typeList.SelectedItem as ListBoxItem)?.Tag as string;
var rows = _all
.Where(r => cat == null || string.Equals(r.Category, cat, StringComparison.OrdinalIgnoreCase))
.Where(r => r.Mini.Length > 0)
.GroupBy(r => r.Mini, StringComparer.OrdinalIgnoreCase)
.OrderBy(g => g.Key, StringComparer.OrdinalIgnoreCase)
.Select((g, i) => new ModelRow($"{i + 1}.", g.Key, g.First().Directory))
.ToList();
modelList.ItemsSource = rows;
}
private static DateTime? ParseDate(string? text) =>
DateTime.TryParse(text, System.Globalization.CultureInfo.InvariantCulture,
System.Globalization.DateTimeStyles.None, out var d) ? d : null;
private void BuildTypeList()
{
typeList.Items.Add(new ListBoxItem { Content = App.Loc["CatAll"], Tag = null });
foreach (string cat in _all.Select(r => r.Category)
.Where(c => c.Length > 0)
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(c => c, StringComparer.OrdinalIgnoreCase))
typeList.Items.Add(new ListBoxItem { Content = cat, Tag = cat });
typeList.SelectedIndex = 0;
BuildModelList();
}
private void Refresh()
{
IEnumerable<Row> q = _all;
@@ -53,6 +97,18 @@ public partial class TextureBaseWindow : Window
q = Filter(q, fAuthorOn, fAuthor, r => r.Author);
q = Filter(q, fStationOn, fStation, r => r.Station);
q = Filter(q, fPhotoOn, fPhoto, r => r.Photo);
q = Filter(q, fModelOn, fModel, r => r.Model);
if ((typeList.SelectedItem as ListBoxItem)?.Tag is string cat)
q = q.Where(r => string.Equals(r.Category, cat, StringComparison.OrdinalIgnoreCase));
if (modelList.SelectedItem is ModelRow mr)
q = q.Where(r => string.Equals(r.Mini, mr.Klasa, StringComparison.OrdinalIgnoreCase));
if (fRevFromOn.IsChecked == true && ParseDate(fRevFrom.Text) is { } from)
q = q.Where(r => r.RevisionDate is { } d && d >= from);
if (fRevToOn.IsChecked == true && ParseDate(fRevTo.Text) is { } to)
q = q.Where(r => r.RevisionDate is { } d && d <= to);
var list = q
.OrderBy(r => r.Mini, StringComparer.OrdinalIgnoreCase)
@@ -98,12 +154,23 @@ public partial class TextureBaseWindow : Window
{
string dir = Path.Combine(Directory.GetCurrentDirectory(), "dynamic",
row.Texture.Directory.Replace('/', Path.DirectorySeparatorChar).TrimEnd('\\', '/'));
if (!Directory.Exists(dir)) return;
if (!Directory.Exists(dir))
dir = Path.Combine(Directory.GetCurrentDirectory(), row.Texture.Directory);
if (!Directory.Exists(dir))
{
StarterNG.Infrastructure.Diagnostics.ReportOnUiThread(
string.Format(App.Loc["FaultFileNotFound"], dir));
return;
}
try
{
Process.Start(new ProcessStartInfo(dir) { UseShellExecute = true });
}
catch { }
catch (Exception ex)
{
StarterNG.Infrastructure.Diagnostics.ReportOnUiThread(
$"{dir}{Environment.NewLine}{Environment.NewLine}{App.Loc["FaultDetail"]} {ex.Message}");
}
};
menu.Items.Add(open);
menu.Open(textureList);
@@ -120,6 +187,10 @@ public partial class TextureBaseWindow : Window
public string Author { get; }
public string Station { get; }
public string Photo { get; }
public string Model { get; }
public string Category { get; }
public string Directory { get; }
public DateTime? RevisionDate { get; }
public Row(VehicleTexture t, VehicleDatabase db)
{
@@ -131,6 +202,11 @@ public partial class TextureBaseWindow : Window
Author = t.Meta?.TextureAuthor ?? "";
Station = t.Meta?.Depot ?? "";
Photo = t.Meta?.PhotoAuthor ?? "";
Model = t.Model ?? "";
Category = t.ResolvedCategory ?? "";
Directory = t.Directory ?? "";
RevisionDate = DateTime.TryParse(Revision, System.Globalization.CultureInfo.InvariantCulture,
System.Globalization.DateTimeStyles.None, out var rev) ? rev : null;
}
}
}

View File

@@ -0,0 +1,129 @@
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Layout;
using Avalonia.Media;
using SettingsModel = StarterNG.Classes.Settings;
namespace StarterNG.Views;
public sealed class UartWindow : Window
{
private readonly CheckBox _enabled = new();
private readonly TextBox _port = new() { PlaceholderText = "COM3", MinWidth = 170 };
private readonly TextBox _tune = new() { MinWidth = 170 };
private readonly CheckBox _debug = new();
private readonly CheckBox _main = new();
private readonly CheckBox _scnd = new();
private readonly CheckBox _train = new();
private readonly CheckBox _local = new();
private readonly CheckBox _radioVolume = new();
private readonly CheckBox _radioChannel = new();
public UartWindow()
{
Title = App.Loc["UartSection"];
Width = 430;
SizeToContent = SizeToContent.Height;
CanResize = false;
WindowStartupLocation = WindowStartupLocation.CenterOwner;
_enabled.Content = App.Loc["UartEnable"];
_debug.Content = App.Loc["UartDebug"];
_main.Content = App.Loc["UartMain"];
_scnd.Content = App.Loc["UartScnd"];
_train.Content = App.Loc["UartTrain"];
_local.Content = App.Loc["UartLocal"];
_radioVolume.Content = App.Loc["UartRadioVolume"];
_radioChannel.Content = App.Loc["UartRadioChannel"];
Load();
var root = new StackPanel { Margin = new Avalonia.Thickness(16), Spacing = 8 };
root.Children.Add(_enabled);
root.Children.Add(Row(App.Loc["UartPort"], _port));
root.Children.Add(Row(App.Loc["UartTune"], _tune));
root.Children.Add(new TextBlock
{
Text = App.Loc["UartHint"],
FontSize = 11,
Opacity = 0.65,
TextWrapping = TextWrapping.Wrap
});
root.Children.Add(_debug);
root.Children.Add(new TextBlock
{
Text = App.Loc["UartFeature"],
FontWeight = FontWeight.Bold,
Margin = new Avalonia.Thickness(0, 6, 0, 0)
});
var top = new UniformGrid { Columns = 2, Margin = new Avalonia.Thickness(0, 0, 0, 2) };
top.Children.Add(_main);
top.Children.Add(_scnd);
top.Children.Add(_train);
top.Children.Add(_local);
root.Children.Add(top);
var mid = new UniformGrid { Columns = 2 };
mid.Children.Add(_radioVolume);
mid.Children.Add(_radioChannel);
root.Children.Add(mid);
var save = new Button
{
Content = App.Loc["UartSaveClose"],
MinWidth = 140,
Margin = new Avalonia.Thickness(0, 10, 0, 0),
HorizontalAlignment = HorizontalAlignment.Right,
Cursor = new Avalonia.Input.Cursor(Avalonia.Input.StandardCursorType.Hand)
};
save.Classes.Add("Accent");
save.Click += (_, _) => { Store(); Close(); };
root.Children.Add(save);
Content = root;
}
private static Control Row(string label, Control input)
{
var sp = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8 };
sp.Children.Add(new TextBlock
{
Text = label,
VerticalAlignment = VerticalAlignment.Center,
MinWidth = 110
});
sp.Children.Add(input);
return sp;
}
private void Load()
{
var s = SettingsModel.Instance;
_enabled.IsChecked = s.UartEnabled;
_port.Text = s.UartPort;
_tune.Text = s.UartTune;
_debug.IsChecked = s.UartDebug;
_main.IsChecked = s.UartMain;
_scnd.IsChecked = s.UartScnd;
_train.IsChecked = s.UartTrain;
_local.IsChecked = s.UartLocal;
_radioVolume.IsChecked = s.UartRadioVolume;
_radioChannel.IsChecked = s.UartRadioChannel;
}
private void Store()
{
var s = SettingsModel.Instance;
s.UartEnabled = _enabled.IsChecked == true;
s.UartPort = _port.Text ?? "";
s.UartTune = _tune.Text ?? "";
s.UartDebug = _debug.IsChecked == true;
s.UartMain = _main.IsChecked == true;
s.UartScnd = _scnd.IsChecked == true;
s.UartTrain = _train.IsChecked == true;
s.UartLocal = _local.IsChecked == true;
s.UartRadioVolume = _radioVolume.IsChecked == true;
s.UartRadioChannel = _radioChannel.IsChecked == true;
}
}