split depot into domain and view classes

This commit is contained in:
maj00r
2026-08-31 00:44:48 +02:00
parent 04b0805703
commit 43a361ff89
16 changed files with 3629 additions and 3149 deletions

147
StarterNG/Domain/Cargo.cs Normal file
View File

@@ -0,0 +1,147 @@
using System;
using System.Collections.Generic;
using System.Linq;
using StarterNG.Classes;
namespace StarterNG.Domain;
public sealed class Cargo
{
private readonly VehicleInfo _info;
private readonly Random _rng;
public Cargo(VehicleInfo info, Random rng)
{
_info = info;
_rng = rng;
}
public bool IsLoadable(ConsistItem item) => IsLoadable(item.Cars[0]);
public bool IsLoadable(Dynamic car)
{
if (_info.CategoryOf(car) is "e" or "s" or "p")
return false;
return !string.IsNullOrWhiteSpace(_info.PhysicsFor(car)?.LoadAccepted);
}
public int MaxFor(Dynamic car, string? type = null)
{
if (!IsLoadable(car)) return 0;
if (Dynamic.IsPantStateType(type ?? car.LoadType)) return Dynamic.PantStateMax;
if (car.MaxLoad >= 0) return car.MaxLoad;
return _info.PhysicsFor(car)?.MaxLoad ?? 0;
}
public int LimitFor(Dynamic car, string? type = null)
{
int max = MaxFor(car, type);
return max > 0 ? max : (IsLoadable(car) ? int.MaxValue : 0);
}
public List<string> AcceptedTypes(Dynamic car)
{
var p = _info.PhysicsFor(car);
if (!IsLoadable(car) || p == null)
return new List<string>();
return p.LoadAccepted.Split(',', ';')
.Select(s => s.Trim())
.Where(s => s.Length > 0)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
}
public bool Accepts(Dynamic car, string? type) =>
!string.IsNullOrEmpty(type) &&
AcceptedTypes(car).Contains(type!, StringComparer.OrdinalIgnoreCase);
public void FillToMax(Dynamic car)
{
int max = MaxFor(car);
if (max <= 0) return;
if (string.IsNullOrEmpty(car.LoadType))
car.LoadType = AcceptedTypes(car).FirstOrDefault();
if (string.IsNullOrEmpty(car.LoadType)) return;
car.LoadCount = max;
}
public void CopyFromPrevious(IReadOnlyList<ConsistItem> consist, ConsistItem item)
{
int idx = IndexOf(consist, item);
if (idx <= 0) return;
var source = consist[idx - 1].Cars[0];
foreach (var c in item.Cars)
{
if (!Accepts(c, source.LoadType)) continue;
c.LoadType = source.LoadType;
c.LoadCount = Math.Min(source.LoadCount, LimitFor(c));
}
}
public void FillUnit(ConsistItem item)
{
foreach (var c in item.Cars)
FillToMax(c);
}
public void CopyToFollowing(IReadOnlyList<ConsistItem> consist, ConsistItem item)
{
int idx = IndexOf(consist, item);
if (idx < 0) return;
var lead = item.Cars[0];
string? type = lead.LoadType;
if (string.IsNullOrEmpty(type)) return;
foreach (var c in consist.Skip(idx + 1).SelectMany(u => u.Cars))
{
if (!AcceptedTypes(c).Contains(type!, StringComparer.OrdinalIgnoreCase))
continue;
c.LoadType = type;
c.LoadCount = Math.Min(lead.LoadCount, MaxFor(c));
}
}
public void RandomTypes(IReadOnlyList<ConsistItem> consist)
{
foreach (var c in consist.SelectMany(u => u.Cars))
{
var types = AcceptedTypes(c);
if (types.Count == 0) continue;
c.LoadType = types[_rng.Next(types.Count)];
}
}
public void MaxAmounts(IReadOnlyList<ConsistItem> consist)
{
foreach (var c in consist.SelectMany(u => u.Cars))
FillToMax(c);
}
public void RandomAmounts(IReadOnlyList<ConsistItem> consist)
{
foreach (var c in consist.SelectMany(u => u.Cars))
{
int max = MaxFor(c);
if (max <= 0) continue;
if (string.IsNullOrEmpty(c.LoadType))
c.LoadType = AcceptedTypes(c).FirstOrDefault();
if (string.IsNullOrEmpty(c.LoadType)) continue;
int min = Math.Max(1, max / 10);
c.LoadCount = _rng.Next(min, max + 1);
}
}
private static int IndexOf(IReadOnlyList<ConsistItem> consist, ConsistItem item)
{
for (int i = 0; i < consist.Count; i++)
if (ReferenceEquals(consist[i], item))
return i;
return -1;
}
}

582
StarterNG/Domain/Consist.cs Normal file
View File

@@ -0,0 +1,582 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using StarterNG.Classes;
namespace StarterNG.Domain;
public sealed class Consist : IReadOnlyList<ConsistItem>
{
private static readonly string[] CarSuffixes = { "sa", "sb", "ra", "rb", "s" };
private readonly VehicleDatabase _db;
private readonly VehicleInfo _info;
private readonly List<ConsistItem> _items = new();
private int _nameCounter;
public Consist(VehicleDatabase db, VehicleInfo info)
{
_db = db;
_info = info;
}
public event Action? Changed;
public Trainset? EditingTrainset { get; set; }
public ConsistItem? Selected { get; set; }
public Dynamic? SelectedCar { get; set; }
public int Count => _items.Count;
public ConsistItem this[int index]
{
get => _items[index];
set => _items[index] = value;
}
public IEnumerator<ConsistItem> GetEnumerator() => _items.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public int IndexOf(ConsistItem item) => _items.IndexOf(item);
public void Raise() => Changed?.Invoke();
public void LoadFrom(Trainset trainset)
{
EditingTrainset = trainset;
_items.Clear();
var cars = trainset.Vehicles.Select(v =>
{
var c = v.Clone();
c.MiniName = _db.MiniForSkin(c.SkinFile) ?? c.MiniName;
return c;
}).ToList();
int i = 0;
while (i < cars.Count)
{
if (HoldsNext(cars[i]))
{
var locked = new List<Dynamic> { cars[i] };
int j = i;
while (j < cars.Count - 1 && HoldsNext(cars[j]))
locked.Add(cars[++j]);
_items.Add(new ConsistItem
{
Cars = locked,
Grouped = true,
Flipped = locked[0].Offset < 0,
Driver = UnitDriver(locked)
});
i = j + 1;
continue;
}
VehicleSet? set = null;
if (_db.TextureForSkin(cars[i].SkinFile)?.Uuid is { } uuid)
_db.SetByTextureUuid.TryGetValue(uuid, out set);
if (set?.TextureRefs is { Count: > 1 })
{
var members = new HashSet<string>(set.TextureRefs.Where(r => !string.IsNullOrEmpty(r)));
var group = new List<Dynamic>();
int j = i;
while (j < cars.Count && group.Count < set.TextureRefs.Count)
{
if (_db.TextureForSkin(cars[j].SkinFile)?.Uuid is { } u
&& members.Contains(u))
group.Add(cars[j++]);
else
break;
}
if (group.Count >= 2)
{
_items.Add(new ConsistItem
{
Cars = group,
Grouped = true,
Flipped = group[0].Offset < 0,
Driver = UnitDriver(group)
});
i = j;
continue;
}
}
if (IsUnitCar(cars[i]))
{
string key = UnitKey(cars[i]);
var group = new List<Dynamic> { cars[i] };
int j = i + 1;
while (j < cars.Count && IsUnitCar(cars[j]) && UnitKey(cars[j]) == key)
group.Add(cars[j++]);
if (group.Count >= 2)
{
_items.Add(new ConsistItem
{
Cars = group,
Grouped = true,
Flipped = group[0].Offset < 0,
Driver = UnitDriver(group)
});
i = j;
continue;
}
}
_items.Add(new ConsistItem
{
Cars = new List<Dynamic> { cars[i] },
Grouped = false,
Flipped = cars[i].Offset < 0,
Driver = cars[i].DriverType
});
i++;
}
Selected = _items.FirstOrDefault();
SyncStartingVehicle();
Raise();
}
public List<Dynamic> Flatten()
{
var flat = new List<Dynamic>();
foreach (var item in _items)
{
var cars = item.Flipped ? item.Cars.AsEnumerable().Reverse().ToList() : item.Cars;
foreach (var car in cars)
flat.Add(car);
}
return flat;
}
public void SyncStartingVehicle()
{
if (Selected is not { Cars.Count: > 0 } item)
return;
var driven = item.Cars.FirstOrDefault(c =>
c.DriverType is eDriverType.Headdriver or eDriverType.Reardriver);
AppState.Instance.StartingVehicleName = (driven ?? item.Cars[0]).Name;
}
public Dynamic MakeDynamic(VehicleTexture texture, Scenery? scenery)
{
string skin = Base(texture.Skinfile);
return new Dynamic
{
RangeMax = -1,
RangeMin = 0,
Name = MakeUniqueName(skin, scenery),
DataFolder = StripDynamicPrefix(texture.Directory),
SkinFile = skin,
MmdFile = string.IsNullOrEmpty(texture.Model) ? skin : texture.Model!,
Offset = 0f,
DriverType = eDriverType.Nobody,
Coupling = new Coupling { Flags = Coupling.Mechanical | Coupling.BrakePipe },
HasVelocity = true,
MiniName = _db.ResolveMiniName(texture)
};
}
private string MakeUniqueName(string baseName, Scenery? scenery)
{
if (string.IsNullOrWhiteSpace(baseName)) baseName = "vehicle";
baseName = baseName.Replace(' ', '_');
var used = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var item in _items)
foreach (var c in item.Cars)
if (!string.IsNullOrEmpty(c.Name))
used.Add(c.Name!);
if (scenery != null)
{
foreach (string n in scenery.LooseVehicleNames)
used.Add(n);
foreach (var ts in scenery.Trainsets)
{
if (ReferenceEquals(ts, EditingTrainset)) continue;
foreach (var v in ts.Vehicles)
if (!string.IsNullOrEmpty(v.Name))
used.Add(v.Name!);
}
}
string candidate;
do
candidate = $"{baseName}_{++_nameCounter}";
while (used.Contains(candidate));
return candidate;
}
private static string StripDynamicPrefix(string directory)
{
string d = directory.Replace('\\', '/').TrimEnd('/');
const string prefix = "dynamic/";
if (d.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
d = d[prefix.Length..];
return d;
}
public void Insert(int at, ConsistItem item) =>
_items.Insert(Math.Clamp(at, 0, _items.Count), item);
public void MoveLeft(ConsistItem item)
{
int i = _items.IndexOf(item);
if (i > 0)
{
(_items[i - 1], _items[i]) = (_items[i], _items[i - 1]);
Raise();
}
}
public void MoveRight(ConsistItem item)
{
int i = _items.IndexOf(item);
if (i >= 0 && i < _items.Count - 1)
{
(_items[i + 1], _items[i]) = (_items[i], _items[i + 1]);
Raise();
}
}
public void Move(int from, int to)
{
if (from < 0 || from >= _items.Count) return;
var item = _items[from];
_items.RemoveAt(from);
to = Math.Clamp(to, 0, _items.Count);
_items.Insert(to, item);
Selected = item;
AutoConnectAll();
Raise();
}
public void Remove(ConsistItem item)
{
int gap = _items.IndexOf(item);
_items.Remove(item);
if (ReferenceEquals(Selected, item))
Selected = _items.Count == 0
? null
: _items[Math.Clamp(gap, 0, _items.Count - 1)];
Raise();
}
public void Flip(ConsistItem item)
{
int index = _items.IndexOf(item);
if (index < 0) return;
int first = index;
while (first > 0 && HoldsTail(_items[first - 1]))
first--;
int last = index;
while (last + 1 < _items.Count && HoldsTail(_items[last]))
last++;
for (int k = first; k <= last; k++)
{
var card = _items[k];
card.Flipped = !card.Flipped;
foreach (var car in card.Cars)
car.Offset = car.Offset >= 0 ? -1f : 0f;
}
if (last > first)
_items.Reverse(first, last - first + 1);
AutoConnectAll();
Raise();
}
public void CycleDriver(ConsistItem item)
{
var car = ActiveCar(item);
car.DriverType = car.DriverType switch
{
eDriverType.Nobody => eDriverType.Headdriver,
eDriverType.Headdriver => eDriverType.Reardriver,
eDriverType.Reardriver => eDriverType.Passenger,
_ => eDriverType.Nobody
};
item.Driver = UnitDriver(item.Cars);
if (ReferenceEquals(Selected, item))
SyncStartingVehicle();
Raise();
AppState.Instance.NotifyChanged();
}
public void Split(ConsistItem item)
{
int i = _items.IndexOf(item);
if (i < 0) return;
var order = item.Flipped
? Enumerable.Reverse(item.Cars).ToList()
: item.Cars;
_items.RemoveAt(i);
for (int c = 0; c < order.Count; c++)
{
_items.Insert(i + c, new ConsistItem
{
Cars = new List<Dynamic> { order[c] },
Grouped = false,
Flipped = item.Flipped,
Driver = order[c].DriverType
});
}
Selected = _items[i];
Raise();
}
public void Join(ConsistItem item)
{
int index = _items.IndexOf(item);
if (index < 0) return;
int first = index;
while (first > 0 && HoldsTail(_items[first - 1]))
first--;
int last = index;
bool inUnit = first < index || HoldsTail(_items[index]);
if (!inUnit && last + 1 < _items.Count)
{
var card = _items[last];
if (card.Cars.Count > 0)
TailCar(card).Coupling.Set(Coupling.WorkshopLock, true);
last++;
}
while (last + 1 < _items.Count && HoldsTail(_items[last]))
last++;
if (last == first) return;
var cars = new List<Dynamic>();
var driver = eDriverType.Nobody;
for (int k = first; k <= last; k++)
{
cars.AddRange(_items[k].Cars);
if (driver == eDriverType.Nobody)
driver = _items[k].Driver;
}
bool flipped = _items[first].Flipped;
if (flipped)
cars.Reverse();
var joined = new ConsistItem
{
Cars = cars,
Grouped = true,
Flipped = flipped,
Driver = driver
};
_items.RemoveRange(first, last - first + 1);
_items.Insert(first, joined);
Selected = joined;
AutoConnectAll();
Raise();
}
public void RandomizeOrder(Random rng)
{
int start = Selected != null ? _items.IndexOf(Selected) : 0;
if (start < 0) start = 0;
var indices = new List<int>();
for (int i = start; i < _items.Count; i++)
{
if (_items[i].Grouped) continue;
string? cat = _info.CategoryOf(_items[i].Cars[0]);
if (cat is { Length: 1 } && char.IsUpper(cat[0]))
indices.Add(i);
}
if (indices.Count < 2) return;
var items = indices.Select(i => _items[i]).ToList();
for (int i = items.Count - 1; i > 0; i--)
{
int j = rng.Next(i + 1);
(items[i], items[j]) = (items[j], items[i]);
}
for (int k = 0; k < indices.Count; k++)
_items[indices[k]] = items[k];
AutoConnectAll();
Raise();
}
public void RandomizeOrientation(Random rng)
{
int start = Selected != null ? _items.IndexOf(Selected) : 0;
if (start < 0) start = 0;
if (start >= _items.Count) return;
for (int i = start; i < _items.Count; i++)
if (rng.Next(2) == 0)
_items[i].Flipped = !_items[i].Flipped;
AutoConnectAll();
Raise();
}
public void AutoConnectAll()
{
var flat = new List<(Dynamic car, bool flipped)>();
foreach (var item in _items)
{
var cars = item.Flipped ? item.Cars.AsEnumerable().Reverse() : item.Cars;
foreach (var c in cars)
flat.Add((c, item.Flipped));
}
for (int i = 0; i < flat.Count - 1; i++)
{
var (left, lf) = flat[i];
var (right, rf) = flat[i + 1];
var lp = _info.PhysicsFor(left);
var rp = _info.PhysicsFor(right);
int leftMax = lp == null ? 3 : (lf ? lp.AllowedFlagA : lp.AllowedFlagB);
int rightMax = rp == null ? 3 : (rf ? rp.AllowedFlagB : rp.AllowedFlagA);
int common = leftMax & rightMax;
string lct = lp == null ? "" : (lf ? lp.ControlTypeA : lp.ControlTypeB);
string rct = rp == null ? "" : (rf ? rp.ControlTypeB : rp.ControlTypeA);
if ((common & Coupling.ControlMU) != 0 &&
!string.Equals(lct, rct, StringComparison.OrdinalIgnoreCase))
common &= ~Coupling.ControlMU;
left.Coupling.Flags = left.Coupling.Locked ? -common : common;
}
}
public bool CanFormUnit(ConsistItem left, ConsistItem right)
{
if (left.Cars.Count == 0 || right.Cars.Count == 0)
return false;
var tail = TailCar(left);
var head = HeadCar(right);
var lp = _info.PhysicsFor(tail);
var rp = _info.PhysicsFor(head);
int leftMax = lp == null ? 3 : (left.Flipped ? lp.AllowedFlagA : lp.AllowedFlagB);
int rightMax = rp == null ? 3 : (right.Flipped ? rp.AllowedFlagB : rp.AllowedFlagA);
return (leftMax & rightMax & Coupling.WorkshopLock) != 0 || SameSet(tail, head);
}
public bool SameSet(Dynamic a, Dynamic b)
{
if (IsUnitCar(a) && IsUnitCar(b) && UnitKey(a) == UnitKey(b))
return true;
string? ua = _db.TextureForSkin(a.SkinFile)?.Uuid;
string? ub = _db.TextureForSkin(b.SkinFile)?.Uuid;
if (string.IsNullOrEmpty(ua) || string.IsNullOrEmpty(ub))
return false;
return _db.SetByTextureUuid.TryGetValue(ua!, out var sa) &&
_db.SetByTextureUuid.TryGetValue(ub!, out var sb) &&
ReferenceEquals(sa, sb) && sa.TextureRefs.Count > 1;
}
public Dynamic ActiveCar(ConsistItem item) =>
SelectedCar != null && item.Cars.Contains(SelectedCar) ? SelectedCar : item.Cars[0];
public static IEnumerable<Dynamic> UnitTargets(ConsistItem item) => item.Cars;
public IEnumerable<Dynamic> MemberTargets(ConsistItem item) =>
item.Cars.Count == 1 ? item.Cars : new[] { ActiveCar(item) };
public static Dynamic TailCar(ConsistItem item) =>
item.Flipped ? item.Cars[0] : item.Cars[^1];
public static Dynamic HeadCar(ConsistItem item) =>
item.Flipped ? item.Cars[^1] : item.Cars[0];
public static bool HoldsTail(ConsistItem item) =>
item.Cars.Count > 0 && HoldsNext(TailCar(item));
public static bool HoldsNext(Dynamic car) =>
car.Coupling.Has(Coupling.WorkshopLock) || car.Coupling.Locked;
public static eDriverType UnitDriver(IReadOnlyList<Dynamic> group) =>
group.Select(c => c.DriverType)
.Where(d => d != eDriverType.Nobody)
.DefaultIfEmpty(eDriverType.Nobody)
.First();
public static string Base(string skinOrName) => System.IO.Path.GetFileNameWithoutExtension(skinOrName);
public static string UnitKey(Dynamic d) => StripCarSuffix(Base(d.SkinFile));
public static bool IsUnitCar(Dynamic d) => UnitKey(d) != Base(d.SkinFile);
private static string StripCarSuffix(string name)
{
foreach (string suf in CarSuffixes)
{
if (name.Length > suf.Length &&
name.EndsWith(suf, StringComparison.OrdinalIgnoreCase) &&
char.IsDigit(name[name.Length - suf.Length - 1]))
return name[..^suf.Length];
}
return name;
}
public static string UnitLabel(ConsistItem item)
{
string key = UnitKey(item.Cars[0]);
return item.Cars.Count > 1 ? $"{key} [{item.Cars.Count}]" : key;
}
public static int GetWagonNumber(Dynamic car)
{
string name = car.Name ?? "";
int hash = name.LastIndexOf('#');
if (hash >= 0 && hash + 1 < name.Length && name[(hash + 1)..].All(char.IsDigit)
&& int.TryParse(name[(hash + 1)..], out int n))
return n;
return 0;
}
public void SetWagonNumber(Dynamic car, int number)
{
string name = car.Name ?? "";
int hash = name.LastIndexOf('#');
string baseName = (hash >= 0 && hash + 1 < name.Length && name[(hash + 1)..].All(char.IsDigit))
? name[..hash]
: name;
car.Name = number > 0 ? $"{baseName}#{number}" : baseName;
Raise();
}
}

View File

@@ -0,0 +1,73 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using StarterNG.Classes;
namespace StarterNG.Domain;
public static class LoadWeights
{
private static Dictionary<string, int>? _weights;
public static int WeightOf(string? name)
{
Load();
return !string.IsNullOrEmpty(name) && _weights!.TryGetValue(name!, out int w) ? w : 1000;
}
public static string Describe(string name) =>
string.Equals(name, Dynamic.PantState, StringComparison.OrdinalIgnoreCase)
? App.Loc["LoadPantState"]
: name;
private static void Load()
{
if (_weights != null)
return;
var weights = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase)
{
[Dynamic.PantState] = 0
};
try
{
string path = Path.Combine("data", "load_weights.txt");
if (File.Exists(path))
{
var body = new StringBuilder();
foreach (var raw in File.ReadAllLines(path, Encoding.GetEncoding(1250)))
{
string line = raw;
int comment = line.IndexOfAny(new[] { '#', ';' });
if (comment >= 0) line = line[..comment];
int slashes = line.IndexOf("//", StringComparison.Ordinal);
if (slashes >= 0) line = line[..slashes];
body.Append(line).Append(' ');
}
var tokens = body.Replace(":", " : ").ToString()
.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
for (int i = 1; i + 1 < tokens.Length; i++)
{
if (tokens[i] != ":") continue;
string name = tokens[i - 1];
if (name is "{" or "}") continue;
if (!int.TryParse(tokens[i + 1], NumberStyles.Integer,
CultureInfo.InvariantCulture, out int w))
continue;
weights[name] = w;
}
}
}
catch (Exception ex)
{
Infrastructure.Diagnostics.Log("data/load_weights.txt", ex);
}
_weights = weights;
}
}

View File

@@ -53,13 +53,13 @@ public sealed class TexRandomizer
_stock.Add(parts); _stock.Add(parts);
} }
} }
catch catch (Exception ex)
{ {
StarterNG.Infrastructure.Diagnostics.Log("texture randomizer stock list", ex);
} }
} }
public int Apply(IList<ConsistItem> consist, Func<VehicleTexture, Dynamic> makeCar) public int Apply(Consist consist, Func<VehicleTexture, Dynamic> makeCar)
{ {
int changed = 0; int changed = 0;
for (int i = 0; i < consist.Count; i++) for (int i = 0; i < consist.Count; i++)
@@ -110,7 +110,7 @@ public sealed class TexRandomizer
return changed; return changed;
} }
private string? ResolveFilterMini(ConsistItem item, int index, IList<ConsistItem> consist, VehicleTexture? current) private string? ResolveFilterMini(ConsistItem item, int index, Consist consist, VehicleTexture? current)
{ {
if (current != null) if (current != null)
{ {

View File

@@ -145,14 +145,14 @@ public static class UData
var fiz = physicsFor(v); var fiz = physicsFor(v);
if (fiz == null) continue; if (fiz == null) continue;
var loads = fiz.LoadAccepted var cargo = fiz.LoadAccepted
.Split(',', StringSplitOptions.RemoveEmptyEntries) .Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(x => x.Trim()) .Select(x => x.Trim())
.Where(x => x.Length > 0) .Where(x => x.Length > 0)
.ToList(); .ToList();
if (loads.Count > 0) if (cargo.Count > 0)
v.LoadType = loads[rng.Next(loads.Count)]; v.LoadType = cargo[rng.Next(cargo.Count)];
} }
} }

View File

@@ -0,0 +1,30 @@
using StarterNG.Classes;
namespace StarterNG.Domain;
public sealed class VehicleInfo
{
private readonly VehicleDatabase _db;
public VehicleInfo(VehicleDatabase db) => _db = db;
public static string ClassOf(VehicleTexture t) => t.ResolvedClass;
public static string? CategoryOf(VehicleTexture t) => t.ResolvedCategory;
public static bool IsPoweredCategory(string? c) =>
c is "e" or "s" or "p" or "z" or "a";
public VehicleTexture? TextureFor(Dynamic car) => _db.TextureForSkin(car.SkinFile);
public string? CategoryOf(Dynamic car) =>
TextureFor(car) is { } t ? CategoryOf(t) : null;
public Physics? PhysicsFor(Dynamic car)
{
string? dbModel = TextureFor(car)?.Model;
return Physics.For(car.DataFolder, dbModel)
?? Physics.For(car.DataFolder, car.MmdFile)
?? Physics.For(car.DataFolder, car.SkinFile);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,396 @@
using System;
using System.Collections.Generic;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using StarterNG.Classes;
using StarterNG.Domain;
using StarterNG.Infrastructure;
namespace StarterNG.Views;
public sealed class ConsistDragging
{
private const double DragThreshold = 6;
private const string DragVehicleFormat = "starterng/vehicle";
private readonly Panel consistStack;
private readonly ScrollViewer consistScroll;
private readonly Canvas consistOverlay;
private readonly Control consistDropTarget;
private readonly ListBox vehicleListBox;
private readonly Control miniPreviewPanel;
private readonly Consist _consist;
private readonly Func<VehicleTexture?> _browserSelected;
private readonly Action<VehicleTexture, int> _insertTexture;
private readonly Action<Dynamic> _selectInBrowser;
private readonly DropGap _dropGap = new();
private readonly EdgeScroller _edgeScroller = new();
private readonly DropIndexTracker _dropTracker = new();
private PointerPressedEventArgs? _pendingDragPress;
private Point _pendingDragOrigin;
private VehicleTexture? _pendingDragTexture;
private int _pendingDragConsistIndex = -1;
private ConsistItem? _pendingBrowserSync;
private bool _dragSessionActive;
private int _pressCardIndex = -1;
private Point _pressPoint;
private bool _cardDragging;
private double _grabOffset;
private double _dragCardWidth = 64;
private Control? _carried;
private VehicleTexture? _dragTexture;
private int _dragConsistIndex = -1;
public ConsistDragging(
Panel consistStack, ScrollViewer consistScroll, Canvas consistOverlay,
Control consistDropTarget, ListBox vehicleListBox, Control miniPreviewPanel,
Consist consist, Func<VehicleTexture?> browserSelected,
Action<VehicleTexture, int> insertTexture, Action<Dynamic> selectInBrowser)
{
this.consistStack = consistStack;
this.consistScroll = consistScroll;
this.consistOverlay = consistOverlay;
this.consistDropTarget = consistDropTarget;
this.vehicleListBox = vehicleListBox;
this.miniPreviewPanel = miniPreviewPanel;
_consist = consist;
_browserSelected = browserSelected;
_insertTexture = insertTexture;
_selectInBrowser = selectInBrowser;
}
public void Attach()
{
consistStack.PointerMoved += Strip_OnPointerMoved;
consistStack.PointerReleased += Strip_OnPointerReleased;
consistStack.PointerCaptureLost += (_, _) => EndCardDrag();
DragDrop.SetAllowDrop(consistDropTarget, true);
consistDropTarget.AddHandler(DragDrop.DragOverEvent, Consist_OnDragOver);
consistDropTarget.AddHandler(DragDrop.DropEvent, Consist_OnDrop);
consistDropTarget.AddHandler(DragDrop.DragLeaveEvent, (_, _) => CloseDropGap());
vehicleListBox.AddHandler(InputElement.PointerMovedEvent, PendingDrag_OnPointerMoved, handledEventsToo: true);
vehicleListBox.AddHandler(InputElement.PointerReleasedEvent, PendingDrag_OnPointerReleased, handledEventsToo: true);
miniPreviewPanel.AddHandler(InputElement.PointerMovedEvent, PendingDrag_OnPointerMoved, handledEventsToo: true);
miniPreviewPanel.AddHandler(InputElement.PointerReleasedEvent, PendingDrag_OnPointerReleased, handledEventsToo: true);
}
public void ArmCardDrag(int index, PointerPressedEventArgs e)
{
_pressCardIndex = index;
_pressPoint = e.GetPosition(consistStack);
_cardDragging = false;
}
public void Reset()
{
_dropGap.Forget();
_dropTracker.Reset();
}
public void MiniPreviewPressed(object? sender, PointerPressedEventArgs e)
{
if (_browserSelected() is null || !e.GetCurrentPoint(miniPreviewPanel).Properties.IsLeftButtonPressed)
return;
ArmVehicleDrag(e, miniPreviewPanel, _browserSelected());
}
public void VehicleListPressed(object? sender, PointerPressedEventArgs e)
{
if (!e.GetCurrentPoint(vehicleListBox).Properties.IsLeftButtonPressed)
return;
var hit = e.Source as Control;
while (hit != null && hit is not ListBoxItem)
hit = hit.Parent as Control;
if (hit is ListBoxItem { Tag: VehicleTexture texture })
ArmVehicleDrag(e, vehicleListBox, texture);
}
private void ArmVehicleDrag(PointerPressedEventArgs e, Visual relativeTo, VehicleTexture texture)
{
ClearPendingDrag();
_pendingDragPress = e;
_pendingDragOrigin = e.GetPosition(relativeTo);
_pendingDragTexture = texture;
_pendingDragConsistIndex = -1;
}
private void ClearPendingDrag()
{
_pendingDragPress = null;
_pendingDragTexture = null;
_pendingDragConsistIndex = -1;
_pendingBrowserSync = null;
_dragSessionActive = false;
}
private async void PendingDrag_OnPointerMoved(object? sender, PointerEventArgs e)
{
if (_pendingDragPress is null || _dragSessionActive || sender is not Visual relativeTo)
return;
if (!e.GetCurrentPoint(relativeTo).Properties.IsLeftButtonPressed)
{
FinishPendingClick();
return;
}
var delta = e.GetPosition(relativeTo) - _pendingDragOrigin;
if (Math.Abs(delta.X) < DragThreshold && Math.Abs(delta.Y) < DragThreshold)
return;
var press = _pendingDragPress;
var texture = _pendingDragTexture;
int consistIndex = _pendingDragConsistIndex;
_pendingBrowserSync = null;
_pendingDragPress = null;
_dragSessionActive = true;
if (texture != null)
{
_dragTexture = texture;
_dragConsistIndex = -1;
CaptureCardMidpoints();
var data = new DataTransfer();
data.Add(DataTransferItem.CreateText(DragVehicleFormat));
try
{
await DragDrop.DoDragDropAsync(press, data, DragDropEffects.Copy);
}
finally
{
_dragTexture = null;
CloseDropGap();
ClearPendingDrag();
}
return;
}
}
private void PendingDrag_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (_dragSessionActive) return;
FinishPendingClick();
}
private void FinishPendingClick()
{
var sync = _pendingBrowserSync;
ClearPendingDrag();
if (sync is { Cars.Count: > 0 })
_selectInBrowser(sync.Cars[0]);
}
private void Consist_OnDragOver(object? sender, DragEventArgs e)
{
if (_dragTexture != null || _dragConsistIndex >= 0)
{
e.DragEffects = _dragConsistIndex >= 0 ? DragDropEffects.Move : DragDropEffects.Copy;
e.Handled = true;
double pointerX = e.GetPosition(consistScroll).X;
UpdateDropIndex(pointerX + consistScroll.Offset.X);
_edgeScroller.Update(consistScroll, pointerX);
}
else
{
e.DragEffects = DragDropEffects.None;
}
}
private void Strip_OnPointerMoved(object? sender, PointerEventArgs e)
{
var point = e.GetPosition(consistStack);
if (!_cardDragging)
{
if (_pressCardIndex < 0 ||
!e.GetCurrentPoint(consistStack).Properties.IsLeftButtonPressed)
return;
var delta = point - _pressPoint;
if (Math.Abs(delta.X) < DragThreshold && Math.Abs(delta.Y) < DragThreshold)
return;
BeginCardDrag(e);
if (!_cardDragging) return;
}
Canvas.SetLeft(_carried!, point.X - _grabOffset);
double pointerX = e.GetPosition(consistScroll).X;
UpdateDropIndex(pointerX + consistScroll.Offset.X);
_edgeScroller.Update(consistScroll, pointerX);
}
private void Strip_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (!_cardDragging)
{
int clicked = _pressCardIndex;
_pressCardIndex = -1;
if (e.InitialPressMouseButton == MouseButton.Left &&
clicked >= 0 && clicked < _consist.Count &&
_consist[clicked].Cars.Count > 0)
_selectInBrowser(_consist[clicked].Cars[0]);
return;
}
int from = _dragConsistIndex;
int target = _dropTracker.Index;
EndCardDrag();
if (from >= 0 && target >= 0)
_consist.Move(from, target);
}
private void BeginCardDrag(PointerEventArgs e)
{
int card = _pressCardIndex;
if (card < 0 || 2 * card + 1 >= consistStack.Children.Count)
return;
var cardVisual = consistStack.Children[2 * card];
var coupler = consistStack.Children[2 * card + 1];
var bounds = cardVisual.Bounds;
_dragConsistIndex = card;
_dragTexture = null;
_dragCardWidth = Math.Max(bounds.Width + coupler.Bounds.Width, 24);
_grabOffset = Math.Clamp(_pressPoint.X - bounds.Left, 0, _dragCardWidth);
CaptureCardMidpoints();
_carried = new Border
{
Width = bounds.Width,
Height = bounds.Height,
Opacity = 0.85,
CornerRadius = new CornerRadius(6),
BorderThickness = new Thickness(2),
BorderBrush = VehicleCardStyle.BorderSelected,
Background = VehicleCardStyle.BackgroundSelected,
Child = new Image
{
Source = CardSnapshot(cardVisual),
Stretch = Stretch.Uniform
}
};
Canvas.SetTop(_carried, bounds.Top);
Canvas.SetLeft(_carried, bounds.Left);
consistOverlay.Children.Add(_carried);
cardVisual.IsVisible = false;
coupler.IsVisible = false;
_cardDragging = true;
e.Pointer.Capture(consistStack);
}
private static Bitmap? CardSnapshot(Control source)
{
if (source.Bounds.Width < 1 || source.Bounds.Height < 1)
return null;
try
{
var size = new PixelSize((int)source.Bounds.Width, (int)source.Bounds.Height);
var bitmap = new RenderTargetBitmap(size, new Vector(96, 96));
bitmap.Render(source);
return bitmap;
}
catch (Exception ex)
{
Infrastructure.Diagnostics.Log("Card snapshot", ex);
return null;
}
}
private void EndCardDrag()
{
if (_dragConsistIndex >= 0 && 2 * _dragConsistIndex + 1 < consistStack.Children.Count)
{
consistStack.Children[2 * _dragConsistIndex].IsVisible = true;
consistStack.Children[2 * _dragConsistIndex + 1].IsVisible = true;
}
if (_carried != null)
consistOverlay.Children.Remove(_carried);
_carried = null;
CloseDropGap();
_dragConsistIndex = -1;
_pressCardIndex = -1;
_cardDragging = false;
}
private void CaptureCardMidpoints()
{
var midpoints = new List<double>();
for (int i = 0; i < consistStack.Children.Count; i += 2)
{
int card = i / 2;
if (card == _dragConsistIndex) continue;
var child = consistStack.Children[i];
if (_dropGap.IsGap(child)) continue;
midpoints.Add(child.Bounds.Center.X -
(_dragConsistIndex >= 0 && card > _dragConsistIndex ? _dragCardWidth : 0));
}
_dropTracker.Capture(midpoints);
}
private void UpdateDropIndex(double contentX)
{
if (!_dropTracker.Update(contentX)) return;
int card = _dropTracker.Index;
if (_dragConsistIndex >= 0 && card > _dragConsistIndex)
card++;
_dropGap.Show(consistStack, 2 * card, _dragCardWidth);
}
private void CloseDropGap()
{
_dropTracker.Reset();
_edgeScroller.Stop();
_dropGap.Hide(consistStack);
}
private void Consist_OnDrop(object? sender, DragEventArgs e)
{
int target = _dropTracker.Index >= 0 ? _dropTracker.Index : _consist.Count;
CloseDropGap();
if (_dragTexture != null)
{
_insertTexture(_dragTexture, target);
e.Handled = true;
}
else if (_dragConsistIndex >= 0)
{
_consist.Move(_dragConsistIndex, target);
e.Handled = true;
}
}
}

View File

@@ -0,0 +1,8 @@
namespace StarterNG.Views;
public static class CouplingBits
{
public static readonly string[] BitKeys =
{ "CplMechanical", "CplBrake", "CplControl", "CplHighVoltage",
"CplGangway", "CplAuxPneumatic", "CplHeating", "CplWorkshop" };
}

View File

@@ -0,0 +1,42 @@
using System.Collections.Generic;
using System.IO;
using Avalonia.Media.Imaging;
using StarterNG.Classes;
namespace StarterNG.Views;
public sealed class MiniTextures
{
private readonly Dictionary<string, Bitmap?> _cache = new();
public Bitmap? Get(string? name, int height, bool strict = false)
{
if (string.IsNullOrEmpty(name))
return null;
if (strict && !VehicleDatabase.HasMini(name))
return null;
string key = $"{(strict ? "!" : "")}{name}@{height}";
if (_cache.TryGetValue(key, out var cached))
return cached;
Bitmap? bmp = null;
string? path = VehicleDatabase.MiniPath(name);
if (path != null)
{
try
{
using var fs = File.OpenRead(path);
bmp = Bitmap.DecodeToHeight(fs, height);
}
catch (System.Exception ex)
{
Infrastructure.Diagnostics.Log($"mini/{name}", ex);
bmp = null;
}
}
_cache[key] = bmp;
return bmp;
}
}

View File

@@ -0,0 +1,166 @@
using System;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Media;
namespace StarterNG.Views;
public sealed class TextureRandomizerDialog
{
public sealed record Options(bool WithoutArchival, int RevisionTolerance, int RevYear);
private readonly Cursor _hand;
public TextureRandomizerDialog(Cursor hand) => _hand = hand;
public async Task<Options?> ShowAsync(Window owner)
{
var win = Build();
bool? ok = await win.ShowDialog<bool?>(owner);
return ok == true ? win.Tag as Options : null;
}
private Window Build()
{
static Control Docked(Control c, Dock side)
{
DockPanel.SetDock(c, side);
c.Margin = new Thickness(8, 0, 0, 0);
c.VerticalAlignment = VerticalAlignment.Center;
return c;
}
static TextBlock Caption(string text) => new()
{
Text = text,
FontSize = 12,
TextWrapping = TextWrapping.Wrap,
VerticalAlignment = VerticalAlignment.Center
};
var revDiff = new CheckBox
{
Content = Caption(App.Loc["RandomizeTexturesRevDiff"]),
IsChecked = true,
FontSize = 12
};
var age = new NumericUpDown
{
Minimum = 2, Maximum = 8, Value = 3, Width = 110,
FormatString = "0", ShowButtonSpinner = true
};
var vsCurrent = new RadioButton
{
Content = Caption(App.Loc["RandomizeTexturesVsCurrent"]),
IsChecked = true,
FontSize = 12,
GroupName = "revBase"
};
var vsYear = new RadioButton
{
Content = Caption(App.Loc["RandomizeTexturesVsYear"]),
FontSize = 12,
GroupName = "revBase"
};
var year = new NumericUpDown
{
Minimum = 1950, Maximum = 2050, Value = DateTime.Now.Year, Width = 150,
FormatString = "0", ShowButtonSpinner = true, IsEnabled = false
};
var noArch = new CheckBox
{
Content = Caption(App.Loc["RandomizeTexturesNoArchival"]),
IsChecked = true,
FontSize = 12
};
revDiff.IsCheckedChanged += (_, _) => age.IsEnabled = revDiff.IsChecked == true;
vsYear.IsCheckedChanged += (_, _) => year.IsEnabled = vsYear.IsChecked == true;
var win = new Window
{
Title = App.Loc["RandomizeTexturesTitle"],
Width = 420,
SizeToContent = SizeToContent.Height,
CanResize = false,
WindowStartupLocation = WindowStartupLocation.CenterOwner
};
var rulesBtn = new Button
{
Content = App.Loc["RandomizeTexturesRules"],
HorizontalAlignment = HorizontalAlignment.Stretch,
Cursor = _hand
};
rulesBtn.Classes.Add("Basic");
rulesBtn.Click += async (_, _) => await new RulesWindow().ShowDialog(win);
var ok = new Button
{
Content = App.Loc["RandomizeTexturesApply"],
MinWidth = 90,
Cursor = _hand
};
ok.Classes.Add("Accent");
ok.Click += (_, _) =>
{
win.Tag = new Options(
WithoutArchival: noArch.IsChecked == true,
RevisionTolerance: revDiff.IsChecked == true ? (int)(age.Value ?? 3) : -1,
RevYear: vsYear.IsChecked == true ? (int)(year.Value ?? DateTime.Now.Year) : -1);
win.Close(true);
};
var cancel = new Button { Content = App.Loc["Cancel"], MinWidth = 90, Cursor = _hand };
cancel.Classes.Add("Flat");
cancel.Click += (_, _) => win.Close(false);
win.Content = new StackPanel
{
Margin = new Thickness(12),
Spacing = 8,
Children =
{
new TextBlock
{
Text = App.Loc["RandomizeTexturesTitle"],
FontWeight = FontWeight.Bold,
HorizontalAlignment = HorizontalAlignment.Center
},
new DockPanel
{
LastChildFill = true,
Children =
{
Docked(age, Dock.Right),
revDiff
}
},
vsCurrent,
new DockPanel
{
LastChildFill = true,
Children =
{
Docked(year, Dock.Right),
vsYear
}
},
noArch,
rulesBtn,
new StackPanel
{
Orientation = Orientation.Horizontal,
HorizontalAlignment = HorizontalAlignment.Right,
Spacing = 8,
Children = { ok, cancel }
}
}
};
return win;
}
}

View File

@@ -0,0 +1,575 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Threading;
using StarterNG.Classes;
using StarterNG.Domain;
namespace StarterNG.Views;
public sealed class VehicleBrowser
{
private readonly ComboBox categoryCombo;
private readonly ComboBox classCombo;
private readonly ListBox vehicleListBox;
private readonly TextBox searchBox;
private readonly CheckBox hideArchivalCheck;
private readonly Image miniPreview;
private readonly Button addVehicleButton;
private readonly VehicleDatabase _db;
private readonly MiniTextures _minis;
private readonly Action _openTextureBase;
public void SyncHideArchival()
{
bool hideArchival = Classes.Settings.Instance.HideArchivalVehicles;
if (hideArchivalCheck.IsChecked == hideArchival)
return;
_suppress = true;
hideArchivalCheck.IsChecked = hideArchival;
_suppress = false;
Rebuild();
}
public VehicleTexture? Selected { get; private set; }
private bool _suppress;
private bool _syncingCombos;
private DispatcherTimer? _searchTimer;
private Func<string?, bool>? _categoryFilter;
private string? _classFilter;
public VehicleBrowser(
ComboBox categoryCombo, ComboBox classCombo, ListBox vehicleListBox, TextBox searchBox,
CheckBox hideArchivalCheck, Image miniPreview, Button addVehicleButton,
VehicleDatabase db, MiniTextures minis, Action openTextureBase)
{
this.categoryCombo = categoryCombo;
this.classCombo = classCombo;
this.vehicleListBox = vehicleListBox;
this.searchBox = searchBox;
this.hideArchivalCheck = hideArchivalCheck;
this.miniPreview = miniPreview;
this.addVehicleButton = addVehicleButton;
_db = db;
_minis = minis;
_openTextureBase = openTextureBase;
vehicleListBox.ContextRequested += List_OnContextRequested;
_searchTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(220) };
_searchTimer.Tick += (_, _) =>
{
_searchTimer!.Stop();
Rebuild();
};
}
private const int MaxListRows = 2000;
private const int MiniPreviewHeight = 54;
private const int CompactThumbHeight = 24;
private static readonly (string LocKey, Func<string?, bool> Match)[] CategoryDefs =
{
("CatElectricLoco", c => c == "e"),
("CatDieselLoco", c => c == "s"),
("CatSteamLoco", c => c == "p"),
("CatEMU", c => c == "z"),
("CatRailbus", c => c == "a"),
("CatDraisine", c => c == "d"),
("CatWork", c => c == "r"),
("CatPrototype", c => c == "x"),
("CatTram", c => c == "t"),
("CatCar", c => c == "o"),
("CatBus", c => c == "b"),
("CatTruck", c => c == "c"),
("CatPeople", c => c == "h"),
("CatAnimals", c => c == "f"),
("CatWagonsA", c => c == "A"),
("CatWagonsB", c => c == "B"),
("CatWagonsC", c => c == "C"),
("CatWagonsD", c => c == "D"),
("CatWagonsE", c => c == "E"),
("CatWagonsF", c => c == "F"),
("CatWagonsG", c => c == "G"),
("CatWagonsH", c => c == "H"),
("CatWagonsI", c => c == "I"),
("CatWagonsJ", c => c == "J"),
("CatWagonsK", c => c == "K"),
("CatWagonsL", c => c == "L"),
("CatWagonsM", c => c == "M"),
("CatWagonsN", c => c == "N"),
("CatWagonsO", c => c == "O"),
("CatWagonsP", c => c == "P"),
("CatWagonsR", c => c == "R"),
("CatWagonsS", c => c == "S"),
("CatWagonsT", c => c == "T"),
("CatWagonsU", c => c == "U"),
("CatWagonsV", c => c == "V"),
("CatWagonsW", c => c == "W"),
("CatWagonsX", c => c == "X"),
("CatWagonsY", c => c == "Y"),
("CatWagonsZ", c => c == "Z"),
("CatOther", IsOtherCat),
};
private static readonly HashSet<string> NamedLowerCats =
new(StringComparer.Ordinal) { "a", "b", "c", "d", "e", "f", "h", "n", "o", "p", "r", "s", "t", "x", "z" };
private static bool IsOtherCat(string? c) =>
string.IsNullOrEmpty(c) ||
(c is { Length: 1 } && char.IsLower(c[0]) && !NamedLowerCats.Contains(c));
public void PopulateCategoryCombo()
{
int keep = categoryCombo.SelectedIndex;
_suppress = true;
categoryCombo.Items.Clear();
foreach (var (locKey, match) in CategoryDefs)
categoryCombo.Items.Add(new ComboBoxItem { Content = App.Loc[locKey], Tag = match });
categoryCombo.SelectedIndex = keep >= 0 && keep < categoryCombo.Items.Count ? keep : -1;
_suppress = false;
_categoryFilter = categoryCombo.SelectedItem is ComboBoxItem { Tag: Func<string?, bool> f }
? f : null;
RebuildClassCombo();
}
private void RebuildClassCombo()
{
_suppress = true;
FillClassCombo();
classCombo.SelectedIndex = -1;
_suppress = false;
_classFilter = null;
Rebuild();
}
public void InitClassComboTemplates()
{
classCombo.ItemTemplate = new FuncDataTemplate<string>(
(cls, _) => ClassComboContent(cls, large: true), false);
classCombo.SelectionBoxItemTemplate = new FuncDataTemplate<string>(
(cls, _) => ClassComboContent(cls, large: false), false);
}
private void FillClassCombo()
{
classCombo.Items.Clear();
foreach (string cls in ClassesForCategory(_categoryFilter))
classCombo.Items.Add(cls);
}
private bool ApplyFilters(string? category, string? cls)
{
var catItem = categoryCombo.Items.OfType<ComboBoxItem>()
.FirstOrDefault(it => it.Tag is Func<string?, bool> f && f(category));
if (catItem != null && ReferenceEquals(categoryCombo.SelectedItem, catItem) &&
string.Equals(_classFilter, cls, StringComparison.OrdinalIgnoreCase))
return false;
_suppress = true;
try
{
if (catItem != null)
{
categoryCombo.SelectedItem = catItem;
_categoryFilter = catItem.Tag as Func<string?, bool>;
}
FillClassCombo();
classCombo.SelectedItem = classCombo.Items.OfType<string>()
.FirstOrDefault(s => string.Equals(s, cls, StringComparison.OrdinalIgnoreCase));
_classFilter = classCombo.SelectedItem as string;
}
finally { _suppress = false; }
return true;
}
private string? CategoryOfClass(string cls) =>
_db.Textures.FirstOrDefault(t => string.Equals(VehicleInfo.ClassOf(t), cls, StringComparison.OrdinalIgnoreCase))
is { } t ? VehicleInfo.CategoryOf(t) : null;
private Control ClassComboContent(string cls, bool large)
{
int height = large ? MiniPreviewHeight : CompactThumbHeight;
var cell = new StackPanel
{
Orientation = large ? Orientation.Vertical : Orientation.Horizontal,
Spacing = large ? 2 : 8,
HorizontalAlignment = large ? HorizontalAlignment.Center : HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Center
};
var bmp = _minis.Get(cls, height);
if (bmp != null)
cell.Children.Add(new Image
{
Source = bmp, Height = height,
Stretch = Stretch.Uniform,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
});
cell.Children.Add(new TextBlock
{
Text = cls,
HorizontalAlignment = large ? HorizontalAlignment.Center : HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Center,
TextAlignment = TextAlignment.Center,
TextTrimming = TextTrimming.CharacterEllipsis
});
return cell;
}
private IEnumerable<string> ClassesForCategory(Func<string?, bool>? category) =>
_db.Textures
.Where(t => category == null || category(VehicleInfo.CategoryOf(t)))
.Where(t => !_db.IsSetFollower(t))
.Where(t => !StarterNG.Classes.Settings.Instance.HideArchivalVehicles || !t.ResolvedArchived)
.Select(VehicleInfo.ClassOf)
.Where(s => !string.IsNullOrEmpty(s))
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(s => s, StringComparer.OrdinalIgnoreCase);
public void CategoryCombo_OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (_suppress) return;
_categoryFilter = (categoryCombo.SelectedItem as ComboBoxItem)?.Tag as Func<string?, bool>;
RebuildClassCombo();
}
public void ClassCombo_OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (_suppress) return;
string? cls = classCombo.SelectedItem as string;
_classFilter = cls;
if (cls != null && _categoryFilter == null &&
PostSyncing(() => { ApplyFilters(CategoryOfClass(cls), cls); Rebuild(); }))
return;
Rebuild();
}
public 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)
index = index <= 0 ? 0 : index - 1;
else if (e.Delta.Y < 0)
index = index < 0 ? 0 : Math.Min(index + 1, count - 1);
else
return;
combo.SelectedIndex = index;
e.Handled = true;
}
public void Rebuild()
{
Selected = null;
miniPreview.Source = null;
addVehicleButton.IsEnabled = false;
vehicleListBox.Items.Clear();
string search = searchBox.Text?.Trim() ?? "";
bool hasSearch = search.Length > 0;
if (_categoryFilter == null && _classFilter == null && !hasSearch)
return;
var matched = _db.Textures
.Where(t => PassesFilters(t, search, hasSearch))
.OrderBy(t => t.TextureMini ?? t.Skinfile, StringComparer.OrdinalIgnoreCase)
.Take(MaxListRows)
.ToList();
foreach (var texture in matched)
{
var row = new ListBoxItem
{
Content = BrowserLabel(texture, _db.ResolveSet(texture)),
Tag = texture
};
vehicleListBox.Items.Add(row);
}
if (vehicleListBox.Items.Count == 0)
vehicleListBox.Items.Add(new ListBoxItem
{
Content = App.Loc["NoVehicles"],
IsEnabled = false
});
}
public void VehicleListBox_OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (vehicleListBox.SelectedItem is ListBoxItem { Tag: VehicleTexture texture })
{
Selected = texture;
miniPreview.Source = _minis.Get(_db.ResolveMiniName(texture), MiniPreviewHeight);
addVehicleButton.IsEnabled = true;
SyncCombosTo(texture);
}
else
{
Selected = null;
miniPreview.Source = null;
addVehicleButton.IsEnabled = false;
}
}
private void List_OnContextRequested(object? sender, ContextRequestedEventArgs e)
{
var hit = e.Source as Control;
while (hit != null && hit is not ListBoxItem)
hit = hit.Parent as Control;
if (hit is not ListBoxItem { Tag: VehicleTexture texture })
return;
BuildBrowserMenu(texture).Open(hit);
e.Handled = true;
}
private ContextMenu BuildBrowserMenu(VehicleTexture texture)
{
var menu = new ContextMenu();
var copy = new MenuItem { Header = App.Loc["CopyTextureName"] };
copy.Click += async (_, _) =>
{
var top = TopLevel.GetTopLevel(vehicleListBox);
if (top?.Clipboard != null)
await top.Clipboard.SetTextAsync(BrowserName(texture));
};
menu.Items.Add(copy);
var open = new MenuItem { Header = App.Loc["OpenTextureFolder"] };
open.Click += (_, _) =>
{
string dir = Path.Combine(Directory.GetCurrentDirectory(), "dynamic",
texture.Directory.Replace('/', Path.DirectorySeparatorChar).TrimEnd('\\', '/'));
if (!Directory.Exists(dir))
dir = Path.Combine(Directory.GetCurrentDirectory(), texture.Directory);
if (!Directory.Exists(dir))
{
StarterNG.Infrastructure.Diagnostics.ReportOnUiThread(
string.Format(App.Loc["FaultFileNotFound"], dir));
return;
}
try
{
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(dir)
{
UseShellExecute = true
});
}
catch (Exception ex)
{
StarterNG.Infrastructure.Diagnostics.ReportOnUiThread(
$"{dir}{Environment.NewLine}{Environment.NewLine}{App.Loc["FaultDetail"]} {ex.Message}");
}
};
menu.Items.Add(open);
menu.Items.Add(new Separator());
var baseItem = new MenuItem { Header = App.Loc["OpenTextureBase"] };
baseItem.Click += (_, _) => _openTextureBase();
menu.Items.Add(baseItem);
return menu;
}
public void ShowTextureInfo(VehicleTexture texture, StackPanel target)
{
target.Children.Clear();
var full = new List<string>();
void Row(string label, string? value)
{
if (string.IsNullOrWhiteSpace(value)) return;
string line = $"{label}: {value}";
full.Add(line);
target.Children.Add(new TextBlock
{
Text = line,
FontSize = 10,
Opacity = 0.85,
TextTrimming = TextTrimming.CharacterEllipsis
});
}
Row(App.Loc["TextureInfo"], Consist.Base(texture.Skinfile));
var meta = texture.Meta;
if (meta != null)
{
Row(App.Loc["TexOperator"], meta.Operator);
Row(App.Loc["TexStation"], meta.Depot);
Row(App.Loc["TexRevision"], meta.RevisionDate);
Row(App.Loc["TexWorks"], meta.RevisionPlace);
Row(App.Loc["TexAuthor"], meta.TextureAuthor);
Row(App.Loc["TexPhoto"], meta.PhotoAuthor);
}
ToolTip.SetTip(target, string.Join("\n", full));
}
public List<VehicleTexture> CurrentTextures()
{
string search = searchBox.Text?.Trim() ?? "";
bool hasSearch = search.Length > 0;
if (_categoryFilter == null && _classFilter == null && !hasSearch)
return new List<VehicleTexture>();
return _db.Textures
.Where(t => PassesFilters(t, search, hasSearch))
.OrderBy(t => t.TextureMini ?? t.Skinfile, StringComparer.OrdinalIgnoreCase)
.ToList();
}
private bool PassesFilters(VehicleTexture t, string search, bool hasSearch)
{
if (_categoryFilter != null && !_categoryFilter(VehicleInfo.CategoryOf(t)))
return false;
if (_classFilter != null &&
!string.Equals(VehicleInfo.ClassOf(t), _classFilter, StringComparison.OrdinalIgnoreCase))
return false;
if (_db.IsSetFollower(t))
return false;
if (StarterNG.Classes.Settings.Instance.HideArchivalVehicles && t.ResolvedArchived)
return false;
if (hasSearch && !Matches(t, search))
return false;
return true;
}
private bool Matches(VehicleTexture t, string f)
{
bool C(string? s) => !string.IsNullOrEmpty(s) &&
s.Contains(f, StringComparison.OrdinalIgnoreCase);
return C(t.Skinfile) || C(t.Model) || C(t.TextureMini) || C(t.MiniRef)
|| C(t.Meta?.Vehicle) || C(t.Meta?.Operator) || C(VehicleInfo.ClassOf(t));
}
private static string BrowserName(VehicleTexture texture)
{
if (!string.IsNullOrEmpty(texture.TextureMini) &&
!string.Equals(texture.TextureMini, texture.ResolvedClass, StringComparison.OrdinalIgnoreCase))
return texture.TextureMini!;
return Consist.Base(texture.Skinfile);
}
private string BrowserLabel(VehicleTexture texture, IReadOnlyList<VehicleTexture>? set)
{
string name = BrowserName(texture);
if (!string.IsNullOrEmpty(texture.Meta?.Operator))
name += $" · {texture.Meta!.Operator}";
if (set is { Count: > 1 })
name += $" [{set.Count}]";
return name;
}
public void SearchBox_OnTextChanged(object? sender, TextChangedEventArgs e)
{
if (_suppress || _searchTimer is null) return;
_searchTimer.Stop();
_searchTimer.Start();
}
public void HideArchivalCheck_OnChanged(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
if (_suppress) return;
StarterNG.Classes.Settings.Instance.HideArchivalVehicles = hideArchivalCheck.IsChecked ?? true;
StarterNG.Classes.Settings.Instance.Save();
Rebuild();
}
public void SelectInBrowser(Dynamic car)
{
var texture = _db.TextureForSkin(car.SkinFile);
if (texture is null)
return;
var set = _db.ResolveSet(texture);
var browserTex = set is { Count: > 0 } ? set[0] : texture;
WhileSyncing(() =>
{
ApplyFilters(VehicleInfo.CategoryOf(browserTex), VehicleInfo.ClassOf(browserTex));
Rebuild();
SelectListEntry(browserTex);
});
}
private void SyncCombosTo(VehicleTexture texture)
{
if (_syncingCombos || !ApplyFilters(VehicleInfo.CategoryOf(texture), VehicleInfo.ClassOf(texture)))
return;
PostSyncing(() => { Rebuild(); SelectListEntry(texture); });
}
private void WhileSyncing(Action work)
{
_syncingCombos = true;
try { work(); }
finally { _syncingCombos = false; }
}
private bool PostSyncing(Action work)
{
if (_syncingCombos)
return false;
_syncingCombos = true;
Dispatcher.UIThread.Post(() => WhileSyncing(work), DispatcherPriority.Background);
return true;
}
private void SelectListEntry(VehicleTexture texture)
{
foreach (var obj in vehicleListBox.Items)
if (obj is ListBoxItem { Tag: VehicleTexture t } entry &&
string.Equals(t.Skinfile, texture.Skinfile, StringComparison.OrdinalIgnoreCase) &&
string.Equals(t.Directory, texture.Directory, StringComparison.OrdinalIgnoreCase))
{
vehicleListBox.SelectedItem = entry;
entry.BringIntoView();
break;
}
}
}

View File

@@ -0,0 +1,15 @@
using Avalonia.Media;
namespace StarterNG.Views;
public static class VehicleCardStyle
{
public static readonly IBrush Border = new SolidColorBrush(Color.Parse("#33888888"));
public static readonly IBrush BorderSelected = new SolidColorBrush(Color.Parse("#AA41C400"));
public static readonly IBrush BackgroundSelected = new SolidColorBrush(Color.Parse("#2241C400"));
public static readonly IBrush Placeholder = new SolidColorBrush(Color.Parse("#22808080"));
public static int ThumbHeight =>
Classes.Settings.Instance.LargeThumbnails ? 68 : 34;
}

View File

@@ -0,0 +1,447 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.VisualTree;
using StarterNG.Classes;
using StarterNG.Domain;
namespace StarterNG.Views;
public sealed class VehicleCards
{
private readonly Panel consistStack;
private readonly VehicleDatabase _db;
private readonly Consist _consist;
private readonly Cargo _cargo;
private readonly MiniTextures _minis;
private readonly Cursor _hand;
private readonly Action _redraw;
private readonly Action _refreshDetails;
private readonly Action<Dynamic> _selectInBrowser;
private readonly Action<int, PointerPressedEventArgs> _armDrag;
private readonly List<(Border Frame, ConsistItem Item, Dynamic Car)> _memberChrome = new();
private readonly List<(Button Badge, ConsistItem Item)> _driverChrome = new();
public Dynamic? PressedCar { get; set; }
public VehicleCards(Panel consistStack, VehicleDatabase db, Consist consist,
Cargo cargo, MiniTextures minis, Cursor hand,
Action redraw, Action refreshDetails, Action<Dynamic> selectInBrowser,
Action<int, PointerPressedEventArgs> armDrag)
{
this.consistStack = consistStack;
_db = db;
_consist = consist;
_cargo = cargo;
_minis = minis;
_hand = hand;
_redraw = redraw;
_refreshDetails = refreshDetails;
_selectInBrowser = selectInBrowser;
_armDrag = armDrag;
}
public void Forget()
{
_memberChrome.Clear();
_driverChrome.Clear();
}
public Control BuildCard(ConsistItem item, int index)
{
bool selected = ReferenceEquals(_consist.Selected, item);
var minis = new StackPanel
{
Orientation = Orientation.Horizontal,
Spacing = 1,
HorizontalAlignment = HorizontalAlignment.Center
};
var cars = item.Flipped ? item.Cars.AsEnumerable().Reverse() : item.Cars;
foreach (var car in cars)
{
var bmp = _minis.Get(car.MiniName, VehicleCardStyle.ThumbHeight, strict: true)
?? _minis.Get(car.SkinFile, VehicleCardStyle.ThumbHeight, strict: true)
?? _minis.Get(car.MiniName, VehicleCardStyle.ThumbHeight);
if (bmp != null)
{
var img = new Image { Source = bmp, Height = VehicleCardStyle.ThumbHeight, Stretch = Stretch.Uniform };
if (item.Flipped)
{
img.RenderTransform = new ScaleTransform(-1, 1);
img.RenderTransformOrigin = RelativePoint.Center;
}
minis.Children.Add(MemberFrame(img, item, car, CardCaption(item, car, index)));
}
else
{
minis.Children.Add(MemberFrame(new Border
{
Height = VehicleCardStyle.ThumbHeight, Width = 90, Background = VehicleCardStyle.Placeholder,
CornerRadius = new CornerRadius(4),
Child = new TextBlock
{
Text = car.SkinFile, FontSize = 10, TextWrapping = TextWrapping.Wrap,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
TextAlignment = TextAlignment.Center
}
}, item, car, CardCaption(item, car, index)));
}
}
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
};
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)));
if (item.Grouped && item.Cars.Count > 1)
controls.Children.Add(SmallButton("⧉", 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)));
var remove = SmallButton("✕", 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 };
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),
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
};
card.PointerPressed += (_, e) =>
{
if (!e.GetCurrentPoint(card).Properties.IsLeftButtonPressed)
return;
if (e.Source is Control src && src.GetVisualAncestors().OfType<Button>().Any())
return;
_consist.Selected = item;
_consist.SelectedCar = PressedCar ?? item.Cars[0];
PressedCar = null;
_consist.SyncStartingVehicle();
RefreshCardSelectionChrome();
_refreshDetails();
_armDrag(index, e);
};
card.ContextMenu = BuildCardMenu(card, item);
return card;
}
private string CardCaption(ConsistItem item, Dynamic car, int index) =>
TrainsetDisplay.VehicleLabel(car,
head: index == 0 && ReferenceEquals(car, Consist.HeadCar(item)) && TrainsetDisplay.IsPowered(car));
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,
Padding = new Thickness(1),
BorderThickness = new Thickness(0, 2),
BorderBrush = Brushes.Transparent,
Cursor = _hand
};
if (!string.IsNullOrWhiteSpace(car.Name) && car.Name != caption)
ToolTip.SetTip(frame, car.Name);
var picked = car;
frame.PointerPressed += (_, _) => PressedCar = picked;
_memberChrome.Add((frame, item, car));
return frame;
}
public void RefreshMemberChrome()
{
foreach (var (badge, item) in _driverChrome)
PaintDriverButton(badge, item);
foreach (var (frame, item, car) in _memberChrome)
{
bool lit = item.Cars.Count > 1 &&
ReferenceEquals(item, _consist.Selected) &&
ReferenceEquals(car, _consist.ActiveCar(item));
frame.BorderBrush = lit ? VehicleCardStyle.BorderSelected : Brushes.Transparent;
}
}
public void RefreshCardSelectionChrome()
{
RefreshMemberChrome();
foreach (var child in consistStack.Children)
{
if (child is not Border { Tag: ConsistItem ci } border)
continue;
bool sel = ReferenceEquals(ci, _consist.Selected);
border.BorderThickness = new Thickness(sel ? 2 : 1);
border.BorderBrush = sel ? VehicleCardStyle.BorderSelected : VehicleCardStyle.Border;
border.Background = sel ? VehicleCardStyle.BackgroundSelected : Brushes.Transparent;
}
}
private void ShowWagonNumberFlyout(Control anchor, Dynamic car)
{
var num = new NumericUpDown
{
Minimum = 0, Maximum = 99999, Increment = 1, FormatString = "0",
Value = Consist.GetWagonNumber(car), MinWidth = 120
};
var apply = new Button { Content = App.Loc["Set"], HorizontalAlignment = HorizontalAlignment.Right };
apply.Classes.Add("Flat");
apply.Classes.Add("Accent");
var panel = new StackPanel { Spacing = 6, Margin = new Thickness(8), MinWidth = 160 };
panel.Children.Add(new TextBlock { Text = App.Loc["WagonNumber"], FontWeight = FontWeight.Bold, FontSize = 12 });
panel.Children.Add(num);
panel.Children.Add(apply);
var flyout = new Flyout { Content = panel, Placement = PlacementMode.Top };
apply.Click += (_, _) =>
{
_consist.SetWagonNumber(car, (int)(num.Value ?? 0));
flyout.Hide();
};
flyout.ShowAt(anchor);
}
public Control BuildCoupler(ConsistItem item, bool trailing = false)
{
var glyph = new TextBlock
{
Text = trailing ? "╡" : "≣",
FontSize = 18,
Opacity = trailing ? 0.55 : 0.7,
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Center
};
var coupler = new Button
{
Padding = new Thickness(2),
MinWidth = 0,
Cursor = _hand,
VerticalAlignment = VerticalAlignment.Center,
Content = glyph,
Flyout = new Flyout
{
Content = BuildCouplingBox(item),
Placement = PlacementMode.Top
}
};
coupler.Classes.Add("Basic");
ToolTip.SetTip(coupler, App.Loc["Coupling"]);
return coupler;
}
private Control BuildCouplingBox(ConsistItem item)
{
var d = Consist.TailCar(item);
int unitIdx = _consist.IndexOf(item);
var panel = new StackPanel { Spacing = 2 };
var copy = new Button
{
Content = "\u00AB",
FontSize = 13,
Padding = new Thickness(6, 1),
Margin = new Thickness(12, 0, 0, 0),
IsEnabled = unitIdx > 0,
Cursor = _hand
};
copy.Classes.Add("Flat");
ToolTip.SetTip(copy, App.Loc["CopyCoupler"]);
copy.Click += (_, _) =>
{
if (unitIdx <= 0) return;
d.Coupling.Flags = Consist.TailCar(_consist[unitIdx - 1]).Coupling.Flags;
_redraw();
};
var header = new DockPanel { Margin = new Thickness(0, 0, 0, 2) };
DockPanel.SetDock(copy, Dock.Right);
header.Children.Add(copy);
header.Children.Add(new TextBlock
{
Text = App.Loc["Coupling"],
FontWeight = FontWeight.Bold,
FontSize = 11,
VerticalAlignment = VerticalAlignment.Center
});
panel.Children.Add(header);
for (int i = 0; i < CouplingBits.BitKeys.Length; i++)
{
int bit = 1 << i;
var check = new CheckBox
{
Content = App.Loc[CouplingBits.BitKeys[i]],
IsChecked = d.Coupling.Has(bit),
FontSize = 11
};
check.IsCheckedChanged += (_, _) => d.Coupling.Set(bit, check.IsChecked == true);
panel.Children.Add(check);
}
var thermo = new CheckBox
{
Content = App.Loc["ThermoAmbient"],
IsChecked = d.Coupling.ThermoDynamic,
FontSize = 11,
Margin = new Thickness(0, 2, 0, 0)
};
thermo.IsCheckedChanged += (_, _) => d.Coupling.ThermoDynamic = thermo.IsChecked == true;
panel.Children.Add(thermo);
return new Border { Padding = new Thickness(8), Child = panel };
}
private const string TextPresentation = "\uFE0E";
private Button SmallButton(string glyph, string tip, Action onClick)
{
var btn = new Button
{
Content = glyph + TextPresentation,
FontSize = 15,
Padding = new Thickness(0),
MinWidth = 0,
MinHeight = 0,
Width = 26,
Height = 22,
HorizontalContentAlignment = HorizontalAlignment.Center,
VerticalContentAlignment = VerticalAlignment.Center,
Cursor = _hand
};
btn.Classes.Add("Basic");
ToolTip.SetTip(btn, tip);
btn.Click += (_, _) => onClick();
return btn;
}
private Button DriverButton(ConsistItem item)
{
var btn = SmallButton("", App.Loc["TipDriver"], () => _consist.CycleDriver(item));
btn.FontWeight = FontWeight.Bold;
_driverChrome.Add((btn, item));
PaintDriverButton(btn, item);
return btn;
}
private void PaintDriverButton(Button btn, ConsistItem item)
{
var type = ReferenceEquals(item, _consist.Selected) && item.Cars.Count > 1
? _consist.ActiveCar(item).DriverType
: item.Driver;
(string glyph, string color) = type switch
{
eDriverType.Headdriver => ("1", "#4CAF50"),
eDriverType.Reardriver => ("2", "#FF9800"),
eDriverType.Passenger => ("P", "#2196F3"),
_ => ("–", "#888888")
};
btn.Content = glyph;
btn.Foreground = new SolidColorBrush(Color.Parse(color));
}
private ContextMenu BuildCardMenu(Control anchor, ConsistItem item)
{
var menu = new ContextMenu();
menu.Opening += (_, _) =>
{
menu.Items.Clear();
var num = new MenuItem { Header = App.Loc["WagonNumber"] };
num.Click += (_, _) => ShowWagonNumberFlyout(anchor, item.Cars[0]);
menu.Items.Add(num);
menu.Items.Add(new Separator());
var copyPrev = new MenuItem
{
Header = App.Loc["LoadCopyPrev"],
IsEnabled = _consist.IndexOf(item) > 0
};
copyPrev.Click += (_, _) => { _cargo.CopyFromPrevious(_consist, item); _redraw(); };
menu.Items.Add(copyPrev);
var copyToAll = new MenuItem
{
Header = App.Loc["LoadCopyToAll"],
IsEnabled = _cargo.IsLoadable(item)
};
copyToAll.Click += (_, _) => { _cargo.CopyToFollowing(_consist, item); _redraw(); };
menu.Items.Add(copyToAll);
var max = new MenuItem
{
Header = App.Loc["LoadMax"],
IsEnabled = _cargo.IsLoadable(item)
};
max.Click += (_, _) => { _cargo.FillUnit(item); _redraw(); };
menu.Items.Add(max);
};
return menu;
}
}

View File

@@ -0,0 +1,597 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Media;
using StarterNG.Classes;
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 StackPanel brakesPanel;
private readonly StackPanel loadsPanel;
private readonly StackPanel damagePanel;
private readonly Button removeVehicleButton;
private readonly VehicleDatabase _db;
private readonly VehicleInfo _info;
private readonly Consist _consist;
private readonly Cargo _cargo;
private readonly Cursor _hand;
private readonly Action _redraw;
private readonly Action _refreshChrome;
private readonly Action<VehicleTexture, StackPanel> _showTextureInfo;
public VehicleDetails(
HeaderedContentControl selectedVehiclePanel,
StackPanel generalPanel, StackPanel consistTexturePanel, StackPanel couplerPanel,
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.brakesPanel = brakesPanel;
this.loadsPanel = loadsPanel;
this.damagePanel = damagePanel;
this.removeVehicleButton = removeVehicleButton;
_db = db;
_info = info;
_consist = consist;
_cargo = cargo;
_hand = hand;
_redraw = redraw;
_refreshChrome = refreshChrome;
_showTextureInfo = showTextureInfo;
}
public void Refresh()
{
generalPanel.Children.Clear();
consistTexturePanel.Children.Clear();
couplerPanel.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"]));
removeVehicleButton.IsEnabled = false;
return;
}
BuildGeneralInfo(_consist.Selected);
if (_db.TextureForSkin(_consist.ActiveCar(_consist.Selected).SkinFile) is { } tex)
_showTextureInfo(tex, consistTexturePanel);
else
consistTexturePanel.Children.Add(DetailHint(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"]));
}
BuildCouplerEditor(_consist.Selected);
BuildBrakesEditor(_consist.Selected);
BuildLoadsEditor(_consist.Selected);
BuildConfigExtras(_consist.Selected);
BuildDamageEditor(_consist.Selected);
}
private void BuildGeneralInfo(ConsistItem item)
{
var car = _consist.ActiveCar(item);
var inv = CultureInfo.InvariantCulture;
bool unit = item.Cars.Count > 1;
var members = item.Cars.Select(_info.PhysicsFor).Where(p => p != null).ToList();
double? length = members.Count == 0 ? null
: unit ? members.Sum(p => p!.Length) : members[0]!.Length;
double? mass = members.Count == 0 ? null
: (unit ? members.Sum(p => p!.Mass) : members[0]!.Mass) / 1000.0;
double? vmax = members.Count == 0 ? null
: unit ? members.Min(p => p!.VMax) : members[0]!.VMax;
generalPanel.Children.Add(InfoRow($"{App.Loc["Length"]} [m]:",
length?.ToString("0.##", inv)));
generalPanel.Children.Add(InfoRow($"{App.Loc["Mass"]} [t]:",
mass?.ToString("0.#", inv)));
generalPanel.Children.Add(InfoRow($"{App.Loc["VMax"]} [km/h]:",
vmax?.ToString("0", inv)));
if (unit)
generalPanel.Children.Add(InfoRow($"{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;
}
private void BuildCouplerEditor(ConsistItem item)
{
int unitIdx = _consist.IndexOf(item);
if (unitIdx < 0)
return;
bool isLast = unitIdx >= _consist.Count - 1;
var d = Consist.TailCar(item);
var grid = new Grid
{
ColumnDefinitions = new ColumnDefinitions("*,8,*"),
RowDefinitions = new RowDefinitions("Auto,Auto,Auto,Auto"),
RowSpacing = 4
};
for (int i = 0; i < CouplingBits.BitKeys.Length; i++)
{
int bit = 1 << i;
var check = new CheckBox
{
Content = App.Loc[CouplingBits.BitKeys[i]],
IsChecked = d.Coupling.Has(bit),
Classes = { "Checklist" }
};
check.IsCheckedChanged += (_, _) =>
{
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);
}
couplerPanel.Children.Add(grid);
var copy = new Button
{
Content = App.Loc["CopyCoupler"],
FontSize = 11,
Cursor = _hand,
HorizontalAlignment = HorizontalAlignment.Stretch,
HorizontalContentAlignment = HorizontalAlignment.Center,
IsEnabled = unitIdx > 0
};
copy.Classes.Add("Flat");
copy.Click += (_, _) =>
{
if (unitIdx <= 0) return;
d.Coupling.Flags = Consist.TailCar(_consist[unitIdx - 1]).Coupling.Flags;
_redraw();
Refresh();
};
var auto = new Button
{
Content = App.Loc["AutoCoupler"],
FontSize = 11,
Cursor = _hand,
HorizontalAlignment = HorizontalAlignment.Stretch,
HorizontalContentAlignment = HorizontalAlignment.Center,
IsEnabled = _consist.Count > 1 && !isLast
};
auto.Classes.Add("Flat");
ToolTip.SetTip(auto, App.Loc["TipAutoCoupler"]);
auto.Click += (_, _) =>
{
_consist.AutoConnectAll();
_redraw();
Refresh();
};
var buttons = new Grid
{
ColumnDefinitions = new ColumnDefinitions("125*,4,48*"),
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);
}
private void BuildConfigExtras(ConsistItem item)
{
var driver = new ComboBox { FontSize = 12, MinWidth = 0 };
foreach (string key in new[] { "DriverHead", "DriverRear", "DriverPassenger", "DriverNobody" })
driver.Items.Add(new ComboBoxItem { Content = App.Loc[key] });
var crewCar = _consist.ActiveCar(item);
driver.SelectedIndex = crewCar.DriverType switch
{
eDriverType.Headdriver => 0,
eDriverType.Reardriver => 1,
eDriverType.Passenger => 2,
_ => 3
};
driver.SelectionChanged += (_, _) =>
{
var picked = driver.SelectedIndex switch
{
0 => eDriverType.Headdriver,
1 => eDriverType.Reardriver,
2 => eDriverType.Passenger,
_ => eDriverType.Nobody
};
if (picked == crewCar.DriverType) return;
crewCar.DriverType = picked;
item.Driver = Consist.UnitDriver(item.Cars);
if (ReferenceEquals(_consist.Selected, item))
_consist.SyncStartingVehicle();
_redraw();
AppState.Instance.NotifyChanged();
};
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 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;
var switches = new Grid
{
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);
}
private static TextBlock UnitScopeHint(string text) => new()
{
Text = text,
FontSize = 11,
Opacity = 0.6,
TextWrapping = TextWrapping.Wrap,
Margin = new Thickness(0, 0, 0, 2)
};
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 });
private void BuildBrakesEditor(ConsistItem item)
{
var brake = item.Cars[0].Coupling.GetBrake();
ComboBox Combo(string?[] codes, string? current, Func<string?, string> label)
{
var combo = new ComboBox { FontSize = 12, MinWidth = 0 };
foreach (var code in codes) AddOption(combo, label(code), code);
combo.SelectedIndex = Math.Max(0, Array.IndexOf(codes, current));
return combo;
}
static string?[] WithDefault(string[] codes) =>
new string?[] { null }.Concat(codes).ToArray();
var modeCombo = Combo(WithDefault(BrakeSetting.Modes), brake?.Mode, BrakeModeLabel);
var switchCombo = Combo(WithDefault(BrakeSetting.Switches), brake?.Switch, SwitchLabel);
var loadCombo = Combo(WithDefault(BrakeSetting.Loads), brake?.Load, LoadLabel);
void Apply()
{
var b = new BrakeSetting
{
Mode = (modeCombo.SelectedItem as ComboBoxItem)?.Tag as string,
Switch = (switchCombo.SelectedItem as ComboBoxItem)?.Tag as string,
Load = (loadCombo.SelectedItem as ComboBoxItem)?.Tag as string
};
foreach (var c in Consist.UnitTargets(item)) c.Coupling.SetBrake(b);
}
modeCombo.SelectionChanged += (_, _) => Apply();
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));
}
private static string BrakeModeLabel(string? code) => code switch
{
"G" => App.Loc["BrakeFreight"],
"P" => App.Loc["BrakePassenger"],
"R" => App.Loc["BrakeExpress"],
"M" => App.Loc["BrakeExpressMg"],
"Q" => App.Loc["BrakeNoAir"],
_ => App.Loc["BrakeActingNone"]
};
private static string LoadLabel(string? code) => code switch
{
"T" => App.Loc["LoadEmpty"],
"H" => App.Loc["LoadMedium"],
"F" => App.Loc["LoadFull"],
_ => App.Loc["LoadNeutral"]
};
private static string SwitchLabel(string? code) => code switch
{
"0" => App.Loc["SwitchOff"],
"1" => App.Loc["SwitchOff10"],
_ => App.Loc["SwitchNormal"]
};
private void BuildDamageEditor(ConsistItem item)
{
var w = _consist.ActiveCar(item).Coupling.GetWheels() ?? new WheelSettings();
NumericUpDown Spin(int value, int max) => new()
{
FontSize = 12, Minimum = 0, Maximum = max, Increment = 1,
Value = value, MinWidth = 0, Width = 100, FormatString = "0"
};
var sway = Spin(w.Sway, 100);
var flat = Spin(w.Flatness, 100);
var flatRand = Spin(w.FlatnessRand, 100);
var flatProb = Spin(w.FlatnessProb, 100);
void Apply()
{
var ws = new WheelSettings
{
Sway = (int)(sway.Value ?? 0),
Flatness = (int)(flat.Value ?? 0),
FlatnessRand = (int)(flatRand.Value ?? 0),
FlatnessProb = (int)(flatProb.Value ?? 0)
};
foreach (var c in _consist.MemberTargets(item))
c.Coupling.SetWheels(ws);
}
sway.ValueChanged += (_, _) => Apply();
flat.ValueChanged += (_, _) => Apply();
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);
}
private Control BuildConsistLoadTools()
{
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;
}
private Button LoadToolButton(string text, Action onClick)
{
var b = new Button
{
Content = text, FontSize = 11, Margin = new Thickness(0, 0, 4, 4),
Padding = new Thickness(8, 3), Cursor = _hand,
IsEnabled = _consist.Count > 0
};
b.Classes.Add("Flat");
b.Click += (_, _) => onClick();
return b;
}
private void BuildLoadsEditor(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;
}
if (!string.IsNullOrEmpty(lead.LoadType) &&
!available.Contains(lead.LoadType!, StringComparer.OrdinalIgnoreCase))
available.Insert(0, lead.LoadType!);
var typeCombo = new ComboBox { FontSize = 12, MinWidth = 0 };
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 });
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;
}
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"
};
var pantHint = DetailHint(App.Loc["LoadPantStateHint"]);
pantHint.IsVisible = lead.IsPantState;
string? SelectedType() => (typeCombo.SelectedItem as ComboBoxItem)?.Tag as string;
void Apply()
{
string? type = SelectedType();
int count = (int)(countBox.Value ?? 0);
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));
}
}
typeCombo.SelectionChanged += (_, _) =>
{
string? type = SelectedType();
pantHint.IsVisible = Dynamic.IsPantStateType(type);
countBox.Maximum = _cargo.LimitFor(lead, type);
Apply();
};
countBox.ValueChanged += (_, _) => Apply();
loadsPanel.Children.Add(LabeledRow(App.Loc["LoadType"], typeCombo));
loadsPanel.Children.Add(LabeledRow(App.Loc["LoadCount"], countBox));
loadsPanel.Children.Add(pantHint);
}
}

View File

@@ -0,0 +1,231 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Media;
using StarterNG.Classes;
namespace StarterNG.Views;
public sealed class Warehouse
{
private readonly ListBox _list;
private readonly Button _removeButton;
private readonly Cursor _hand;
private readonly Func<Trainset?> _target;
private readonly Action<Trainset, List<Dynamic>> _apply;
public Warehouse(ListBox list, Button removeButton, Cursor hand,
Func<Trainset?> target, Action<Trainset, List<Dynamic>> apply)
{
_list = list;
_removeButton = removeButton;
_hand = hand;
_target = target;
_apply = apply;
}
public void Refresh()
{
string? current = SelectedName();
_list.Items.Clear();
foreach (var preset in PresetStore.All())
{
var entry = new ListBoxItem
{
Tag = preset.Name,
Content = new TextBlock
{
Text = preset.Name,
TextTrimming = TextTrimming.CharacterEllipsis
}
};
_list.Items.Add(entry);
if (current is not null && string.Equals(current, preset.Name, StringComparison.Ordinal))
_list.SelectedItem = entry;
}
_list.ContextMenu ??= BuildMenu();
_removeButton.IsEnabled = _list.Items.Count > 0;
}
private string? SelectedName() => (_list.SelectedItem as ListBoxItem)?.Tag as string;
private static TrainsetPreset? PresetNamed(string name) =>
PresetStore.All().FirstOrDefault(p => string.Equals(p.Name, name, StringComparison.Ordinal));
private ContextMenu BuildMenu()
{
var menu = new ContextMenu();
menu.Opening += (_, _) =>
{
menu.Items.Clear();
bool hasPreset = SelectedName() != null;
bool hasTarget = _target() != null;
void Add(string key, Action run, bool enabled = true)
{
var mi = new MenuItem { Header = App.Loc[key], IsEnabled = enabled };
mi.Click += (_, _) => run();
menu.Items.Add(mi);
}
Add("WarehouseApply", ApplySelected, hasPreset && hasTarget);
Add("WarehouseAppend", AppendSelected, hasPreset && hasTarget);
Add("ChangeName", RenameSelected, hasPreset);
menu.Items.Add(new Separator());
Add("PresetImportMagazyn", () => _ = ImportMagazynAsync());
Add("WarehouseImportFile", () => _ = ImportFileAsync());
menu.Items.Add(new Separator());
var sort = new MenuItem { Header = App.Loc["PresetSort"] };
sort.Items.Add(SortItem(PresetSort.Name, "PresetSortName"));
sort.Items.Add(SortItem(PresetSort.Track, "PresetSortTrack"));
menu.Items.Add(sort);
};
return menu;
}
private MenuItem SortItem(PresetSort mode, string locKey)
{
var item = new MenuItem
{
Header = (PresetStore.SortMode == mode ? "● " : "○ ") + App.Loc[locKey]
};
item.Click += (_, _) => { PresetStore.SortMode = mode; Refresh(); };
return item;
}
public async Task SaveCurrentAsync()
{
if (_target() is not { } trainset)
return;
string name = trainset.Name;
if (string.IsNullOrWhiteSpace(name))
name = App.Loc["PresetEmpty"];
try
{
PresetStore.Save(name, ConsistText.Serialize(trainset));
}
catch (Exception ex)
{
await Infrastructure.Diagnostics.ReportAsync(PresetStore.FilePath, ex);
return;
}
Refresh();
}
public void ApplySelected()
{
if (SelectedName() is not { } name || _target() is not { } target)
return;
if (PresetNamed(name) is not { } preset)
return;
if (ConsistText.VehiclesFrom(preset.Entry) is { } vehicles)
_apply(target, vehicles);
}
public void AppendSelected()
{
if (SelectedName() is not { } name || _target() is not { } target)
return;
var vehicles = ConsistText.VehiclesFrom(PresetNamed(name)?.Entry);
if (vehicles is null || vehicles.Count == 0)
return;
var merged = new List<Dynamic>(target.Vehicles);
merged.AddRange(vehicles);
_apply(target, merged);
}
public void RenameSelected()
{
if (SelectedName() is not { } name || PresetNamed(name) is not { } preset)
return;
var nameBox = new TextBox { Text = name, MinWidth = 240 };
var ok = new Button { Content = App.Loc["Ok"], Cursor = _hand };
ok.Classes.Add("Accent");
var panel = new StackPanel { Spacing = 6, Margin = new Thickness(8) };
panel.Children.Add(new TextBlock
{
Text = App.Loc["PresetNamePrompt"],
FontWeight = FontWeight.Bold,
FontSize = 12
});
panel.Children.Add(nameBox);
panel.Children.Add(ok);
var flyout = new Flyout { Content = panel };
ok.Click += (_, _) =>
{
string? fresh = nameBox.Text?.Trim();
if (!string.IsNullOrEmpty(fresh) && !string.Equals(fresh, name, StringComparison.Ordinal))
{
PresetStore.Save(fresh, preset.Entry);
PresetStore.Delete(name);
Refresh();
}
flyout.Hide();
};
flyout.ShowAt(_list);
nameBox.Focus();
}
public async Task ImportFileAsync()
{
if (TopLevel.GetTopLevel(_list) is not { } top)
return;
var picked = await top.StorageProvider.OpenFilePickerAsync(
new Avalonia.Platform.Storage.FilePickerOpenOptions
{
Title = App.Loc["WarehouseImportFile"],
AllowMultiple = false
});
string? path = picked.FirstOrDefault()?.Path.LocalPath;
if (string.IsNullOrEmpty(path))
return;
await ReportImport(PresetStore.ImportMagazyn(path));
}
public async Task ImportMagazynAsync() => await ReportImport(PresetStore.ImportMagazyn());
private async Task ReportImport(int added)
{
Refresh();
if (added == 0)
await Infrastructure.Diagnostics.ReportAsync(App.Loc["WarehouseImportNothing"]);
}
public async Task RemoveSelectedAsync()
{
if (SelectedName() is not { } name)
return;
try
{
PresetStore.Delete(name);
}
catch (Exception ex)
{
await Infrastructure.Diagnostics.ReportAsync(PresetStore.FilePath, ex);
return;
}
Refresh();
}
}