Fix loads, some UI improvements

This commit is contained in:
2026-06-24 10:08:17 +02:00
parent 60f61a122d
commit 5908200bc4
8 changed files with 207 additions and 48 deletions

View File

@@ -29,13 +29,25 @@
<!-- Row 0: title / caption strip. Transparent background makes the
whole strip a drag handle (PointerPressed -> BeginMoveDrag),
and the OS buttons render at its top-right. -->
<Border Grid.Row="0" Background="Transparent"
PointerPressed="TitleBar_OnPointerPressed">
<TextBlock Text="Starter MaSzyna" VerticalAlignment="Center"
Margin="14,0,0,0" FontSize="11"
Foreground="{StaticResource CarbonFgDim}" />
</Border>
and the OS buttons render at its top-right. The log button sits
on top of the strip, just left of the OS min/max/close buttons. -->
<StackPanel Orientation="Horizontal" Spacing="16" Grid.Row="0">
<Border Background="Transparent"
PointerPressed="TitleBar_OnPointerPressed">
<TextBlock Text="Starter MaSzyna" VerticalAlignment="Center"
Margin="14,0,0,0" FontSize="11"
Foreground="{StaticResource CarbonFgDim}" />
</Border>
<!-- Reserve ~140px on the right for the OS caption buttons so the
log button lands immediately to their left. -->
<Button Classes="Basic" Click="openLastLogClick"
HorizontalAlignment="Left" VerticalAlignment="Stretch"
Margin="0,0,0,0" Padding="10,0" Cursor="Hand"
ToolTip.Tip="{Binding [ShowLatestLogTooltip], Source={x:Static starterNg:App.Loc}}">
<materialIcons:MaterialIcon Kind="FileDocument" Height="16" Width="16" />
</Button>
</StackPanel>
<!-- Row 1: logo + navigation (left) and the right cluster (right) -->
<Grid Grid.Row="1" ColumnDefinitions="Auto,Auto,*,Auto">
@@ -65,7 +77,7 @@
HorizontalContentAlignment="Center" />
</StackPanel>
<!-- Right cluster: version, language, log, social links.
<!-- Right cluster: version, language, social links.
In the toolbar row, so it sits below the OS caption buttons. -->
<StackPanel Grid.Column="3" Orientation="Horizontal" VerticalAlignment="Center"
Margin="0,0,8,0" Spacing="4">
@@ -82,10 +94,6 @@
<ComboBoxItem>English</ComboBoxItem>
</ComboBox>
<Button Classes="Basic" Click="openLastLogClick"
ToolTip.Tip="{Binding [ShowLatestLogTooltip], Source={x:Static starterNg:App.Loc}}">
<materialIcons:MaterialIcon Kind="FileDocument" />
</Button>
<Button Classes="Basic" Cursor="Hand" Tag="http://eu07.pl/" Click="linkClick">
<materialIcons:MaterialIcon Kind="Web" />
</Button>

View File

@@ -181,8 +181,12 @@
</Style>
<!-- ── Compact list (tight rows that fill the available space) ─────────── -->
<!-- Transparent surface so the list blends into the GroupBox panel instead
of showing the Fluent theme's default light-grey ListBox background. -->
<Style Selector="ListBox.Compact">
<Setter Property="Padding" Value="0" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
</Style>
<Style Selector="ListBox.Compact ListBoxItem">
<Setter Property="Padding" Value="6,2" />

View File

@@ -27,9 +27,11 @@
<Grid DockPanel.Dock="Top" ColumnDefinitions="*,*" ColumnSpacing="6" Margin="0,0,0,6">
<ComboBox Name="categoryCombo" Grid.Column="0" HorizontalAlignment="Stretch"
PlaceholderText="{Binding [Category], Source={x:Static starterNg:App.Loc}}"
PointerWheelChanged="Combo_OnPointerWheel"
SelectionChanged="CategoryCombo_OnSelectionChanged" />
<ComboBox Name="classCombo" Grid.Column="1" HorizontalAlignment="Stretch"
PlaceholderText="{Binding [Class], Source={x:Static starterNg:App.Loc}}"
PointerWheelChanged="Combo_OnPointerWheel"
SelectionChanged="ClassCombo_OnSelectionChanged" />
</Grid>
<TextBox Name="searchBox" DockPanel.Dock="Top" Margin="0,0,0,8"

View File

@@ -337,6 +337,30 @@ public partial class Depot : UserControl
BuildList();
}
// Hovering over the Type/Class combos and scrolling cycles the selection
// without having to open the dropdown (scroll up = previous, down = next).
// When the dropdown is open the wheel scrolls the list as usual.
private void Combo_OnPointerWheel(object? sender, PointerWheelEventArgs e)
{
if (sender is not ComboBox combo || combo.IsDropDownOpen)
return;
int count = combo.ItemCount;
if (count == 0)
return;
int index = combo.SelectedIndex;
if (e.Delta.Y > 0) // scroll up -> previous
index = index <= 0 ? 0 : index - 1;
else if (e.Delta.Y < 0) // scroll down -> next
index = index < 0 ? 0 : Math.Min(index + 1, count - 1);
else
return;
combo.SelectedIndex = index;
e.Handled = true;
}
// ----------------------------------------------------------- vehicle browser
// Flat list of plain-text vehicle entries (no expanders, like the original
@@ -1270,20 +1294,33 @@ public partial class Depot : UserControl
var phys = PhysicsFor(lead);
// Prefer the cargo this vehicle actually accepts (.fiz LoadAccepted);
// otherwise suggest everything from data/load_weights.txt. Free text is
// still allowed.
var accepted = phys != null && !string.IsNullOrWhiteSpace(phys.LoadAccepted)
// otherwise offer everything from data/load_weights.txt.
var available = phys != null && !string.IsNullOrWhiteSpace(phys.LoadAccepted)
? phys.LoadAccepted.Split(',', ';').Select(s => s.Trim()).Where(s => s.Length > 0).ToList()
: null;
: LoadTypes().ToList();
var typeBox = new AutoCompleteBox
{
FontSize = 12,
MinWidth = 200,
Text = lead.LoadType ?? "",
ItemsSource = accepted ?? (IEnumerable<string>)LoadTypes(),
FilterMode = AutoCompleteFilterMode.ContainsOrdinal
};
// Keep a load already set on the vehicle selectable even when it is not in
// the accepted/known list (e.g. a custom load read from the scenery).
if (!string.IsNullOrEmpty(lead.LoadType) &&
!available.Contains(lead.LoadType!, StringComparer.OrdinalIgnoreCase))
available.Insert(0, lead.LoadType!);
// Dropdown of available cargo: the leading "(none)" entry clears the load.
var typeCombo = new ComboBox { FontSize = 12, MinWidth = 200 };
typeCombo.Items.Add(new ComboBoxItem { Content = App.Loc["None"], Tag = null });
foreach (var name in available)
typeCombo.Items.Add(new ComboBoxItem { Content = name, Tag = name });
// Pre-select the vehicle's current load (or "(none)").
typeCombo.SelectedIndex = 0;
if (!string.IsNullOrEmpty(lead.LoadType))
foreach (var obj in typeCombo.Items)
if (obj is ComboBoxItem ci && ci.Tag is string t &&
string.Equals(t, lead.LoadType, StringComparison.OrdinalIgnoreCase))
{
typeCombo.SelectedItem = ci;
break;
}
// Cap the amount at the .fiz MaxLoad when it is defined.
int maxLoad = phys != null && phys.MaxLoad > 0 ? phys.MaxLoad : 1000;
@@ -1295,22 +1332,25 @@ public partial class Depot : UserControl
void Apply()
{
string type = typeBox.Text?.Trim() ?? "";
string? type = (typeCombo.SelectedItem as ComboBoxItem)?.Tag as string;
int count = (int)(countBox.Value ?? 0);
foreach (var c in item.Cars)
{
c.LoadCount = count;
c.LoadType = count > 0 && !string.IsNullOrEmpty(type) ? type : c.LoadType;
if (count > 0)
// "(none)" -> no cargo at all; otherwise store the chosen type/amount.
c.LoadType = string.IsNullOrEmpty(type) ? null : type;
c.LoadCount = string.IsNullOrEmpty(type) ? 0 : count;
if (c.LoadCount > 0)
c.HasVelocity = true; // velocity token must precede loadcount
}
}
typeBox.LostFocus += (_, _) => Apply();
// Wire the handlers only after the initial selection so we don't apply
// (and mark the consist dirty) while merely populating the editor.
typeCombo.SelectionChanged += (_, _) => Apply();
countBox.ValueChanged += (_, _) => Apply();
loadsPanel.Children.Add(UnitTitle(item));
loadsPanel.Children.Add(LabeledRow(App.Loc["LoadType"], typeBox));
loadsPanel.Children.Add(LabeledRow(App.Loc["LoadType"], typeCombo));
loadsPanel.Children.Add(LabeledRow(App.Loc["LoadCount"], countBox));
loadsPanel.Children.Add(new TextBlock
{

View File

@@ -59,9 +59,15 @@
<TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center"
Text="{Binding [Time], Source={x:Static starterNg:App.Loc}}" />
<TextBox Grid.Row="0" Grid.Column="1" Name="weatherTime" Width="90"
HorizontalAlignment="Left" Watermark="hh:mm"
LostFocus="Weather_OnChanged" />
<StackPanel Grid.Row="0" Grid.Column="1" Orientation="Horizontal" Spacing="8">
<TimePicker Name="weatherTime" ClockIdentifier="24HourClock"
MinuteIncrement="1" VerticalAlignment="Center"
SelectedTimeChanged="WeatherTime_OnChanged" />
<Button Name="weatherTimeNow" Classes="Flat" Cursor="Hand"
VerticalAlignment="Center" Padding="10,4"
Click="WeatherTimeNow_OnClick"
Content="{Binding [CurrentTime], Source={x:Static starterNg:App.Loc}}" />
</StackPanel>
<TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center"
Text="{Binding [Season], Source={x:Static starterNg:App.Loc}}" />
@@ -94,8 +100,8 @@
<TextBlock Grid.Row="4" Grid.Column="0" VerticalAlignment="Center"
Text="{Binding [Visibility], Source={x:Static starterNg:App.Loc}}" />
<StackPanel Grid.Row="4" Grid.Column="1" Orientation="Horizontal" Spacing="8">
<Slider Name="weatherFog" Minimum="50" Maximum="2500" Width="220"
TickFrequency="50" IsSnapToTickEnabled="True"
<Slider Name="weatherFog" Minimum="0" Maximum="10000" Width="220"
TickFrequency="250" IsSnapToTickEnabled="True"
ValueChanged="Weather_OnSliderChanged" />
<TextBlock Name="weatherFogLabel" VerticalAlignment="Center" MinWidth="56" />
</StackPanel>

View File

@@ -1,9 +1,11 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
@@ -21,6 +23,9 @@ public partial class Scenarios : UserControl
private Scenery? _currentScenery;
// suppresses weather change handlers while the form is being populated
private bool _loadingWeather;
// "Today" season: the day field only previews the current day-of-year; the
// stored Day stays 0 so the scenery keeps using the live date.
private bool _todaySeason;
public Scenarios()
{
@@ -30,6 +35,7 @@ public partial class Scenarios : UserControl
Sceneries = GameData.Instance.Sceneries;
var groupNodes = new Dictionary<string, TreeViewItem>();
var topLevel = new List<TreeViewItem>();
for (int i = 0; i < Sceneries.Count; i++)
{
@@ -38,7 +44,7 @@ public partial class Scenarios : UserControl
// case 1: no group - put without parent
if (string.IsNullOrEmpty(scenery.Group))
{
sceneryList.Items.Add(new TreeViewItem
topLevel.Add(new TreeViewItem
{
Header = Path.GetFileNameWithoutExtension(scenery.Path),
Tag = i
@@ -57,7 +63,7 @@ public partial class Scenarios : UserControl
};
groupNodes[scenery.Group] = groupNode;
sceneryList.Items.Add(groupNode);
topLevel.Add(groupNode);
}
groupNode.Items.Add(new TreeViewItem
@@ -67,17 +73,59 @@ public partial class Scenarios : UserControl
});
}
// Sort the scenery tree alphabetically: group folders' inner sceneries
// first, then the top-level (group folders + ungrouped sceneries). The Tag
// keeps each node's original scenery index, so sorting only reorders the
// display - the vehicles/trainsets list stays in scenery order.
foreach (var node in groupNodes.Values)
{
var children = node.Items.Cast<TreeViewItem>()
.OrderBy(c => c.Header as string, StringComparer.OrdinalIgnoreCase)
.ToList();
node.Items.Clear();
foreach (var child in children)
node.Items.Add(child);
}
foreach (var item in topLevel.OrderBy(t => t.Header as string, StringComparer.OrdinalIgnoreCase))
sceneryList.Items.Add(item);
// refresh the consist preview when the view is shown again (e.g. after
// editing the consist in the depot). Switching tabs only toggles IsVisible
// (the view stays in the tree), so listen for that too.
AttachedToVisualTree += (_, _) => RefreshSelectedConsist();
AttachedToVisualTree += (_, _) => OnReentered();
PropertyChanged += (_, e) =>
{
if (e.Property == IsVisibleProperty && IsVisible)
RefreshSelectedConsist();
OnReentered();
};
}
// Called whenever this view is shown again (tab switch / re-attach). Edits made
// in the depot mutate the same Trainset objects, so refresh both the consist
// preview and the vehicle-list captions that depend on them.
private void OnReentered()
{
RefreshVehicleLabels();
RefreshSelectedConsist();
}
// Rebuilds the caption of each entry in the Vehicles list from its trainset's
// current vehicles, so a consist edited in the depot is reflected here too.
// Selection and order are preserved (only the text is updated).
private void RefreshVehicleLabels()
{
if (sceneryList.SelectedItem is not TreeViewItem { Tag: int tag } || tag >= Sceneries.Count)
return;
var scn = Sceneries[tag];
foreach (var obj in vehicleList.Items)
{
if (obj is ListBoxItem { Tag: int vi } lbi && vi < scn.Trainsets.Count)
lbi.Content = string.Join(" + ", scn.Trainsets[vi].Vehicles.Select(d => d.Name));
}
}
private void SceneryList_OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
var item = sceneryList.SelectedItem as TreeViewItem;
@@ -183,12 +231,14 @@ public partial class Scenarios : UserControl
try
{
weatherForm.IsEnabled = true;
weatherTime.Text = scenery.WeatherTime;
weatherTime.SelectedTime = ParseTime(scenery.WeatherTime);
weatherDay.Value = scenery.Day;
weatherTemp.Value = scenery.Temperature;
weatherFog.Value = System.Math.Clamp(scenery.FogEnd, 50, 2500);
weatherFog.Value = System.Math.Clamp(scenery.FogEnd, 0, 10000);
SelectByTag(weatherSeason, scenery.Day.ToString());
SelectByTag(weatherOvercast, scenery.Overcast.ToString(CultureInfo.InvariantCulture));
// Day 0 maps to the "Today" season entry: preview the live day-of-year.
ApplyTodayPreview(scenery.Day == 0);
UpdateWeatherLabels();
}
finally
@@ -204,9 +254,11 @@ public partial class Scenarios : UserControl
return;
var scn = _currentScenery;
if (!string.IsNullOrWhiteSpace(weatherTime.Text))
scn.WeatherTime = weatherTime.Text!.Trim();
scn.Day = (int)(weatherDay.Value ?? 0);
if (weatherTime.SelectedTime is { } t)
scn.WeatherTime = $"{t.Hours:D2}:{t.Minutes:D2}";
// In "Today" mode the day field is only a preview, so keep the stored day
// at 0 (the scenery then follows the live date).
scn.Day = _todaySeason ? 0 : (int)(weatherDay.Value ?? 0);
scn.Temperature = weatherTemp.Value;
scn.FogEnd = (int)weatherFog.Value;
if ((weatherOvercast.SelectedItem as ComboBoxItem)?.Tag is string ov &&
@@ -235,7 +287,44 @@ public partial class Scenarios : UserControl
}
// --- weather form event handlers ---
private void Weather_OnChanged(object? sender, RoutedEventArgs e) => CaptureWeather();
private void WeatherTime_OnChanged(object? sender, TimePickerSelectedValueChangedEventArgs e) => CaptureWeather();
// "Current time" button: drop the live wall-clock time into the picker (the
// SelectedTimeChanged handler then captures it onto the scenery).
private void WeatherTimeNow_OnClick(object? sender, RoutedEventArgs e)
{
var now = DateTime.Now;
weatherTime.SelectedTime = new TimeSpan(now.Hour, now.Minute, 0);
}
// Parses a stored "hh:mm" string into a TimeSpan, or null when it is missing
// or malformed (so the picker simply shows no selection).
private static TimeSpan? ParseTime(string? text)
{
if (string.IsNullOrWhiteSpace(text))
return null;
var m = Regex.Match(text, @"^\s*(\d{1,2})[:.](\d{2})");
if (m.Success &&
int.TryParse(m.Groups[1].Value, out int h) && h is >= 0 and <= 23 &&
int.TryParse(m.Groups[2].Value, out int min) && min is >= 0 and <= 59)
return new TimeSpan(h, min, 0);
return null;
}
// Toggles "Today" preview: the day field shows the live day-of-year (read-only)
// and CaptureWeather keeps the stored Day at 0; any other season re-enables it.
private void ApplyTodayPreview(bool today)
{
_todaySeason = today;
weatherDay.IsEnabled = !today;
if (today)
{
bool prev = _loadingWeather;
_loadingWeather = true;
weatherDay.Value = DateTime.Now.DayOfYear;
_loadingWeather = prev;
}
}
private void Weather_OnSelectionChanged(object? sender, SelectionChangedEventArgs e) => CaptureWeather();
@@ -251,9 +340,17 @@ public partial class Scenarios : UserControl
if ((weatherSeason.SelectedItem as ComboBoxItem)?.Tag is string tag &&
int.TryParse(tag, out int day))
{
_loadingWeather = true;
weatherDay.Value = day;
_loadingWeather = false;
if (day == 0) // "Today" - preview the live day-of-year, keep stored day at 0
{
ApplyTodayPreview(true);
}
else
{
ApplyTodayPreview(false);
_loadingWeather = true;
weatherDay.Value = day;
_loadingWeather = false;
}
}
CaptureWeather();
}

View File

@@ -22,6 +22,7 @@
<String key="NoWeatherData">No weather data in the scenery.</String>
<String key="NoTimetable">No timetable for the selected consist.</String>
<String key="Time">Time</String>
<String key="CurrentTime">Now</String>
<String key="Day">Day of year</String>
<String key="Sky">Sky</String>
<String key="Fog">Fog / visibility</String>

View File

@@ -22,6 +22,7 @@
<String key="NoWeatherData">Brak danych o pogodzie w scenerii.</String>
<String key="NoTimetable">Brak rozkładu jazdy dla wybranego składu.</String>
<String key="Time">Godzina</String>
<String key="CurrentTime">Czas aktualny</String>
<String key="Day">Dzień roku</String>
<String key="Sky">Niebo</String>
<String key="Fog">Mgła / widoczność</String>