mirror of
https://github.com/MaSzyna-EU07/StarterNG.git
synced 2026-11-01 20:34:57 +01:00
Rebuild the settings page
A sticky quality bar, a level and a reset per section, key bindings in their own tab. UART moves into the page. A log and tools window replaces AdvancedSettingsWindow, and the developer tab is gone.
This commit is contained in:
21
StarterNG/Controls/KeySlot.cs
Normal file
21
StarterNG/Controls/KeySlot.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace StarterNG.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// Where the settings layout wants the row of one of the SimulatorKeys, so it
|
||||
/// sits among the rows it belongs with; the row is put in when the page is built.
|
||||
/// </summary>
|
||||
public sealed class KeySlot : Decorator
|
||||
{
|
||||
public static readonly StyledProperty<string?> KeyProperty =
|
||||
AvaloniaProperty.Register<KeySlot, string?>(nameof(Key));
|
||||
|
||||
/// <summary>The eu07.ini key whose row goes here.</summary>
|
||||
public string? Key
|
||||
{
|
||||
get => GetValue(KeyProperty);
|
||||
set => SetValue(KeyProperty, value);
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,13 @@ public class SettingRow : ContentControl
|
||||
public static readonly StyledProperty<string?> ValueTextProperty =
|
||||
AvaloniaProperty.Register<SettingRow, string?>(nameof(ValueText));
|
||||
|
||||
/// <summary>
|
||||
/// Why the simulator to be started would pass this setting over; the row dims
|
||||
/// and says so. The input stays usable - the file may serve another build.
|
||||
/// </summary>
|
||||
public static readonly StyledProperty<string?> IdleNoteProperty =
|
||||
AvaloniaProperty.Register<SettingRow, string?>(nameof(IdleNote));
|
||||
|
||||
public string? Label
|
||||
{
|
||||
get => GetValue(LabelProperty);
|
||||
@@ -41,6 +48,12 @@ public class SettingRow : ContentControl
|
||||
set => SetValue(ValueTextProperty, value);
|
||||
}
|
||||
|
||||
public string? IdleNote
|
||||
{
|
||||
get => GetValue(IdleNoteProperty);
|
||||
set => SetValue(IdleNoteProperty, value);
|
||||
}
|
||||
|
||||
public SettingRow()
|
||||
{
|
||||
UpdatePseudoClasses();
|
||||
@@ -54,8 +67,12 @@ public class SettingRow : ContentControl
|
||||
change.Property == DescriptionProperty ||
|
||||
change.Property == ConfigKeyProperty ||
|
||||
change.Property == ValueTextProperty ||
|
||||
change.Property == IdleNoteProperty ||
|
||||
change.Property == ContentProperty)
|
||||
UpdatePseudoClasses();
|
||||
|
||||
if (change.Property == IdleNoteProperty)
|
||||
ToolTip.SetTip(this, string.IsNullOrEmpty(IdleNote) ? null : IdleNote);
|
||||
}
|
||||
|
||||
private void UpdatePseudoClasses()
|
||||
@@ -65,5 +82,6 @@ public class SettingRow : ContentControl
|
||||
PseudoClasses.Set(":no-key", string.IsNullOrEmpty(ConfigKey));
|
||||
PseudoClasses.Set(":no-value", string.IsNullOrEmpty(ValueText));
|
||||
PseudoClasses.Set(":no-content", Content is null);
|
||||
PseudoClasses.Set(":idle", !string.IsNullOrEmpty(IdleNote));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
|
||||
namespace StarterNG.Controls;
|
||||
@@ -8,9 +10,56 @@ public class SettingsSection : HeaderedContentControl
|
||||
public static readonly StyledProperty<object?> ToggleProperty =
|
||||
AvaloniaProperty.Register<SettingsSection, object?>(nameof(Toggle));
|
||||
|
||||
public static readonly StyledProperty<string?> ToggleKeyProperty =
|
||||
AvaloniaProperty.Register<SettingsSection, string?>(nameof(ToggleKey));
|
||||
|
||||
public static readonly StyledProperty<string?> ResetLabelProperty =
|
||||
AvaloniaProperty.Register<SettingsSection, string?>(nameof(ResetLabel));
|
||||
|
||||
public static readonly StyledProperty<object?> LevelProperty =
|
||||
AvaloniaProperty.Register<SettingsSection, object?>(nameof(Level));
|
||||
|
||||
private Button? _reset;
|
||||
|
||||
/// <summary>The header's choice of a level of detail for this section alone; none where it has no such thing.</summary>
|
||||
public object? Level
|
||||
{
|
||||
get => GetValue(LevelProperty);
|
||||
set => SetValue(LevelProperty, value);
|
||||
}
|
||||
|
||||
public object? Toggle
|
||||
{
|
||||
get => GetValue(ToggleProperty);
|
||||
set => SetValue(ToggleProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>The eu07.ini key the header's switch sets.</summary>
|
||||
public string? ToggleKey
|
||||
{
|
||||
get => GetValue(ToggleKeyProperty);
|
||||
set => SetValue(ToggleKeyProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>The words on the header's button that puts the section back as the simulator has it; none, no button.</summary>
|
||||
public string? ResetLabel
|
||||
{
|
||||
get => GetValue(ResetLabelProperty);
|
||||
set => SetValue(ResetLabelProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>Raised when the header's reset button is pressed.</summary>
|
||||
public event Action<SettingsSection>? ResetRequested;
|
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
if (_reset is not null)
|
||||
_reset.Click -= OnResetClick;
|
||||
_reset = e.NameScope.Find<Button>("PART_Reset");
|
||||
if (_reset is not null)
|
||||
_reset.Click += OnResetClick;
|
||||
}
|
||||
|
||||
private void OnResetClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e) => ResetRequested?.Invoke(this);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,12 @@
|
||||
<SolidColorBrush x:Key="CarbonBorderDark" Color="#0E0F12" />
|
||||
<SolidColorBrush x:Key="CarbonFg" Color="#E6E8EA" />
|
||||
<SolidColorBrush x:Key="CarbonFgDim" Color="#9098A0" />
|
||||
<SolidColorBrush x:Key="TextControlPlaceholderForeground" Color="#9098A0" />
|
||||
<SolidColorBrush x:Key="TextControlPlaceholderForegroundPointerOver" Color="#9098A0" />
|
||||
<SolidColorBrush x:Key="TextControlPlaceholderForegroundFocused" Color="#9098A0" />
|
||||
<SolidColorBrush x:Key="ComboBoxPlaceHolderForeground" Color="#9098A0" />
|
||||
<SolidColorBrush x:Key="ComboBoxPlaceHolderForegroundPointerOver" Color="#9098A0" />
|
||||
<SolidColorBrush x:Key="ComboBoxPlaceHolderForegroundFocused" Color="#9098A0" />
|
||||
<SolidColorBrush x:Key="CarbonAccent" Color="#177f00" />
|
||||
<SolidColorBrush x:Key="CarbonAccentHover" Color="#41c400" />
|
||||
<SolidColorBrush x:Key="CarbonAccentBright" Color="#41c400" />
|
||||
@@ -43,6 +49,7 @@
|
||||
VerticalAlignment="Center" />
|
||||
<TextBlock Text="{TemplateBinding Header}"
|
||||
FontWeight="SemiBold"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{StaticResource CarbonFg}" />
|
||||
</DockPanel>
|
||||
@@ -325,9 +332,6 @@
|
||||
<Setter Property="MinHeight" Value="0" />
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
</Style>
|
||||
<Style Selector="RadioButton /template/ Grid:not(#RootGrid)">
|
||||
<Setter Property="Height" Value="20" />
|
||||
</Style>
|
||||
<Style Selector="Label">
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
<Setter Property="Padding" Value="0,2" />
|
||||
@@ -491,6 +495,11 @@
|
||||
<Style Selector="CheckBox:focus-visible /template/ Border#NormalRectangle">
|
||||
<Setter Property="BorderBrush" Value="{StaticResource CarbonAccentBright}" />
|
||||
</Style>
|
||||
<!-- The condition is what lets this win: the theme sets the half opacity in its template. -->
|
||||
<Style Selector="TextBox:not(:disabled) /template/ TextBlock#PART_Placeholder">
|
||||
<Setter Property="Opacity" Value="1" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="TextBox:focus /template/ Border#PART_BorderElement">
|
||||
<Setter Property="BorderBrush" Value="{StaticResource CarbonAccentBright}" />
|
||||
</Style>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate>
|
||||
<Grid RowDefinitions="Auto,Auto">
|
||||
<Grid RowDefinitions="Auto,Auto,Auto">
|
||||
<Grid.ColumnDefinitions>
|
||||
<!-- Every row is its own Grid, so the label column only
|
||||
lines up across rows when a shared-size scope makes
|
||||
@@ -79,6 +79,18 @@
|
||||
TextWrapping="Wrap"
|
||||
Margin="0,3,0,0" />
|
||||
|
||||
<!-- Why the chosen simulator would ignore the setting. Under
|
||||
the explanation, at full strength while the rest dims. -->
|
||||
<TextBlock Name="PART_IdleNote"
|
||||
Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2"
|
||||
Text="{TemplateBinding IdleNote}"
|
||||
IsVisible="False"
|
||||
FontSize="12"
|
||||
FontStyle="Italic"
|
||||
Foreground="{StaticResource CarbonFg}"
|
||||
TextWrapping="Wrap"
|
||||
Margin="0,3,0,0" />
|
||||
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
@@ -99,6 +111,24 @@
|
||||
<Setter Property="Grid.Column" Value="1" />
|
||||
<Setter Property="Grid.ColumnSpan" Value="2" />
|
||||
</Style>
|
||||
<Style Selector="^:idle /template/ TextBlock#PART_IdleNote">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
</Style>
|
||||
<Style Selector="^:idle /template/ TextBlock#PART_Label">
|
||||
<Setter Property="Opacity" Value="0.45" />
|
||||
</Style>
|
||||
<Style Selector="^:idle /template/ ContentPresenter#PART_Content">
|
||||
<Setter Property="Opacity" Value="0.45" />
|
||||
</Style>
|
||||
<Style Selector="^:idle /template/ TextBlock#PART_Value">
|
||||
<Setter Property="Opacity" Value="0.45" />
|
||||
</Style>
|
||||
<Style Selector="^:idle /template/ TextBlock#PART_Description">
|
||||
<Setter Property="Opacity" Value="0.45" />
|
||||
</Style>
|
||||
<Style Selector="^:idle /template/ SelectableTextBlock#PART_ConfigKey">
|
||||
<Setter Property="Opacity" Value="0.45" />
|
||||
</Style>
|
||||
<!-- A checkbox is its own label, so let it take the label column too.
|
||||
Alignment stays Stretch: a bare slider in a labelless row still
|
||||
wants the full width. -->
|
||||
@@ -118,6 +148,13 @@
|
||||
<ContentPresenter DockPanel.Dock="Right"
|
||||
Content="{TemplateBinding Toggle}"
|
||||
VerticalAlignment="Center" />
|
||||
<Button Name="PART_Reset" DockPanel.Dock="Right" Classes="Outline SectionReset"
|
||||
Content="{TemplateBinding ResetLabel}"
|
||||
IsVisible="{Binding $parent[ctl:SettingsSection].ResetLabel, Converter={x:Static ObjectConverters.IsNotNull}}"
|
||||
VerticalAlignment="Center" Margin="0,0,12,0" />
|
||||
<ContentPresenter DockPanel.Dock="Right"
|
||||
Content="{TemplateBinding Level}"
|
||||
VerticalAlignment="Center" />
|
||||
<TextBlock Text="{TemplateBinding Header}"
|
||||
FontSize="12"
|
||||
FontWeight="SemiBold"
|
||||
@@ -194,8 +231,27 @@
|
||||
<!-- Controls inside a row: keep them from stretching to absurd widths in the
|
||||
single-column layout, and give the row a predictable height. -->
|
||||
<Style Selector="ctl|SettingRow:not(.Compact) > ComboBox">
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Property="MaxWidth" Value="360" />
|
||||
<Setter Property="HorizontalAlignment" Value="Left" />
|
||||
<Setter Property="Width" Value="360" />
|
||||
</Style>
|
||||
<!-- The same width for a list that has a warning under it. -->
|
||||
<Style Selector="ctl|SettingRow:not(.Compact) > StackPanel.Field">
|
||||
<Setter Property="HorizontalAlignment" Value="Left" />
|
||||
<Setter Property="Width" Value="360" />
|
||||
</Style>
|
||||
<!-- The key of the row above holds for this one too. -->
|
||||
<Style Selector="ctl|SettingRow.KeyAbove /template/ SelectableTextBlock#PART_ConfigKey">
|
||||
<Setter Property="IsVisible" Value="False" />
|
||||
</Style>
|
||||
<!-- Content of several rows: the label and its key stay together at the first. -->
|
||||
<Style Selector="ctl|SettingRow.TopLabel /template/ TextBlock#PART_Label">
|
||||
<Setter Property="VerticalAlignment" Value="Top" />
|
||||
<Setter Property="Margin" Value="0,2,14,0" />
|
||||
</Style>
|
||||
<Style Selector="ctl|SettingRow.TopLabel /template/ SelectableTextBlock#PART_ConfigKey">
|
||||
<Setter Property="Grid.Row" Value="0" />
|
||||
<Setter Property="VerticalAlignment" Value="Top" />
|
||||
<Setter Property="Margin" Value="0,21,14,0" />
|
||||
</Style>
|
||||
<Style Selector="ctl|SettingRow:not(.Compact) > NumericUpDown">
|
||||
<Setter Property="HorizontalAlignment" Value="Left" />
|
||||
@@ -276,6 +332,23 @@
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
||||
</Style>
|
||||
|
||||
<!-- The header's "back to defaults": quiet until pointed at. -->
|
||||
<!-- An Outline button, only smaller: without the frame it read as a caption. -->
|
||||
<Style Selector="Button.SectionReset">
|
||||
<Setter Property="Padding" Value="8,2" />
|
||||
<Setter Property="MinHeight" Value="0" />
|
||||
<Setter Property="FontSize" Value="11" />
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="ComboBox.SectionLevel">
|
||||
<Setter Property="MinHeight" Value="0" />
|
||||
<Setter Property="MinWidth" Value="118" />
|
||||
<Setter Property="Padding" Value="8,1" />
|
||||
<Setter Property="FontSize" Value="11" />
|
||||
<Setter Property="Margin" Value="0,0,8,0" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="ctl|SettingsSection.Compact">
|
||||
<Setter Property="Margin" Value="0,0,0,9" />
|
||||
</Style>
|
||||
|
||||
@@ -1,187 +0,0 @@
|
||||
using System;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
using StarterNG.Infrastructure;
|
||||
using StarterNG.Application;
|
||||
|
||||
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 = AppServices.Current.Settings;
|
||||
_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 = AppServices.Current.Settings;
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:starterNg="clr-namespace:StarterNG"
|
||||
x:CompileBindings="True"
|
||||
mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="700"
|
||||
x:Class="StarterNG.Views.Developer">
|
||||
<!-- Read-only view of what the starter made of the installation: the counts a
|
||||
package author needs before slicing anything out of it, and the state of
|
||||
the JSON copy those packages will be generated from. -->
|
||||
<StackPanel Margin="3" Spacing="3">
|
||||
<Border Background="{StaticResource CarbonPanelBg}" BorderBrush="{StaticResource CarbonBorderLight}"
|
||||
BorderThickness="1" CornerRadius="2" Padding="10" HorizontalAlignment="Left" MinWidth="440">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Text="{Binding [DevDatabase], Source={x:Static starterNg:App.Loc}}"
|
||||
FontWeight="Bold" FontSize="14" />
|
||||
|
||||
<Grid ColumnDefinitions="190,*" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto"
|
||||
RowSpacing="4">
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Foreground="{StaticResource CarbonFgDim}"
|
||||
Text="{Binding [DevVehicles], Source={x:Static starterNg:App.Loc}}" />
|
||||
<TextBlock Grid.Row="0" Grid.Column="1" x:Name="VehiclesValue" />
|
||||
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" Foreground="{StaticResource CarbonFgDim}"
|
||||
Text="{Binding [DevLiveries], Source={x:Static starterNg:App.Loc}}" />
|
||||
<TextBlock Grid.Row="1" Grid.Column="1" x:Name="LiveriesValue" />
|
||||
|
||||
<TextBlock Grid.Row="2" Grid.Column="0" Foreground="{StaticResource CarbonFgDim}"
|
||||
Text="{Binding [DevGroups], Source={x:Static starterNg:App.Loc}}" />
|
||||
<TextBlock Grid.Row="2" Grid.Column="1" x:Name="GroupsValue" />
|
||||
|
||||
<TextBlock Grid.Row="3" Grid.Column="0" Foreground="{StaticResource CarbonFgDim}"
|
||||
Text="{Binding [DevSets], Source={x:Static starterNg:App.Loc}}" />
|
||||
<TextBlock Grid.Row="3" Grid.Column="1" x:Name="SetsValue" />
|
||||
|
||||
<TextBlock Grid.Row="4" Grid.Column="0" Foreground="{StaticResource CarbonFgDim}"
|
||||
Text="{Binding [DevFiles], Source={x:Static starterNg:App.Loc}}" />
|
||||
<TextBlock Grid.Row="4" Grid.Column="1" x:Name="FilesValue" />
|
||||
|
||||
<TextBlock Grid.Row="5" Grid.Column="0" Foreground="{StaticResource CarbonFgDim}"
|
||||
Text="{Binding [DevSource], Source={x:Static starterNg:App.Loc}}" />
|
||||
<TextBlock Grid.Row="5" Grid.Column="1" x:Name="SourceValue" />
|
||||
|
||||
<TextBlock Grid.Row="6" Grid.Column="0" Foreground="{StaticResource CarbonFgDim}"
|
||||
Text="{Binding [DevWritten], Source={x:Static starterNg:App.Loc}}" />
|
||||
<TextBlock Grid.Row="6" Grid.Column="1" x:Name="WrittenValue" />
|
||||
|
||||
<TextBlock Grid.Row="7" Grid.Column="0" Foreground="{StaticResource CarbonFgDim}"
|
||||
Text="{Binding [DevLocation], Source={x:Static starterNg:App.Loc}}"
|
||||
VerticalAlignment="Top" />
|
||||
<SelectableTextBlock Grid.Row="7" Grid.Column="1" x:Name="LocationValue"
|
||||
TextWrapping="Wrap" FontSize="11" />
|
||||
</Grid>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button x:Name="RebuildButton" Click="RebuildButton_OnClick" Cursor="Hand"
|
||||
Content="{Binding [DevRebuild], Source={x:Static starterNg:App.Loc}}" />
|
||||
<TextBlock x:Name="RebuildStatus" VerticalAlignment="Center"
|
||||
Foreground="{StaticResource CarbonFgDim}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@@ -1,78 +0,0 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using StarterNG.Application;
|
||||
using StarterNG.Domain.Vehicles;
|
||||
|
||||
namespace StarterNG.Views;
|
||||
|
||||
/// <summary>
|
||||
/// The developer tab: what the starter read out of the installation, and the state
|
||||
/// of the JSON copy it keeps of it. Read-only apart from rebuilding that copy.
|
||||
/// </summary>
|
||||
public partial class Developer : UserControl
|
||||
{
|
||||
public Developer()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void InitializeComponent() => AvaloniaXamlLoader.Load(this);
|
||||
|
||||
/// <summary>Fills the panel from the catalogue as it stands right now.</summary>
|
||||
public void Refresh()
|
||||
{
|
||||
var catalog = AppServices.Current.Library.Vehicles;
|
||||
var status = AppServices.Current.VehicleCache.Status;
|
||||
|
||||
VehiclesValue.Text = Count(catalog.TextureByUuid.Values
|
||||
.Select(texture => texture.Directory)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.Count());
|
||||
LiveriesValue.Text = Wrecks(catalog);
|
||||
GroupsValue.Text = Count(catalog.GroupsById.Count);
|
||||
SetsValue.Text = Count(catalog.Sets.Count);
|
||||
FilesValue.Text = Count(status.Sources);
|
||||
SourceValue.Text = App.Loc[status.ServedFromCopy ? "DevSourceCopy" : "DevSourceTextures"];
|
||||
WrittenValue.Text = status.Written is { } written
|
||||
? written.ToLocalTime().ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture)
|
||||
: "-";
|
||||
LocationValue.Text = status.Directory;
|
||||
RebuildStatus.Text = "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rebuilds the copy on disk without disturbing the catalogue the other tabs
|
||||
/// are already drawn from - the vehicles on screen keep pointing at the entries
|
||||
/// they were built with.
|
||||
/// </summary>
|
||||
private void RebuildButton_OnClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
RebuildButton.IsEnabled = false;
|
||||
try
|
||||
{
|
||||
int liveries = AppServices.Current.VehicleCache
|
||||
.Rebuild(new VehicleCatalog(AppServices.Current.MiniTextures));
|
||||
Refresh();
|
||||
RebuildStatus.Text = $"{App.Loc["DevRebuilt"]} ({liveries})";
|
||||
}
|
||||
finally
|
||||
{
|
||||
RebuildButton.IsEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Liveries offered, saying so when wrecks are being held back.</summary>
|
||||
private static string Wrecks(VehicleCatalog catalog)
|
||||
{
|
||||
int wrecks = catalog.TextureByUuid.Count - catalog.Textures.Count;
|
||||
return wrecks > 0
|
||||
? $"{Count(catalog.Textures.Count)} (+{Count(wrecks)} {App.Loc["DevWrecks"]})"
|
||||
: Count(catalog.Textures.Count);
|
||||
}
|
||||
|
||||
private static string Count(int value) => value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
@@ -8,6 +8,7 @@ public static class ExeProblemText
|
||||
public static string Key(ExeProblem problem) => problem switch
|
||||
{
|
||||
ExeProblem.NotExecutable => "ExeNotExecutable",
|
||||
ExeProblem.NotAProgram => "ExeNotAProgram",
|
||||
ExeProblem.WrongPlatform => "ExeWrongPlatform",
|
||||
_ => "ExeNotFound"
|
||||
};
|
||||
|
||||
93
StarterNG/Views/IniChangesWindow.cs
Normal file
93
StarterNG/Views/IniChangesWindow.cs
Normal file
@@ -0,0 +1,93 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Documents;
|
||||
using Avalonia.Media;
|
||||
using StarterNG.Application;
|
||||
using StarterNG.Infrastructure;
|
||||
|
||||
namespace StarterNG.Views;
|
||||
|
||||
/// <summary>What a save would change in eu07.ini, key by key, before it is made.</summary>
|
||||
internal static class IniChangesWindow
|
||||
{
|
||||
private static readonly IBrush Dim = new SolidColorBrush(Color.Parse("#9098A0"));
|
||||
private static readonly IBrush New = new SolidColorBrush(Color.Parse("#41C400"));
|
||||
|
||||
/// <returns>Whether the save was asked for.</returns>
|
||||
public static async Task<bool> Show(Window owner, string path, IReadOnlyList<IniChange> changes,
|
||||
bool keyboardChanged)
|
||||
{
|
||||
var win = MessageBox.FixedDialog(App.Loc["IniChangesTitle"], 680);
|
||||
|
||||
var list = new StackPanel { Spacing = 2 };
|
||||
AddGroup(list, App.Loc["IniChangesChanged"], changes.Where(c => c.Before is not null && c.After is not null));
|
||||
AddGroup(list, App.Loc["IniChangesAdded"], changes.Where(c => c.Before is null));
|
||||
AddGroup(list, App.Loc["IniChangesRemoved"], changes.Where(c => c.After is null));
|
||||
|
||||
var body = new StackPanel { Margin = new Thickness(16), Spacing = 12 };
|
||||
body.Children.Add(new TextBlock
|
||||
{
|
||||
Text = path, Foreground = Dim, FontSize = 12, TextTrimming = TextTrimming.CharacterEllipsis
|
||||
});
|
||||
body.Children.Add(changes.Count == 0
|
||||
? new TextBlock { Text = App.Loc["IniChangesNone"], TextWrapping = TextWrapping.Wrap }
|
||||
: new ScrollViewer { MaxHeight = 440, Content = list });
|
||||
if (keyboardChanged)
|
||||
body.Children.Add(new TextBlock { Text = App.Loc["IniChangesKeyboard"], TextWrapping = TextWrapping.Wrap });
|
||||
|
||||
bool save = false;
|
||||
var saveBtn = new Button { Content = App.Loc["Save"], MinWidth = 80 };
|
||||
saveBtn.Classes.Add("Accent");
|
||||
saveBtn.IsEnabled = changes.Count > 0 || keyboardChanged;
|
||||
saveBtn.Click += (_, _) => { save = true; win.Close(); };
|
||||
var closeBtn = new Button { Content = App.Loc["Close"], MinWidth = 80 };
|
||||
closeBtn.Classes.Add("Flat");
|
||||
closeBtn.Click += (_, _) => win.Close();
|
||||
body.Children.Add(MessageBox.ButtonRow(saveBtn, closeBtn));
|
||||
|
||||
win.Content = body;
|
||||
await win.ShowDialog(owner);
|
||||
return save;
|
||||
}
|
||||
|
||||
private static void AddGroup(Panel list, string title, IEnumerable<IniChange> group)
|
||||
{
|
||||
var changes = group.ToList();
|
||||
if (changes.Count == 0)
|
||||
return;
|
||||
|
||||
list.Children.Add(new TextBlock
|
||||
{
|
||||
Text = $"{title} ({changes.Count})",
|
||||
FontWeight = FontWeight.SemiBold,
|
||||
Margin = new Thickness(0, list.Children.Count == 0 ? 0 : 12, 0, 4)
|
||||
});
|
||||
foreach (var change in changes)
|
||||
{
|
||||
var row = new Grid { ColumnDefinitions = new ColumnDefinitions("240,*") };
|
||||
row.Children.Add(new TextBlock
|
||||
{
|
||||
Text = change.Key, FontFamily = MonoFont.Family, FontSize = 12,
|
||||
TextTrimming = TextTrimming.CharacterEllipsis
|
||||
});
|
||||
|
||||
var value = new TextBlock { FontFamily = MonoFont.Family, FontSize = 12, TextWrapping = TextWrapping.Wrap };
|
||||
if (change.Before is not null)
|
||||
value.Inlines!.Add(new Run(change.Before)
|
||||
{
|
||||
Foreground = Dim,
|
||||
TextDecorations = change.After is null ? TextDecorations.Strikethrough : null
|
||||
});
|
||||
if (change.Before is not null && change.After is not null)
|
||||
value.Inlines!.Add(new Run(" → ") { Foreground = Dim });
|
||||
if (change.After is not null)
|
||||
value.Inlines!.Add(new Run(change.After) { Foreground = New });
|
||||
Grid.SetColumn(value, 1);
|
||||
row.Children.Add(value);
|
||||
list.Children.Add(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
688
StarterNG/Views/KeyBindingsTab.cs
Normal file
688
StarterNG/Views/KeyBindingsTab.cs
Normal file
@@ -0,0 +1,688 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
using StarterNG.Classes;
|
||||
using StarterNG.Infrastructure;
|
||||
|
||||
using KeyBinding = StarterNG.Classes.KeyBinding;
|
||||
|
||||
namespace StarterNG.Views;
|
||||
|
||||
/// <summary>
|
||||
/// The key bindings of the settings screen: the list of commands, the keyboard they
|
||||
/// sit on, and the wait for a keypress.
|
||||
/// </summary>
|
||||
internal sealed class KeyBindingsTab(Grid host, TextBox search)
|
||||
{
|
||||
/// <summary>Raised on an edit by hand.</summary>
|
||||
public event Action? Changed;
|
||||
|
||||
private StackPanel? _bindingListPanel;
|
||||
private StackPanel? _keyboardPanel;
|
||||
|
||||
/// <summary>
|
||||
/// True while a cab-control binding is waiting for a keypress. The window's own
|
||||
/// shortcuts stand down for the duration, otherwise Ctrl+N would never reach here.
|
||||
/// </summary>
|
||||
public bool IsCapturing => _capturing is not null;
|
||||
|
||||
private KeyBinding? _capturing;
|
||||
private Button? _capturingButton;
|
||||
|
||||
private readonly Dictionary<string, Grid> _keyCells = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private bool _flyoutLoading;
|
||||
|
||||
private static readonly IBrush KeyUnassignedBrush = new SolidColorBrush(Color.Parse("#2A3036"));
|
||||
private static readonly IBrush KeyFillerBrush = new SolidColorBrush(Color.Parse("#21262B"));
|
||||
private static readonly IBrush KeyPlainBrush = new SolidColorBrush(Color.Parse("#2E9E1F"));
|
||||
private static readonly IBrush KeyShiftBrush = new SolidColorBrush(Color.Parse("#C9A227"));
|
||||
private static readonly IBrush KeyCtrlBrush = new SolidColorBrush(Color.Parse("#2D7FD3"));
|
||||
private static readonly IBrush KeyShiftCtrlBrush = new SolidColorBrush(Color.Parse("#C8372D"));
|
||||
private static readonly IBrush KeyBorderBrush = new SolidColorBrush(Color.Parse("#3A424A"));
|
||||
private static readonly IBrush FgBrush = new SolidColorBrush(Color.Parse("#E6E8EA"));
|
||||
private static readonly IBrush FgDimBrush = new SolidColorBrush(Color.Parse("#9098A0"));
|
||||
private static readonly IBrush ConflictBrush = new SolidColorBrush(Color.Parse("#E06C5A"));
|
||||
|
||||
// The four combinations a key can be on. After the brushes: static fields start in text order.
|
||||
private static readonly (bool Shift, bool Ctrl, string SlotTextKey, IBrush Colour)[] ModifierSlots =
|
||||
{
|
||||
(false, false, "BindNoMod", KeyPlainBrush),
|
||||
(true, false, "BindShift", KeyShiftBrush),
|
||||
(false, true, "BindCtrl", KeyCtrlBrush),
|
||||
(true, true, "BindShiftCtrl", KeyShiftCtrlBrush),
|
||||
};
|
||||
|
||||
private const double KeyUnit = 30;
|
||||
private const double KeyHeight = 34;
|
||||
private const double KeyGap = 4;
|
||||
|
||||
public void Build()
|
||||
{
|
||||
CancelCapture();
|
||||
host.Children.Clear();
|
||||
|
||||
var head = new StackPanel { Spacing = 6, Margin = new Thickness(0, 0, 0, 8) };
|
||||
var topBar = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8 };
|
||||
topBar.Children.Add(new TextBlock
|
||||
{
|
||||
Text = App.Loc["BindingsHint"],
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Foreground = FgDimBrush
|
||||
});
|
||||
var restoreBtn = new Button { Content = App.Loc["RestoreDefaults"] };
|
||||
restoreBtn.Classes.Add("Flat");
|
||||
restoreBtn.Click += (_, _) =>
|
||||
{
|
||||
CancelCapture();
|
||||
KeyboardConfig.Instance.LoadDefaults();
|
||||
BindingsEdited();
|
||||
};
|
||||
topBar.Children.Add(restoreBtn);
|
||||
head.Children.Add(topBar);
|
||||
|
||||
_numpadText = new TextBlock
|
||||
{
|
||||
Foreground = FgBrush,
|
||||
FontSize = 12,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
};
|
||||
var numpadShow = new Button
|
||||
{
|
||||
Content = App.Loc["BindingsNumpadShow"],
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
HorizontalContentAlignment = HorizontalAlignment.Center
|
||||
};
|
||||
numpadShow.Classes.Add("Flat");
|
||||
numpadShow.Click += (_, _) => search.Text = KeyMap.NumpadPrefix;
|
||||
Grid.SetColumn(numpadShow, 1);
|
||||
_numpadNote = new Border
|
||||
{
|
||||
BorderBrush = ConflictBrush,
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(2),
|
||||
Padding = new Thickness(8, 4),
|
||||
Child = new Grid
|
||||
{
|
||||
ColumnDefinitions = new ColumnDefinitions("*,Auto"),
|
||||
ColumnSpacing = 8,
|
||||
Children = { _numpadText, numpadShow }
|
||||
}
|
||||
};
|
||||
head.Children.Add(_numpadNote);
|
||||
Grid.SetRow(head, 0);
|
||||
host.Children.Add(head);
|
||||
|
||||
_bindingListPanel = new StackPanel { Spacing = 3 };
|
||||
var listScroll = new ScrollViewer
|
||||
{
|
||||
Content = _bindingListPanel,
|
||||
VerticalScrollBarVisibility = ScrollBarVisibility.Auto
|
||||
};
|
||||
Grid.SetRow(listScroll, 1);
|
||||
host.Children.Add(listScroll);
|
||||
|
||||
var bottom = new StackPanel { Spacing = 8, Margin = new Thickness(0, 10, 0, 0) };
|
||||
bottom.Children.Add(BuildLegend());
|
||||
_keyboardPanel = new StackPanel();
|
||||
bottom.Children.Add(new ScrollViewer
|
||||
{
|
||||
Content = _keyboardPanel,
|
||||
HorizontalScrollBarVisibility = ScrollBarVisibility.Auto,
|
||||
VerticalScrollBarVisibility = ScrollBarVisibility.Disabled
|
||||
});
|
||||
Grid.SetRow(bottom, 2);
|
||||
host.Children.Add(bottom);
|
||||
|
||||
RedrawBindings();
|
||||
}
|
||||
|
||||
/// <summary>The bindings as the file has them, and no key waited for.</summary>
|
||||
public void Reload()
|
||||
{
|
||||
CancelCapture();
|
||||
KeyboardConfig.Instance.Load();
|
||||
RedrawBindings();
|
||||
}
|
||||
|
||||
private void BindingsEdited()
|
||||
{
|
||||
KeyboardConfig.Instance.Dirty = true;
|
||||
Changed?.Invoke();
|
||||
RedrawBindings();
|
||||
}
|
||||
|
||||
private void RedrawBindings()
|
||||
{
|
||||
RebuildList();
|
||||
RebuildKeyboard();
|
||||
}
|
||||
|
||||
public void RebuildList()
|
||||
{
|
||||
if (_bindingListPanel is null)
|
||||
return;
|
||||
|
||||
_bindingListPanel.Children.Clear();
|
||||
// The sidebar search box serves this list too - see Settings.ApplyFilter.
|
||||
string filter = search.Text?.Trim() ?? string.Empty;
|
||||
var conflicts = KeyboardConfig.Instance.ConflictingCombos();
|
||||
|
||||
foreach (var b in KeyboardConfig.Instance.Bindings)
|
||||
{
|
||||
if (filter.Length > 0 && !MatchesFilter(b, filter))
|
||||
continue;
|
||||
_bindingListPanel.Children.Add(BuildBindingRow(b, conflicts));
|
||||
}
|
||||
|
||||
UpdateNumpadNote();
|
||||
}
|
||||
|
||||
private Border? _numpadNote;
|
||||
private TextBlock? _numpadText;
|
||||
|
||||
/// <summary>
|
||||
/// Said whenever a binding uses the numpad: neither Windows nor Linux tells whether a
|
||||
/// keyboard has one, a keyboard without it reports the same as any other.
|
||||
/// </summary>
|
||||
private void UpdateNumpadNote()
|
||||
{
|
||||
if (_numpadNote is null || _numpadText is null)
|
||||
return;
|
||||
|
||||
int onNumpad = KeyboardConfig.Instance.BindingsOnNumpad();
|
||||
_numpadNote.IsVisible = onNumpad > 0;
|
||||
_numpadText.Text = string.Format(App.Loc["BindingsNumpadNote"], onNumpad);
|
||||
}
|
||||
|
||||
private static bool MatchesFilter(KeyBinding b, string filter) =>
|
||||
TextMatch.Contains(b.Command, filter) ||
|
||||
TextMatch.Contains(b.Key, filter) ||
|
||||
TextMatch.Contains(KeyboardConfig.Instance.DescriptionOf(b, TranslatedCommand), filter);
|
||||
|
||||
// Polish reads the bundled key file, which has the descriptions with Polish letters.
|
||||
private static string TranslatedCommand(string command) =>
|
||||
string.Equals(App.Loc.CurrentLangCode, "pl", StringComparison.OrdinalIgnoreCase)
|
||||
? string.Empty
|
||||
: App.Loc.OptOrEnglish("Cmd." + command);
|
||||
|
||||
private Control BuildBindingRow(KeyBinding b, HashSet<string> conflicts)
|
||||
{
|
||||
var grid = new Grid
|
||||
{
|
||||
ColumnDefinitions = new ColumnDefinitions("*,Auto,Auto"),
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
};
|
||||
|
||||
var label = new StackPanel { VerticalAlignment = VerticalAlignment.Center };
|
||||
label.Children.Add(new TextBlock
|
||||
{
|
||||
Text = CommandLabel(b),
|
||||
Foreground = FgBrush,
|
||||
FontSize = 12,
|
||||
TextTrimming = TextTrimming.CharacterEllipsis
|
||||
});
|
||||
label.Children.Add(new TextBlock
|
||||
{
|
||||
Text = b.Command,
|
||||
Foreground = FgDimBrush,
|
||||
FontSize = 12,
|
||||
TextTrimming = TextTrimming.CharacterEllipsis
|
||||
});
|
||||
Grid.SetColumn(label, 0);
|
||||
grid.Children.Add(label);
|
||||
|
||||
var simulatorUse = b.IsAssigned ? KeyMap.SimulatorUse(b.Key, b.Shift, b.Ctrl) : null;
|
||||
bool conflict = (b.IsAssigned && conflicts.Contains(b.ComboKey)) || simulatorUse is not null;
|
||||
var comboButton = new Button
|
||||
{
|
||||
Content = _capturing == b ? App.Loc["PressKey"] : ComboText(b),
|
||||
MinWidth = 140,
|
||||
HorizontalContentAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Foreground = conflict ? ConflictBrush : FgBrush
|
||||
};
|
||||
comboButton.Classes.Add("Flat");
|
||||
if (simulatorUse is { } use)
|
||||
ToolTip.SetTip(comboButton, SimulatorUseText(b.Key, b.Shift, b.Ctrl, use));
|
||||
else if (conflict)
|
||||
ToolTip.SetTip(comboButton, App.Loc["ConflictTooltip"]);
|
||||
comboButton.Click += (_, _) => StartCapture(b, comboButton);
|
||||
Grid.SetColumn(comboButton, 1);
|
||||
grid.Children.Add(comboButton);
|
||||
|
||||
var clearButton = new Button
|
||||
{
|
||||
Content = "✕",
|
||||
Margin = new Thickness(6, 0, 0, 0),
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
};
|
||||
clearButton.Classes.Add("Basic");
|
||||
ToolTip.SetTip(clearButton, App.Loc["ClearBinding"]);
|
||||
clearButton.Click += (_, _) =>
|
||||
{
|
||||
CancelCapture();
|
||||
b.Clear();
|
||||
BindingsEdited();
|
||||
};
|
||||
Grid.SetColumn(clearButton, 2);
|
||||
grid.Children.Add(clearButton);
|
||||
|
||||
return new Border
|
||||
{
|
||||
Background = KeyUnassignedBrush,
|
||||
BorderBrush = KeyBorderBrush,
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(2),
|
||||
Padding = new Thickness(8, 4),
|
||||
Child = grid
|
||||
};
|
||||
}
|
||||
|
||||
private void StartCapture(KeyBinding b, Button button)
|
||||
{
|
||||
|
||||
if (_capturing is not null && _capturingButton is not null)
|
||||
_capturingButton.Content = ComboText(_capturing);
|
||||
|
||||
_capturing = b;
|
||||
_capturingButton = button;
|
||||
button.Content = App.Loc["PressKey"];
|
||||
button.Focus();
|
||||
}
|
||||
|
||||
private void CancelCapture()
|
||||
{
|
||||
if (_capturing is not null && _capturingButton is not null)
|
||||
_capturingButton.Content = ComboText(_capturing);
|
||||
_capturing = null;
|
||||
_capturingButton = null;
|
||||
}
|
||||
|
||||
public void OnKeyDown(object? sender, KeyEventArgs e)
|
||||
{
|
||||
if (_capturing is null)
|
||||
return;
|
||||
|
||||
if (e.Key == Key.Escape)
|
||||
{
|
||||
CancelCapture();
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (KeyMap.IsModifierKey(e.Key))
|
||||
return;
|
||||
|
||||
string? token = KeyMap.FromInput(e.Key, e.PhysicalKey);
|
||||
if (token is null)
|
||||
{
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
bool shift = e.KeyModifiers.HasFlag(KeyModifiers.Shift);
|
||||
bool ctrl = e.KeyModifiers.HasFlag(KeyModifiers.Control);
|
||||
if (KeyMap.SimulatorUse(token, shift, ctrl) is { Blocks: true } taken)
|
||||
{
|
||||
// Bound here it would never fire: the wait for a key goes on.
|
||||
if (_capturingButton is not null)
|
||||
_capturingButton.Content = string.Format(App.Loc["KbTaken"], App.Loc[taken.TextKey]);
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// As the key flyout does: the combination moves here, not doubled up.
|
||||
KeyboardConfig.Instance.Put(_capturing, token, shift, ctrl);
|
||||
e.Handled = true;
|
||||
|
||||
_capturing = null;
|
||||
_capturingButton = null;
|
||||
BindingsEdited();
|
||||
}
|
||||
|
||||
private void RebuildKeyboard()
|
||||
{
|
||||
if (_keyboardPanel is null)
|
||||
return;
|
||||
|
||||
_keyCells.Clear();
|
||||
_keyboardPanel.Children.Clear();
|
||||
var states = ComputeKeyStates();
|
||||
|
||||
var row = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 14 };
|
||||
row.Children.Add(BuildKeyBlock(KeyMap.MainBlock, states));
|
||||
row.Children.Add(BuildKeyBlock(KeyMap.NavBlock, states));
|
||||
row.Children.Add(BuildKeyBlock(KeyMap.NumpadBlock, states));
|
||||
_keyboardPanel.Children.Add(row);
|
||||
}
|
||||
|
||||
private void UpdateKeyboardColors()
|
||||
{
|
||||
var states = ComputeKeyStates();
|
||||
foreach (var (token, content) in _keyCells)
|
||||
{
|
||||
if (content.Children.Count > 0)
|
||||
content.Children.RemoveAt(0);
|
||||
content.Children.Insert(0, BuildKeyBackground(token, states));
|
||||
states.TryGetValue(token, out var state);
|
||||
ToolTip.SetTip(content, BuildKeyTooltip(token, state));
|
||||
}
|
||||
}
|
||||
|
||||
private Control BuildKeyBlock(KeyCap[][] rows, Dictionary<string, KeyState> states)
|
||||
{
|
||||
var block = new StackPanel { Spacing = KeyGap, VerticalAlignment = VerticalAlignment.Top };
|
||||
foreach (var rowCaps in rows)
|
||||
{
|
||||
var rowPanel = new StackPanel { Orientation = Orientation.Horizontal, Spacing = KeyGap };
|
||||
foreach (var cap in rowCaps)
|
||||
rowPanel.Children.Add(BuildKeyCap(cap, states));
|
||||
block.Children.Add(rowPanel);
|
||||
}
|
||||
return block;
|
||||
}
|
||||
|
||||
private Control BuildKeyCap(KeyCap cap, Dictionary<string, KeyState> states)
|
||||
{
|
||||
double width = cap.Width * KeyUnit + (cap.Width - 1) * KeyGap;
|
||||
var content = new Grid { Width = width, Height = KeyHeight };
|
||||
|
||||
var keyBorder = new Border
|
||||
{
|
||||
BorderBrush = KeyBorderBrush,
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(3),
|
||||
ClipToBounds = true,
|
||||
Child = content
|
||||
};
|
||||
|
||||
if (cap.Token is null)
|
||||
{
|
||||
|
||||
content.Children.Add(new Border { Background = KeyFillerBrush });
|
||||
content.Children.Add(new TextBlock
|
||||
{
|
||||
Text = cap.Label,
|
||||
Foreground = FgDimBrush,
|
||||
FontSize = 9,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
});
|
||||
keyBorder.Opacity = 0.55;
|
||||
if (!string.IsNullOrEmpty(cap.Label))
|
||||
ToolTip.SetTip(keyBorder, App.Loc[cap.Label switch
|
||||
{
|
||||
"Shift" or "Ctrl" => "KbModifier",
|
||||
"Alt" => "KbAlt",
|
||||
_ => "KbNotInSimulator"
|
||||
}]);
|
||||
return keyBorder;
|
||||
}
|
||||
|
||||
string token = cap.Token;
|
||||
states.TryGetValue(token.ToLowerInvariant(), out var st);
|
||||
content.Children.Add(BuildKeyBackground(token, states));
|
||||
content.Children.Add(new TextBlock
|
||||
{
|
||||
Text = cap.Label,
|
||||
Foreground = FgBrush,
|
||||
FontSize = 10,
|
||||
FontWeight = FontWeight.SemiBold,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
});
|
||||
ToolTip.SetTip(content, BuildKeyTooltip(token, st));
|
||||
|
||||
keyBorder.Cursor = new Cursor(StandardCursorType.Hand);
|
||||
keyBorder.PointerPressed += (_, _) => ShowKeyFlyout(token, keyBorder);
|
||||
_keyCells[token] = content;
|
||||
return keyBorder;
|
||||
}
|
||||
|
||||
private Control BuildKeyBackground(string token, Dictionary<string, KeyState> states)
|
||||
{
|
||||
states.TryGetValue(token.ToLowerInvariant(), out var state);
|
||||
var colours = new List<IBrush>();
|
||||
for (int i = 0; i < ModifierSlots.Length; i++)
|
||||
if (state is not null && state.SlotOn[i])
|
||||
colours.Add(ModifierSlots[i].Colour);
|
||||
|
||||
if (colours.Count == 0)
|
||||
return new Border { Background = KeyUnassignedBrush };
|
||||
|
||||
var stripes = new UniformGrid { Rows = 1, Columns = colours.Count };
|
||||
foreach (var c in colours)
|
||||
stripes.Children.Add(new Border { Background = c });
|
||||
return stripes;
|
||||
}
|
||||
|
||||
private string BuildKeyTooltip(string token, KeyState? state)
|
||||
{
|
||||
string head = KeyMap.DisplayName(token);
|
||||
var lines = new List<string>();
|
||||
if (state is null || state.Tips.Count == 0)
|
||||
{
|
||||
lines.Add($"{head}: {App.Loc["KbUnassigned"]}");
|
||||
}
|
||||
else
|
||||
{
|
||||
lines.Add(head);
|
||||
lines.AddRange(state.Tips);
|
||||
}
|
||||
lines.AddRange(SimulatorNotes(token));
|
||||
return string.Join("\n", lines);
|
||||
}
|
||||
|
||||
private static string SimulatorUseText(string token, bool shift, bool ctrl, SimulatorKeyUse use) =>
|
||||
string.Format(App.Loc[use.Blocks ? "KbSimBlocks" : "KbSimReplaces"],
|
||||
KeyMap.ComboText(token, shift, ctrl),
|
||||
App.Loc[use.TextKey]);
|
||||
|
||||
/// <summary>What the simulator does with this key by itself, once per thing it does.</summary>
|
||||
private static List<string> SimulatorNotes(string token)
|
||||
{
|
||||
var notes = new List<string>();
|
||||
var seen = new HashSet<SimulatorKeyUse>();
|
||||
foreach (var (shift, ctrl, _, _) in ModifierSlots)
|
||||
if (KeyMap.SimulatorUse(token, shift, ctrl) is { } use && seen.Add(use))
|
||||
notes.Add(SimulatorUseText(token, shift, ctrl, use));
|
||||
return notes;
|
||||
}
|
||||
|
||||
private void ShowKeyFlyout(string token, Control anchor)
|
||||
{
|
||||
CancelCapture();
|
||||
var commands = KeyboardConfig.Instance.Bindings;
|
||||
|
||||
var items = new List<string> { App.Loc["BindNone"] };
|
||||
foreach (var c in commands)
|
||||
items.Add(CommandLabel(c));
|
||||
|
||||
var combos = new ComboBox[ModifierSlots.Length];
|
||||
var grid = new Grid
|
||||
{
|
||||
ColumnDefinitions = new ColumnDefinitions("Auto,*"),
|
||||
RowDefinitions = new RowDefinitions("Auto,Auto,Auto,Auto")
|
||||
};
|
||||
|
||||
for (int i = 0; i < ModifierSlots.Length; i++)
|
||||
{
|
||||
var (shift, ctrl, labelKey, colour) = ModifierSlots[i];
|
||||
|
||||
// The swatch ties the row to the key colours on the keyboard above.
|
||||
var lbl = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
Spacing = 6,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 4, 12, 4),
|
||||
Children =
|
||||
{
|
||||
new Border
|
||||
{
|
||||
Width = 10, Height = 10, CornerRadius = new CornerRadius(2),
|
||||
Background = colour, VerticalAlignment = VerticalAlignment.Center
|
||||
},
|
||||
new TextBlock { Text = App.Loc[labelKey], Foreground = FgBrush, VerticalAlignment = VerticalAlignment.Center }
|
||||
}
|
||||
};
|
||||
Grid.SetRow(lbl, i);
|
||||
Grid.SetColumn(lbl, 0);
|
||||
grid.Children.Add(lbl);
|
||||
|
||||
var cb = new ComboBox
|
||||
{
|
||||
ItemsSource = items,
|
||||
MinWidth = 280,
|
||||
MaxDropDownHeight = 320,
|
||||
Margin = new Thickness(0, 4),
|
||||
HorizontalAlignment = HorizontalAlignment.Stretch
|
||||
};
|
||||
if (KeyMap.SimulatorUse(token, shift, ctrl) is { } use)
|
||||
{
|
||||
ToolTip.SetTip(cb, SimulatorUseText(token, shift, ctrl, use));
|
||||
// What a hand-edited file put there can still be taken off.
|
||||
cb.IsEnabled = !use.Blocks || KeyboardConfig.Instance.BindingAt(token, shift, ctrl) is not null;
|
||||
}
|
||||
cb.SelectionChanged += (_, _) =>
|
||||
{
|
||||
if (_flyoutLoading)
|
||||
return;
|
||||
AssignSlot(token, shift, ctrl, cb.SelectedIndex, commands);
|
||||
SyncFlyoutCombos(token, combos);
|
||||
UpdateKeyboardColors();
|
||||
RebuildList();
|
||||
};
|
||||
Grid.SetRow(cb, i);
|
||||
Grid.SetColumn(cb, 1);
|
||||
grid.Children.Add(cb);
|
||||
combos[i] = cb;
|
||||
}
|
||||
|
||||
SyncFlyoutCombos(token, combos);
|
||||
|
||||
var panel = new StackPanel { Spacing = 8, Margin = new Thickness(12), MinWidth = 360 };
|
||||
panel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = $"{App.Loc["BindKeyTitle"]} {KeyMap.DisplayName(token)}",
|
||||
FontWeight = FontWeight.SemiBold,
|
||||
Foreground = FgBrush
|
||||
});
|
||||
panel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = App.Loc["BindKeyHint"],
|
||||
Foreground = FgDimBrush,
|
||||
FontSize = 12,
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
});
|
||||
panel.Children.Add(grid);
|
||||
foreach (string note in SimulatorNotes(token))
|
||||
panel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = note,
|
||||
Foreground = ConflictBrush,
|
||||
FontSize = 12,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
MaxWidth = 460
|
||||
});
|
||||
|
||||
var flyout = new Flyout { Content = panel };
|
||||
flyout.ShowAt(anchor);
|
||||
}
|
||||
|
||||
private void SyncFlyoutCombos(string token, ComboBox[] combos)
|
||||
{
|
||||
var commands = KeyboardConfig.Instance.Bindings;
|
||||
_flyoutLoading = true;
|
||||
for (int i = 0; i < combos.Length; i++)
|
||||
{
|
||||
var cmd = KeyboardConfig.Instance.BindingAt(token, ModifierSlots[i].Shift, ModifierSlots[i].Ctrl);
|
||||
combos[i].SelectedIndex = cmd is null ? 0 : commands.IndexOf(cmd) + 1;
|
||||
}
|
||||
_flyoutLoading = false;
|
||||
}
|
||||
|
||||
private void AssignSlot(string token, bool shift, bool ctrl, int selectedIndex, List<KeyBinding> commands)
|
||||
{
|
||||
KeyBinding? chosen = selectedIndex <= 0 ? null : commands[selectedIndex - 1];
|
||||
KeyboardConfig.Instance.Put(chosen, token, shift, ctrl);
|
||||
KeyboardConfig.Instance.Dirty = true;
|
||||
Changed?.Invoke();
|
||||
}
|
||||
|
||||
private Control BuildLegend()
|
||||
{
|
||||
var legend = new WrapPanel { Orientation = Orientation.Horizontal };
|
||||
legend.Children.Add(LegendItem(KeyUnassignedBrush, App.Loc["KbUnassigned"]));
|
||||
legend.Children.Add(LegendItem(KeyPlainBrush, App.Loc["KbAssigned"]));
|
||||
legend.Children.Add(LegendItem(KeyShiftBrush, App.Loc["KbShift"]));
|
||||
legend.Children.Add(LegendItem(KeyCtrlBrush, App.Loc["KbCtrl"]));
|
||||
legend.Children.Add(LegendItem(KeyShiftCtrlBrush, App.Loc["KbShiftCtrl"]));
|
||||
return legend;
|
||||
}
|
||||
|
||||
private Control LegendItem(IBrush brush, string text)
|
||||
{
|
||||
var sp = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
Spacing = 6,
|
||||
Margin = new Thickness(0, 0, 16, 0),
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
};
|
||||
sp.Children.Add(new Border
|
||||
{
|
||||
Width = 16,
|
||||
Height = 16,
|
||||
Background = brush,
|
||||
BorderBrush = KeyBorderBrush,
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(2),
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
});
|
||||
sp.Children.Add(new TextBlock { Text = text, Foreground = FgDimBrush, VerticalAlignment = VerticalAlignment.Center });
|
||||
return sp;
|
||||
}
|
||||
|
||||
private sealed class KeyState
|
||||
{
|
||||
/// <summary>Which of the <see cref="ModifierSlots"/> have a binding.</summary>
|
||||
public readonly bool[] SlotOn = new bool[ModifierSlots.Length];
|
||||
public readonly List<string> Tips = new();
|
||||
}
|
||||
|
||||
private static Dictionary<string, KeyState> ComputeKeyStates()
|
||||
{
|
||||
var map = new Dictionary<string, KeyState>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var b in KeyboardConfig.Instance.Bindings)
|
||||
{
|
||||
if (!b.IsAssigned)
|
||||
continue;
|
||||
string key = b.Key.ToLowerInvariant();
|
||||
if (!map.TryGetValue(key, out var state))
|
||||
map[key] = state = new KeyState();
|
||||
|
||||
state.SlotOn[Array.FindIndex(ModifierSlots, slot => slot.Shift == b.Shift && slot.Ctrl == b.Ctrl)] = true;
|
||||
|
||||
state.Tips.Add($"{ComboText(b)} — {CommandLabel(b)}");
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private static string ComboText(KeyBinding b) => KeyMap.ComboText(b.Key, b.Shift, b.Ctrl);
|
||||
|
||||
private static string CommandLabel(KeyBinding b) =>
|
||||
KeyboardConfig.Instance.DescriptionOf(b, TranslatedCommand) is { Length: > 0 } description
|
||||
? Capitalize(description)
|
||||
: b.Command;
|
||||
|
||||
private static string Capitalize(string s) =>
|
||||
string.IsNullOrEmpty(s) ? s : char.ToUpperInvariant(s[0]) + s.Substring(1);
|
||||
}
|
||||
105
StarterNG/Views/LogAndToolsWindow.cs
Normal file
105
StarterNG/Views/LogAndToolsWindow.cs
Normal file
@@ -0,0 +1,105 @@
|
||||
using System;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
using StarterNG.Infrastructure;
|
||||
|
||||
namespace StarterNG.Views;
|
||||
|
||||
/// <summary>
|
||||
/// The starter's event log and the bulk depot tools. The options it used to carry
|
||||
/// live in the Advanced category of the settings now, where they are found.
|
||||
/// </summary>
|
||||
public sealed class LogAndToolsWindow : Window
|
||||
{
|
||||
private readonly TextBox _log = new()
|
||||
{
|
||||
IsReadOnly = true,
|
||||
AcceptsReturn = true,
|
||||
TextWrapping = TextWrapping.NoWrap,
|
||||
FontFamily = MonoFont.Family,
|
||||
FontSize = 12
|
||||
};
|
||||
|
||||
public event Action<string>? ToolRequested;
|
||||
|
||||
public LogAndToolsWindow()
|
||||
{
|
||||
Title = App.Loc["LogAndTools"];
|
||||
Width = 620;
|
||||
Height = 470;
|
||||
MinWidth = 480;
|
||||
MinHeight = 360;
|
||||
WindowStartupLocation = WindowStartupLocation.CenterOwner;
|
||||
|
||||
var tabs = new TabControl();
|
||||
tabs.Items.Add(new TabItem { Header = App.Loc["AdvLogPage"], Content = BuildLogPage() });
|
||||
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 += (_, _) => 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 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;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Layout;
|
||||
@@ -18,34 +19,13 @@ public static class MessageBox
|
||||
public static async Task<bool> Show(Window owner, string message, string title, MessageBoxButtons buttons,
|
||||
string? yesText = null, string? noText = null)
|
||||
{
|
||||
var win = new Window
|
||||
{
|
||||
Title = title,
|
||||
Width = 420,
|
||||
SizeToContent = SizeToContent.Height,
|
||||
WindowStartupLocation = WindowStartupLocation.CenterOwner,
|
||||
CanResize = false
|
||||
};
|
||||
var win = FixedDialog(title, 420);
|
||||
|
||||
bool yes = false;
|
||||
var yesBtn = new Button { Content = App.Loc["Yes"], MinWidth = 80, Cursor = new Avalonia.Input.Cursor(Avalonia.Input.StandardCursorType.Hand) };
|
||||
yesBtn.Classes.Add("Accent");
|
||||
yesBtn.Click += (_, _) => { yes = true; win.Close(); };
|
||||
|
||||
var noBtn = new Button { Content = App.Loc["No"], MinWidth = 80, Cursor = new Avalonia.Input.Cursor(Avalonia.Input.StandardCursorType.Hand) };
|
||||
noBtn.Classes.Add("Flat");
|
||||
noBtn.Click += (_, _) => win.Close();
|
||||
|
||||
if (yesText is not null)
|
||||
yesBtn.Content = yesText;
|
||||
if (noText is not null)
|
||||
noBtn.Content = noText;
|
||||
|
||||
if (buttons == MessageBoxButtons.Ok)
|
||||
{
|
||||
yesBtn.Content = App.Loc["Ok"];
|
||||
noBtn.IsVisible = false;
|
||||
}
|
||||
var yesBtn = DialogButton(buttons == MessageBoxButtons.Ok ? App.Loc["Ok"] : yesText ?? App.Loc["Yes"],
|
||||
"Accent", 80, () => { yes = true; win.Close(); });
|
||||
var noBtn = DialogButton(noText ?? App.Loc["No"], "Flat", 80, () => win.Close());
|
||||
noBtn.IsVisible = buttons != MessageBoxButtons.Ok;
|
||||
|
||||
win.Content = new StackPanel
|
||||
{
|
||||
@@ -54,17 +34,81 @@ public static class MessageBox
|
||||
Children =
|
||||
{
|
||||
new TextBlock { Text = message, TextWrapping = TextWrapping.Wrap },
|
||||
new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
Spacing = 8,
|
||||
Children = { yesBtn, noBtn }
|
||||
}
|
||||
ButtonRow(yesBtn, noBtn)
|
||||
}
|
||||
};
|
||||
|
||||
await win.ShowDialog(owner);
|
||||
return yes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A notice with one action besides closing it, and a box to stop it coming
|
||||
/// back, which counts however the window is closed.
|
||||
/// </summary>
|
||||
public static async Task<(bool Act, bool Silence)> ShowNotice(Window owner, string message, string title,
|
||||
string actionText, string silenceText)
|
||||
{
|
||||
var win = FixedDialog(title, 460);
|
||||
|
||||
bool act = false;
|
||||
var actBtn = DialogButton(actionText, "Accent", null, () => { act = true; win.Close(); });
|
||||
var okBtn = DialogButton(App.Loc["Ok"], "Flat", 80, () => win.Close());
|
||||
|
||||
var silence = new CheckBox { Content = silenceText, VerticalAlignment = VerticalAlignment.Center };
|
||||
|
||||
win.Content = new StackPanel
|
||||
{
|
||||
Margin = new Avalonia.Thickness(16),
|
||||
Spacing = 16,
|
||||
Children =
|
||||
{
|
||||
new SelectableTextBlock { Text = message, TextWrapping = TextWrapping.Wrap },
|
||||
new DockPanel
|
||||
{
|
||||
Children =
|
||||
{
|
||||
silence,
|
||||
ButtonRow(actBtn, okBtn)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await win.ShowDialog(owner);
|
||||
return (act, silence.IsChecked == true);
|
||||
}
|
||||
|
||||
/// <summary>A dialog over its owner, as tall as what it holds, not to be resized.</summary>
|
||||
internal static Window FixedDialog(string title, double width) => new()
|
||||
{
|
||||
Title = title,
|
||||
Width = width,
|
||||
SizeToContent = SizeToContent.Height,
|
||||
WindowStartupLocation = WindowStartupLocation.CenterOwner,
|
||||
CanResize = false
|
||||
};
|
||||
|
||||
/// <summary>A dialog's buttons, at its right edge.</summary>
|
||||
internal static StackPanel ButtonRow(params Button[] buttons)
|
||||
{
|
||||
var row = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
Spacing = 8
|
||||
};
|
||||
row.Children.AddRange(buttons);
|
||||
return row;
|
||||
}
|
||||
|
||||
private static Button DialogButton(string content, string cssClass, double? minWidth, Action onClick)
|
||||
{
|
||||
var button = new Button { Content = content, Cursor = new Avalonia.Input.Cursor(Avalonia.Input.StandardCursorType.Hand) };
|
||||
if (minWidth is { } width)
|
||||
button.MinWidth = width;
|
||||
button.Classes.Add(cssClass);
|
||||
button.Click += (_, _) => onClick();
|
||||
return button;
|
||||
}
|
||||
}
|
||||
|
||||
124
StarterNG/Views/SettingTexts.cs
Normal file
124
StarterNG/Views/SettingTexts.cs
Normal file
@@ -0,0 +1,124 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using StarterNG.Domain.Settings;
|
||||
|
||||
namespace StarterNG.Views;
|
||||
|
||||
/// <summary>The captions of the settings' rows and the words for their values.</summary>
|
||||
internal static class SettingTexts
|
||||
{
|
||||
public static string[] QualityWords => new[]
|
||||
{
|
||||
App.Loc["QualityVeryLow"], App.Loc["QualityLow"], App.Loc["QualityNormal"],
|
||||
App.Loc["QualityHigh"], App.Loc["QualityVeryHigh"]
|
||||
};
|
||||
|
||||
/// <summary>The words of a slider that goes from 1 to 4, which has no middle.</summary>
|
||||
public static string Fidelity(int level)
|
||||
{
|
||||
var q = QualityWords;
|
||||
string[] four = { q[0], q[1], q[3], q[4] };
|
||||
return four[Math.Clamp(level, 1, 4) - 1];
|
||||
}
|
||||
|
||||
public static string Anisotropy(double value) => "x" + value.ToString("0.#", CultureInfo.CurrentCulture);
|
||||
|
||||
public static string Multisampling(int level) => level <= 0 ? App.Loc["MsaaNone"] : "x" + (1 << level);
|
||||
|
||||
public static string TwoDecimals(double value) => value.ToString("0.00", CultureInfo.CurrentCulture);
|
||||
|
||||
public 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"]
|
||||
};
|
||||
|
||||
public static string ShadowRangeWord(int metres) => metres switch
|
||||
{
|
||||
<= 100 => App.Loc["RangeLow"],
|
||||
<= 150 => App.Loc["RangeModerate"],
|
||||
<= 250 => App.Loc["RangeHigh"],
|
||||
_ => App.Loc["RangeVeryHigh"]
|
||||
};
|
||||
|
||||
public 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"]
|
||||
};
|
||||
|
||||
/// <summary>A rate below one a second reads better as the time between updates.</summary>
|
||||
public static string ReflectionsInterval(double perSecond)
|
||||
{
|
||||
if (perSecond >= 1)
|
||||
return string.Format(App.Loc["TimesPerSecond"], perSecond.ToString("0.##", CultureInfo.CurrentCulture));
|
||||
double seconds = 1 / perSecond;
|
||||
return seconds >= 60
|
||||
? string.Format(App.Loc["EveryMinutes"], (seconds / 60).ToString("0.#", CultureInfo.CurrentCulture))
|
||||
: string.Format(App.Loc["EverySeconds"], seconds.ToString("0.#", CultureInfo.CurrentCulture));
|
||||
}
|
||||
|
||||
/// <summary>What a level sets, in the words and the units of the rows it sets.</summary>
|
||||
public static string PresetSummary(GraphicsPreset p)
|
||||
{
|
||||
var lines = new List<string>();
|
||||
void Add(string label, string value) => lines.Add(App.Loc[label] + ": " + value);
|
||||
string YesNo(bool on) => App.Loc[on ? "Yes" : "No"];
|
||||
string Whole(double value) => value.ToString("0.#", CultureInfo.CurrentCulture);
|
||||
|
||||
Add("MaxTexResolution", p.TextureSize + " px");
|
||||
Add("MaxTexCabResolution", p.CabTextureSize + " px");
|
||||
Add("TexFilteringQuality", Anisotropy(p.Anisotropy));
|
||||
Add("Multisampling", Multisampling(p.Multisampling));
|
||||
Add("DynamicLights", p.DynamicLights.ToString(CultureInfo.CurrentCulture));
|
||||
Add("SmokeParticlesMultiplier", Fidelity(p.SmokeFidelity));
|
||||
Add("RenderShadows", YesNo(p.Shadows));
|
||||
if (p.Shadows)
|
||||
{
|
||||
Add("ShadowsResolution", p.ShadowMap + " px");
|
||||
Add("ShaderRange", SimulatorSettings.ShadowReach(p.ShadowRange, p.ShadowMap, legacyRenderer: false) + " m");
|
||||
Add("ShadowDisplay", App.Loc[p.ShadowRank switch { 1 => "CSROnlyImportant", 2 => "CSRLimited", _ => "CSRAll" }]);
|
||||
}
|
||||
Add("ReflectionsCubeMap", YesNo(p.EnvMap));
|
||||
if (p.EnvMap)
|
||||
{
|
||||
Add("ReflectionsDetails", App.Loc[p.ReflectionsFidelity switch { 0 => "RDTerrain", 1 => "RDTerrainModels", _ => "RDTerrainModelsVehicles" }]);
|
||||
Add("ReflectionsFramerate", ReflectionsInterval(p.ReflectionsRate));
|
||||
}
|
||||
Add("AdditionalShadersEffects", YesNo(p.ExtraEffects));
|
||||
Add("TrackCurvesQuality", Fidelity(p.SplineFidelity));
|
||||
Add("Ini.basedrawrange", Whole(p.DrawRange) + " m");
|
||||
Add("Ini.heightbase", "×" + p.DistanceFactor.ToString("0.##", CultureInfo.CurrentCulture));
|
||||
Add("BufforResolution", p.BufferScale + "%");
|
||||
Add("Ini." + GraphicsPreset.SleeperKey, Whole(p.SleeperDistance) + " m");
|
||||
Add("RenderRange", "×" + Whole(p.DrawRangeFactor));
|
||||
Add("Ini." + GraphicsPreset.ReflectedInstancesKey, Whole(p.ReflectedInstances) + " m");
|
||||
Add("Ini." + GraphicsPreset.ReflectedVehiclesKey, Whole(p.ReflectedVehicles) + " m");
|
||||
Add("CabShadowsRange", p.CabShadowsRange + " m");
|
||||
|
||||
if (p.Extras is { } extras)
|
||||
{
|
||||
Add("MotionBlur", YesNo(extras.MotionBlur));
|
||||
Add("ChromaticAberration", YesNo(extras.ChromaticAberration));
|
||||
Add("SmokeDisplay", YesNo(extras.Smoke));
|
||||
Add("PyScreenPriority", App.Loc[extras.PythonUpdateMs <= 200 ? "PyPriorityNormal" : "PyPriorityLowest"]);
|
||||
}
|
||||
if (p.Top is { } top)
|
||||
{
|
||||
lines.Insert(0, App.Loc["PresetUltraNote"]);
|
||||
Add("ShadowAngleLimit", TwoDecimals(top.ShadowAngleLimit));
|
||||
Add("Ini." + GraphicsTop.TargetFpsKey, YesNo(top.TargetFps > 0));
|
||||
Add("Ini." + GraphicsTop.TextureReleaseKey, YesNo(top.TextureRelease > 0));
|
||||
Add("CompressTextures", YesNo(top.CompressTextures));
|
||||
Add("IgnoreIrrelevantTrains", YesNo(top.SkipDecor));
|
||||
}
|
||||
return string.Join("\n", lines);
|
||||
}
|
||||
}
|
||||
@@ -14,14 +14,17 @@
|
||||
<Border DockPanel.Dock="Bottom" Padding="16,10" Background="{StaticResource CarbonPanelBg}"
|
||||
BorderBrush="{StaticResource CarbonBorderLight}" BorderThickness="0,1,0,0">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
|
||||
<TextBlock VerticalAlignment="Center" Opacity="0.6" FontSize="12"
|
||||
<!-- A dock rather than a stack, so a long path is cut short instead of running under the buttons. -->
|
||||
<DockPanel Grid.Column="0" VerticalAlignment="Center" Margin="0,0,12,0">
|
||||
<TextBlock DockPanel.Dock="Left" VerticalAlignment="Center" Opacity="0.6" FontSize="12" Margin="0,0,6,0"
|
||||
Text="{Binding [SettingsPath], Source={x:Static starterNg:App.Loc}}" />
|
||||
<TextBlock x:Name="SettingsPathText" VerticalAlignment="Center" Opacity="0.8" FontSize="12"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
</StackPanel>
|
||||
TextTrimming="LeadingCharacterEllipsis" />
|
||||
</DockPanel>
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="12" HorizontalAlignment="Right">
|
||||
<TextBlock x:Name="SaveStatus" VerticalAlignment="Center" Opacity="0.8" Text="" />
|
||||
<Button x:Name="ChangesButton" Classes="Flat" Click="ChangesButton_OnClick"
|
||||
Content="{Binding [IniChangesButton], 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" Click="SaveButton_OnClick"
|
||||
@@ -64,11 +67,30 @@
|
||||
<ListBoxItem Tag="physics" Content="{Binding [Physics], Source={x:Static starterNg:App.Loc}}" />
|
||||
<ListBoxItem Tag="devices" Content="{Binding [Communication], Source={x:Static starterNg:App.Loc}}" />
|
||||
<ListBoxItem Tag="controls" Content="{Binding [Controls], Source={x:Static starterNg:App.Loc}}" />
|
||||
<ListBoxItem Tag="advanced" Content="{Binding [Advanced], Source={x:Static starterNg:App.Loc}}" />
|
||||
</ListBox>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<Panel Grid.Column="1">
|
||||
<Grid Grid.Column="1" RowDefinitions="Auto,*">
|
||||
<!-- A level of detail in one go; stays in sight while the rows below follow it. -->
|
||||
<Border x:Name="PresetBar" IsVisible="False" Padding="20,8"
|
||||
Background="{StaticResource CarbonPanelBg}"
|
||||
BorderBrush="{StaticResource CarbonBorderLight}" BorderThickness="0,0,0,1">
|
||||
<StackPanel Spacing="4" MaxWidth="880">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" ColumnSpacing="10">
|
||||
<TextBlock VerticalAlignment="Center" FontWeight="Bold"
|
||||
Text="{Binding [GfxColPresets], Source={x:Static starterNg:App.Loc}}" />
|
||||
<StackPanel x:Name="PresetButtons" Grid.Column="1" Orientation="Horizontal" Spacing="6" />
|
||||
<TextBlock x:Name="PresetStateText" Grid.Column="2" VerticalAlignment="Center" FontSize="12"
|
||||
Foreground="{StaticResource CarbonFgDim}" />
|
||||
</Grid>
|
||||
<TextBlock FontSize="12" TextWrapping="Wrap" Foreground="{StaticResource CarbonFgDim}"
|
||||
Text="{Binding [GfxPresetDesc], Source={x:Static starterNg:App.Loc}}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Panel Grid.Row="1">
|
||||
<ScrollViewer x:Name="SettingsScroll"
|
||||
VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
|
||||
<StackPanel x:Name="SettingsStack" Margin="20,18,20,18" MaxWidth="880">
|
||||
@@ -79,7 +101,15 @@
|
||||
Text="{Binding [General], Source={x:Static starterNg:App.Loc}}" />
|
||||
<ctl:SettingsSection Header="{Binding [General], Source={x:Static starterNg:App.Loc}}">
|
||||
<StackPanel>
|
||||
<ctl:SettingRow ConfigKey="lang" Label="{Binding [Language], Source={x:Static starterNg:App.Loc}}">
|
||||
<ctl:SettingRow ConfigKey="starter.exe.path" Label="{Binding [SelectEXE], Source={x:Static starterNg:App.Loc}}">
|
||||
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="6">
|
||||
<ComboBox x:Name="SelectExeCb" Classes="Localized" SelectedIndex="0" HorizontalAlignment="Stretch" />
|
||||
<Button x:Name="BrowseExeButton" Grid.Column="1" Classes="Flat" Cursor="Hand"
|
||||
Click="BrowseExeButton_OnClick"
|
||||
Content="{Binding [SelectEXEBrowse], Source={x:Static starterNg:App.Loc}}" />
|
||||
</Grid>
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="lang" x:Name="LanguageRow" Label="{Binding [Language], Source={x:Static starterNg:App.Loc}}">
|
||||
<ComboBox x:Name="ChangeLanguageCb"
|
||||
SelectionChanged="LanguageComboBox_OnSelectionChanged" />
|
||||
</ctl:SettingRow>
|
||||
@@ -121,7 +151,7 @@
|
||||
<ctl:SettingRow ConfigKey="starter.batterydefault"
|
||||
Label="{Binding [ReadyOverride], Source={x:Static starterNg:App.Loc}}"
|
||||
Description="{Binding [ReadyOverrideDesc], Source={x:Static starterNg:App.Loc}}">
|
||||
<ComboBox x:Name="ReadyOverrideCb" SelectedIndex="0">
|
||||
<ComboBox x:Name="ReadyOverrideCb" Classes="Localized" SelectedIndex="0">
|
||||
<ComboBoxItem Content="{Binding [ReadyOverrideScenery], Source={x:Static starterNg:App.Loc}}" />
|
||||
<ComboBoxItem Content="{Binding [ReadyOverrideCold], Source={x:Static starterNg:App.Loc}}" />
|
||||
<ComboBoxItem Content="{Binding [ReadyOverrideReady], Source={x:Static starterNg:App.Loc}}" />
|
||||
@@ -132,14 +162,6 @@
|
||||
|
||||
<ctl:SettingsSection Header="{Binding [Others], Source={x:Static starterNg:App.Loc}}">
|
||||
<StackPanel>
|
||||
<ctl:SettingRow ConfigKey="starter.exe.path" Label="{Binding [SelectEXE], Source={x:Static starterNg:App.Loc}}">
|
||||
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="6">
|
||||
<ComboBox x:Name="SelectExeCb" SelectedIndex="0" HorizontalAlignment="Stretch" />
|
||||
<Button x:Name="BrowseExeButton" Grid.Column="1" Classes="Flat" Cursor="Hand"
|
||||
Click="BrowseExeButton_OnClick"
|
||||
Content="{Binding [SelectEXEBrowse], Source={x:Static starterNg:App.Loc}}" />
|
||||
</Grid>
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow Description="{Binding [DebugModeDesc], Source={x:Static starterNg:App.Loc}}" ConfigKey="debugmode">
|
||||
<CheckBox x:Name="DebugModeCb" Content="{Binding [DebugMode], Source={x:Static starterNg:App.Loc}}" />
|
||||
</ctl:SettingRow>
|
||||
@@ -149,9 +171,6 @@
|
||||
<ctl:SettingRow ConfigKey="starter.logmissingvehicles" Description="{Binding [LogMissingVehicleFilesDesc], Source={x:Static starterNg:App.Loc}}">
|
||||
<CheckBox x:Name="LogMissingVehicleFilesCb" Content="{Binding [LogMissingVehicleFiles], Source={x:Static starterNg:App.Loc}}" />
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="starter.developertools" Description="{Binding [DeveloperToolsDesc], Source={x:Static starterNg:App.Loc}}">
|
||||
<CheckBox x:Name="DeveloperToolsCb" Content="{Binding [DeveloperTools], Source={x:Static starterNg:App.Loc}}" />
|
||||
</ctl:SettingRow>
|
||||
</StackPanel>
|
||||
</ctl:SettingsSection>
|
||||
</StackPanel>
|
||||
@@ -160,17 +179,48 @@
|
||||
<StackPanel Tag="graphics" IsVisible="False">
|
||||
<TextBlock Classes="CategoryCaption" IsVisible="False"
|
||||
Text="{Binding [Graphic], Source={x:Static starterNg:App.Loc}}" />
|
||||
<!-- Which renderer comes first: it decides which of the rows below the simulator reads. -->
|
||||
<ctl:SettingsSection Header="{Binding [GfxColRendering], Source={x:Static starterNg:App.Loc}}">
|
||||
<StackPanel>
|
||||
<ctl:SettingRow ConfigKey="gfxrenderer" Label="{Binding [RenderEngine], Source={x:Static starterNg:App.Loc}}"
|
||||
Description="{Binding [RenderEngineDesc], Source={x:Static starterNg:App.Loc}}">
|
||||
<StackPanel Spacing="6">
|
||||
<ComboBox x:Name="RenderEngineCb" Classes="Localized" SelectedIndex="0"
|
||||
SelectionChanged="RenderEngineCb_OnSelectionChanged">
|
||||
<ComboBoxItem Content="{Binding [REFull], Source={x:Static starterNg:App.Loc}}" />
|
||||
<ComboBoxItem Content="{Binding [REOld], Source={x:Static starterNg:App.Loc}}" />
|
||||
<ComboBoxItem Content="{Binding [RESimp], Source={x:Static starterNg:App.Loc}}" />
|
||||
<ComboBoxItem Content="{Binding [REExperimental], Source={x:Static starterNg:App.Loc}}" />
|
||||
</ComboBox>
|
||||
<TextBlock x:Name="RenderEngineWarning" IsVisible="False" TextWrapping="Wrap"
|
||||
Foreground="{StaticResource CarbonAccentBright}"
|
||||
Text="{Binding [REExperimentalWarning], Source={x:Static starterNg:App.Loc}}" />
|
||||
<!-- Amber like the UART warnings: this one stops the simulator. -->
|
||||
</StackPanel>
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow x:Name="GpuRow" ConfigKey="starter.gpu" Label="{Binding [GpuChoice], Source={x:Static starterNg:App.Loc}}">
|
||||
<ComboBox x:Name="GpuCb" Classes="Localized" SelectedIndex="0"
|
||||
SelectionChanged="GpuCb_OnSelectionChanged">
|
||||
<ComboBoxItem Content="{Binding [GpuDiscrete], Source={x:Static starterNg:App.Loc}}" />
|
||||
<ComboBoxItem Content="{Binding [GpuSystem], Source={x:Static starterNg:App.Loc}}" />
|
||||
</ComboBox>
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="gfx.angleplatform" Description="{Binding [RenderAngleVulkanDesc], Source={x:Static starterNg:App.Loc}}">
|
||||
<CheckBox x:Name="RenderAngleVulkanCb" Content="{Binding [RenderAngleVulkan], Source={x:Static starterNg:App.Loc}}" />
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="gfx.skippipeline" Description="{Binding [SkipPipelineDesc], Source={x:Static starterNg:App.Loc}}">
|
||||
<CheckBox x:Name="SkipPipelineCb" Content="{Binding [SkipPipeline], Source={x:Static starterNg:App.Loc}}" />
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="usevbo" Description="{Binding [RenderVBODesc], Source={x:Static starterNg:App.Loc}}">
|
||||
<CheckBox x:Name="RenderVBOCb" Content="{Binding [RenderVBO], Source={x:Static starterNg:App.Loc}}" />
|
||||
</ctl:SettingRow>
|
||||
</StackPanel>
|
||||
</ctl:SettingsSection>
|
||||
|
||||
<ctl:SettingsSection Header="{Binding [GfxColResolution], Source={x:Static starterNg:App.Loc}}">
|
||||
<StackPanel>
|
||||
<ctl:SettingRow ConfigKey="width, height" Label="{Binding [Resolution], Source={x:Static starterNg:App.Loc}}">
|
||||
<ComboBox x:Name="ResolutionCb" SelectedIndex="0">
|
||||
<ComboBoxItem>3840x2160</ComboBoxItem>
|
||||
<ComboBoxItem>2560x1440</ComboBoxItem>
|
||||
<ComboBoxItem>1920x1080</ComboBoxItem>
|
||||
<ComboBoxItem>1600x900</ComboBoxItem>
|
||||
<ComboBoxItem>1366x768</ComboBoxItem>
|
||||
<ComboBoxItem>1280x720</ComboBoxItem>
|
||||
</ComboBox>
|
||||
<ComboBox x:Name="ResolutionCb" />
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="fullscreenwindowed">
|
||||
<CheckBox x:Name="FullscreenWindowedCb" Content="{Binding [FullscreenWindowed], Source={x:Static starterNg:App.Loc}}" />
|
||||
@@ -181,32 +231,45 @@
|
||||
<Slider Name="bufferScale" Minimum="50" Maximum="200" TickFrequency="1"
|
||||
IsSnapToTickEnabled="True" Value="100" />
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="maxtexturesize" x:Name="TexResolutionRow"
|
||||
Label="{Binding [MaxTexResolution], Source={x:Static starterNg:App.Loc}}">
|
||||
<Slider Name="textureResolutionSlider" Minimum="9" Maximum="14" TickFrequency="1"
|
||||
IsSnapToTickEnabled="True" Value="12"
|
||||
ValueChanged="TextureResolutionSlider_OnValueChanged" />
|
||||
<ctl:SettingRow ConfigKey="fieldofview" Label="{Binding [FieldOfView], Source={x:Static starterNg:App.Loc}}"
|
||||
Description="{Binding [FieldOfViewDesc], Source={x:Static starterNg:App.Loc}}"
|
||||
ValueText="{Binding #fovSlider.Value, StringFormat='{}{0:0}°'}">
|
||||
<Slider Name="fovSlider" Minimum="10" Maximum="75" TickFrequency="5"
|
||||
IsSnapToTickEnabled="True" Value="45" />
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="maxcabtexturesize" x:Name="CabTexResolutionRow"
|
||||
Label="{Binding [MaxTexCabResolution], Source={x:Static starterNg:App.Loc}}">
|
||||
<Slider Name="cabTextureResolutionSlider" Minimum="9" Maximum="14" TickFrequency="1"
|
||||
IsSnapToTickEnabled="True" Value="12"
|
||||
ValueChanged="CabTextureResolutionSlider_OnValueChanged" />
|
||||
</StackPanel>
|
||||
</ctl:SettingsSection>
|
||||
|
||||
<ctl:SettingsSection Header="{Binding [GfxColSmoothness], Source={x:Static starterNg:App.Loc}}">
|
||||
<StackPanel>
|
||||
<ctl:SettingRow ConfigKey="vsync" Description="{Binding [VSyncDesc], Source={x:Static starterNg:App.Loc}}">
|
||||
<CheckBox x:Name="VSyncCb" Content="{Binding [VSync], Source={x:Static starterNg:App.Loc}}" />
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="anisotropicfiltering" x:Name="TexFilteringRow"
|
||||
Label="{Binding [TexFilteringQuality], Source={x:Static starterNg:App.Loc}}"
|
||||
Description="{Binding [TexFilteringQualityDesc], Source={x:Static starterNg:App.Loc}}">
|
||||
<Slider x:Name="TexFilteringSlider" Minimum="1" Maximum="5" TickFrequency="1"
|
||||
IsSnapToTickEnabled="True" Value="4"
|
||||
ValueChanged="QualitySlider_OnValueChanged" />
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="multisampling" x:Name="MultisamplingRow"
|
||||
Label="{Binding [Multisampling], Source={x:Static starterNg:App.Loc}}"
|
||||
Description="{Binding [MultisamplingDesc], Source={x:Static starterNg:App.Loc}}">
|
||||
<Slider x:Name="MultisamplingSlider" Minimum="1" Maximum="4" TickFrequency="1"
|
||||
IsSnapToTickEnabled="True" Value="3"
|
||||
<ctl:SettingRow ConfigKey="fpslimit" x:Name="FpsLimitRow"
|
||||
Label="{Binding [FpsLimit], Source={x:Static starterNg:App.Loc}}"
|
||||
Description="{Binding [FpsLimitDesc], Source={x:Static starterNg:App.Loc}}">
|
||||
<StackPanel Spacing="4">
|
||||
<!-- Moves through no limit and Steps.FrameRateLimits; see SliderSteps. -->
|
||||
<Slider x:Name="FpsLimitSlider" ValueChanged="QualitySlider_OnValueChanged" />
|
||||
<!-- The screens found, each with a button to match the limit to it. -->
|
||||
<WrapPanel x:Name="MonitorRates" IsVisible="False" />
|
||||
</StackPanel>
|
||||
</ctl:SettingRow>
|
||||
</StackPanel>
|
||||
</ctl:SettingsSection>
|
||||
|
||||
<ctl:SettingsSection x:Name="DrawRangeSection" Header="{Binding [GfxColDrawRange], Source={x:Static starterNg:App.Loc}}">
|
||||
<StackPanel>
|
||||
<ctl:KeySlot Key="basedrawrange" />
|
||||
<ctl:KeySlot Key="targetfps" />
|
||||
<ctl:SettingRow ConfigKey="gfx.drawrange.factor.max" x:Name="RenderRangeRow"
|
||||
Label="{Binding [RenderRange], Source={x:Static starterNg:App.Loc}}"
|
||||
Description="{Binding [RenderRangeDesc], Source={x:Static starterNg:App.Loc}}">
|
||||
<!-- Moves through Steps.DrawRangeFactors; see SliderSteps. -->
|
||||
<Slider x:Name="RenderRangeSlider"
|
||||
ValueChanged="QualitySlider_OnValueChanged" />
|
||||
</ctl:SettingRow>
|
||||
<ctl:KeySlot Key="sleeperdistance" />
|
||||
<ctl:SettingRow ConfigKey="splinefidelity" x:Name="TrackCurvesRow"
|
||||
Label="{Binding [TrackCurvesQuality], Source={x:Static starterNg:App.Loc}}"
|
||||
Description="{Binding [TrackCurvesQualityDesc], Source={x:Static starterNg:App.Loc}}">
|
||||
@@ -214,38 +277,44 @@
|
||||
IsSnapToTickEnabled="True" Value="4"
|
||||
ValueChanged="QualitySlider_OnValueChanged" />
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="gfx.drawrange.factor.max" x:Name="RenderRangeRow"
|
||||
Label="{Binding [RenderRange], Source={x:Static starterNg:App.Loc}}"
|
||||
Description="{Binding [RenderRangeDesc], Source={x:Static starterNg:App.Loc}}">
|
||||
<Slider x:Name="RenderRangeSlider" Minimum="1" Maximum="3" TickFrequency="1"
|
||||
IsSnapToTickEnabled="True" Value="1"
|
||||
ValueChanged="QualitySlider_OnValueChanged" />
|
||||
</ctl:SettingRow>
|
||||
</StackPanel>
|
||||
</ctl:SettingsSection>
|
||||
|
||||
<ctl:SettingsSection Header="{Binding [GfxColRendering], Source={x:Static starterNg:App.Loc}}">
|
||||
<ctl:SettingsSection x:Name="TexturesSection" Header="{Binding [GfxColTextures], Source={x:Static starterNg:App.Loc}}">
|
||||
<StackPanel>
|
||||
<ctl:SettingRow ConfigKey="gfxrenderer" Label="{Binding [RenderEngine], Source={x:Static starterNg:App.Loc}}"
|
||||
Description="{Binding [RenderEngineDesc], Source={x:Static starterNg:App.Loc}}">
|
||||
<StackPanel Spacing="6">
|
||||
<ComboBox x:Name="RenderEngineCb" SelectedIndex="0"
|
||||
SelectionChanged="RenderEngineCb_OnSelectionChanged">
|
||||
<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>
|
||||
<TextBlock x:Name="RenderEngineWarning" IsVisible="False" TextWrapping="Wrap"
|
||||
Foreground="{StaticResource CarbonAccentBright}"
|
||||
Text="{Binding [REExperimentalWarning], Source={x:Static starterNg:App.Loc}}" />
|
||||
</StackPanel>
|
||||
<ctl:SettingRow ConfigKey="maxtexturesize" x:Name="TexResolutionRow"
|
||||
Label="{Binding [MaxTexResolution], Source={x:Static starterNg:App.Loc}}">
|
||||
<Slider Name="textureResolutionSlider" Minimum="6" Maximum="13" TickFrequency="1"
|
||||
IsSnapToTickEnabled="True" Value="12"
|
||||
ValueChanged="TextureResolutionSlider_OnValueChanged" />
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="gfx.angleplatform" Description="{Binding [RenderAngleVulkanDesc], Source={x:Static starterNg:App.Loc}}">
|
||||
<CheckBox x:Name="RenderAngleVulkanCb" Content="{Binding [RenderAngleVulkan], Source={x:Static starterNg:App.Loc}}" />
|
||||
<ctl:SettingRow ConfigKey="maxcabtexturesize" x:Name="CabTexResolutionRow"
|
||||
Label="{Binding [MaxTexCabResolution], Source={x:Static starterNg:App.Loc}}">
|
||||
<Slider Name="cabTextureResolutionSlider" Minimum="9" Maximum="13" TickFrequency="1"
|
||||
IsSnapToTickEnabled="True" Value="12"
|
||||
ValueChanged="CabTextureResolutionSlider_OnValueChanged" />
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="anisotropicfiltering" x:Name="TexFilteringRow"
|
||||
Label="{Binding [TexFilteringQuality], Source={x:Static starterNg:App.Loc}}"
|
||||
Description="{Binding [TexFilteringQualityDesc], Source={x:Static starterNg:App.Loc}}">
|
||||
<!-- Moves through Steps.Anisotropy; see SliderSteps. -->
|
||||
<Slider x:Name="TexFilteringSlider"
|
||||
ValueChanged="QualitySlider_OnValueChanged" />
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="multisampling" x:Name="MultisamplingRow"
|
||||
Label="{Binding [Multisampling], Source={x:Static starterNg:App.Loc}}"
|
||||
Description="{Binding [MultisamplingDesc], Source={x:Static starterNg:App.Loc}}">
|
||||
<!-- Moves through Steps.MultisamplingLevels; see SliderSteps. -->
|
||||
<Slider x:Name="MultisamplingSlider"
|
||||
ValueChanged="QualitySlider_OnValueChanged" />
|
||||
</ctl:SettingRow>
|
||||
<ctl:KeySlot Key="gfx.textures.streaming" />
|
||||
<ctl:KeySlot Key="gfx.textures.releasedistance" />
|
||||
</StackPanel>
|
||||
</ctl:SettingsSection>
|
||||
|
||||
<ctl:SettingsSection x:Name="EffectsSection" Header="{Binding [GfxColEffects], Source={x:Static starterNg:App.Loc}}">
|
||||
<StackPanel>
|
||||
<ctl:SettingRow ConfigKey="gfx.extraeffects">
|
||||
<CheckBox x:Name="AdditionalShadersCb" Content="{Binding [AdditionalShadersEffects], Source={x:Static starterNg:App.Loc}}" />
|
||||
</ctl:SettingRow>
|
||||
@@ -255,26 +324,7 @@
|
||||
<ctl:SettingRow ConfigKey="gfx.postfx.motionblur.enabled">
|
||||
<CheckBox x:Name="MotionBlurCb" Content="{Binding [MotionBlur], Source={x:Static starterNg:App.Loc}}" />
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="gfx.skippipeline" Description="{Binding [SkipPipelineDesc], Source={x:Static starterNg:App.Loc}}">
|
||||
<CheckBox x:Name="SkipPipelineCb" Content="{Binding [SkipPipeline], Source={x:Static starterNg:App.Loc}}" />
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="usevbo" Description="{Binding [RenderVBODesc], Source={x:Static starterNg:App.Loc}}">
|
||||
<CheckBox x:Name="RenderVBOCb" Content="{Binding [RenderVBO], Source={x:Static starterNg:App.Loc}}" />
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="vsync" Description="{Binding [VSyncDesc], Source={x:Static starterNg:App.Loc}}">
|
||||
<CheckBox x:Name="VSyncCb" Content="{Binding [VSync], Source={x:Static starterNg:App.Loc}}" />
|
||||
</ctl:SettingRow>
|
||||
|
||||
<ctl:SettingRow ConfigKey="fpslimit" Margin="0,0,0,4"
|
||||
ValueText="{Binding #FpsLimitSlider.Value, StringFormat='{}{0:0} FPS'}">
|
||||
<CheckBox x:Name="FpsLimitEnableCb" Content="{Binding [FpsLimitEnable], Source={x:Static starterNg:App.Loc}}" />
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="fpslimit">
|
||||
<Slider x:Name="FpsLimitSlider" Minimum="1" Maximum="100" TickFrequency="1"
|
||||
IsSnapToTickEnabled="True" Value="30"
|
||||
IsEnabled="{Binding #FpsLimitEnableCb.IsChecked}" />
|
||||
</ctl:SettingRow>
|
||||
|
||||
<ctl:KeySlot Key="gfx.postfx.motionblur.shutter" />
|
||||
<ctl:SettingRow ConfigKey="gfx.smoke" Margin="0,0,0,4">
|
||||
<CheckBox x:Name="SmokeDisplayCb" Content="{Binding [SmokeDisplay], Source={x:Static starterNg:App.Loc}}" />
|
||||
</ctl:SettingRow>
|
||||
@@ -285,29 +335,26 @@
|
||||
IsSnapToTickEnabled="True" Value="2"
|
||||
ValueChanged="QualitySlider_OnValueChanged" />
|
||||
</ctl:SettingRow>
|
||||
|
||||
<ctl:SettingRow Description="{Binding [DynamicLightsDesc], Source={x:Static starterNg:App.Loc}}" ConfigKey="dynamiclights" Label="{Binding [DynamicLights], Source={x:Static starterNg:App.Loc}}"
|
||||
ValueText="{Binding #DynamicLightsSlider.Value, StringFormat='{}{0:0}'}">
|
||||
<Slider x:Name="DynamicLightsSlider" Minimum="0" Maximum="7" TickFrequency="1"
|
||||
IsSnapToTickEnabled="True" Value="7" />
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="fieldofview" Label="{Binding [FieldOfView], Source={x:Static starterNg:App.Loc}}"
|
||||
Description="{Binding [FieldOfViewDesc], Source={x:Static starterNg:App.Loc}}"
|
||||
ValueText="{Binding #fovSlider.Value, StringFormat='{}{0:0}°'}">
|
||||
<Slider Name="fovSlider" Minimum="15" Maximum="75" TickFrequency="5"
|
||||
IsSnapToTickEnabled="True" Value="45" />
|
||||
</ctl:SettingRow>
|
||||
<ctl:KeySlot Key="skyenabled" />
|
||||
<ctl:KeySlot Key="rendercab" />
|
||||
<ctl:KeySlot Key="newaircouplers" />
|
||||
<ctl:KeySlot Key="smoothtraction" />
|
||||
</StackPanel>
|
||||
</ctl:SettingsSection>
|
||||
|
||||
<ctl:SettingsSection Header="{Binding [GfxColShadows], Source={x:Static starterNg:App.Loc}}">
|
||||
<ctl:SettingsSection ToggleKey="shadows" x:Name="ShadowsSection" Header="{Binding [GfxColShadows], Source={x:Static starterNg:App.Loc}}">
|
||||
<ctl:SettingsSection.Toggle>
|
||||
<CheckBox x:Name="RenderShadowsCb" FontSize="12" MinHeight="0"
|
||||
Content="{Binding [RenderShadows], Source={x:Static starterNg:App.Loc}}" />
|
||||
</ctl:SettingsSection.Toggle>
|
||||
<StackPanel IsEnabled="{Binding #RenderShadowsCb.IsChecked}">
|
||||
<ctl:SettingRow Description="{Binding [ShadowDisplayDesc], Source={x:Static starterNg:App.Loc}}" ConfigKey="gfx.shadow.rank.cutoff" Label="{Binding [ShadowDisplay], Source={x:Static starterNg:App.Loc}}">
|
||||
<ComboBox x:Name="ShadowDisplayCb" SelectedIndex="0">
|
||||
<ComboBox x:Name="ShadowDisplayCb" Classes="Localized" 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}}" />
|
||||
@@ -315,13 +362,15 @@
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="shadowtune" x:Name="ShadowResolutionRow"
|
||||
Label="{Binding [ShadowsResolution], Source={x:Static starterNg:App.Loc}}">
|
||||
<Slider Name="shaderResolutionSlider" Minimum="9" Maximum="14" TickFrequency="1"
|
||||
<Slider Name="shaderResolutionSlider" Minimum="9" Maximum="13" TickFrequency="1"
|
||||
IsSnapToTickEnabled="True" Value="12"
|
||||
ValueChanged="shaderResolutionSlider_OnValueChanged" />
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="shadowtune" x:Name="ShadowRangeRow"
|
||||
Label="{Binding [ShaderRange], Source={x:Static starterNg:App.Loc}}">
|
||||
<Slider Name="shaderRange" Minimum="25" Maximum="400" TickFrequency="5"
|
||||
Label="{Binding [ShaderRange], Source={x:Static starterNg:App.Loc}}"
|
||||
Description="{Binding [ShaderRangeDesc], Source={x:Static starterNg:App.Loc}}">
|
||||
<!-- The simulator's floor follows the map size: see shaderResolutionSlider_OnValueChanged. -->
|
||||
<Slider Name="shaderRange" Minimum="75" Maximum="600" TickFrequency="5"
|
||||
IsSnapToTickEnabled="True" Value="150"
|
||||
ValueChanged="QualitySlider_OnValueChanged" />
|
||||
</ctl:SettingRow>
|
||||
@@ -332,12 +381,13 @@
|
||||
IsSnapToTickEnabled="True" Value="30"
|
||||
ValueChanged="QualitySlider_OnValueChanged" />
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="gfx.shadow.angle.min" Label="{Binding [ShadowAngleLimit], Source={x:Static starterNg:App.Loc}}"
|
||||
Description="{Binding [ShadowAngleLimitDesc], Source={x:Static starterNg:App.Loc}}"
|
||||
ValueText="{Binding #ShadowAngleLimitSlider.Value, StringFormat='{}{0:0.0}'}">
|
||||
<ctl:SettingRow ConfigKey="gfx.shadow.angle.min" x:Name="ShadowAngleLimitRow"
|
||||
Label="{Binding [ShadowAngleLimit], Source={x:Static starterNg:App.Loc}}"
|
||||
Description="{Binding [ShadowAngleLimitDesc], Source={x:Static starterNg:App.Loc}}">
|
||||
<StackPanel>
|
||||
<Slider x:Name="ShadowAngleLimitSlider" Minimum="0.2" Maximum="1.0"
|
||||
TickFrequency="0.1" Value="0.2" HorizontalAlignment="Stretch" />
|
||||
TickFrequency="0.05" IsSnapToTickEnabled="True" Value="0.2" HorizontalAlignment="Stretch"
|
||||
ValueChanged="QualitySlider_OnValueChanged" />
|
||||
<DockPanel>
|
||||
<TextBlock DockPanel.Dock="Right" Classes="FieldHint"
|
||||
Text="{Binding [ShadowLimitShort], Source={x:Static starterNg:App.Loc}}" />
|
||||
@@ -349,25 +399,30 @@
|
||||
</StackPanel>
|
||||
</ctl:SettingsSection>
|
||||
|
||||
<ctl:SettingsSection Header="{Binding [GfxColReflections], Source={x:Static starterNg:App.Loc}}">
|
||||
<ctl:SettingsSection x:Name="ReflectionsSection" Header="{Binding [GfxColReflections], Source={x:Static starterNg:App.Loc}}">
|
||||
<StackPanel>
|
||||
<ctl:SettingRow ConfigKey="gfx.envmap.enabled" Description="{Binding [ReflectionsCubeMapDesc], Source={x:Static starterNg:App.Loc}}">
|
||||
<CheckBox x:Name="ReflectionsCubeMapCb" Content="{Binding [ReflectionsCubeMap], Source={x:Static starterNg:App.Loc}}" />
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="gfx.reflections.fidelity" Label="{Binding [ReflectionsDetails], Source={x:Static starterNg:App.Loc}}"
|
||||
Description="{Binding [ReflectionsDetailsDesc], Source={x:Static starterNg:App.Loc}}">
|
||||
<ComboBox x:Name="ReflectionsDetailsCb" 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>
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="gfx.reflections.framerate" x:Name="ReflectionsFramerateRow"
|
||||
Label="{Binding [ReflectionsFramerate], Source={x:Static starterNg:App.Loc}}">
|
||||
<Slider Name="reflectionsFramerate" Minimum="5" Maximum="120" TickFrequency="5"
|
||||
IsSnapToTickEnabled="True" Value="30"
|
||||
ValueChanged="QualitySlider_OnValueChanged" />
|
||||
</ctl:SettingRow>
|
||||
<StackPanel x:Name="ReflectionRows">
|
||||
<ctl:SettingRow ConfigKey="gfx.reflections.fidelity" Label="{Binding [ReflectionsDetails], Source={x:Static starterNg:App.Loc}}"
|
||||
Description="{Binding [ReflectionsDetailsDesc], Source={x:Static starterNg:App.Loc}}">
|
||||
<ComboBox x:Name="ReflectionsDetailsCb" Classes="Localized" 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>
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="gfx.reflections.framerate" x:Name="ReflectionsFramerateRow"
|
||||
Label="{Binding [ReflectionsFramerate], Source={x:Static starterNg:App.Loc}}"
|
||||
Description="{Binding [ReflectionsFramerateDesc], Source={x:Static starterNg:App.Loc}}">
|
||||
<!-- Moves through Steps.ReflectionUpdates; see SliderSteps. -->
|
||||
<Slider Name="reflectionsFramerate"
|
||||
ValueChanged="QualitySlider_OnValueChanged" />
|
||||
</ctl:SettingRow>
|
||||
<ctl:KeySlot Key="gfx.reflections.range_instances" />
|
||||
<ctl:KeySlot Key="gfx.reflections.range_vehicles" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ctl:SettingsSection>
|
||||
|
||||
@@ -376,18 +431,23 @@
|
||||
<ctl:SettingRow ConfigKey="python.enabled" Description="{Binding [RenderScreensDesc], Source={x:Static starterNg:App.Loc}}">
|
||||
<CheckBox x:Name="RenderScreensCb" Content="{Binding [RenderScreens], Source={x:Static starterNg:App.Loc}}" />
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="python.threadedupload">
|
||||
<CheckBox x:Name="RenderScreensThreadCb" Content="{Binding [RenderScreensThread], Source={x:Static starterNg:App.Loc}}" />
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="python.updatetime" Label="{Binding [PyScreenPriority], Source={x:Static starterNg:App.Loc}}"
|
||||
Description="{Binding [PyScreenPriorityDesc], Source={x:Static starterNg:App.Loc}}">
|
||||
<ComboBox x:Name="PyScreenPriorityCb">
|
||||
<ComboBoxItem Tag="200" Content="{Binding [PyPriorityNormal], Source={x:Static starterNg:App.Loc}}" />
|
||||
<ComboBoxItem Tag="400" Content="{Binding [PyPriorityLower], Source={x:Static starterNg:App.Loc}}" />
|
||||
<ComboBoxItem Tag="800" Content="{Binding [PyPriorityLowest], Source={x:Static starterNg:App.Loc}}" />
|
||||
<ComboBoxItem Tag="1600" Content="{Binding [PyPriorityIdle], Source={x:Static starterNg:App.Loc}}" />
|
||||
</ComboBox>
|
||||
</ctl:SettingRow>
|
||||
<StackPanel IsEnabled="{Binding #RenderScreensCb.IsChecked}">
|
||||
<ctl:SettingRow ConfigKey="python.threadedupload">
|
||||
<CheckBox x:Name="RenderScreensThreadCb" Content="{Binding [RenderScreensThread], Source={x:Static starterNg:App.Loc}}" />
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="python.updatetime" Label="{Binding [PyScreenPriority], Source={x:Static starterNg:App.Loc}}"
|
||||
Description="{Binding [PyScreenPriorityDesc], Source={x:Static starterNg:App.Loc}}">
|
||||
<ComboBox x:Name="PyScreenPriorityCb" Classes="Localized">
|
||||
<ComboBoxItem Tag="200" Content="{Binding [PyPriorityNormal], Source={x:Static starterNg:App.Loc}}" />
|
||||
<ComboBoxItem Tag="400" Content="{Binding [PyPriorityLower], Source={x:Static starterNg:App.Loc}}" />
|
||||
<ComboBoxItem Tag="800" Content="{Binding [PyPriorityLowest], Source={x:Static starterNg:App.Loc}}" />
|
||||
<ComboBoxItem Tag="1600" Content="{Binding [PyPriorityIdle], Source={x:Static starterNg:App.Loc}}" />
|
||||
</ComboBox>
|
||||
</ctl:SettingRow>
|
||||
<ctl:KeySlot Key="python.fpslimit" />
|
||||
<ctl:KeySlot Key="python.vsync" />
|
||||
<ctl:KeySlot Key="python.displaywindows" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ctl:SettingsSection>
|
||||
</StackPanel>
|
||||
@@ -396,7 +456,7 @@
|
||||
<StackPanel Tag="sound" IsVisible="False">
|
||||
<TextBlock Classes="CategoryCaption" IsVisible="False"
|
||||
Text="{Binding [Sounds], Source={x:Static starterNg:App.Loc}}" />
|
||||
<ctl:SettingsSection Header="{Binding [Sounds], Source={x:Static starterNg:App.Loc}}">
|
||||
<ctl:SettingsSection ToggleKey="soundenabled" Header="{Binding [Sounds], Source={x:Static starterNg:App.Loc}}">
|
||||
<ctl:SettingsSection.Toggle>
|
||||
<CheckBox x:Name="EnableSoundsCb" FontSize="12" MinHeight="0"
|
||||
Content="{Binding [EnableSounds], Source={x:Static starterNg:App.Loc}}" />
|
||||
@@ -404,27 +464,27 @@
|
||||
<StackPanel IsEnabled="{Binding #EnableSoundsCb.IsChecked}">
|
||||
<ctl:SettingRow ConfigKey="sound.volume" Label="{Binding [Volume], Source={x:Static starterNg:App.Loc}}"
|
||||
ValueText="{Binding #VolumeSlider.Value, StringFormat='{}{0:0}%'}">
|
||||
<Slider x:Name="VolumeSlider" Minimum="1" Maximum="100" TickFrequency="5"
|
||||
<Slider x:Name="VolumeSlider" Minimum="0" Maximum="100" TickFrequency="5"
|
||||
IsSnapToTickEnabled="True" Value="100" />
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="sound.volume.radio" Label="{Binding [RadiophoneVolume], Source={x:Static starterNg:App.Loc}}"
|
||||
ValueText="{Binding #RadioVolumeSlider.Value, StringFormat='{}{0:0}%'}">
|
||||
<Slider x:Name="RadioVolumeSlider" Minimum="1" Maximum="100" TickFrequency="5"
|
||||
<Slider x:Name="RadioVolumeSlider" Minimum="0" Maximum="100" TickFrequency="5"
|
||||
IsSnapToTickEnabled="True" Value="100" />
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="sound.volume.vehicle" Label="{Binding [VehiclesVolume], Source={x:Static starterNg:App.Loc}}"
|
||||
ValueText="{Binding #VehiclesVolumeSlider.Value, StringFormat='{}{0:0}%'}">
|
||||
<Slider x:Name="VehiclesVolumeSlider" Minimum="1" Maximum="100" TickFrequency="5"
|
||||
<Slider x:Name="VehiclesVolumeSlider" Minimum="0" Maximum="100" TickFrequency="5"
|
||||
IsSnapToTickEnabled="True" Value="100" />
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="sound.volume.positional" Label="{Binding [PositionedSoundsVolume], Source={x:Static starterNg:App.Loc}}"
|
||||
ValueText="{Binding #PositionalVolumeSlider.Value, StringFormat='{}{0:0}%'}">
|
||||
<Slider x:Name="PositionalVolumeSlider" Minimum="1" Maximum="100" TickFrequency="5"
|
||||
<Slider x:Name="PositionalVolumeSlider" Minimum="0" Maximum="100" TickFrequency="5"
|
||||
IsSnapToTickEnabled="True" Value="100" />
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="sound.volume.ambient" Label="{Binding [AmbientVolume], Source={x:Static starterNg:App.Loc}}"
|
||||
ValueText="{Binding #AmbientVolumeSlider.Value, StringFormat='{}{0:0}%'}">
|
||||
<Slider x:Name="AmbientVolumeSlider" Minimum="1" Maximum="100" TickFrequency="5"
|
||||
<Slider x:Name="AmbientVolumeSlider" Minimum="0" Maximum="100" TickFrequency="5"
|
||||
IsSnapToTickEnabled="True" Value="100" />
|
||||
</ctl:SettingRow>
|
||||
</StackPanel>
|
||||
@@ -465,20 +525,20 @@
|
||||
<CheckBox x:Name="CrashDamageCb" Content="{Binding [CrashDamage], Source={x:Static starterNg:App.Loc}}" />
|
||||
</ctl:SettingRow>
|
||||
|
||||
<ctl:SettingRow ConfigKey="friction" Label="{Binding [Friction], Source={x:Static starterNg:App.Loc}}"
|
||||
Description="{Binding [FrictionDesc], Source={x:Static starterNg:App.Loc}}"
|
||||
ValueText="{Binding #FrictionSlider.Value, StringFormat='{}{0:0.00}'}">
|
||||
<Slider x:Name="FrictionSlider" Minimum="0.1" Maximum="3.0" TickFrequency="0.1" Value="1.0" />
|
||||
<ctl:SettingRow ConfigKey="friction" x:Name="FrictionRow" Label="{Binding [Friction], Source={x:Static starterNg:App.Loc}}"
|
||||
Description="{Binding [FrictionDesc], Source={x:Static starterNg:App.Loc}}">
|
||||
<Slider x:Name="FrictionSlider" Minimum="0.1" Maximum="3.0" TickFrequency="0.1" Value="1.0"
|
||||
ValueChanged="QualitySlider_OnValueChanged" />
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="brakestep" Label="{Binding [BrakeStep], Source={x:Static starterNg:App.Loc}}"
|
||||
Description="{Binding [BrakeStepDesc], Source={x:Static starterNg:App.Loc}}"
|
||||
ValueText="{Binding #BrakeStepSlider.Value, StringFormat='{}{0:0.00}'}">
|
||||
<Slider x:Name="BrakeStepSlider" Minimum="0.4" Maximum="3.0" TickFrequency="0.1" Value="1.0" />
|
||||
<ctl:SettingRow ConfigKey="brakestep" x:Name="BrakeStepRow" Label="{Binding [BrakeStep], Source={x:Static starterNg:App.Loc}}"
|
||||
Description="{Binding [BrakeStepDesc], Source={x:Static starterNg:App.Loc}}">
|
||||
<Slider x:Name="BrakeStepSlider" Minimum="0.4" Maximum="3.0" TickFrequency="0.1" Value="1.0"
|
||||
ValueChanged="QualitySlider_OnValueChanged" />
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="brakespeed" Label="{Binding [BrakeSpeed], Source={x:Static starterNg:App.Loc}}"
|
||||
Description="{Binding [BrakeSpeedDesc], Source={x:Static starterNg:App.Loc}}"
|
||||
ValueText="{Binding #BrakeSpeedSlider.Value, StringFormat='{}{0:0.00}'}">
|
||||
<Slider x:Name="BrakeSpeedSlider" Minimum="0.4" Maximum="3.0" TickFrequency="0.1" Value="1.0" />
|
||||
<ctl:SettingRow ConfigKey="brakespeed" x:Name="BrakeSpeedRow" Label="{Binding [BrakeSpeed], Source={x:Static starterNg:App.Loc}}"
|
||||
Description="{Binding [BrakeSpeedDesc], Source={x:Static starterNg:App.Loc}}">
|
||||
<Slider x:Name="BrakeSpeedSlider" Minimum="0.4" Maximum="3.0" TickFrequency="0.1" Value="1.0"
|
||||
ValueChanged="QualitySlider_OnValueChanged" />
|
||||
</ctl:SettingRow>
|
||||
</StackPanel>
|
||||
</ctl:SettingsSection>
|
||||
@@ -495,7 +555,7 @@
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="feedbackmode" Label="{Binding [Feedback], Source={x:Static starterNg:App.Loc}}"
|
||||
Description="{Binding [FeedbackDesc], Source={x:Static starterNg:App.Loc}}">
|
||||
<ComboBox x:Name="FeedbackCb" SelectedIndex="0">
|
||||
<ComboBox x:Name="FeedbackCb" Classes="Localized" 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}}" />
|
||||
@@ -510,11 +570,104 @@
|
||||
<NumericUpDown x:Name="FeedbackPortNud" Minimum="0" Maximum="65535" Value="888"
|
||||
Increment="1" FormatString="0" />
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="uart" x:Name="ComButtonRow" IsVisible="False">
|
||||
<Button x:Name="ComButton" Classes="Flat" Cursor="Hand"
|
||||
</StackPanel>
|
||||
</ctl:SettingsSection>
|
||||
|
||||
<!-- Shown for the serial port feedback; saved with everything else on this page. -->
|
||||
<ctl:SettingsSection x:Name="UartSettings" Header="{Binding [UartSection], Source={x:Static starterNg:App.Loc}}">
|
||||
<StackPanel>
|
||||
<ctl:SettingRow ConfigKey="uart">
|
||||
<CheckBox x:Name="UartEnabledCb" Content="{Binding [UartEnable], Source={x:Static starterNg:App.Loc}}" />
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="uart" Classes="KeyAbove" Label="{Binding [UartPort], Source={x:Static starterNg:App.Loc}}">
|
||||
<StackPanel Classes="Field" Spacing="4">
|
||||
<ComboBox x:Name="UartPortCb" IsEditable="True" HorizontalAlignment="Stretch"
|
||||
PlaceholderText="{Binding [UartPortNone], Source={x:Static starterNg:App.Loc}}" />
|
||||
<TextBlock x:Name="UartPortWarning" IsVisible="False" FontSize="12" TextWrapping="Wrap"
|
||||
Foreground="#E0A030"
|
||||
Text="{Binding [UartPortMissing], Source={x:Static starterNg:App.Loc}}" />
|
||||
</StackPanel>
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="uart" Classes="KeyAbove" Label="{Binding [UartBaud], Source={x:Static starterNg:App.Loc}}">
|
||||
<ComboBox x:Name="UartBaudCb" />
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="uart" Classes="KeyAbove" Label="{Binding [UartEvery], Source={x:Static starterNg:App.Loc}}">
|
||||
<NumericUpDown x:Name="UartEveryNud" Minimum="0" Maximum="10" Increment="0.05" FormatString="0.###" />
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="uarttune" Label="{Binding [UartTune], Source={x:Static starterNg:App.Loc}}">
|
||||
<StackPanel Classes="Field" Spacing="4">
|
||||
<TextBox x:Name="UartTuneBox" TextWrapping="Wrap"
|
||||
PlaceholderText="{Binding [UartTunePlaceholder], Source={x:Static starterNg:App.Loc}}" />
|
||||
<TextBlock x:Name="UartTuneWarning" IsVisible="False" FontSize="12" TextWrapping="Wrap"
|
||||
Foreground="#E0A030" />
|
||||
</StackPanel>
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="uartdebug">
|
||||
<CheckBox x:Name="UartDebugCb" Content="{Binding [UartDebug], Source={x:Static starterNg:App.Loc}}" />
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="uartfeature" Classes="TopLabel" Label="{Binding [UartFeature], Source={x:Static starterNg:App.Loc}}">
|
||||
<UniformGrid Columns="2" RowSpacing="8" ColumnSpacing="8">
|
||||
<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}}" />
|
||||
</UniformGrid>
|
||||
</ctl:SettingRow>
|
||||
</StackPanel>
|
||||
</ctl:SettingsSection>
|
||||
</StackPanel>
|
||||
|
||||
<!-- ============================ ADVANCED ============================ -->
|
||||
<StackPanel Tag="advanced" IsVisible="False">
|
||||
<TextBlock Classes="CategoryCaption" IsVisible="False"
|
||||
Text="{Binding [Advanced], Source={x:Static starterNg:App.Loc}}" />
|
||||
<ctl:SettingsSection Header="{Binding [AdvancedResources], Source={x:Static starterNg:App.Loc}}">
|
||||
<StackPanel>
|
||||
<ctl:SettingRow ConfigKey="compresstex" Description="{Binding [CompressTexturesDesc], Source={x:Static starterNg:App.Loc}}">
|
||||
<CheckBox x:Name="CompressTexCb" Content="{Binding [CompressTextures], Source={x:Static starterNg:App.Loc}}" />
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="scalespeculars" Description="{Binding [ScaleSpecularsDesc], Source={x:Static starterNg:App.Loc}}">
|
||||
<CheckBox x:Name="ScaleSpecularsCb" Content="{Binding [ScaleSpeculars], Source={x:Static starterNg:App.Loc}}" />
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="python.mipmaps" Description="{Binding [ScreenMipmapsDesc], Source={x:Static starterNg:App.Loc}}">
|
||||
<CheckBox x:Name="ScreenMipmapsCb" Content="{Binding [ScreenMipmaps], Source={x:Static starterNg:App.Loc}}" />
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="gfx.shadergamma" Description="{Binding [ShaderGammaDesc], Source={x:Static starterNg:App.Loc}}">
|
||||
<CheckBox x:Name="ShaderGammaCb" Content="{Binding [ShaderGamma], Source={x:Static starterNg:App.Loc}}" />
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="gfx.usegles" Description="{Binding [UseGLESDesc], Source={x:Static starterNg:App.Loc}}">
|
||||
<CheckBox x:Name="UseGlesCb" Content="{Binding [UseGLES], Source={x:Static starterNg:App.Loc}}" />
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="convertmodels" Description="{Binding [ExtendedModelConversionDesc], Source={x:Static starterNg:App.Loc}}">
|
||||
<CheckBox x:Name="ConvertModelsCb" Content="{Binding [ExtendedModelConversion], Source={x:Static starterNg:App.Loc}}" />
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="gfx.resource.sweep" Description="{Binding [GfxResourceSweepDesc], Source={x:Static starterNg:App.Loc}}">
|
||||
<CheckBox x:Name="ResourceSweepCb" Content="{Binding [GfxResourceSweep], Source={x:Static starterNg:App.Loc}}" />
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="gfx.resource.move" Description="{Binding [GfxResourceMoveDesc], Source={x:Static starterNg:App.Loc}}">
|
||||
<CheckBox x:Name="ResourceMoveCb" Content="{Binding [GfxResourceMove], Source={x:Static starterNg:App.Loc}}" />
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="async.trainThreads" x:Name="TrainThreadsRow"
|
||||
Label="{Binding [TrainPhysicsThreads], Source={x:Static starterNg:App.Loc}}"
|
||||
Description="{Binding [TrainPhysicsThreadsDesc], Source={x:Static starterNg:App.Loc}}">
|
||||
<Slider x:Name="TrainThreadsSlider" Minimum="0" Maximum="16" TickFrequency="1"
|
||||
IsSnapToTickEnabled="True" ValueChanged="QualitySlider_OnValueChanged" />
|
||||
</ctl:SettingRow>
|
||||
<ctl:SettingRow ConfigKey="starter.ignoreirrelevant" Description="{Binding [IgnoreIrrelevantTrainsDesc], Source={x:Static starterNg:App.Loc}}">
|
||||
<CheckBox x:Name="IgnoreIrrelevantCb" Content="{Binding [IgnoreIrrelevantTrains], Source={x:Static starterNg:App.Loc}}" />
|
||||
</ctl:SettingRow>
|
||||
</StackPanel>
|
||||
</ctl:SettingsSection>
|
||||
|
||||
<ctl:SettingsSection Header="{Binding [AdvancedLogTools], Source={x:Static starterNg:App.Loc}}">
|
||||
<StackPanel>
|
||||
<ctl:SettingRow Description="{Binding [OpenLogToolsDesc], Source={x:Static starterNg:App.Loc}}">
|
||||
<Button x:Name="OpenLogToolsButton" Classes="Flat" Cursor="Hand"
|
||||
HorizontalContentAlignment="Center"
|
||||
Click="ComButton_OnClick"
|
||||
Content="{Binding [UartSection], Source={x:Static starterNg:App.Loc}}" />
|
||||
Click="OpenLogToolsButton_OnClick"
|
||||
Content="{Binding [OpenLogTools], Source={x:Static starterNg:App.Loc}}" />
|
||||
</ctl:SettingRow>
|
||||
</StackPanel>
|
||||
</ctl:SettingsSection>
|
||||
@@ -526,7 +679,8 @@
|
||||
<!-- Key bindings bring their own scrolling and a star-sized middle
|
||||
row, so they live beside the scroller, not inside it. -->
|
||||
<Grid x:Name="ControlsHost" RowDefinitions="Auto,*,Auto" Margin="8" IsVisible="False" />
|
||||
</Panel>
|
||||
</Panel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DockPanel>
|
||||
</UserControl>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
251
StarterNG/Views/SimulatorKeyRows.cs
Normal file
251
StarterNG/Views/SimulatorKeyRows.cs
Normal file
@@ -0,0 +1,251 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.LogicalTree;
|
||||
using StarterNG.Controls;
|
||||
using StarterNG.Domain.Settings;
|
||||
|
||||
namespace StarterNG.Views;
|
||||
|
||||
/// <summary>
|
||||
/// The rows of the <see cref="SimulatorKeys"/>, one section per group, added to
|
||||
/// the end of their settings category. A row writes its key only once the user
|
||||
/// moves it away from what it showed, so a value the control cannot show - a draw
|
||||
/// range above the slider's end - survives a save untouched.
|
||||
/// </summary>
|
||||
internal sealed class SimulatorKeyRows
|
||||
{
|
||||
private sealed record Row(SimulatorKey Key, SettingRow Setting, Control Input)
|
||||
{
|
||||
public string Shown = string.Empty;
|
||||
}
|
||||
|
||||
private readonly List<Row> _rows = new();
|
||||
|
||||
private readonly List<(SettingsSection Section, string HeaderKey)> _sections = new();
|
||||
|
||||
public event Action? Changed;
|
||||
|
||||
/// <summary>Raised on every move of a row, the starter's own setting of it included.</summary>
|
||||
public event Action<SimulatorKey>? Moved;
|
||||
|
||||
/// <summary>
|
||||
/// Puts each row where the layout has a <see cref="KeySlot"/> for it; the rest
|
||||
/// go to sections of their own at the end of their category.
|
||||
/// </summary>
|
||||
public void Build(Panel categories)
|
||||
{
|
||||
var slots = categories.GetLogicalDescendants().OfType<KeySlot>()
|
||||
.Where(slot => slot.Key is not null)
|
||||
.ToDictionary(slot => slot.Key!, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var key in SimulatorKeys.All)
|
||||
if (slots.TryGetValue(key.Key, out var slot))
|
||||
slot.Child = BuildRow(key);
|
||||
|
||||
foreach (var group in SimulatorKeys.All.Where(k => !slots.ContainsKey(k.Key))
|
||||
.GroupBy(k => (k.Category, k.Section)))
|
||||
{
|
||||
var category = categories.Children.OfType<StackPanel>()
|
||||
.FirstOrDefault(p => string.Equals(p.Tag as string, group.Key.Category, StringComparison.Ordinal));
|
||||
if (category is null)
|
||||
continue;
|
||||
|
||||
var rows = new StackPanel();
|
||||
foreach (var key in group)
|
||||
rows.Children.Add(BuildRow(key));
|
||||
|
||||
var section = new SettingsSection { Content = rows };
|
||||
_sections.Add((section, group.Key.Section));
|
||||
category.Children.Add(section);
|
||||
}
|
||||
|
||||
UpdateTexts();
|
||||
}
|
||||
|
||||
private SettingRow BuildRow(SimulatorKey key)
|
||||
{
|
||||
Control input = key.Kind switch
|
||||
{
|
||||
SimulatorKeyKind.Toggle => new CheckBox(),
|
||||
SimulatorKeyKind.Number => new Slider
|
||||
{
|
||||
Minimum = key.Minimum,
|
||||
Maximum = key.Maximum,
|
||||
TickFrequency = key.Step,
|
||||
IsSnapToTickEnabled = true
|
||||
},
|
||||
_ => new TextBox { MinWidth = 200 }
|
||||
};
|
||||
|
||||
// Marks the input as one of these rows, which tell about their own changes.
|
||||
input.Tag = key;
|
||||
|
||||
var setting = new SettingRow { ConfigKey = key.Key, Content = input };
|
||||
var row = new Row(key, setting, input);
|
||||
_rows.Add(row);
|
||||
|
||||
switch (input)
|
||||
{
|
||||
case CheckBox box:
|
||||
box.IsCheckedChanged += (_, _) => OnEdited(row);
|
||||
break;
|
||||
case Slider slider:
|
||||
slider.ValueChanged += (_, _) =>
|
||||
{
|
||||
UpdateValueText(row);
|
||||
OnEdited(row);
|
||||
};
|
||||
break;
|
||||
case TextBox text:
|
||||
text.TextChanged += (_, _) => OnEdited(row);
|
||||
break;
|
||||
}
|
||||
|
||||
return setting;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Only a value other than the one shown counts: a text box reports the text it
|
||||
/// was given after the fact, and a slider reports its own clamping.
|
||||
/// </summary>
|
||||
private void OnEdited(Row row)
|
||||
{
|
||||
Moved?.Invoke(row.Key);
|
||||
if (Current(row) != row.Shown)
|
||||
Changed?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>The row of <paramref name="key"/>, wherever it was put.</summary>
|
||||
public SettingRow? RowOf(string key) => Find(key)?.Setting;
|
||||
|
||||
private Row? Find(string key) =>
|
||||
_rows.FirstOrDefault(row => string.Equals(row.Key.Key, key, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
/// <summary>
|
||||
/// The number a row holds once it says something: moved by the user, or read
|
||||
/// from a file that has the key. Null while it only shows the default, which
|
||||
/// the simulator may not share.
|
||||
/// </summary>
|
||||
public double? NumberIfSet(string key, SimulatorSettings settings)
|
||||
{
|
||||
var row = Find(key);
|
||||
if (row is null || row.Input is not Slider slider)
|
||||
return null;
|
||||
if (Current(row) == row.Shown && !settings.Extra.ContainsKey(row.Key.Key))
|
||||
return null;
|
||||
return slider.Value;
|
||||
}
|
||||
|
||||
/// <summary>Labels in the current language; called again when it changes.</summary>
|
||||
public void UpdateTexts()
|
||||
{
|
||||
foreach (var (section, headerKey) in _sections)
|
||||
section.Header = App.Loc[headerKey];
|
||||
|
||||
foreach (var row in _rows)
|
||||
{
|
||||
string label = App.Loc[row.Key.LabelKey];
|
||||
row.Setting.Description = App.Loc[row.Key.DescriptionKey];
|
||||
|
||||
switch (row.Input)
|
||||
{
|
||||
case CheckBox box:
|
||||
box.Content = label;
|
||||
break;
|
||||
case TextBox text:
|
||||
row.Setting.Label = label;
|
||||
text.PlaceholderText = App.Loc["IniDefault"];
|
||||
break;
|
||||
default:
|
||||
row.Setting.Label = label;
|
||||
UpdateValueText(row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Apply(SimulatorSettings settings)
|
||||
{
|
||||
foreach (var row in _rows)
|
||||
{
|
||||
Put(row, row.Key.RawIn(settings));
|
||||
row.Shown = Current(row);
|
||||
}
|
||||
}
|
||||
|
||||
/// <param name="remember">
|
||||
/// Whether what is handed over counts as shown from now on: for the settings a
|
||||
/// save writes, not for a copy to try something on.
|
||||
/// </param>
|
||||
public void Capture(SimulatorSettings settings, bool remember)
|
||||
{
|
||||
foreach (var row in _rows)
|
||||
{
|
||||
string now = Current(row);
|
||||
if (now == row.Shown)
|
||||
continue;
|
||||
|
||||
settings.Extra[row.Key.Key] = now;
|
||||
if (remember)
|
||||
row.Shown = now;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Puts <paramref name="raw"/> in the row of <paramref name="key"/> as an edit
|
||||
/// by hand would: what it shows stays what it showed, so a save writes it.
|
||||
/// </summary>
|
||||
public void Show(string key, string raw)
|
||||
{
|
||||
if (Find(key) is { } row)
|
||||
Put(row, raw);
|
||||
}
|
||||
|
||||
/// <summary>Puts the simulator's own value in the row of <paramref name="key"/>, as <see cref="Show"/> does.</summary>
|
||||
public void ShowDefault(string key)
|
||||
{
|
||||
if (Find(key) is { } row)
|
||||
Put(row, row.Key.Default);
|
||||
}
|
||||
|
||||
private static void Put(Row row, string raw)
|
||||
{
|
||||
switch (row.Input)
|
||||
{
|
||||
case CheckBox box:
|
||||
box.IsChecked = SimulatorKey.ParseToggle(raw);
|
||||
break;
|
||||
case Slider slider:
|
||||
slider.Value = Math.Clamp(row.Key.ParseNumber(raw), row.Key.Minimum, row.Key.Maximum);
|
||||
break;
|
||||
case TextBox text:
|
||||
text.Text = raw.Trim();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static string Current(Row row) => row.Input switch
|
||||
{
|
||||
CheckBox box => SimulatorKey.FormatToggle(box.IsChecked == true),
|
||||
Slider slider => row.Key.FormatNumber(slider.Value),
|
||||
TextBox text => (text.Text ?? string.Empty).Trim(),
|
||||
_ => string.Empty
|
||||
};
|
||||
|
||||
private static void UpdateValueText(Row row)
|
||||
{
|
||||
if (row.Input is not Slider slider)
|
||||
return;
|
||||
|
||||
double value = slider.Value;
|
||||
row.Setting.ValueText = value == 0 && row.Key.ZeroIsOff
|
||||
? App.Loc["RangeDisabled"]
|
||||
: value.ToString(NumberFormat(row.Key.Step), CultureInfo.CurrentCulture) + row.Key.Unit;
|
||||
}
|
||||
|
||||
private static string NumberFormat(double step) =>
|
||||
step >= 1 ? "0" : step >= 0.1 ? "0.0" : step >= 0.01 ? "0.00" : "0.000";
|
||||
}
|
||||
42
StarterNG/Views/SliderSteps.cs
Normal file
42
StarterNG/Views/SliderSteps.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Avalonia.Controls;
|
||||
using StarterNG.Domain.Settings;
|
||||
|
||||
namespace StarterNG.Views;
|
||||
|
||||
/// <summary>
|
||||
/// A slider that moves through a list of values rather than along a range: its
|
||||
/// position is an index, so the values sit evenly on the track however far apart
|
||||
/// they are - 30 and 144 frames alike. A value the list lacks joins it while
|
||||
/// shown, so opening and saving the settings never moves it.
|
||||
/// </summary>
|
||||
internal sealed class SliderSteps
|
||||
{
|
||||
private readonly Slider _slider;
|
||||
private readonly IReadOnlyList<double> _steps;
|
||||
private IReadOnlyList<double> _shown;
|
||||
|
||||
public SliderSteps(Slider slider, IReadOnlyList<double> steps)
|
||||
{
|
||||
_slider = slider;
|
||||
_steps = steps;
|
||||
_shown = steps;
|
||||
slider.Minimum = 0;
|
||||
slider.TickFrequency = 1;
|
||||
slider.IsSnapToTickEnabled = true;
|
||||
slider.Maximum = steps.Count - 1;
|
||||
}
|
||||
|
||||
public double Value
|
||||
{
|
||||
get => _shown[Math.Clamp((int)Math.Round(_slider.Value), 0, _shown.Count - 1)];
|
||||
set
|
||||
{
|
||||
_shown = Steps.With(_steps, value);
|
||||
_slider.Maximum = _shown.Count - 1;
|
||||
int at = Steps.IndexOf(_shown, value);
|
||||
_slider.Value = at;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
using StarterNG.Application;
|
||||
|
||||
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 = 12,
|
||||
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 = AppServices.Current.Settings;
|
||||
_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 = AppServices.Current.Settings;
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user