mirror of
https://github.com/MaSzyna-EU07/StarterNG.git
synced 2026-10-16 23:49:19 +02:00
split depot into domain and view classes
This commit is contained in:
147
StarterNG/Domain/Cargo.cs
Normal file
147
StarterNG/Domain/Cargo.cs
Normal 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
582
StarterNG/Domain/Consist.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
73
StarterNG/Domain/LoadWeights.cs
Normal file
73
StarterNG/Domain/LoadWeights.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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)];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
30
StarterNG/Domain/VehicleInfo.cs
Normal file
30
StarterNG/Domain/VehicleInfo.cs
Normal 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
396
StarterNG/Views/Depot/ConsistDragging.cs
Normal file
396
StarterNG/Views/Depot/ConsistDragging.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
StarterNG/Views/Depot/CouplingBits.cs
Normal file
8
StarterNG/Views/Depot/CouplingBits.cs
Normal 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" };
|
||||||
|
}
|
||||||
42
StarterNG/Views/Depot/MiniTextures.cs
Normal file
42
StarterNG/Views/Depot/MiniTextures.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
166
StarterNG/Views/Depot/TextureRandomizerDialog.cs
Normal file
166
StarterNG/Views/Depot/TextureRandomizerDialog.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
575
StarterNG/Views/Depot/VehicleBrowser.cs
Normal file
575
StarterNG/Views/Depot/VehicleBrowser.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
StarterNG/Views/Depot/VehicleCardStyle.cs
Normal file
15
StarterNG/Views/Depot/VehicleCardStyle.cs
Normal 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;
|
||||||
|
}
|
||||||
447
StarterNG/Views/Depot/VehicleCards.cs
Normal file
447
StarterNG/Views/Depot/VehicleCards.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
597
StarterNG/Views/Depot/VehicleDetails.cs
Normal file
597
StarterNG/Views/Depot/VehicleDetails.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
231
StarterNG/Views/Depot/Warehouse.cs
Normal file
231
StarterNG/Views/Depot/Warehouse.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user