mirror of
https://github.com/MaSzyna-EU07/StarterNG.git
synced 2026-11-02 20:56:23 +01:00
WIP: settings sections, shortcuts window, depot tweaks
This commit is contained in:
@@ -3,27 +3,35 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:starterNg="clr-namespace:StarterNG"
|
||||
xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||
xmlns:views="clr-namespace:StarterNG.Views"
|
||||
x:CompileBindings="True"
|
||||
mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="700"
|
||||
x:Class="StarterNG.Views.Depot">
|
||||
<!-- TabIndex follows the workflow - pick a consist, browse, build the train,
|
||||
then the editors - not the column order the grid happens to have. -->
|
||||
<Grid Margin="0" ColumnSpacing="3" RowSpacing="3">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="270" MinWidth="220" />
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition Width="340" MinWidth="300" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<!-- The vehicle panel keeps its old height on a roomy window (MaxHeight)
|
||||
but shrinks with everything else towards the 666 px floor instead of
|
||||
being clipped. The consist row measures itself, so a whole card fits
|
||||
whether the thumbnails are the small ones or the large ones. -->
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" MinHeight="230" />
|
||||
<RowDefinition Height="275" MinHeight="260" />
|
||||
<RowDefinition Height="175" MinHeight="165" />
|
||||
<RowDefinition Height="*" MinHeight="150" />
|
||||
<RowDefinition Height="1.57*" MinHeight="120" MaxHeight="192" />
|
||||
<RowDefinition Height="Auto" MinHeight="140" MaxHeight="230" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Border Grid.Column="0" Grid.Row="0" Background="{StaticResource CarbonPanelBg}"
|
||||
<Border Grid.Column="0" Grid.Row="0" TabIndex="2" Background="{StaticResource CarbonPanelBg}"
|
||||
BorderBrush="{StaticResource CarbonBorderLight}" BorderThickness="1"
|
||||
CornerRadius="2" Padding="8">
|
||||
CornerRadius="2" Padding="6">
|
||||
<Grid RowDefinitions="Auto,Auto,*,Auto,Auto">
|
||||
<DockPanel Grid.Row="0" Margin="0,0,0,6" LastChildFill="True">
|
||||
<CheckBox Name="hideArchivalCheck" DockPanel.Dock="Right" FontSize="11"
|
||||
<CheckBox Name="hideArchivalCheck" DockPanel.Dock="Right" FontSize="12"
|
||||
VerticalAlignment="Center" Margin="8,0,0,0"
|
||||
Content="{Binding [HideArchivalVehicles], Source={x:Static starterNg:App.Loc}}"
|
||||
IsCheckedChanged="HideArchivalCheck_OnChanged" />
|
||||
@@ -34,10 +42,12 @@
|
||||
<Grid Grid.Row="1" RowDefinitions="Auto,Auto" RowSpacing="6" Margin="0,0,0,6">
|
||||
<DockPanel Grid.Row="0" LastChildFill="True">
|
||||
<Button Name="textureBaseButton" DockPanel.Dock="Right" Classes="Flat"
|
||||
Width="30" Margin="4,0,0,0" Cursor="Hand" Content="🔍"
|
||||
Width="30" Margin="4,0,0,0" Cursor="Hand"
|
||||
HorizontalContentAlignment="Center"
|
||||
Click="TextureBaseButton_OnClick"
|
||||
ToolTip.Tip="{Binding [OpenTextureBase], Source={x:Static starterNg:App.Loc}}" />
|
||||
ToolTip.Tip="{Binding [OpenTextureBase], Source={x:Static starterNg:App.Loc}}">
|
||||
<materialIcons:MaterialIcon Kind="Magnify" Width="15" Height="15" />
|
||||
</Button>
|
||||
<ComboBox Name="categoryCombo" HorizontalAlignment="Stretch"
|
||||
PlaceholderText="{Binding [Category], Source={x:Static starterNg:App.Loc}}"
|
||||
PointerWheelChanged="Combo_OnPointerWheel"
|
||||
@@ -49,7 +59,7 @@
|
||||
SelectionChanged="ClassCombo_OnSelectionChanged" />
|
||||
</Grid>
|
||||
|
||||
<Border Name="miniPreviewPanel" Grid.Row="3" Height="66"
|
||||
<Border Name="miniPreviewPanel" Grid.Row="3" Height="46"
|
||||
Margin="0,6,0,0"
|
||||
Background="{StaticResource CarbonInputBg}"
|
||||
BorderBrush="{StaticResource CarbonBorderLight}" BorderThickness="1"
|
||||
@@ -71,10 +81,15 @@
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<HeaderedContentControl Name="mapConsistsPanel" Grid.Column="1" Grid.Row="0"
|
||||
Theme="{StaticResource GroupBox}">
|
||||
<!-- No group header here: the tab strip already names the list, and the
|
||||
scenery it belongs to is written into the tab itself. -->
|
||||
<Border Grid.Column="1" Grid.Row="0" TabIndex="1"
|
||||
Background="{StaticResource CarbonPanelBg}"
|
||||
BorderBrush="{StaticResource CarbonBorderLight}"
|
||||
BorderThickness="1" CornerRadius="2" Padding="6">
|
||||
<TabControl>
|
||||
<TabItem Header="{Binding [SceneryConsists], Source={x:Static starterNg:App.Loc}}">
|
||||
<TabItem Name="sceneryConsistsTab"
|
||||
Header="{Binding [SceneryConsists], Source={x:Static starterNg:App.Loc}}">
|
||||
<DockPanel LastChildFill="True">
|
||||
<Button Name="addToWarehouseButton" DockPanel.Dock="Bottom" Classes="Flat"
|
||||
Margin="0,6,0,0" HorizontalAlignment="Stretch" Cursor="Hand"
|
||||
@@ -83,10 +98,10 @@
|
||||
Content="{Binding [AddToWarehouse], Source={x:Static starterNg:App.Loc}}" />
|
||||
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" Spacing="16"
|
||||
Margin="2,6,0,0">
|
||||
<CheckBox Name="depotDrivableOnlyCheck" FontSize="11"
|
||||
<CheckBox Name="depotDrivableOnlyCheck" FontSize="12"
|
||||
IsCheckedChanged="ConsistFilter_OnChanged"
|
||||
Content="{Binding [ShowDrivableOnly], Source={x:Static starterNg:App.Loc}}" />
|
||||
<CheckBox Name="depotShowAiCheck" FontSize="11"
|
||||
<CheckBox Name="depotShowAiCheck" FontSize="12"
|
||||
IsCheckedChanged="ConsistFilter_OnChanged"
|
||||
Content="{Binding [ShowAiVehicles], Source={x:Static starterNg:App.Loc}}" />
|
||||
</StackPanel>
|
||||
@@ -107,74 +122,107 @@
|
||||
</DockPanel>
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
</HeaderedContentControl>
|
||||
</Border>
|
||||
|
||||
<HeaderedContentControl Name="selectedVehiclePanel" Grid.Row="1" Grid.ColumnSpan="2"
|
||||
Theme="{StaticResource GroupBox}">
|
||||
<Grid ColumnDefinitions="290*,3,380*,3,280*">
|
||||
<TabControl Grid.Column="0">
|
||||
<TabItem Header="{Binding [GeneralInfo], Source={x:Static starterNg:App.Loc}}">
|
||||
<DockPanel LastChildFill="True" Margin="4">
|
||||
<Button Name="removeVehicleButton" DockPanel.Dock="Bottom" Classes="Flat"
|
||||
Margin="0,6,0,0" HorizontalAlignment="Stretch" Cursor="Hand"
|
||||
HorizontalContentAlignment="Center"
|
||||
Click="RemoveVehicleButton_OnClick"
|
||||
Content="{Binding [RemoveVehicle], Source={x:Static starterNg:App.Loc}}" />
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Name="generalPanel" Spacing="6" />
|
||||
</ScrollViewer>
|
||||
</DockPanel>
|
||||
</TabItem>
|
||||
<TabItem Header="{Binding [AboutTexture], Source={x:Static starterNg:App.Loc}}">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Name="consistTexturePanel" Margin="4" Spacing="6" />
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
|
||||
<TabControl Grid.Column="2">
|
||||
<!-- The configuration is the one panel here that is actually edited, so it
|
||||
gets a column of its own spanning both rows - from the top edge down to
|
||||
the consist - instead of a third of one squeezed row. -->
|
||||
<Border Grid.Column="2" Grid.Row="0" Grid.RowSpan="2" TabIndex="6"
|
||||
Background="{StaticResource CarbonPanelBg}"
|
||||
BorderBrush="{StaticResource CarbonBorderLight}"
|
||||
BorderThickness="1" CornerRadius="2" Padding="6">
|
||||
<DockPanel LastChildFill="True">
|
||||
<Button Name="removeVehicleButton" DockPanel.Dock="Bottom" Classes="Flat"
|
||||
Margin="0,6,0,0" HorizontalAlignment="Stretch" Cursor="Hand"
|
||||
HorizontalContentAlignment="Center"
|
||||
Click="RemoveVehicleButton_OnClick"
|
||||
Content="{Binding [RemoveVehicle], Source={x:Static starterNg:App.Loc}}" />
|
||||
<TabControl>
|
||||
<TabItem Header="{Binding [Configuration], Source={x:Static starterNg:App.Loc}}">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Name="loadsPanel" Margin="4" Spacing="6" />
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
<TabItem Header="{Binding [Brakes], Source={x:Static starterNg:App.Loc}}">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Name="brakesPanel" Margin="4" Spacing="6" />
|
||||
<StackPanel Name="loadsPanel" Margin="4" Grid.IsSharedSizeScope="True" />
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<TabControl Grid.Column="4">
|
||||
<TabItem Header="{Binding [Coupling], Source={x:Static starterNg:App.Loc}}">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Name="couplerPanel" Margin="4" Spacing="6" />
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
<TabItem Header="{Binding [Damage], Source={x:Static starterNg:App.Loc}}">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Name="damagePanel" Margin="4" Spacing="6" />
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
</Grid>
|
||||
</HeaderedContentControl>
|
||||
<!-- No title band either: the tab strips start straight away, and the
|
||||
vehicle being edited is the highlighted card in the consist below.
|
||||
Two cards, not one: the read-only pair (general info / texture) is
|
||||
visually separate from the editors (coupling / damage / brakes), and
|
||||
each lines up with the panel above it. -->
|
||||
<Border Grid.Row="1" Grid.Column="0" TabIndex="4"
|
||||
Background="{StaticResource CarbonPanelBg}"
|
||||
BorderBrush="{StaticResource CarbonBorderLight}"
|
||||
BorderThickness="1" CornerRadius="2" Padding="6">
|
||||
<TabControl>
|
||||
<TabItem Header="{Binding [GeneralInfo], Source={x:Static starterNg:App.Loc}}">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Name="generalPanel" Margin="4" Grid.IsSharedSizeScope="True" />
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
<TabItem Header="{Binding [AboutTexture], Source={x:Static starterNg:App.Loc}}">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Name="consistTexturePanel" Margin="4" Spacing="3" />
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
</Border>
|
||||
|
||||
<HeaderedContentControl Grid.Row="2" Grid.ColumnSpan="2" Theme="{StaticResource GroupBox}"
|
||||
<Border Grid.Row="1" Grid.Column="1" TabIndex="5"
|
||||
Background="{StaticResource CarbonPanelBg}"
|
||||
BorderBrush="{StaticResource CarbonBorderLight}"
|
||||
BorderThickness="1" CornerRadius="2" Padding="6">
|
||||
<TabControl>
|
||||
<TabItem Header="{Binding [Coupling], Source={x:Static starterNg:App.Loc}}">
|
||||
<!-- The coupling grid now uses the full tab width. The two
|
||||
actions sit underneath, so they do not steal horizontal
|
||||
space from the checkbox columns. -->
|
||||
<DockPanel LastChildFill="True" Margin="4">
|
||||
<Panel Name="couplerActions" DockPanel.Dock="Bottom" />
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Name="couplerPanel" />
|
||||
</ScrollViewer>
|
||||
</DockPanel>
|
||||
</TabItem>
|
||||
<TabItem Header="{Binding [Damage], Source={x:Static starterNg:App.Loc}}">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Name="damagePanel" Margin="4" Grid.IsSharedSizeScope="True" />
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
<TabItem Header="{Binding [Brakes], Source={x:Static starterNg:App.Loc}}">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<!-- The scope is what makes every row's label column in this
|
||||
panel settle on one width. -->
|
||||
<StackPanel Name="brakesPanel" Margin="4"
|
||||
Grid.IsSharedSizeScope="True" />
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
</Border>
|
||||
|
||||
<HeaderedContentControl Grid.Row="2" Grid.ColumnSpan="3" TabIndex="3" Theme="{StaticResource GroupBox}"
|
||||
Header="{Binding [Consist], Source={x:Static starterNg:App.Loc}}">
|
||||
<!-- Mass, length and the rest ride in the title bar instead of taking
|
||||
a row of their own above the cards. -->
|
||||
<HeaderedContentControl.Tag>
|
||||
<StackPanel Name="trainStats" Orientation="Horizontal"
|
||||
VerticalAlignment="Center" />
|
||||
</HeaderedContentControl.Tag>
|
||||
<DockPanel LastChildFill="True">
|
||||
<DockPanel DockPanel.Dock="Top" Margin="0,0,0,6">
|
||||
<StackPanel Name="trainStats" Orientation="Horizontal"
|
||||
VerticalAlignment="Center" />
|
||||
</DockPanel>
|
||||
<Panel Name="consistDropTarget">
|
||||
<TextBlock Name="emptyHint" VerticalAlignment="Center" HorizontalAlignment="Center"
|
||||
TextWrapping="Wrap" MaxWidth="520" Opacity="0.6" TextAlignment="Center"
|
||||
Text="{Binding [ConsistEmpty], Source={x:Static starterNg:App.Loc}}" />
|
||||
<ScrollViewer Name="consistScroll"
|
||||
HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
|
||||
HorizontalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Disabled"
|
||||
Padding="0,0,0,4">
|
||||
<Panel>
|
||||
<!-- Tab reaches the strip once and lands on the selected
|
||||
card; the arrow keys walk it from there. -->
|
||||
<StackPanel Name="consistStack" Orientation="Horizontal"
|
||||
KeyboardNavigation.TabNavigation="Once"
|
||||
VerticalAlignment="Center" Spacing="0" />
|
||||
<Canvas Name="consistOverlay" IsHitTestVisible="False" />
|
||||
</Panel>
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Controls.Templates;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Input.Platform;
|
||||
@@ -76,8 +77,11 @@ public partial class Depot : UserControl
|
||||
_cards = new VehicleCards(consistStack, _db, _consist, _cargo, _minis, _hand,
|
||||
RebuildConsist, () => _details.Refresh(), _browser.SelectInBrowser, _drag.ArmCardDrag);
|
||||
|
||||
// Hidden still scrolls - it only drops the bar itself.
|
||||
MiniTextures.Sharp(miniPreview);
|
||||
|
||||
_details = new VehicleDetails(
|
||||
selectedVehiclePanel, generalPanel, consistTexturePanel, couplerPanel,
|
||||
generalPanel, consistTexturePanel, couplerPanel, couplerActions,
|
||||
brakesPanel, loadsPanel, damagePanel, removeVehicleButton,
|
||||
_db, _info, _consist, _cargo, _hand,
|
||||
RebuildConsist, _cards.RefreshMemberChrome, _browser.ShowTextureInfo);
|
||||
@@ -163,6 +167,9 @@ public partial class Depot : UserControl
|
||||
_details.Refresh();
|
||||
}
|
||||
|
||||
/// <summary>Redraws the consist cards, e.g. after the thumbnail size changed.</summary>
|
||||
public void RefreshConsistView() => RebuildConsist();
|
||||
|
||||
private void RebuildConsist()
|
||||
{
|
||||
consistStack.Children.Clear();
|
||||
@@ -183,6 +190,10 @@ public partial class Depot : UserControl
|
||||
_details.Refresh();
|
||||
UpdateTrainStats();
|
||||
WriteBackToScenery();
|
||||
|
||||
// The cards are thrown away and rebuilt on every change, so a keyboard-driven
|
||||
// edit has to be handed its card back or focus escapes the strip.
|
||||
_cards.FocusSelectedCard(onlyIfRequested: true);
|
||||
}
|
||||
|
||||
private void WriteBackToScenery()
|
||||
@@ -232,9 +243,9 @@ public partial class Depot : UserControl
|
||||
depotDrivableOnlyCheck.IsChecked = StarterNG.Classes.Settings.Instance.DrivableOnly;
|
||||
|
||||
_listScenery = AppState.Instance.CurrentScenery ?? _sceneries.FirstOrDefault();
|
||||
mapConsistsPanel.Header = _listScenery != null
|
||||
? $"{App.Loc["MapConsists"]}: {Path.GetFileNameWithoutExtension(_listScenery.Path)}"
|
||||
: App.Loc["NoSceneryConsists"];
|
||||
sceneryConsistsTab.Header = App.Loc["SceneryConsists"];
|
||||
ToolTip.SetTip(sceneryConsistsTab,
|
||||
_listScenery != null ? Path.GetFileNameWithoutExtension(_listScenery.Path) : App.Loc["NoSceneryConsists"]);
|
||||
|
||||
if (_listScenery != null)
|
||||
{
|
||||
@@ -425,6 +436,25 @@ public partial class Depot : UserControl
|
||||
AddTexture(_browser.Selected);
|
||||
}
|
||||
|
||||
/// <summary>Keyboard shortcut entry points. See <see cref="Infrastructure.Shortcuts"/>.</summary>
|
||||
public void KeyAddVehicle() => AddVehicleButton_OnClick(null, null!);
|
||||
|
||||
public void KeyRemoveVehicle() => RemoveVehicleButton_OnClick(null, null!);
|
||||
|
||||
public void KeyClearConsist()
|
||||
{
|
||||
if (_consist.EditingTrainset is { } ts && ts.Vehicles.Count > 0)
|
||||
RemoveAllVehicles(ts);
|
||||
}
|
||||
|
||||
public void KeySetWagonNumber() => _cards.ShowNumberFlyoutForSelection();
|
||||
|
||||
public void KeyFocusSearch()
|
||||
{
|
||||
searchBox.Focus();
|
||||
searchBox.SelectAll();
|
||||
}
|
||||
|
||||
public void ToolAddAllCategory() => AddAllCategoryButton_OnClick(null, null!);
|
||||
|
||||
public void ToolAddUniqueMmd() => AddUniqueMmdButton_OnClick(null, null!);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Media.Imaging;
|
||||
|
||||
using StarterNG.Classes;
|
||||
@@ -10,6 +12,18 @@ public sealed class MiniTextures
|
||||
{
|
||||
private readonly Dictionary<string, Bitmap?> _cache = new();
|
||||
|
||||
/// <summary>
|
||||
/// Draws a mini without smoothing. These files are about 80x30 and get shown up
|
||||
/// to 68 px tall, so bilinear just smears the upscale; nearest keeps the edges.
|
||||
/// (Has to be set in code - Avalonia 12 exposes RenderOptions as a struct with
|
||||
/// Get/Set helpers, not as a styleable attached property.)
|
||||
/// </summary>
|
||||
public static Image Sharp(Image image)
|
||||
{
|
||||
RenderOptions.SetBitmapInterpolationMode(image, BitmapInterpolationMode.None);
|
||||
return image;
|
||||
}
|
||||
|
||||
public Bitmap? Get(string? name, int height, bool strict = false)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name))
|
||||
@@ -27,8 +41,17 @@ public sealed class MiniTextures
|
||||
{
|
||||
try
|
||||
{
|
||||
// Decode at native size and let the renderer scale. The minis are
|
||||
// about 80x30, so DecodeToHeight would bake a 2x upscale into the
|
||||
// cache - blurry, and five times the memory. Only shrink here.
|
||||
using var fs = File.OpenRead(path);
|
||||
bmp = Bitmap.DecodeToHeight(fs, height);
|
||||
bmp = new Bitmap(fs);
|
||||
if (bmp.PixelSize.Height > height)
|
||||
{
|
||||
bmp.Dispose();
|
||||
fs.Position = 0;
|
||||
bmp = Bitmap.DecodeToHeight(fs, height);
|
||||
}
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
|
||||
@@ -222,13 +222,13 @@ public sealed class VehicleBrowser
|
||||
};
|
||||
var bmp = _minis.Get(cls, height);
|
||||
if (bmp != null)
|
||||
cell.Children.Add(new Image
|
||||
cell.Children.Add(MiniTextures.Sharp(new Image
|
||||
{
|
||||
Source = bmp, Height = height,
|
||||
Stretch = Stretch.Uniform,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
});
|
||||
}));
|
||||
cell.Children.Add(new TextBlock
|
||||
{
|
||||
Text = cls,
|
||||
@@ -421,7 +421,7 @@ public sealed class VehicleBrowser
|
||||
target.Children.Add(new TextBlock
|
||||
{
|
||||
Text = line,
|
||||
FontSize = 10,
|
||||
FontSize = 12,
|
||||
Opacity = 0.85,
|
||||
TextTrimming = TextTrimming.CharacterEllipsis
|
||||
});
|
||||
|
||||
@@ -8,6 +8,9 @@ public static class VehicleCardStyle
|
||||
public static readonly IBrush BorderSelected = new SolidColorBrush(Color.Parse("#AA41C400"));
|
||||
public static readonly IBrush BackgroundSelected = new SolidColorBrush(Color.Parse("#2241C400"));
|
||||
|
||||
/// <summary>Lit state of the flip button on a reversed vehicle.</summary>
|
||||
public static readonly IBrush Flipped = new SolidColorBrush(Color.Parse("#41C400"));
|
||||
|
||||
public static readonly IBrush Placeholder = new SolidColorBrush(Color.Parse("#22808080"));
|
||||
|
||||
public static int ThumbHeight =>
|
||||
|
||||
@@ -7,6 +7,8 @@ using Avalonia.Input;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.VisualTree;
|
||||
using Material.Icons;
|
||||
using Material.Icons.Avalonia;
|
||||
|
||||
using StarterNG.Classes;
|
||||
using StarterNG.Domain;
|
||||
@@ -75,7 +77,10 @@ public sealed class VehicleCards
|
||||
?? _minis.Get(car.MiniName, VehicleCardStyle.ThumbHeight);
|
||||
if (bmp != null)
|
||||
{
|
||||
var img = new Image { Source = bmp, Height = VehicleCardStyle.ThumbHeight, Stretch = Stretch.Uniform };
|
||||
var img = MiniTextures.Sharp(new Image
|
||||
{
|
||||
Source = bmp, Height = VehicleCardStyle.ThumbHeight, Stretch = Stretch.Uniform
|
||||
});
|
||||
if (item.Flipped)
|
||||
{
|
||||
img.RenderTransform = new ScaleTransform(-1, 1);
|
||||
@@ -101,57 +106,50 @@ public sealed class VehicleCards
|
||||
}
|
||||
}
|
||||
|
||||
var nameBlock = new TextBlock
|
||||
{
|
||||
Text = item.Grouped && item.Cars.Count > 1 ? Consist.UnitLabel(item) : string.Empty,
|
||||
FontSize = 11,
|
||||
MaxWidth = Math.Max(120, item.Cars.Count * 96),
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
TextAlignment = TextAlignment.Center,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
IsVisible = item.Grouped && item.Cars.Count > 1
|
||||
};
|
||||
|
||||
var controls = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
Spacing = 2,
|
||||
HorizontalAlignment = HorizontalAlignment.Center
|
||||
Spacing = 3,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
Margin = new Thickness(0, 1, 0, 2)
|
||||
};
|
||||
controls.Children.Add(SmallButton("◀", App.Loc["TipMoveLeft"], () => _consist.MoveLeft(item)));
|
||||
controls.Children.Add(DriverButton(item));
|
||||
controls.Children.Add(SmallButton("⇄", App.Loc["TipFlip"], () => _consist.Flip(item)));
|
||||
controls.Children.Add(SmallButton("▶", App.Loc["TipMoveRight"], () => _consist.MoveRight(item)));
|
||||
controls.Children.Add(FlipButton(item));
|
||||
|
||||
if (item.Grouped && item.Cars.Count > 1)
|
||||
controls.Children.Add(SmallButton("⧉", App.Loc["TipSplit"], () => _consist.Split(item)));
|
||||
controls.Children.Add(SmallButton(MaterialIconKind.CallSplit, App.Loc["TipSplit"], () => _consist.Split(item)));
|
||||
else if ((index + 1 < _consist.Count &&
|
||||
(Consist.HoldsTail(item) || _consist.CanFormUnit(item, _consist[index + 1]))) ||
|
||||
(index > 0 && Consist.HoldsTail(_consist[index - 1])))
|
||||
controls.Children.Add(SmallButton("⛓", App.Loc["TipJoin"], () => _consist.Join(item)));
|
||||
controls.Children.Add(SmallButton(MaterialIconKind.Link, App.Loc["TipJoin"], () => _consist.Join(item)));
|
||||
|
||||
var remove = SmallButton("✕", App.Loc["RemoveSelectedVehicle"], () => _consist.Remove(item));
|
||||
var remove = SmallButton(MaterialIconKind.Close, App.Loc["RemoveSelectedVehicle"], () => _consist.Remove(item));
|
||||
remove.Foreground = new SolidColorBrush(Color.Parse("#E05252"));
|
||||
remove.FontWeight = FontWeight.Bold;
|
||||
controls.Children.Add(remove);
|
||||
|
||||
var inner = new StackPanel { Spacing = 4 };
|
||||
var inner = new StackPanel { Spacing = 3 };
|
||||
inner.Children.Add(minis);
|
||||
inner.Children.Add(nameBlock);
|
||||
inner.Children.Add(controls);
|
||||
|
||||
var card = new Border
|
||||
{
|
||||
Margin = new Thickness(2, 0),
|
||||
Padding = new Thickness(6),
|
||||
Margin = new Thickness(3, 2, 3, 6),
|
||||
Padding = new Thickness(selected ? 4 : 5),
|
||||
CornerRadius = new CornerRadius(6),
|
||||
BorderThickness = new Thickness(selected ? 2 : 1),
|
||||
BorderBrush = selected ? VehicleCardStyle.BorderSelected : VehicleCardStyle.Border,
|
||||
Background = selected ? VehicleCardStyle.BackgroundSelected : Brushes.Transparent,
|
||||
Cursor = _hand,
|
||||
Child = inner,
|
||||
Tag = item
|
||||
Tag = item,
|
||||
// Focusable so the strip can be reached with Tab and walked with the
|
||||
// arrow keys; the focus ring itself comes from Carbon.axaml.
|
||||
Focusable = true
|
||||
};
|
||||
card.Classes.Add("ConsistCard");
|
||||
card.GotFocus += (_, _) => Select(item, null);
|
||||
card.KeyDown += (_, e) => OnCardKey(card, item, e);
|
||||
card.PointerPressed += (_, e) =>
|
||||
{
|
||||
|
||||
@@ -167,6 +165,7 @@ public sealed class VehicleCards
|
||||
_consist.SyncStartingVehicle();
|
||||
RefreshCardSelectionChrome();
|
||||
_refreshDetails();
|
||||
_selectInBrowser(_consist.SelectedCar);
|
||||
|
||||
_armDrag(index, e);
|
||||
};
|
||||
@@ -178,31 +177,30 @@ public sealed class VehicleCards
|
||||
TrainsetDisplay.VehicleLabel(car,
|
||||
head: index == 0 && ReferenceEquals(car, Consist.HeadCar(item)) && TrainsetDisplay.IsPowered(car));
|
||||
|
||||
/// <summary>
|
||||
/// The name is not printed under the thumbnail - it costs a row on every card
|
||||
/// and the tooltip says the same thing when it is actually wanted.
|
||||
/// </summary>
|
||||
private Control MemberFrame(Control visual, ConsistItem item, Dynamic car, string caption)
|
||||
{
|
||||
var stack = new StackPanel { Spacing = 2 };
|
||||
stack.Children.Add(visual);
|
||||
stack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = caption,
|
||||
FontSize = 10,
|
||||
MaxWidth = 96,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
TextAlignment = TextAlignment.Center,
|
||||
HorizontalAlignment = HorizontalAlignment.Center
|
||||
});
|
||||
|
||||
var frame = new Border
|
||||
{
|
||||
Child = stack,
|
||||
Child = visual,
|
||||
Padding = new Thickness(1),
|
||||
BorderThickness = new Thickness(0, 2),
|
||||
BorderBrush = Brushes.Transparent,
|
||||
Cursor = _hand
|
||||
};
|
||||
|
||||
var lines = new List<string> { caption };
|
||||
if (!string.IsNullOrWhiteSpace(car.Name) && car.Name != caption)
|
||||
ToolTip.SetTip(frame, car.Name);
|
||||
lines.Add(car.Name);
|
||||
if (item.Grouped && item.Cars.Count > 1)
|
||||
lines.Add(Consist.UnitLabel(item));
|
||||
|
||||
string tip = string.Join("\n", lines.Where(l => !string.IsNullOrWhiteSpace(l)));
|
||||
if (tip.Length > 0)
|
||||
ToolTip.SetTip(frame, tip);
|
||||
|
||||
var picked = car;
|
||||
frame.PointerPressed += (_, _) => PressedCar = picked;
|
||||
@@ -233,12 +231,154 @@ public sealed class VehicleCards
|
||||
if (child is not Border { Tag: ConsistItem ci } border)
|
||||
continue;
|
||||
bool sel = ReferenceEquals(ci, _consist.Selected);
|
||||
border.Padding = new Thickness(sel ? 4 : 5);
|
||||
border.BorderThickness = new Thickness(sel ? 2 : 1);
|
||||
border.BorderBrush = sel ? VehicleCardStyle.BorderSelected : VehicleCardStyle.Border;
|
||||
border.Background = sel ? VehicleCardStyle.BackgroundSelected : Brushes.Transparent;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects an item without going through a redraw. <paramref name="car"/> picks the
|
||||
/// active member of a multi-vehicle unit; null keeps the current one when possible.
|
||||
/// </summary>
|
||||
private void Select(ConsistItem item, Dynamic? car)
|
||||
{
|
||||
var target = car ?? (ReferenceEquals(_consist.Selected, item)
|
||||
? _consist.ActiveCar(item)
|
||||
: item.Cars[0]);
|
||||
|
||||
if (ReferenceEquals(_consist.Selected, item) && ReferenceEquals(_consist.SelectedCar, target))
|
||||
return;
|
||||
|
||||
_consist.Selected = item;
|
||||
_consist.SelectedCar = target;
|
||||
_consist.SyncStartingVehicle();
|
||||
RefreshCardSelectionChrome();
|
||||
_refreshDetails();
|
||||
_selectInBrowser(target);
|
||||
}
|
||||
|
||||
private Border? CardFor(ConsistItem item) =>
|
||||
consistStack.Children.OfType<Border>()
|
||||
.FirstOrDefault(b => ReferenceEquals(b.Tag, item));
|
||||
|
||||
/// <summary>
|
||||
/// Set before an action that rebuilds the strip, so focus lands back on the card
|
||||
/// instead of falling out to whatever comes next in the tab order.
|
||||
/// </summary>
|
||||
private bool _refocusAfterRedraw;
|
||||
|
||||
public void FocusSelectedCard(bool onlyIfRequested = false)
|
||||
{
|
||||
if (onlyIfRequested && !_refocusAfterRedraw)
|
||||
return;
|
||||
|
||||
_refocusAfterRedraw = false;
|
||||
if (_consist.Selected is { } item && CardFor(item) is { } card)
|
||||
{
|
||||
card.Focus();
|
||||
card.BringIntoView();
|
||||
}
|
||||
}
|
||||
|
||||
private void Redraw(bool keepFocus)
|
||||
{
|
||||
_refocusAfterRedraw = keepFocus;
|
||||
_redraw();
|
||||
}
|
||||
|
||||
private void OnCardKey(Border card, ConsistItem item, KeyEventArgs e)
|
||||
{
|
||||
int index = _consist.IndexOf(item);
|
||||
if (index < 0)
|
||||
return;
|
||||
|
||||
bool ctrl = e.KeyModifiers.HasFlag(KeyModifiers.Control);
|
||||
|
||||
switch (e.Key)
|
||||
{
|
||||
case Key.Left when ctrl:
|
||||
_consist.MoveLeft(item);
|
||||
Redraw(keepFocus: true);
|
||||
break;
|
||||
|
||||
case Key.Right when ctrl:
|
||||
_consist.MoveRight(item);
|
||||
Redraw(keepFocus: true);
|
||||
break;
|
||||
|
||||
case Key.Left:
|
||||
case Key.Right:
|
||||
FocusCardAt(index + (e.Key == Key.Left ? -1 : 1));
|
||||
break;
|
||||
|
||||
case Key.Home:
|
||||
FocusCardAt(0);
|
||||
break;
|
||||
|
||||
case Key.End:
|
||||
FocusCardAt(_consist.Count - 1);
|
||||
break;
|
||||
|
||||
// Within a coupled unit the arrow keys pick which member the detail
|
||||
// panels talk about - the same thing clicking a thumbnail does.
|
||||
case Key.Up:
|
||||
case Key.Down:
|
||||
StepMember(item, e.Key == Key.Down ? 1 : -1);
|
||||
break;
|
||||
|
||||
case Key.Space:
|
||||
_consist.Flip(item);
|
||||
Redraw(keepFocus: true);
|
||||
break;
|
||||
|
||||
case Key.Enter:
|
||||
card.ContextMenu?.Open(card);
|
||||
break;
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void FocusCardAt(int index)
|
||||
{
|
||||
if (index < 0 || index >= _consist.Count)
|
||||
return;
|
||||
|
||||
var item = _consist[index];
|
||||
Select(item, null);
|
||||
if (CardFor(item) is { } card)
|
||||
{
|
||||
card.Focus();
|
||||
// Focusing alone does not scroll a plain Border into view.
|
||||
card.BringIntoView();
|
||||
}
|
||||
}
|
||||
|
||||
private void StepMember(ConsistItem item, int delta)
|
||||
{
|
||||
if (item.Cars.Count < 2)
|
||||
return;
|
||||
|
||||
int at = item.Cars.IndexOf(_consist.ActiveCar(item));
|
||||
int next = Math.Clamp(at + delta, 0, item.Cars.Count - 1);
|
||||
if (next != at)
|
||||
Select(item, item.Cars[next]);
|
||||
}
|
||||
|
||||
/// <summary>Ctrl+N on the selected card, from the window's shortcut table.</summary>
|
||||
public void ShowNumberFlyoutForSelection()
|
||||
{
|
||||
if (_consist.Selected is not { } item || CardFor(item) is not { } card)
|
||||
return;
|
||||
|
||||
ShowWagonNumberFlyout(card, _consist.ActiveCar(item));
|
||||
}
|
||||
|
||||
private void ShowWagonNumberFlyout(Control anchor, Dynamic car)
|
||||
{
|
||||
var num = new NumericUpDown
|
||||
@@ -276,7 +416,7 @@ public sealed class VehicleCards
|
||||
};
|
||||
var coupler = new Button
|
||||
{
|
||||
Padding = new Thickness(2),
|
||||
Padding = new Thickness(4, 6, 4, 6),
|
||||
MinWidth = 0,
|
||||
Cursor = _hand,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
@@ -288,7 +428,7 @@ public sealed class VehicleCards
|
||||
}
|
||||
};
|
||||
coupler.Classes.Add("Basic");
|
||||
ToolTip.SetTip(coupler, App.Loc["Coupling"]);
|
||||
ToolTip.SetTip(coupler, $"{App.Loc["Coupling"]} — {App.Loc["TipReorder"]}");
|
||||
return coupler;
|
||||
}
|
||||
|
||||
@@ -302,7 +442,7 @@ public sealed class VehicleCards
|
||||
var copy = new Button
|
||||
{
|
||||
Content = "\u00AB",
|
||||
FontSize = 13,
|
||||
FontSize = 12,
|
||||
Padding = new Thickness(6, 1),
|
||||
Margin = new Thickness(12, 0, 0, 0),
|
||||
IsEnabled = unitIdx > 0,
|
||||
@@ -324,11 +464,20 @@ public sealed class VehicleCards
|
||||
{
|
||||
Text = App.Loc["Coupling"],
|
||||
FontWeight = FontWeight.Bold,
|
||||
FontSize = 11,
|
||||
FontSize = 12,
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
});
|
||||
panel.Children.Add(header);
|
||||
|
||||
var grid = new Grid
|
||||
{
|
||||
ColumnDefinitions = new ColumnDefinitions("*,*"),
|
||||
RowSpacing = 2,
|
||||
ColumnSpacing = 12
|
||||
};
|
||||
for (int r = 0; r < (CouplingBits.BitKeys.Length + 1) / 2; r++)
|
||||
grid.RowDefinitions.Add(new RowDefinition(GridLength.Auto));
|
||||
|
||||
for (int i = 0; i < CouplingBits.BitKeys.Length; i++)
|
||||
{
|
||||
int bit = 1 << i;
|
||||
@@ -336,17 +485,20 @@ public sealed class VehicleCards
|
||||
{
|
||||
Content = App.Loc[CouplingBits.BitKeys[i]],
|
||||
IsChecked = d.Coupling.Has(bit),
|
||||
FontSize = 11
|
||||
FontSize = 12
|
||||
};
|
||||
check.IsCheckedChanged += (_, _) => d.Coupling.Set(bit, check.IsChecked == true);
|
||||
panel.Children.Add(check);
|
||||
Grid.SetRow(check, i / 2);
|
||||
Grid.SetColumn(check, i % 2);
|
||||
grid.Children.Add(check);
|
||||
}
|
||||
panel.Children.Add(grid);
|
||||
|
||||
var thermo = new CheckBox
|
||||
{
|
||||
Content = App.Loc["ThermoAmbient"],
|
||||
IsChecked = d.Coupling.ThermoDynamic,
|
||||
FontSize = 11,
|
||||
FontSize = 12,
|
||||
Margin = new Thickness(0, 2, 0, 0)
|
||||
};
|
||||
thermo.IsCheckedChanged += (_, _) => d.Coupling.ThermoDynamic = thermo.IsChecked == true;
|
||||
@@ -355,18 +507,26 @@ public sealed class VehicleCards
|
||||
return new Border { Padding = new Thickness(8), Child = panel };
|
||||
}
|
||||
|
||||
private const string TextPresentation = "\uFE0E";
|
||||
/// <summary>
|
||||
/// Card buttons draw vector icons rather than glyphs - Windows renders half
|
||||
/// the arrow and link characters as colour emoji, whatever the font asks for.
|
||||
/// </summary>
|
||||
private Button SmallButton(MaterialIconKind icon, string tip, Action onClick)
|
||||
{
|
||||
var btn = SmallButton(tip, onClick);
|
||||
btn.Content = new MaterialIcon { Kind = icon, Width = 16, Height = 16 };
|
||||
return btn;
|
||||
}
|
||||
|
||||
private Button SmallButton(string glyph, string tip, Action onClick)
|
||||
private Button SmallButton(string tip, Action onClick)
|
||||
{
|
||||
var btn = new Button
|
||||
{
|
||||
Content = glyph + TextPresentation,
|
||||
FontSize = 15,
|
||||
FontSize = 13,
|
||||
Padding = new Thickness(0),
|
||||
MinWidth = 0,
|
||||
MinHeight = 0,
|
||||
Width = 26,
|
||||
Width = 28,
|
||||
Height = 22,
|
||||
HorizontalContentAlignment = HorizontalAlignment.Center,
|
||||
VerticalContentAlignment = VerticalAlignment.Center,
|
||||
@@ -378,9 +538,26 @@ public sealed class VehicleCards
|
||||
return btn;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Orientation is a state, not just an action, so the button carries it: lit means
|
||||
/// this vehicle sits reversed. Reading it off the card beats a checkbox in a panel
|
||||
/// two columns away, and the card is rebuilt on every flip so the paint is current.
|
||||
/// </summary>
|
||||
private Button FlipButton(ConsistItem item)
|
||||
{
|
||||
var btn = SmallButton(MaterialIconKind.SwapHorizontal, App.Loc["TipFlip"],
|
||||
() => _consist.Flip(item));
|
||||
if (item.Flipped)
|
||||
{
|
||||
btn.Foreground = VehicleCardStyle.Flipped;
|
||||
ToolTip.SetTip(btn, $"{App.Loc["TipFlip"]} \u2014 {App.Loc["Reversed"]}");
|
||||
}
|
||||
return btn;
|
||||
}
|
||||
|
||||
private Button DriverButton(ConsistItem item)
|
||||
{
|
||||
var btn = SmallButton("", App.Loc["TipDriver"], () => _consist.CycleDriver(item));
|
||||
var btn = SmallButton(App.Loc["TipDriver"], () => _consist.CycleDriver(item));
|
||||
btn.FontWeight = FontWeight.Bold;
|
||||
_driverChrome.Add((btn, item));
|
||||
PaintDriverButton(btn, item);
|
||||
|
||||
@@ -10,16 +10,17 @@ using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
|
||||
using StarterNG.Classes;
|
||||
using StarterNG.Controls;
|
||||
using StarterNG.Domain;
|
||||
|
||||
namespace StarterNG.Views;
|
||||
|
||||
public sealed class VehicleDetails
|
||||
{
|
||||
private readonly HeaderedContentControl selectedVehiclePanel;
|
||||
private readonly StackPanel generalPanel;
|
||||
private readonly StackPanel consistTexturePanel;
|
||||
private readonly StackPanel couplerPanel;
|
||||
private readonly Panel couplerActions;
|
||||
private readonly StackPanel brakesPanel;
|
||||
private readonly StackPanel loadsPanel;
|
||||
private readonly StackPanel damagePanel;
|
||||
@@ -36,17 +37,17 @@ public sealed class VehicleDetails
|
||||
private readonly Action<VehicleTexture, StackPanel> _showTextureInfo;
|
||||
|
||||
public VehicleDetails(
|
||||
HeaderedContentControl selectedVehiclePanel,
|
||||
StackPanel generalPanel, StackPanel consistTexturePanel, StackPanel couplerPanel,
|
||||
Panel couplerActions,
|
||||
StackPanel brakesPanel, StackPanel loadsPanel, StackPanel damagePanel,
|
||||
Button removeVehicleButton,
|
||||
VehicleDatabase db, VehicleInfo info, Consist consist, Cargo cargo, Cursor hand,
|
||||
Action redraw, Action refreshChrome, Action<VehicleTexture, StackPanel> showTextureInfo)
|
||||
{
|
||||
this.selectedVehiclePanel = selectedVehiclePanel;
|
||||
this.generalPanel = generalPanel;
|
||||
this.consistTexturePanel = consistTexturePanel;
|
||||
this.couplerPanel = couplerPanel;
|
||||
this.couplerActions = couplerActions;
|
||||
this.brakesPanel = brakesPanel;
|
||||
this.loadsPanel = loadsPanel;
|
||||
this.damagePanel = damagePanel;
|
||||
@@ -67,31 +68,27 @@ public sealed class VehicleDetails
|
||||
generalPanel.Children.Clear();
|
||||
consistTexturePanel.Children.Clear();
|
||||
couplerPanel.Children.Clear();
|
||||
couplerActions.Children.Clear();
|
||||
brakesPanel.Children.Clear();
|
||||
loadsPanel.Children.Clear();
|
||||
damagePanel.Children.Clear();
|
||||
|
||||
selectedVehiclePanel.Header = _consist.Selected is null
|
||||
? App.Loc["SelectedVehicle"]
|
||||
: $"{App.Loc["SelectedVehicle"]}: " +
|
||||
(_consist.Selected.Grouped ? Consist.UnitLabel(_consist.Selected) : _consist.Selected.Cars[0].Name);
|
||||
|
||||
if (_consist.Selected != null && !_consist.Selected.Cars.Contains(_consist.SelectedCar!))
|
||||
_consist.SelectedCar = _consist.Selected.Cars[0];
|
||||
|
||||
_refreshChrome();
|
||||
|
||||
loadsPanel.Children.Add(BuildConsistLoadTools());
|
||||
|
||||
if (_consist.Selected is null)
|
||||
{
|
||||
generalPanel.Children.Add(DetailHint(App.Loc["SelectVehicleHint"]));
|
||||
consistTexturePanel.Children.Add(DetailHint(App.Loc["SelectVehicleHint"]));
|
||||
couplerPanel.Children.Add(DetailHint(App.Loc["SelectVehicleHint"]));
|
||||
brakesPanel.Children.Add(DetailHint(App.Loc["SelectVehicleHint"]));
|
||||
loadsPanel.Children.Add(DetailHint(App.Loc["SelectVehicleHint"]));
|
||||
damagePanel.Children.Add(DetailHint(App.Loc["SelectVehicleHint"]));
|
||||
generalPanel.Children.Add(PanelNote(App.Loc["SelectVehicleHint"]));
|
||||
consistTexturePanel.Children.Add(PanelNote(App.Loc["SelectVehicleHint"]));
|
||||
couplerPanel.Children.Add(PanelNote(App.Loc["SelectVehicleHint"]));
|
||||
brakesPanel.Children.Add(PanelNote(App.Loc["SelectVehicleHint"]));
|
||||
loadsPanel.Children.Add(PanelNote(App.Loc["SelectVehicleHint"]));
|
||||
damagePanel.Children.Add(PanelNote(App.Loc["SelectVehicleHint"]));
|
||||
removeVehicleButton.IsEnabled = false;
|
||||
// The whole-consist tools still apply with nothing selected.
|
||||
AddConsistLoadSection();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -100,21 +97,27 @@ public sealed class VehicleDetails
|
||||
if (_db.TextureForSkin(_consist.ActiveCar(_consist.Selected).SkinFile) is { } tex)
|
||||
_showTextureInfo(tex, consistTexturePanel);
|
||||
else
|
||||
consistTexturePanel.Children.Add(DetailHint(App.Loc["NoTextureInfo"]));
|
||||
consistTexturePanel.Children.Add(PanelNote(App.Loc["NoTextureInfo"]));
|
||||
|
||||
removeVehicleButton.IsEnabled = true;
|
||||
|
||||
if (_consist.Selected.Cars.Count > 1)
|
||||
{
|
||||
couplerPanel.Children.Add(UnitScopeHint(App.Loc["CouplingUnitRear"]));
|
||||
brakesPanel.Children.Add(UnitScopeHint(App.Loc["AppliesToUnit"]));
|
||||
couplerPanel.Children.Add(PanelNote(App.Loc["CouplingUnitRear"]));
|
||||
brakesPanel.Children.Add(PanelNote(App.Loc["AppliesToUnit"]));
|
||||
}
|
||||
|
||||
BuildCouplerEditor(_consist.Selected);
|
||||
BuildBrakesEditor(_consist.Selected);
|
||||
BuildLoadsEditor(_consist.Selected);
|
||||
BuildConfigExtras(_consist.Selected);
|
||||
BuildDamageEditor(_consist.Selected);
|
||||
|
||||
// Configuration tab: what this vehicle is, what it carries, then the actions
|
||||
// that reach past it - each under a header that says which is which.
|
||||
loadsPanel.Children.Add(
|
||||
Section(App.Loc["Vehicle"], BuildVehicleSection(_consist.Selected)));
|
||||
loadsPanel.Children.Add(
|
||||
Section(App.Loc["Load"], BuildLoadSection(_consist.Selected)));
|
||||
AddConsistLoadSection();
|
||||
}
|
||||
|
||||
private void BuildGeneralInfo(ConsistItem item)
|
||||
@@ -122,6 +125,9 @@ public sealed class VehicleDetails
|
||||
var car = _consist.ActiveCar(item);
|
||||
var inv = CultureInfo.InvariantCulture;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(car.Name))
|
||||
generalPanel.Children.Add(Reading(App.Loc["Name"], car.Name));
|
||||
|
||||
bool unit = item.Cars.Count > 1;
|
||||
var members = item.Cars.Select(_info.PhysicsFor).Where(p => p != null).ToList();
|
||||
|
||||
@@ -132,49 +138,19 @@ public sealed class VehicleDetails
|
||||
double? vmax = members.Count == 0 ? null
|
||||
: unit ? members.Min(p => p!.VMax) : members[0]!.VMax;
|
||||
|
||||
generalPanel.Children.Add(InfoRow($"{App.Loc["Length"]} [m]:",
|
||||
generalPanel.Children.Add(Reading($"{App.Loc["Length"]} [m]",
|
||||
length?.ToString("0.##", inv)));
|
||||
generalPanel.Children.Add(InfoRow($"{App.Loc["Mass"]} [t]:",
|
||||
generalPanel.Children.Add(Reading($"{App.Loc["Mass"]} [t]",
|
||||
mass?.ToString("0.#", inv)));
|
||||
generalPanel.Children.Add(InfoRow($"{App.Loc["VMax"]} [km/h]:",
|
||||
generalPanel.Children.Add(Reading($"{App.Loc["VMax"]} [km/h]",
|
||||
vmax?.ToString("0", inv)));
|
||||
|
||||
if (unit)
|
||||
generalPanel.Children.Add(InfoRow($"{App.Loc["VehicleCount"]}:",
|
||||
generalPanel.Children.Add(Reading(App.Loc["VehicleCount"],
|
||||
item.Cars.Count.ToString(inv)));
|
||||
|
||||
generalPanel.Children.Add(InfoRow($"{App.Loc["Model"]}:", car.MmdFile));
|
||||
generalPanel.Children.Add(InfoRow($"{App.Loc["Texture"]}:", car.SkinFile));
|
||||
}
|
||||
|
||||
private static Control InfoRow(string label, string? value)
|
||||
{
|
||||
var grid = new Grid { ColumnDefinitions = new ColumnDefinitions("Auto,10,*") };
|
||||
|
||||
var caption = new TextBlock
|
||||
{
|
||||
Text = label,
|
||||
FontSize = 12,
|
||||
MinWidth = 110,
|
||||
TextAlignment = TextAlignment.Right,
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
};
|
||||
|
||||
var reading = new TextBlock
|
||||
{
|
||||
Text = string.IsNullOrWhiteSpace(value) ? "–" : value,
|
||||
FontSize = 12,
|
||||
TextAlignment = TextAlignment.Right,
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
};
|
||||
|
||||
Grid.SetColumn(reading, 2);
|
||||
grid.Children.Add(caption);
|
||||
grid.Children.Add(reading);
|
||||
return grid;
|
||||
generalPanel.Children.Add(Reading(App.Loc["Model"], car.MmdFile));
|
||||
generalPanel.Children.Add(Reading(App.Loc["Texture"], car.SkinFile));
|
||||
}
|
||||
|
||||
private void BuildCouplerEditor(ConsistItem item)
|
||||
@@ -187,12 +163,17 @@ public sealed class VehicleDetails
|
||||
|
||||
var d = Consist.TailCar(item);
|
||||
|
||||
var grid = new Grid
|
||||
// Two columns: the tab is wide enough and the list is short enough
|
||||
// to avoid scrolling while still reading left-to-right, top-to-bottom.
|
||||
var list = new Grid
|
||||
{
|
||||
ColumnDefinitions = new ColumnDefinitions("*,8,*"),
|
||||
RowDefinitions = new RowDefinitions("Auto,Auto,Auto,Auto"),
|
||||
RowSpacing = 4
|
||||
ColumnDefinitions = new ColumnDefinitions("*,*"),
|
||||
RowSpacing = 2,
|
||||
ColumnSpacing = 16
|
||||
};
|
||||
for (int r = 0; r < (CouplingBits.BitKeys.Length + 1) / 2; r++)
|
||||
list.RowDefinitions.Add(new RowDefinition(GridLength.Auto));
|
||||
|
||||
for (int i = 0; i < CouplingBits.BitKeys.Length; i++)
|
||||
{
|
||||
int bit = 1 << i;
|
||||
@@ -207,17 +188,16 @@ public sealed class VehicleDetails
|
||||
d.Coupling.Set(bit, check.IsChecked == true);
|
||||
_redraw();
|
||||
};
|
||||
int half = (CouplingBits.BitKeys.Length + 1) / 2;
|
||||
Grid.SetColumn(check, i < half ? 0 : 2);
|
||||
Grid.SetRow(check, i % half);
|
||||
grid.Children.Add(check);
|
||||
Grid.SetRow(check, i / 2);
|
||||
Grid.SetColumn(check, i % 2);
|
||||
list.Children.Add(check);
|
||||
}
|
||||
couplerPanel.Children.Add(grid);
|
||||
couplerPanel.Children.Add(list);
|
||||
|
||||
var copy = new Button
|
||||
{
|
||||
Content = App.Loc["CopyCoupler"],
|
||||
FontSize = 11,
|
||||
FontSize = 12,
|
||||
Cursor = _hand,
|
||||
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||||
HorizontalContentAlignment = HorizontalAlignment.Center,
|
||||
@@ -235,7 +215,7 @@ public sealed class VehicleDetails
|
||||
var auto = new Button
|
||||
{
|
||||
Content = App.Loc["AutoCoupler"],
|
||||
FontSize = 11,
|
||||
FontSize = 12,
|
||||
Cursor = _hand,
|
||||
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||||
HorizontalContentAlignment = HorizontalAlignment.Center,
|
||||
@@ -250,19 +230,23 @@ public sealed class VehicleDetails
|
||||
Refresh();
|
||||
};
|
||||
|
||||
var buttons = new Grid
|
||||
// Laid out horizontally under the checkbox list so the grid can use the full tab width
|
||||
var buttons = new StackPanel
|
||||
{
|
||||
ColumnDefinitions = new ColumnDefinitions("125*,4,48*"),
|
||||
Orientation = Orientation.Horizontal,
|
||||
Spacing = 4,
|
||||
Margin = new Thickness(0, 6, 0, 0)
|
||||
};
|
||||
Grid.SetColumn(copy, 0);
|
||||
Grid.SetColumn(auto, 2);
|
||||
buttons.Children.Add(copy);
|
||||
buttons.Children.Add(auto);
|
||||
couplerPanel.Children.Add(buttons);
|
||||
couplerActions.Children.Add(buttons);
|
||||
}
|
||||
|
||||
private void BuildConfigExtras(ConsistItem item)
|
||||
/// <summary>
|
||||
/// The vehicle's own settings. Every write path is unchanged: crew and wagon number
|
||||
/// go through the unit's active car, the coolant flag to its first car only.
|
||||
/// </summary>
|
||||
private Control BuildVehicleSection(ConsistItem item)
|
||||
{
|
||||
var driver = new ComboBox { FontSize = 12, MinWidth = 0 };
|
||||
foreach (string key in new[] { "DriverHead", "DriverRear", "DriverPassenger", "DriverNobody" })
|
||||
@@ -296,75 +280,143 @@ public sealed class VehicleDetails
|
||||
};
|
||||
driver.MinWidth = 0;
|
||||
driver.HorizontalAlignment = HorizontalAlignment.Stretch;
|
||||
loadsPanel.Children.Insert(0, LabeledRow(App.Loc["CrewLabel"], driver, labelWidth: 70));
|
||||
|
||||
var reversed = new CheckBox
|
||||
{
|
||||
Content = App.Loc["Reversed"],
|
||||
IsChecked = item.Flipped,
|
||||
FontSize = 11
|
||||
};
|
||||
reversed.IsCheckedChanged += (_, _) =>
|
||||
{
|
||||
if ((reversed.IsChecked == true) != item.Flipped)
|
||||
_consist.Flip(item);
|
||||
};
|
||||
var rows = new StackPanel();
|
||||
rows.Children.Add(Row(App.Loc["CrewLabel"], driver));
|
||||
|
||||
if (IsPassengerVehicle(item))
|
||||
{
|
||||
var lead = item.Cars[0];
|
||||
var numBox = new NumericUpDown
|
||||
{
|
||||
FontSize = 12,
|
||||
Minimum = 0,
|
||||
Maximum = 99999,
|
||||
Increment = 1,
|
||||
Value = Consist.GetWagonNumber(lead),
|
||||
FormatString = "0"
|
||||
};
|
||||
numBox.ValueChanged += (_, _) =>
|
||||
{
|
||||
_consist.SetWagonNumber(lead, (int)(numBox.Value ?? 0));
|
||||
_redraw();
|
||||
};
|
||||
rows.Children.Add(Row(App.Loc["WagonNumber"], numBox));
|
||||
}
|
||||
|
||||
// Orientation is not here any more: it reads and toggles off the flip button on
|
||||
// the vehicle's own card, which lights up when the vehicle sits reversed.
|
||||
// The flag rides on the unit's first car, so that is the car that has to own a
|
||||
// diesel engine for it to mean anything: the simulator runs its heat model only
|
||||
// for DieselEngine and DieselElectric, and on anything else ".TA" is written into
|
||||
// the scenario and then ignored. Shown anyway when the flag is already set, so an
|
||||
// inherited one stays visible and removable.
|
||||
var d = item.Cars[0];
|
||||
var thermo = new CheckBox
|
||||
{
|
||||
Content = App.Loc["ThermoAmbient"],
|
||||
IsChecked = d.Coupling.ThermoDynamic,
|
||||
FontSize = 11
|
||||
};
|
||||
thermo.IsCheckedChanged += (_, _) => d.Coupling.ThermoDynamic = thermo.IsChecked == true;
|
||||
bool thermoApplies = _info.PhysicsFor(d)?.IsDieselEngine == true
|
||||
|| d.Coupling.ThermoDynamic;
|
||||
|
||||
var switches = new Grid
|
||||
if (thermoApplies)
|
||||
{
|
||||
ColumnDefinitions = new ColumnDefinitions("*,8,*"),
|
||||
Margin = new Thickness(0, 4, 0, 0)
|
||||
};
|
||||
Grid.SetColumn(thermo, 2);
|
||||
switches.Children.Add(reversed);
|
||||
switches.Children.Add(thermo);
|
||||
loadsPanel.Children.Add(switches);
|
||||
var thermo = new CheckBox
|
||||
{
|
||||
Content = App.Loc["ThermoAmbient"],
|
||||
IsChecked = d.Coupling.ThermoDynamic,
|
||||
FontSize = 12
|
||||
};
|
||||
thermo.IsCheckedChanged += (_, _) => d.Coupling.ThermoDynamic = thermo.IsChecked == true;
|
||||
|
||||
// Label-less row, so the box indents to the control column with everything
|
||||
// else instead of starting at the panel's left edge on its own.
|
||||
rows.Children.Add(Row(content: thermo));
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
private static TextBlock UnitScopeHint(string text) => new()
|
||||
private bool IsPassengerVehicle(ConsistItem item)
|
||||
{
|
||||
var lead = _consist.ActiveCar(item);
|
||||
var phys = _info.PhysicsFor(lead);
|
||||
if (phys == null)
|
||||
return false;
|
||||
return phys.LoadAccepted.Split(',', ';')
|
||||
.Select(s => s.Trim())
|
||||
.Where(s => s.Length > 0)
|
||||
.Any(s => s.Equals("passengers", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The one row shape every detail tab uses. The label column negotiates its width
|
||||
/// through the shared-size group on the host panel, so all rows in a panel line up
|
||||
/// without anyone naming a pixel count - see the Compact styles in Settings.axaml.
|
||||
/// Omitting <paramref name="content"/> makes it a read-only line; omitting
|
||||
/// <paramref name="label"/> indents a bare widget to the control column.
|
||||
/// </summary>
|
||||
private static SettingRow Row(string? label = null, Control? content = null,
|
||||
string? value = null, string? description = null,
|
||||
string? tip = null)
|
||||
{
|
||||
var row = new SettingRow
|
||||
{
|
||||
Label = label,
|
||||
Description = description,
|
||||
ValueText = value,
|
||||
Content = content
|
||||
};
|
||||
row.Classes.Add("Compact");
|
||||
if (!string.IsNullOrWhiteSpace(tip))
|
||||
ToolTip.SetTip(row, tip);
|
||||
return row;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A read-only label/value line. The dash keeps the row from collapsing when a
|
||||
/// vehicle has no reading for the field.
|
||||
/// </summary>
|
||||
private static SettingRow Reading(string label, string? value)
|
||||
{
|
||||
bool empty = string.IsNullOrWhiteSpace(value);
|
||||
return Row(label, value: empty ? "\u2013" : value, tip: empty ? null : value);
|
||||
}
|
||||
|
||||
private static SettingsSection Section(string header, Control content, Control? meta = null)
|
||||
{
|
||||
var section = new SettingsSection
|
||||
{
|
||||
Header = header,
|
||||
Content = content,
|
||||
// Passive text only: Toggle sits outside the logical tree, so nothing put
|
||||
// here can be found later with GetLogicalDescendants.
|
||||
Toggle = meta
|
||||
};
|
||||
section.Classes.Add("Compact");
|
||||
return section;
|
||||
}
|
||||
|
||||
private static StackPanel Stack(params Control[] children)
|
||||
{
|
||||
var panel = new StackPanel { Spacing = 3 };
|
||||
foreach (var c in children)
|
||||
panel.Children.Add(c);
|
||||
return panel;
|
||||
}
|
||||
|
||||
private static readonly IBrush DimBrush = new SolidColorBrush(Color.Parse("#9098A0"));
|
||||
|
||||
/// <summary>
|
||||
/// A note that governs a whole panel or section rather than one row - same look as a
|
||||
/// row's description line. Replaces the old DetailHint/UnitScopeHint pair, which
|
||||
/// differed only by a 2 px margin.
|
||||
/// </summary>
|
||||
private static TextBlock PanelNote(string text) => new()
|
||||
{
|
||||
Text = text,
|
||||
FontSize = 11,
|
||||
Opacity = 0.6,
|
||||
FontSize = 12,
|
||||
Foreground = DimBrush,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Margin = new Thickness(0, 0, 0, 2)
|
||||
Margin = new Thickness(0, 0, 0, 8)
|
||||
};
|
||||
|
||||
private static TextBlock DetailHint(string text) => new()
|
||||
{
|
||||
Text = text, Opacity = 0.6, FontSize = 12, TextWrapping = TextWrapping.Wrap
|
||||
};
|
||||
|
||||
private static Control LabeledRow(string label, Control control, double labelWidth = 120)
|
||||
{
|
||||
var grid = new Grid { ColumnDefinitions = new ColumnDefinitions("Auto,8,*") };
|
||||
|
||||
var caption = new TextBlock
|
||||
{
|
||||
Text = label,
|
||||
FontSize = 12,
|
||||
MinWidth = labelWidth,
|
||||
TextAlignment = TextAlignment.Right,
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
};
|
||||
|
||||
control.HorizontalAlignment = HorizontalAlignment.Stretch;
|
||||
Grid.SetColumn(control, 2);
|
||||
grid.Children.Add(caption);
|
||||
grid.Children.Add(control);
|
||||
return grid;
|
||||
}
|
||||
|
||||
private static void AddOption(ComboBox combo, string content, string? tag)
|
||||
=> combo.Items.Add(new ComboBoxItem { Content = content, Tag = tag });
|
||||
|
||||
@@ -402,9 +454,9 @@ public sealed class VehicleDetails
|
||||
switchCombo.SelectionChanged += (_, _) => Apply();
|
||||
loadCombo.SelectionChanged += (_, _) => Apply();
|
||||
|
||||
brakesPanel.Children.Add(LabeledRow(App.Loc["BrakeMode"], modeCombo, labelWidth: 96));
|
||||
brakesPanel.Children.Add(LabeledRow(App.Loc["BrakeSwitch"], switchCombo, labelWidth: 96));
|
||||
brakesPanel.Children.Add(LabeledRow(App.Loc["BrakeLoad"], loadCombo, labelWidth: 96));
|
||||
brakesPanel.Children.Add(Row(App.Loc["BrakeMode"], modeCombo));
|
||||
brakesPanel.Children.Add(Row(App.Loc["BrakeSwitch"], switchCombo));
|
||||
brakesPanel.Children.Add(Row(App.Loc["BrakeLoad"], loadCombo));
|
||||
}
|
||||
|
||||
private static string BrakeModeLabel(string? code) => code switch
|
||||
@@ -413,7 +465,6 @@ public sealed class VehicleDetails
|
||||
"P" => App.Loc["BrakePassenger"],
|
||||
"R" => App.Loc["BrakeExpress"],
|
||||
"M" => App.Loc["BrakeExpressMg"],
|
||||
"Q" => App.Loc["BrakeNoAir"],
|
||||
_ => App.Loc["BrakeActingNone"]
|
||||
};
|
||||
|
||||
@@ -427,8 +478,12 @@ public sealed class VehicleDetails
|
||||
|
||||
private static string SwitchLabel(string? code) => code switch
|
||||
{
|
||||
"<>" => App.Loc["SwitchEmergencyTest"],
|
||||
"0" => App.Loc["SwitchOff"],
|
||||
"1" => App.Loc["SwitchOff10"],
|
||||
"X" => App.Loc["SwitchAgonal"],
|
||||
"E" => App.Loc["SwitchEmptied"],
|
||||
"Q" => App.Loc["BrakeNoAir"],
|
||||
_ => App.Loc["SwitchNormal"]
|
||||
};
|
||||
|
||||
@@ -439,7 +494,7 @@ public sealed class VehicleDetails
|
||||
NumericUpDown Spin(int value, int max) => new()
|
||||
{
|
||||
FontSize = 12, Minimum = 0, Maximum = max, Increment = 1,
|
||||
Value = value, MinWidth = 0, Width = 100, FormatString = "0"
|
||||
Value = value, MinWidth = 0, FormatString = "0"
|
||||
};
|
||||
|
||||
var sway = Spin(w.Sway, 100);
|
||||
@@ -465,84 +520,100 @@ public sealed class VehicleDetails
|
||||
flatRand.ValueChanged += (_, _) => Apply();
|
||||
flatProb.ValueChanged += (_, _) => Apply();
|
||||
|
||||
var grid = new Grid
|
||||
{
|
||||
ColumnDefinitions = new ColumnDefinitions("*,8,Auto"),
|
||||
RowSpacing = 6
|
||||
};
|
||||
void Row(string label, Control control)
|
||||
{
|
||||
int r = grid.RowDefinitions.Count;
|
||||
grid.RowDefinitions.Add(new RowDefinition(GridLength.Auto));
|
||||
|
||||
var caption = new TextBlock
|
||||
{
|
||||
Text = label, FontSize = 12,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
TextAlignment = TextAlignment.Right,
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
};
|
||||
Grid.SetRow(caption, r);
|
||||
Grid.SetColumn(caption, 0);
|
||||
|
||||
control.HorizontalAlignment = HorizontalAlignment.Right;
|
||||
Grid.SetRow(control, r);
|
||||
Grid.SetColumn(control, 2);
|
||||
|
||||
grid.Children.Add(caption);
|
||||
grid.Children.Add(control);
|
||||
}
|
||||
|
||||
Row(App.Loc["DamageSway"], sway);
|
||||
Row(App.Loc["DamageFlatness"], flat);
|
||||
Row(App.Loc["DamageFlatnessRand"], flatRand);
|
||||
Row(App.Loc["DamageFlatnessProb"], flatProb);
|
||||
damagePanel.Children.Add(grid);
|
||||
damagePanel.Children.Add(Row(App.Loc["DamageSway"], sway));
|
||||
damagePanel.Children.Add(Row(App.Loc["DamageFlatness"], flat));
|
||||
damagePanel.Children.Add(Row(App.Loc["DamageFlatnessRand"], flatRand));
|
||||
damagePanel.Children.Add(Row(App.Loc["DamageFlatnessProb"], flatProb));
|
||||
}
|
||||
|
||||
private Control BuildConsistLoadTools()
|
||||
/// <summary>
|
||||
/// The three actions that fill the selected vehicle from its neighbours. They keep
|
||||
/// the primary (filled) look because they act on what you have selected.
|
||||
/// </summary>
|
||||
private Control BuildVehicleLoadTools(ConsistItem sel)
|
||||
{
|
||||
var panel = new StackPanel { Spacing = 4, Margin = new Thickness(0, 0, 0, 4) };
|
||||
var row = new WrapPanel { Orientation = Orientation.Horizontal };
|
||||
row.Children.Add(LoadToolButton(App.Loc["ConsistRandomType"],
|
||||
() => { _cargo.RandomTypes(_consist); _redraw(); }));
|
||||
row.Children.Add(LoadToolButton(App.Loc["ConsistMaxAmount"],
|
||||
() => { _cargo.MaxAmounts(_consist); _redraw(); }));
|
||||
row.Children.Add(LoadToolButton(App.Loc["ConsistRandomAmount"],
|
||||
() => { _cargo.RandomAmounts(_consist); _redraw(); }));
|
||||
panel.Children.Add(row);
|
||||
return panel;
|
||||
bool loadable = _cargo.IsLoadable(sel);
|
||||
|
||||
var prev = LoadToolButton(App.Loc["LoadPrevShort"],
|
||||
() => { if (_consist.Selected is { } s) { _cargo.CopyFromPrevious(_consist, s); _redraw(); } });
|
||||
prev.IsEnabled = loadable && _consist.IndexOf(sel) > 0;
|
||||
ToolTip.SetTip(prev, App.Loc["LoadCopyPrev"]);
|
||||
|
||||
var max = LoadToolButton(App.Loc["LoadMaxShort"],
|
||||
() => { if (_consist.Selected is { } s) { _cargo.FillUnit(s); _redraw(); } });
|
||||
max.IsEnabled = loadable;
|
||||
ToolTip.SetTip(max, App.Loc["LoadMax"]);
|
||||
|
||||
var toAll = LoadToolButton(App.Loc["LoadToAllShort"],
|
||||
() => { if (_consist.Selected is { } s) { _cargo.CopyToFollowing(_consist, s); _redraw(); } });
|
||||
toAll.IsEnabled = loadable;
|
||||
ToolTip.SetTip(toAll, App.Loc["LoadCopyToAll"]);
|
||||
|
||||
return Stack(prev, max, toAll);
|
||||
}
|
||||
|
||||
private Button LoadToolButton(string text, Action onClick)
|
||||
/// <summary>
|
||||
/// The whole-consist actions. Secondary styling plus a header that names the scope
|
||||
/// and counts the vehicles, because these reach far past the selected card. No
|
||||
/// tooltips: theirs used to repeat the button label word for word.
|
||||
/// </summary>
|
||||
private void AddConsistLoadSection()
|
||||
{
|
||||
if (_consist.Count == 0)
|
||||
return;
|
||||
|
||||
var buttons = Stack(
|
||||
LoadToolButton(App.Loc["ConsistRandomType"],
|
||||
() => { _cargo.RandomTypes(_consist); _redraw(); }, secondary: true),
|
||||
LoadToolButton(App.Loc["ConsistMaxAmount"],
|
||||
() => { _cargo.MaxAmounts(_consist); _redraw(); }, secondary: true),
|
||||
LoadToolButton(App.Loc["ConsistRandomAmount"],
|
||||
() => { _cargo.RandomAmounts(_consist); _redraw(); }, secondary: true));
|
||||
|
||||
var count = new TextBlock
|
||||
{
|
||||
Text = $"{App.Loc["VehicleCount"]}: {_consist.Count}",
|
||||
FontSize = 12,
|
||||
Foreground = DimBrush,
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
};
|
||||
|
||||
loadsPanel.Children.Add(Section(App.Loc["ConsistLoadTools"], buttons, count));
|
||||
}
|
||||
|
||||
private Button LoadToolButton(string text, Action onClick, bool secondary = false)
|
||||
{
|
||||
var b = new Button
|
||||
{
|
||||
Content = text, FontSize = 11, Margin = new Thickness(0, 0, 4, 4),
|
||||
Padding = new Thickness(8, 3), Cursor = _hand,
|
||||
Content = text, FontSize = 12,
|
||||
Padding = new Thickness(6, 2), Cursor = _hand,
|
||||
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||||
HorizontalContentAlignment = HorizontalAlignment.Center,
|
||||
IsEnabled = _consist.Count > 0
|
||||
};
|
||||
b.Classes.Add("Flat");
|
||||
b.Classes.Add(secondary ? "Outline" : "Flat");
|
||||
b.Click += (_, _) => onClick();
|
||||
return b;
|
||||
}
|
||||
|
||||
private void BuildLoadsEditor(ConsistItem item)
|
||||
/// <summary>
|
||||
/// What the selected vehicle carries, and the actions that fill it. A vehicle that
|
||||
/// takes no cargo gets the note alone - previously the three actions stayed behind,
|
||||
/// greyed out and useless.
|
||||
/// </summary>
|
||||
private Control BuildLoadSection(ConsistItem item)
|
||||
{
|
||||
var lead = _consist.ActiveCar(item);
|
||||
var available = _cargo.AcceptedTypes(lead);
|
||||
|
||||
if (available.Count == 0 && string.IsNullOrEmpty(lead.LoadType))
|
||||
{
|
||||
loadsPanel.Children.Add(DetailHint(App.Loc["LoadNotLoadable"]));
|
||||
return;
|
||||
}
|
||||
return PanelNote(App.Loc["LoadNotLoadable"]);
|
||||
|
||||
if (!string.IsNullOrEmpty(lead.LoadType) &&
|
||||
!available.Contains(lead.LoadType!, StringComparer.OrdinalIgnoreCase))
|
||||
available.Insert(0, lead.LoadType!);
|
||||
|
||||
var typeCombo = new ComboBox { FontSize = 12, MinWidth = 0 };
|
||||
var typeCombo = new ComboBox { FontSize = 12, MinWidth = 120 };
|
||||
typeCombo.Items.Add(new ComboBoxItem { Content = App.Loc["None"], Tag = null });
|
||||
foreach (var name in available)
|
||||
typeCombo.Items.Add(new ComboBoxItem { Content = LoadWeights.Describe(name), Tag = name });
|
||||
@@ -557,41 +628,129 @@ public sealed class VehicleDetails
|
||||
break;
|
||||
}
|
||||
|
||||
string? SelectedType() => (typeCombo.SelectedItem as ComboBoxItem)?.Tag as string;
|
||||
bool IsPant() => Dynamic.IsPantStateType(SelectedType());
|
||||
|
||||
int limit = _cargo.LimitFor(lead);
|
||||
var countBox = new NumericUpDown
|
||||
{
|
||||
FontSize = 12, Minimum = 0, Maximum = limit, Increment = 1,
|
||||
Value = Math.Min(lead.LoadCount, limit), MinWidth = 120, FormatString = "0"
|
||||
Value = Math.Min(lead.LoadCount, limit), FormatString = "0",
|
||||
HorizontalAlignment = HorizontalAlignment.Stretch
|
||||
};
|
||||
|
||||
var pantHint = DetailHint(App.Loc["LoadPantStateHint"]);
|
||||
pantHint.IsVisible = lead.IsPantState;
|
||||
var pantStateCombo = new ComboBox
|
||||
{
|
||||
FontSize = 12, MinWidth = 0,
|
||||
HorizontalAlignment = HorizontalAlignment.Stretch
|
||||
};
|
||||
pantStateCombo.Items.Add(new ComboBoxItem { Content = App.Loc["PantStateNone"], Tag = 0 });
|
||||
pantStateCombo.Items.Add(new ComboBoxItem { Content = "A", Tag = 1 });
|
||||
pantStateCombo.Items.Add(new ComboBoxItem { Content = "B", Tag = 2 });
|
||||
pantStateCombo.Items.Add(new ComboBoxItem { Content = App.Loc["PantStateBoth"], Tag = 3 });
|
||||
|
||||
string? SelectedType() => (typeCombo.SelectedItem as ComboBoxItem)?.Tag as string;
|
||||
var countContainer = new Panel();
|
||||
countContainer.Children.Add(countBox);
|
||||
countContainer.Children.Add(pantStateCombo);
|
||||
|
||||
string UnitText()
|
||||
{
|
||||
var phys = _info.PhysicsFor(lead);
|
||||
if (phys == null) return App.Loc["LoadUnitGeneric"];
|
||||
if (string.Equals(phys.LoadQ, "pieces", StringComparison.OrdinalIgnoreCase))
|
||||
return App.Loc["LoadUnitPieces"];
|
||||
if (string.Equals(phys.LoadQ, "tonns", StringComparison.OrdinalIgnoreCase))
|
||||
return App.Loc["LoadUnitTons"];
|
||||
return App.Loc["LoadUnitGeneric"];
|
||||
}
|
||||
|
||||
var countRow = Row(App.Loc["LoadCount"], countContainer);
|
||||
|
||||
void SyncPantStateFromCount()
|
||||
{
|
||||
int value = Math.Clamp((int)(countBox.Value ?? 0), 0, Dynamic.PantStateMax);
|
||||
foreach (var obj in pantStateCombo.Items)
|
||||
if (obj is ComboBoxItem ci && ci.Tag is int t && t == value)
|
||||
{
|
||||
pantStateCombo.SelectedItem = ci;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void SyncCountFromPantState()
|
||||
{
|
||||
int value = pantStateCombo.SelectedItem is ComboBoxItem { Tag: int t } ? t : 0;
|
||||
countBox.Value = Math.Clamp(value, 0, countBox.Maximum);
|
||||
}
|
||||
|
||||
void UpdateVisibility()
|
||||
{
|
||||
bool pant = IsPant();
|
||||
countBox.IsVisible = !pant;
|
||||
pantStateCombo.IsVisible = pant;
|
||||
// The unit slot is reserved by the Compact style even when empty, so the
|
||||
// spinner does not jump sideways as the unit comes and goes.
|
||||
countRow.Label = pant ? App.Loc["Pantograph"] : App.Loc["LoadCount"];
|
||||
countRow.ValueText = pant ? null : UnitText();
|
||||
// The "set an amount above 0" advice is only true while the amount is 0;
|
||||
// past that it is a line of noise in a panel with no height to spare.
|
||||
countRow.Description = pant ? App.Loc["LoadPantStateHint"]
|
||||
: GetCount() == 0 ? App.Loc["LoadHint"]
|
||||
: null;
|
||||
}
|
||||
|
||||
int GetCount()
|
||||
{
|
||||
if (IsPant())
|
||||
return pantStateCombo.SelectedItem is ComboBoxItem { Tag: int t } ? t : 0;
|
||||
return (int)(countBox.Value ?? 0);
|
||||
}
|
||||
|
||||
void Apply()
|
||||
{
|
||||
string? type = SelectedType();
|
||||
int count = (int)(countBox.Value ?? 0);
|
||||
int count = GetCount();
|
||||
foreach (var c in _consist.MemberTargets(item))
|
||||
{
|
||||
|
||||
c.LoadType = string.IsNullOrEmpty(type) ? null : type;
|
||||
c.LoadCount = string.IsNullOrEmpty(type) ? 0 : Math.Min(count, _cargo.LimitFor(c, type));
|
||||
}
|
||||
}
|
||||
|
||||
// Seed the pantograph combo from the model before any handler is listening.
|
||||
SyncPantStateFromCount();
|
||||
bool wasPant = IsPant();
|
||||
|
||||
typeCombo.SelectionChanged += (_, _) =>
|
||||
{
|
||||
string? type = SelectedType();
|
||||
pantHint.IsVisible = Dynamic.IsPantStateType(type);
|
||||
countBox.Maximum = _cargo.LimitFor(lead, type);
|
||||
countBox.Maximum = _cargo.LimitFor(lead, SelectedType());
|
||||
|
||||
// Both widgets edit the same LoadCount, so the value is carried across only
|
||||
// when the mode actually flips. Doing it unconditionally - as UpdateVisibility
|
||||
// used to - reset the amount to zero on every rebuild, which is why setting
|
||||
// the maximum load appeared to do nothing: the model was written, the panel
|
||||
// refreshed, and the refresh immediately wrote zero back.
|
||||
bool pant = IsPant();
|
||||
if (pant != wasPant)
|
||||
{
|
||||
if (pant)
|
||||
SyncPantStateFromCount();
|
||||
else
|
||||
SyncCountFromPantState();
|
||||
wasPant = pant;
|
||||
}
|
||||
|
||||
UpdateVisibility();
|
||||
Apply();
|
||||
};
|
||||
countBox.ValueChanged += (_, _) => Apply();
|
||||
pantStateCombo.SelectionChanged += (_, _) => Apply();
|
||||
countBox.ValueChanged += (_, _) => { Apply(); UpdateVisibility(); };
|
||||
|
||||
loadsPanel.Children.Add(LabeledRow(App.Loc["LoadType"], typeCombo));
|
||||
loadsPanel.Children.Add(LabeledRow(App.Loc["LoadCount"], countBox));
|
||||
loadsPanel.Children.Add(pantHint);
|
||||
UpdateVisibility();
|
||||
|
||||
return Stack(
|
||||
Row(App.Loc["LoadType"], typeCombo),
|
||||
countRow,
|
||||
BuildVehicleLoadTools(item));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition Height="150" />
|
||||
<!-- Measures itself, so a whole card fits at either thumbnail size. -->
|
||||
<RowDefinition Height="Auto" MinHeight="120" MaxHeight="200" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<HeaderedContentControl Grid.Column="0" Grid.Row="0" Theme="{StaticResource GroupBox}"
|
||||
@@ -23,7 +24,7 @@
|
||||
Header="{Binding [Sceneries], Source={x:Static starterNg:App.Loc}}">
|
||||
<DockPanel LastChildFill="True">
|
||||
<CheckBox DockPanel.Dock="Bottom" Name="archivalSwitch"
|
||||
Margin="2,4,0,0" FontSize="11"
|
||||
Margin="2,4,0,0" FontSize="12"
|
||||
IsCheckedChanged="ArchivalSwitch_OnChanged"
|
||||
Content="{Binding [HideArchivalSceneries], Source={x:Static starterNg:App.Loc}}" />
|
||||
<TreeView Name="sceneryList" Classes="Compact"
|
||||
@@ -54,7 +55,7 @@
|
||||
Header="{Binding [SceneryConsists], Source={x:Static starterNg:App.Loc}}">
|
||||
<HeaderedContentControl.Tag>
|
||||
<Button Name="randomTexturesButton" Classes="Flat" Cursor="Hand"
|
||||
FontSize="11" MinHeight="0" Padding="10,1"
|
||||
FontSize="12" MinHeight="0" Padding="10,1"
|
||||
Click="RandomTexturesButton_OnClick"
|
||||
Content="{Binding [RandomizeTextures], Source={x:Static starterNg:App.Loc}}"
|
||||
ToolTip.Tip="{Binding [TipRandomizeTextures], Source={x:Static starterNg:App.Loc}}" />
|
||||
@@ -144,9 +145,9 @@
|
||||
VerticalAlignment="Center" TextAlignment="Right"
|
||||
FontWeight="SemiBold" />
|
||||
<DockPanel Grid.Row="5" Grid.Column="2" Margin="0,-6,0,0">
|
||||
<TextBlock DockPanel.Dock="Right" FontSize="11"
|
||||
<TextBlock DockPanel.Dock="Right" FontSize="12"
|
||||
Foreground="{StaticResource CarbonFgDim}" Text="+50 °C" />
|
||||
<TextBlock FontSize="11"
|
||||
<TextBlock FontSize="12"
|
||||
Foreground="{StaticResource CarbonFgDim}" Text="-50 °C" />
|
||||
</DockPanel>
|
||||
|
||||
@@ -161,10 +162,10 @@
|
||||
VerticalAlignment="Center" TextAlignment="Right"
|
||||
FontWeight="SemiBold" />
|
||||
<DockPanel Grid.Row="8" Grid.Column="2" Margin="0,-6,0,0">
|
||||
<TextBlock DockPanel.Dock="Right" FontSize="11"
|
||||
<TextBlock DockPanel.Dock="Right" FontSize="12"
|
||||
Foreground="{StaticResource CarbonFgDim}"
|
||||
Text="{Binding [VisibilityGood], Source={x:Static starterNg:App.Loc}}" />
|
||||
<TextBlock FontSize="11" Foreground="{StaticResource CarbonFgDim}"
|
||||
<TextBlock FontSize="12" Foreground="{StaticResource CarbonFgDim}"
|
||||
Text="{Binding [VisibilityBad], Source={x:Static starterNg:App.Loc}}" />
|
||||
</DockPanel>
|
||||
|
||||
@@ -217,9 +218,13 @@
|
||||
|
||||
<HeaderedContentControl Grid.Row="1" Grid.ColumnSpan="3" Theme="{StaticResource GroupBox}"
|
||||
Header="{Binding [Consist], Source={x:Static starterNg:App.Loc}}">
|
||||
<!-- Mass, length and the rest ride in the title bar, same as in the
|
||||
depot, instead of taking a row of their own above the cards. -->
|
||||
<HeaderedContentControl.Tag>
|
||||
<StackPanel Name="trainStats" Orientation="Horizontal"
|
||||
VerticalAlignment="Center" />
|
||||
</HeaderedContentControl.Tag>
|
||||
<DockPanel LastChildFill="True">
|
||||
<StackPanel DockPanel.Dock="Top" Name="trainStats" Orientation="Horizontal"
|
||||
VerticalAlignment="Center" Margin="0,0,0,6" />
|
||||
<ScrollViewer Name="consistScroll"
|
||||
HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
|
||||
<Panel>
|
||||
|
||||
@@ -896,13 +896,13 @@ public partial class Scenarios : UserControl
|
||||
|
||||
var bmp = LoadMini(miniName, thumbH);
|
||||
Control visual = bmp is not null
|
||||
? new Image
|
||||
? MiniTextures.Sharp(new Image
|
||||
{
|
||||
Source = bmp,
|
||||
Height = thumbH,
|
||||
Stretch = Avalonia.Media.Stretch.Uniform,
|
||||
Margin = new Thickness(0)
|
||||
}
|
||||
})
|
||||
: MissingMiniBox(train.SkinFile, thumbH);
|
||||
|
||||
bool selected = !string.IsNullOrEmpty(train.Name) &&
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,15 +3,19 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.LogicalTree;
|
||||
using Avalonia.Markup.Xaml.Styling;
|
||||
using Avalonia.Media;
|
||||
using StarterNG.Classes;
|
||||
using StarterNG.Controls;
|
||||
using StarterNG.Infrastructure;
|
||||
using StarterNG.Services;
|
||||
|
||||
using KeyBinding = StarterNG.Classes.KeyBinding;
|
||||
@@ -22,6 +26,15 @@ public partial class Settings : UserControl
|
||||
{
|
||||
private bool _loading;
|
||||
|
||||
/// <summary>
|
||||
/// Raised once a saved (or applied) setting changes something the other tabs
|
||||
/// have already drawn, so they can redraw instead of waiting for a restart.
|
||||
/// </summary>
|
||||
public event Action? ThumbnailSizeChanged;
|
||||
|
||||
/// <summary>Thumbnail size the other tabs were last drawn at.</summary>
|
||||
private bool _drawnThumbs = StarterNG.Classes.Settings.Instance.LargeThumbnails;
|
||||
|
||||
public Settings()
|
||||
{
|
||||
InitializeComponent();
|
||||
@@ -41,6 +54,7 @@ public partial class Settings : UserControl
|
||||
|
||||
App.Loc.LanguageChanged += SelectActiveLanguage;
|
||||
App.Loc.LanguageChanged += UpdateQualityCaptions;
|
||||
App.Loc.LanguageChanged += UpdateSaveState;
|
||||
App.Loc.LanguageChanged += () => shaderResolutionSlider_OnValueChanged(null, null);
|
||||
|
||||
FeedbackCb.SelectionChanged += FeedbackCb_OnSelectionChanged;
|
||||
@@ -54,6 +68,10 @@ public partial class Settings : UserControl
|
||||
KeyboardConfig.Instance.Load();
|
||||
BuildControlsTab();
|
||||
AddHandler(KeyDownEvent, OnControlsKeyDown, RoutingStrategies.Tunnel);
|
||||
|
||||
HookDirtyTracking();
|
||||
ApplyFilter();
|
||||
UpdateSaveState();
|
||||
}
|
||||
|
||||
private static int IndexOfTag(ComboBox cb, string? tag)
|
||||
@@ -95,6 +113,7 @@ public partial class Settings : UserControl
|
||||
if (!File.Exists(path)) return;
|
||||
StarterNG.Classes.Settings.Instance.LoadFrom(path);
|
||||
ApplyToUi();
|
||||
RedrawThumbnailsIfNeeded();
|
||||
}
|
||||
|
||||
private void ProfileSaveAsButton_OnClick(object? sender, RoutedEventArgs e)
|
||||
@@ -232,10 +251,9 @@ public partial class Settings : UserControl
|
||||
VehiclesVolumeSlider.Value = s.VehiclesVolume;
|
||||
PositionalVolumeSlider.Value = s.PositionalVolume;
|
||||
AmbientVolumeSlider.Value = s.AmbientVolume;
|
||||
VolumePausedSlider.Value = s.VolumePaused;
|
||||
SkipPipelineCb.IsChecked = s.SkipPipeline;
|
||||
DebugLogVisibleCb.IsChecked = s.DebugLogVisible;
|
||||
PyScreenPriorityCb.SelectedIndex = Math.Max(0, IndexOfTag(PyScreenPriorityCb, s.PyScreenPriority));
|
||||
PyScreenPriorityCb.SelectedIndex = Math.Max(0, IndexOfTag(PyScreenPriorityCb, s.PythonScreenUpdateRate.ToString(CultureInfo.InvariantCulture)));
|
||||
|
||||
AutoCloseStarterCb.IsChecked = s.AutoCloseStarter;
|
||||
LargeThumbnailsCb.IsChecked = s.LargeThumbnails;
|
||||
@@ -327,10 +345,9 @@ public partial class Settings : UserControl
|
||||
s.VehiclesVolume = (int)VehiclesVolumeSlider.Value;
|
||||
s.PositionalVolume = (int)PositionalVolumeSlider.Value;
|
||||
s.AmbientVolume = (int)AmbientVolumeSlider.Value;
|
||||
s.VolumePaused = (int)VolumePausedSlider.Value;
|
||||
s.SkipPipeline = IsChecked(SkipPipelineCb);
|
||||
s.DebugLogVisible = IsChecked(DebugLogVisibleCb);
|
||||
s.PyScreenPriority = (PyScreenPriorityCb.SelectedItem as ComboBoxItem)?.Tag as string ?? "normal";
|
||||
s.PythonScreenUpdateRate = ParsePriorityTag((PyScreenPriorityCb.SelectedItem as ComboBoxItem)?.Tag);
|
||||
|
||||
s.AutoCloseStarter = IsChecked(AutoCloseStarterCb);
|
||||
s.LargeThumbnails = IsChecked(LargeThumbnailsCb);
|
||||
@@ -344,31 +361,39 @@ public partial class Settings : UserControl
|
||||
new UartWindow().ShowDialog(owner);
|
||||
}
|
||||
|
||||
private void AboutButton_OnClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (TopLevel.GetTopLevel(this) is Window owner)
|
||||
AboutWindow.Create().ShowDialog(owner);
|
||||
}
|
||||
|
||||
private void SaveButton_OnClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
ReadFromUi();
|
||||
StarterNG.Classes.Settings.Instance.Save();
|
||||
KeyboardConfig.Instance.Save();
|
||||
RedrawThumbnailsIfNeeded();
|
||||
_dirty = false;
|
||||
UpdateSaveState();
|
||||
if (SaveStatus is not null)
|
||||
SaveStatus.Text = App.Loc["SettingsSaved"];
|
||||
}
|
||||
|
||||
private void RedrawThumbnailsIfNeeded()
|
||||
{
|
||||
bool large = StarterNG.Classes.Settings.Instance.LargeThumbnails;
|
||||
if (large == _drawnThumbs)
|
||||
return;
|
||||
|
||||
_drawnThumbs = large;
|
||||
ThumbnailSizeChanged?.Invoke();
|
||||
}
|
||||
|
||||
private void ResetButton_OnClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
StarterNG.Classes.Settings.Instance.Load();
|
||||
ApplyToUi();
|
||||
RedrawThumbnailsIfNeeded();
|
||||
CancelCapture();
|
||||
KeyboardConfig.Instance.Load();
|
||||
RebuildBindingList();
|
||||
RebuildKeyboard();
|
||||
if (SaveStatus is not null)
|
||||
SaveStatus.Text = string.Empty;
|
||||
_dirty = false;
|
||||
UpdateSaveState();
|
||||
}
|
||||
|
||||
private void SelectResolution(int width, int height)
|
||||
@@ -404,29 +429,29 @@ public partial class Settings : UserControl
|
||||
|
||||
private void TextureResolutionSlider_OnValueChanged(object? sender, RangeBaseValueChangedEventArgs e)
|
||||
{
|
||||
if (texResolution is null || textureResolutionSlider is null)
|
||||
if (TexResolutionRow is null || textureResolutionSlider is null)
|
||||
return;
|
||||
|
||||
int resolution = 1 << (int)textureResolutionSlider.Value;
|
||||
texResolution.Text = $"{resolution} px";
|
||||
TexResolutionRow.ValueText = $"{resolution} px";
|
||||
}
|
||||
|
||||
private void CabTextureResolutionSlider_OnValueChanged(object? sender, RangeBaseValueChangedEventArgs e)
|
||||
{
|
||||
if (cabTexResolution is null || cabTextureResolutionSlider is null)
|
||||
if (CabTexResolutionRow is null || cabTextureResolutionSlider is null)
|
||||
return;
|
||||
|
||||
int resolution = 1 << (int)cabTextureResolutionSlider.Value;
|
||||
cabTexResolution.Text = $"{resolution} px";
|
||||
CabTexResolutionRow.ValueText = $"{resolution} px";
|
||||
}
|
||||
|
||||
private void shaderResolutionSlider_OnValueChanged(object? sender, RangeBaseValueChangedEventArgs e)
|
||||
{
|
||||
if (shaderResolution is null || shaderResolutionSlider is null)
|
||||
if (ShadowResolutionRow is null || shaderResolutionSlider is null)
|
||||
return;
|
||||
|
||||
int resolution = 1 << (int)shaderResolutionSlider.Value;
|
||||
shaderResolution.Text = $"{resolution} px";
|
||||
ShadowResolutionRow.ValueText = $"{resolution} px";
|
||||
ToolTip.SetTip(shaderResolutionSlider, ShadowMapWord(resolution));
|
||||
}
|
||||
|
||||
@@ -464,16 +489,196 @@ public partial class Settings : UserControl
|
||||
private void FeedbackCb_OnSelectionChanged(object? sender, SelectionChangedEventArgs e) =>
|
||||
UpdateFeedbackDetailVisibility();
|
||||
|
||||
private void UpdateFeedbackDetailVisibility()
|
||||
/// <summary>
|
||||
/// The port and UART rows only apply to some feedback modes. Visibility is
|
||||
/// decided in one place — <see cref="ApplyFilter"/> — so that a search cannot
|
||||
/// reveal a row the selected mode does not use.
|
||||
/// </summary>
|
||||
private void UpdateFeedbackDetailVisibility() => ApplyFilter();
|
||||
|
||||
private bool ConditionAllows(SettingRow row)
|
||||
{
|
||||
FeedbackPortRow.IsVisible = FeedbackCb.SelectedIndex == 3;
|
||||
ComButton.IsVisible = FeedbackCb.SelectedIndex == 5;
|
||||
if (ReferenceEquals(row, FeedbackPortRow))
|
||||
return FeedbackCb.SelectedIndex == 3;
|
||||
if (ReferenceEquals(row, ComButtonRow))
|
||||
return FeedbackCb.SelectedIndex == 5;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── Category navigation, search and unsaved-changes state ────────────────
|
||||
|
||||
private bool _dirty;
|
||||
|
||||
private void CategoryList_OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
// Picking a category is how you leave a search, so the query goes with it.
|
||||
if (SettingsSearch is { Text.Length: > 0 })
|
||||
SettingsSearch.Text = string.Empty; // raises TextChanged -> ApplyFilter
|
||||
else
|
||||
ApplyFilter();
|
||||
}
|
||||
|
||||
private void SettingsSearch_OnTextChanged(object? sender, TextChangedEventArgs e) => ApplyFilter();
|
||||
|
||||
/// <summary>
|
||||
/// Single owner of what is on screen. With an empty query it shows the selected
|
||||
/// category; with a query it stacks every category and keeps only the rows that
|
||||
/// match, collapsing sections and categories that end up empty.
|
||||
/// </summary>
|
||||
private void ApplyFilter()
|
||||
{
|
||||
if (SettingsStack is null || CategoryList is null || ControlsHost is null)
|
||||
return;
|
||||
|
||||
string query = SettingsSearch?.Text?.Trim() ?? string.Empty;
|
||||
bool searching = query.Length > 0;
|
||||
string category = (CategoryList.SelectedItem as ListBoxItem)?.Tag as string ?? string.Empty;
|
||||
|
||||
// Key bindings are their own world: they bring their own scrolling and a
|
||||
// star-sized row, so they replace the settings scroller rather than living
|
||||
// in it, and the search box filters them instead of the settings.
|
||||
bool bindings = category == "controls";
|
||||
ControlsHost.IsVisible = bindings;
|
||||
SettingsScroll.IsVisible = !bindings;
|
||||
|
||||
if (bindings)
|
||||
{
|
||||
RebuildBindingList();
|
||||
foreach (var entry in CategoryList.Items.OfType<ListBoxItem>())
|
||||
entry.Classes.Set("NoHits", false);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var panel in SettingsStack.Children.OfType<StackPanel>())
|
||||
{
|
||||
bool anyRow = false;
|
||||
|
||||
foreach (var section in panel.GetLogicalDescendants().OfType<SettingsSection>())
|
||||
{
|
||||
bool anyInSection = false;
|
||||
|
||||
foreach (var row in section.GetLogicalDescendants().OfType<SettingRow>())
|
||||
{
|
||||
bool visible = ConditionAllows(row) &&
|
||||
(!searching || TextMatch.Contains(RowSearchText(row), query));
|
||||
row.IsVisible = visible;
|
||||
anyInSection |= visible;
|
||||
}
|
||||
|
||||
section.IsVisible = anyInSection;
|
||||
anyRow |= anyInSection;
|
||||
}
|
||||
|
||||
string tag = panel.Tag as string ?? string.Empty;
|
||||
panel.IsVisible = anyRow && (searching || tag == category);
|
||||
|
||||
// The caption only earns its place when several categories are stacked.
|
||||
foreach (var caption in panel.Children.OfType<TextBlock>())
|
||||
caption.IsVisible = searching;
|
||||
|
||||
if (CategoryItem(tag) is { } item)
|
||||
item.Classes.Set("NoHits", searching && !anyRow);
|
||||
}
|
||||
|
||||
if (searching)
|
||||
SettingsScroll.Offset = new Vector(0, 0);
|
||||
}
|
||||
|
||||
private ListBoxItem? CategoryItem(string tag) =>
|
||||
CategoryList.Items.OfType<ListBoxItem>()
|
||||
.FirstOrDefault(i => string.Equals(i.Tag as string, tag, StringComparison.Ordinal));
|
||||
|
||||
/// <summary>
|
||||
/// Everything about a row a user might type: its label, its explanation and
|
||||
/// the text of the control itself (a checkbox caption, a dropdown's options).
|
||||
/// </summary>
|
||||
private static string RowSearchText(SettingRow row)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append(row.Label).Append(' ').Append(row.Description).Append(' ');
|
||||
|
||||
switch (row.Content)
|
||||
{
|
||||
case CheckBox cb:
|
||||
sb.Append(cb.Content);
|
||||
break;
|
||||
case Button btn:
|
||||
sb.Append(btn.Content);
|
||||
break;
|
||||
case ComboBox combo:
|
||||
foreach (var item in combo.Items.OfType<ComboBoxItem>())
|
||||
sb.Append(item.Content).Append(' ');
|
||||
break;
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes every input in the panel to <see cref="MarkDirty"/> once, so the
|
||||
/// Save button can tell the user there is something to save. Section master
|
||||
/// switches hang off SettingsSection.Toggle rather than the logical tree, so
|
||||
/// they are collected separately.
|
||||
/// </summary>
|
||||
private void HookDirtyTracking()
|
||||
{
|
||||
foreach (var control in this.GetLogicalDescendants().OfType<Control>())
|
||||
{
|
||||
switch (control)
|
||||
{
|
||||
// Choosing a profile changes nothing until Apply is pressed.
|
||||
case ComboBox combo when ReferenceEquals(combo, ProfileCb):
|
||||
break;
|
||||
case CheckBox cb:
|
||||
cb.IsCheckedChanged += (_, _) => MarkDirty();
|
||||
break;
|
||||
case Slider slider:
|
||||
slider.ValueChanged += (_, _) => MarkDirty();
|
||||
break;
|
||||
case ComboBox combo:
|
||||
combo.SelectionChanged += (_, _) => MarkDirty();
|
||||
break;
|
||||
case NumericUpDown nud:
|
||||
nud.ValueChanged += (_, _) => MarkDirty();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var section in this.GetLogicalDescendants().OfType<SettingsSection>())
|
||||
if (section.Toggle is CheckBox toggle)
|
||||
toggle.IsCheckedChanged += (_, _) => MarkDirty();
|
||||
}
|
||||
|
||||
private void MarkDirty()
|
||||
{
|
||||
if (_loading || _dirty)
|
||||
return;
|
||||
|
||||
_dirty = true;
|
||||
UpdateSaveState();
|
||||
}
|
||||
|
||||
private void UpdateSaveState()
|
||||
{
|
||||
if (SaveButton is null || SaveStatus is null)
|
||||
return;
|
||||
|
||||
bool dirty = _dirty || KeyboardConfig.Instance.Dirty;
|
||||
SaveButton.Classes.Set("Accent", dirty);
|
||||
SaveStatus.Text = dirty ? App.Loc["UnsavedChanges"] : string.Empty;
|
||||
}
|
||||
|
||||
private static bool IsChecked(CheckBox cb) => cb.IsChecked == true;
|
||||
|
||||
private static int Clamp(int v, int lo, int hi) => v < lo ? lo : (v > hi ? hi : v);
|
||||
|
||||
private static int ParsePriorityTag(object? tag)
|
||||
{
|
||||
if (tag is int i) return i;
|
||||
if (tag is string s && int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out int n)) return n;
|
||||
return 200;
|
||||
}
|
||||
|
||||
private static int Log2(int value, int fallback)
|
||||
{
|
||||
if (value <= 0) return fallback;
|
||||
@@ -490,10 +695,15 @@ public partial class Settings : UserControl
|
||||
return 4;
|
||||
}
|
||||
|
||||
private TextBox? _controlsSearch;
|
||||
private StackPanel? _bindingListPanel;
|
||||
private StackPanel? _keyboardPanel;
|
||||
|
||||
/// <summary>
|
||||
/// True while a cab-control binding is waiting for a keypress. The window's own
|
||||
/// shortcuts stand down for the duration, otherwise Ctrl+N would never reach here.
|
||||
/// </summary>
|
||||
public bool IsCapturingKey => _capturing is not null;
|
||||
|
||||
private KeyBinding? _capturing;
|
||||
private Button? _capturingButton;
|
||||
|
||||
@@ -527,10 +737,6 @@ public partial class Settings : UserControl
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Foreground = FgDimBrush
|
||||
});
|
||||
_controlsSearch = new TextBox { Width = 240, PlaceholderText = App.Loc["Search"] };
|
||||
_controlsSearch.TextChanged += (_, _) => RebuildBindingList();
|
||||
topBar.Children.Add(_controlsSearch);
|
||||
|
||||
var restoreBtn = new Button { Content = App.Loc["RestoreDefaults"] };
|
||||
restoreBtn.Classes.Add("Flat");
|
||||
restoreBtn.Click += (_, _) =>
|
||||
@@ -538,6 +744,7 @@ public partial class Settings : UserControl
|
||||
CancelCapture();
|
||||
KeyboardConfig.Instance.LoadDefaults();
|
||||
KeyboardConfig.Instance.Dirty = true;
|
||||
MarkDirty();
|
||||
RebuildBindingList();
|
||||
RebuildKeyboard();
|
||||
};
|
||||
@@ -576,7 +783,8 @@ public partial class Settings : UserControl
|
||||
return;
|
||||
|
||||
_bindingListPanel.Children.Clear();
|
||||
string filter = _controlsSearch?.Text?.Trim().ToLowerInvariant() ?? string.Empty;
|
||||
// The sidebar search box serves this list too - see ApplyFilter.
|
||||
string filter = SettingsSearch?.Text?.Trim() ?? string.Empty;
|
||||
var conflicts = ComputeConflicts();
|
||||
|
||||
foreach (var b in KeyboardConfig.Instance.Bindings)
|
||||
@@ -588,8 +796,8 @@ public partial class Settings : UserControl
|
||||
}
|
||||
|
||||
private static bool MatchesFilter(KeyBinding b, string filter) =>
|
||||
b.Command.ToLowerInvariant().Contains(filter) ||
|
||||
b.Description.ToLowerInvariant().Contains(filter);
|
||||
TextMatch.Contains(b.Command, filter) ||
|
||||
TextMatch.Contains(b.Description, filter);
|
||||
|
||||
private Control BuildBindingRow(KeyBinding b, HashSet<string> conflicts)
|
||||
{
|
||||
@@ -604,14 +812,14 @@ public partial class Settings : UserControl
|
||||
{
|
||||
Text = CommandLabel(b),
|
||||
Foreground = FgBrush,
|
||||
FontSize = 13,
|
||||
FontSize = 12,
|
||||
TextTrimming = TextTrimming.CharacterEllipsis
|
||||
});
|
||||
label.Children.Add(new TextBlock
|
||||
{
|
||||
Text = b.Command,
|
||||
Foreground = FgDimBrush,
|
||||
FontSize = 11,
|
||||
FontSize = 12,
|
||||
TextTrimming = TextTrimming.CharacterEllipsis
|
||||
});
|
||||
Grid.SetColumn(label, 0);
|
||||
@@ -647,6 +855,7 @@ public partial class Settings : UserControl
|
||||
b.Shift = b.Ctrl = false;
|
||||
b.Key = "none";
|
||||
KeyboardConfig.Instance.Dirty = true;
|
||||
MarkDirty();
|
||||
RebuildBindingList();
|
||||
RebuildKeyboard();
|
||||
};
|
||||
@@ -710,6 +919,7 @@ public partial class Settings : UserControl
|
||||
_capturing.Ctrl = e.KeyModifiers.HasFlag(KeyModifiers.Control);
|
||||
_capturing.Key = token;
|
||||
KeyboardConfig.Instance.Dirty = true;
|
||||
MarkDirty();
|
||||
e.Handled = true;
|
||||
|
||||
_capturing = null;
|
||||
@@ -912,7 +1122,7 @@ public partial class Settings : UserControl
|
||||
{
|
||||
Text = App.Loc["BindKeyHint"],
|
||||
Foreground = FgDimBrush,
|
||||
FontSize = 11,
|
||||
FontSize = 12,
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
});
|
||||
panel.Children.Add(grid);
|
||||
@@ -957,6 +1167,7 @@ public partial class Settings : UserControl
|
||||
}
|
||||
|
||||
KeyboardConfig.Instance.Dirty = true;
|
||||
MarkDirty();
|
||||
}
|
||||
|
||||
private static KeyBinding? CommandInSlot(string token, bool shift, bool ctrl) =>
|
||||
@@ -1074,25 +1285,25 @@ public partial class Settings : UserControl
|
||||
{
|
||||
var q = QualityWords;
|
||||
|
||||
if (texFilteringValue != null)
|
||||
texFilteringValue.Text = q[Clamp((int)TexFilteringSlider.Value, 1, 5) - 1];
|
||||
if (TexFilteringRow != null)
|
||||
TexFilteringRow.ValueText = q[Clamp((int)TexFilteringSlider.Value, 1, 5) - 1];
|
||||
|
||||
string[] four = { q[0], q[1], q[3], q[4] };
|
||||
if (trackCurvesValue != null)
|
||||
trackCurvesValue.Text = four[Clamp((int)TrackCurvesSlider.Value, 1, 4) - 1];
|
||||
if (smokeParticlesValue != null)
|
||||
smokeParticlesValue.Text = four[Clamp((int)SmokeParticlesSlider.Value, 1, 4) - 1];
|
||||
if (TrackCurvesRow != null)
|
||||
TrackCurvesRow.ValueText = four[Clamp((int)TrackCurvesSlider.Value, 1, 4) - 1];
|
||||
if (SmokeParticlesRow != null)
|
||||
SmokeParticlesRow.ValueText = four[Clamp((int)SmokeParticlesSlider.Value, 1, 4) - 1];
|
||||
|
||||
string[] msaa = { App.Loc["MsaaNone"], "x2", "x4", "x8" };
|
||||
if (multisamplingValue != null)
|
||||
multisamplingValue.Text = msaa[Clamp((int)MultisamplingSlider.Value, 1, 4) - 1];
|
||||
if (MultisamplingRow != null)
|
||||
MultisamplingRow.ValueText = msaa[Clamp((int)MultisamplingSlider.Value, 1, 4) - 1];
|
||||
|
||||
string[] range = { App.Loc["RangeNormal"], App.Loc["RangeHigh"], App.Loc["RangeVeryHigh"] };
|
||||
if (renderRangeValue != null)
|
||||
renderRangeValue.Text = range[Clamp((int)RenderRangeSlider.Value, 1, 3) - 1];
|
||||
if (RenderRangeRow != null)
|
||||
RenderRangeRow.ValueText = range[Clamp((int)RenderRangeSlider.Value, 1, 3) - 1];
|
||||
|
||||
if (cursorSensitivityValue != null)
|
||||
cursorSensitivityValue.Text = (int)CursorSensitivitySlider.Value switch
|
||||
if (CursorSensitivityRow != null)
|
||||
CursorSensitivityRow.ValueText = (int)CursorSensitivitySlider.Value switch
|
||||
{
|
||||
<= 1 => q[1],
|
||||
2 => App.Loc["QualityStandard"],
|
||||
@@ -1100,25 +1311,25 @@ public partial class Settings : UserControl
|
||||
_ => q[4]
|
||||
};
|
||||
|
||||
if (shaderRangeValue != null)
|
||||
if (ShadowRangeRow != null)
|
||||
{
|
||||
int metres = (int)shaderRange.Value;
|
||||
shaderRangeValue.Text = $"{metres} m";
|
||||
ShadowRangeRow.ValueText = $"{metres} m";
|
||||
ToolTip.SetTip(shaderRange, ShadowRangeWord(metres));
|
||||
}
|
||||
|
||||
if (cabShadowRangeValue != null)
|
||||
if (CabShadowRangeRow != null)
|
||||
{
|
||||
int metres = (int)cabShaderSourceRange.Value;
|
||||
cabShadowRangeValue.Text = metres <= 0 ? App.Loc["RangeDisabled"] : $"{metres} m";
|
||||
CabShadowRangeRow.ValueText = metres <= 0 ? App.Loc["RangeDisabled"] : $"{metres} m";
|
||||
ToolTip.SetTip(cabShaderSourceRange, CabShadowRangeWord(metres));
|
||||
}
|
||||
|
||||
if (reflectionsFramerateValue != null)
|
||||
if (ReflectionsFramerateRow != null)
|
||||
{
|
||||
int fps = (int)reflectionsFramerate.Value;
|
||||
string word = ReflectionsRefreshWord(fps);
|
||||
reflectionsFramerateValue.Text = $"{fps} FPS";
|
||||
ReflectionsFramerateRow.ValueText = $"{fps} FPS";
|
||||
ToolTip.SetTip(reflectionsFramerate, word.Length == 0 ? null : word);
|
||||
}
|
||||
}
|
||||
|
||||
103
StarterNG/Views/ShortcutsWindow.cs
Normal file
103
StarterNG/Views/ShortcutsWindow.cs
Normal file
@@ -0,0 +1,103 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
|
||||
using StarterNG.Infrastructure;
|
||||
|
||||
namespace StarterNG.Views;
|
||||
|
||||
/// <summary>
|
||||
/// The cheat sheet, printed straight from <see cref="Shortcuts.All"/> so it can never
|
||||
/// list a gesture the window does not actually dispatch.
|
||||
/// </summary>
|
||||
public static class ShortcutsWindow
|
||||
{
|
||||
public static Window Create()
|
||||
{
|
||||
var list = new StackPanel { Spacing = 2, Margin = new Thickness(20, 12, 20, 20) };
|
||||
|
||||
foreach (var scope in new[] { ShortcutScope.Global, ShortcutScope.Depot, ShortcutScope.Consist })
|
||||
{
|
||||
list.Children.Add(new TextBlock
|
||||
{
|
||||
Text = Shortcuts.ScopeLabel(scope),
|
||||
FontWeight = FontWeight.Bold,
|
||||
Margin = new Thickness(0, 12, 0, 4)
|
||||
});
|
||||
|
||||
foreach (var s in Shortcuts.InScope(scope))
|
||||
list.Children.Add(Row(s.Display, App.Loc[s.LabelKey]));
|
||||
}
|
||||
|
||||
list.Children.Add(new TextBlock
|
||||
{
|
||||
Text = App.Loc["ShortcutsTabHint"],
|
||||
Opacity = 0.7,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Margin = new Thickness(0, 16, 0, 0)
|
||||
});
|
||||
|
||||
var win = new Window
|
||||
{
|
||||
Title = App.Loc["ShortcutsTitle"],
|
||||
Width = 460,
|
||||
Height = 560,
|
||||
MinWidth = 380,
|
||||
MinHeight = 320,
|
||||
WindowStartupLocation = WindowStartupLocation.CenterOwner,
|
||||
Content = new ScrollViewer
|
||||
{
|
||||
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
|
||||
Content = list
|
||||
}
|
||||
};
|
||||
|
||||
win.KeyDown += (_, e) =>
|
||||
{
|
||||
if (e.Key is Key.Escape or Key.F1)
|
||||
{
|
||||
win.Close();
|
||||
e.Handled = true;
|
||||
}
|
||||
};
|
||||
return win;
|
||||
}
|
||||
|
||||
private static Control Row(string gesture, string label)
|
||||
{
|
||||
var grid = new Grid { ColumnDefinitions = new ColumnDefinitions("130,12,*") };
|
||||
|
||||
var keys = new Border
|
||||
{
|
||||
Background = new SolidColorBrush(Color.Parse("#2A3037")),
|
||||
BorderBrush = new SolidColorBrush(Color.Parse("#3A424A")),
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(3),
|
||||
Padding = new Thickness(6, 1),
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
Child = new TextBlock
|
||||
{
|
||||
Text = gesture,
|
||||
FontFamily = new FontFamily("Consolas, Menlo, monospace"),
|
||||
FontSize = 12
|
||||
}
|
||||
};
|
||||
|
||||
var text = new TextBlock
|
||||
{
|
||||
Text = label,
|
||||
FontSize = 12,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
};
|
||||
Grid.SetColumn(text, 2);
|
||||
|
||||
grid.Children.Add(keys);
|
||||
grid.Children.Add(text);
|
||||
grid.Margin = new Thickness(0, 2);
|
||||
return grid;
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@
|
||||
</Grid>
|
||||
|
||||
<TextBlock Name="statusText" DockPanel.Dock="Bottom" Margin="0,6,0,0"
|
||||
FontSize="11" Opacity="0.75" />
|
||||
FontSize="12" Opacity="0.75" />
|
||||
|
||||
<Grid DockPanel.Dock="Left" Width="260" RowDefinitions="*,4,*">
|
||||
<ListBox Name="typeList" Grid.Row="0" Classes="Compact" />
|
||||
|
||||
@@ -45,7 +45,7 @@ public sealed class UartWindow : Window
|
||||
root.Children.Add(new TextBlock
|
||||
{
|
||||
Text = App.Loc["UartHint"],
|
||||
FontSize = 11,
|
||||
FontSize = 12,
|
||||
Opacity = 0.65,
|
||||
TextWrapping = TextWrapping.Wrap
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user