diff --git a/StarterNG/Views/Depot.axaml b/StarterNG/Views/Depot.axaml
index 04d6870..477b399 100644
--- a/StarterNG/Views/Depot.axaml
+++ b/StarterNG/Views/Depot.axaml
@@ -24,6 +24,11 @@
+
-
+
+
+
+
+
@@ -78,6 +99,11 @@
+
@@ -102,10 +128,26 @@
-
-
-
+
+
+
+
+
+
+
+
+
+
diff --git a/StarterNG/Views/Depot.axaml.cs b/StarterNG/Views/Depot.axaml.cs
index 9bfbf0e..88b8f37 100644
--- a/StarterNG/Views/Depot.axaml.cs
+++ b/StarterNG/Views/Depot.axaml.cs
@@ -3,14 +3,18 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
+using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
+using Avalonia.Input.Platform;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Threading;
+using Avalonia.VisualTree;
using StarterNG.Classes;
+using StarterNG.Domain;
namespace StarterNG.Views;
@@ -30,7 +34,8 @@ public partial class Depot : UserControl
// Shared, size-limited thumbnail cache (decoded down to display height).
private readonly Dictionary _miniCache = new();
- private const int ConsistThumbHeight = 34;
+ private int ConsistThumbHeight =>
+ StarterNG.Classes.Settings.Instance.LargeThumbnails ? 68 : 34;
private readonly Cursor _hand = new(StandardCursorType.Hand);
private int _nameCounter;
@@ -55,6 +60,22 @@ public partial class Depot : UserControl
// and the single Add button)
private VehicleTexture? _browserSelected;
+ // drag-drop payload while a pointer drag is in progress
+ private VehicleTexture? _dragTexture;
+ private int _dragConsistIndex = -1;
+ private const string DragVehicleFormat = "starterng/vehicle";
+ private const string DragConsistFormat = "starterng/consist-index";
+
+ // Armed on press, started only after the pointer moves past DragThreshold.
+ // Starting DoDragDropAsync on every click freezes Avalonia (pointer capture).
+ private PointerPressedEventArgs? _pendingDragPress;
+ private Point _pendingDragOrigin;
+ private VehicleTexture? _pendingDragTexture;
+ private int _pendingDragConsistIndex = -1;
+ private ConsistItem? _pendingBrowserSync;
+ private bool _dragSessionActive;
+ private const double DragThreshold = 6;
+
// hard cap on how many entries the flat list shows at once
private const int MaxListRows = 2000;
@@ -144,11 +165,22 @@ public partial class Depot : UserControl
// Defer population so the tab renders immediately, then fills in.
Dispatcher.UIThread.Post(() =>
{
+ hideArchivalCheck.IsChecked = StarterNG.Classes.Settings.Instance.HideArchivalVehicles;
PopulateCategoryCombo(); // -> populates class combo -> builds browser
PopulateSceneryConsists(); // -> lists the current scenery's consists
RebuildConsist();
}, DispatcherPriority.Background);
+ // Consist strip accepts drops from the browser and reorders via card drag.
+ DragDrop.SetAllowDrop(consistDropTarget, true);
+ consistDropTarget.AddHandler(DragDrop.DragOverEvent, Consist_OnDragOver);
+ consistDropTarget.AddHandler(DragDrop.DropEvent, Consist_OnDrop);
+
+ 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);
+
// Switching tabs only toggles IsVisible (the view stays in the visual
// tree), so refresh whenever the depot becomes visible - this picks up a
// scenery changed in the Scenarios tab in the meantime.
@@ -162,12 +194,24 @@ public partial class Depot : UserControl
private void SyncFromSelection()
{
+ // pick up Settings-tab changes to the archival-vehicle filter
+ bool hideArchival = StarterNG.Classes.Settings.Instance.HideArchivalVehicles;
+ if (hideArchivalCheck.IsChecked != hideArchival)
+ {
+ _suppress = true;
+ hideArchivalCheck.IsChecked = hideArchival;
+ _suppress = false;
+ BuildList();
+ }
+
// refresh the on-map consists list to the scenery picked in the Scenarios tab
PopulateSceneryConsists();
var trainset = AppState.Instance.CurrentTrainset;
if (trainset != null && !ReferenceEquals(trainset, _editingTrainset))
LoadTrainset(trainset);
+ else if (_editingTrainset != null)
+ RebuildConsist(); // pick up LargeThumbnails / filter-driven list changes
}
// Loads a scenery trainset into the editor and binds edits back to it.
@@ -214,7 +258,9 @@ public partial class Depot : UserControl
{
Cars = group,
Grouped = true,
- Driver = group.Select(c => c.DriverType).FirstOrDefault(d => d != eDriverType.Nobody)
+ // FirstOrDefault(predicate) defaults the enum to Headdriver(0) when
+ // every car is Nobody — that re-staffed decorative consists (#12).
+ Driver = UnitDriver(group)
});
i = j;
continue;
@@ -236,7 +282,7 @@ public partial class Depot : UserControl
{
Cars = group,
Grouped = true,
- Driver = group.Select(c => c.DriverType).FirstOrDefault(d => d != eDriverType.Nobody)
+ Driver = UnitDriver(group)
});
i = j;
continue;
@@ -253,9 +299,23 @@ public partial class Depot : UserControl
}
_selected = _consist.FirstOrDefault();
+ SyncStartingVehicle();
RebuildConsist();
}
+ // First staffed occupancy on the unit, or Nobody when the whole unit is unstaffed.
+ private static eDriverType UnitDriver(IReadOnlyList group) =>
+ group.Select(c => c.DriverType)
+ .Where(d => d != eDriverType.Nobody)
+ .DefaultIfEmpty(eDriverType.Nobody)
+ .First();
+
+ private void SyncStartingVehicle()
+ {
+ if (_selected?.Cars.Count > 0)
+ AppState.Instance.StartingVehicleName = _selected.Cars[0].Name;
+ }
+
// ----------------------------------------------------- series / unit helpers
// Removes a trailing car-type suffix that directly follows a digit
@@ -307,10 +367,11 @@ public partial class Depot : UserControl
private void RebuildClassCombo()
{
// No "All" entry: an unselected class means "every class in the category".
+ // Each class row shows its category mini thumbnail (like the Pascal starter).
_suppress = true;
classCombo.Items.Clear();
foreach (string cls in ClassesForCategory(_categoryFilter))
- classCombo.Items.Add(new ComboBoxItem { Content = cls, Tag = cls });
+ classCombo.Items.Add(new ComboBoxItem { Content = ClassComboContent(cls), Tag = cls });
classCombo.SelectedIndex = -1;
_suppress = false;
@@ -318,9 +379,34 @@ public partial class Depot : UserControl
BuildList();
}
+ private Control ClassComboContent(string cls)
+ {
+ var row = new StackPanel
+ {
+ Orientation = Orientation.Horizontal,
+ Spacing = 8,
+ VerticalAlignment = VerticalAlignment.Center
+ };
+ var bmp = GetMiniBitmap(cls, 28);
+ if (bmp != null)
+ row.Children.Add(new Image
+ {
+ Source = bmp, Height = 28, Width = 56,
+ Stretch = Stretch.Uniform, VerticalAlignment = VerticalAlignment.Center
+ });
+ row.Children.Add(new TextBlock
+ {
+ Text = cls,
+ VerticalAlignment = VerticalAlignment.Center
+ });
+ return row;
+ }
+
private IEnumerable ClassesForCategory(Func? category) =>
_db.Textures
.Where(t => category == null || category(CategoryOf(t)))
+ .Where(t => !_db.IsSetFollower(t))
+ .Where(t => !StarterNG.Classes.Settings.Instance.HideArchivalVehicles || !t.ResolvedArchived)
.Select(ClassOf)
.Where(s => !string.IsNullOrEmpty(s))
.Distinct(StringComparer.OrdinalIgnoreCase)
@@ -393,11 +479,13 @@ public partial class Depot : UserControl
foreach (var texture in matched)
{
- vehicleListBox.Items.Add(new ListBoxItem
+ var row = new ListBoxItem
{
Content = BrowserLabel(texture, _db.ResolveSet(texture)),
Tag = texture
- });
+ };
+ row.ContextMenu = BuildBrowserMenu(texture);
+ vehicleListBox.Items.Add(row);
}
if (vehicleListBox.Items.Count == 0)
@@ -416,15 +504,75 @@ public partial class Depot : UserControl
_browserSelected = texture;
miniPreview.Source = GetMiniBitmap(_db.ResolveMiniName(texture), 54);
addVehicleButton.IsEnabled = true;
+ ShowTextureInfo(texture);
}
else
{
_browserSelected = null;
miniPreview.Source = null;
addVehicleButton.IsEnabled = false;
+ textureInfoPanel.Children.Clear();
}
}
+ private ContextMenu BuildBrowserMenu(VehicleTexture texture)
+ {
+ var menu = new ContextMenu();
+ var copy = new MenuItem { Header = App.Loc["CopyTextureName"] };
+ copy.Click += async (_, _) =>
+ {
+ var top = TopLevel.GetTopLevel(this);
+ if (top?.Clipboard != null)
+ await top.Clipboard.SetTextAsync(BrowserLabel(texture, _db.ResolveSet(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)) return;
+ try
+ {
+ System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(dir)
+ {
+ UseShellExecute = true
+ });
+ }
+ catch { }
+ };
+ menu.Items.Add(open);
+ return menu;
+ }
+
+ private void ShowTextureInfo(VehicleTexture texture)
+ {
+ textureInfoPanel.Children.Clear();
+ void Row(string label, string? value)
+ {
+ if (string.IsNullOrWhiteSpace(value)) return;
+ textureInfoPanel.Children.Add(new TextBlock
+ {
+ Text = $"{label}: {value}",
+ FontSize = 10,
+ Opacity = 0.85,
+ TextTrimming = TextTrimming.CharacterEllipsis
+ });
+ }
+
+ Row(App.Loc["TextureInfo"], Base(texture.Skinfile));
+ var meta = texture.Meta;
+ if (meta == null) return;
+ Row(App.Loc["TexOperator"], meta.Operator);
+ Row(App.Loc["TexStation"], meta.Depot);
+ Row(App.Loc["TexRevision"], meta.RevisionDate);
+ Row(App.Loc["TexAuthor"], meta.TextureAuthor);
+ Row(App.Loc["TexPhoto"], meta.PhotoAuthor);
+ }
+
// Double-click an entry to replace the selected consist vehicle.
private void VehicleListBox_OnDoubleTapped(object? sender, Avalonia.Input.TappedEventArgs e)
{
@@ -440,6 +588,36 @@ public partial class Depot : UserControl
AddTexture(_browserSelected);
}
+ // Pascal actAddVehiclesCategory — every skin currently listed in the browser.
+ private void AddAllCategoryButton_OnClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
+ {
+ foreach (var t in CurrentBrowserTextures())
+ InsertTextureAt(t, _consist.Count);
+ }
+
+ // Pascal actAddVehiclesMMD — first skin per distinct model/MMD.
+ private void AddUniqueMmdButton_OnClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
+ {
+ var seen = new HashSet(StringComparer.OrdinalIgnoreCase);
+ foreach (var t in CurrentBrowserTextures())
+ {
+ string key = string.IsNullOrEmpty(t.Model) ? t.Skinfile : t.Model!;
+ if (!seen.Add(key)) continue;
+ InsertTextureAt(t, _consist.Count);
+ }
+ }
+
+ private List CurrentBrowserTextures()
+ {
+ var list = new List();
+ foreach (var obj in vehicleListBox.Items)
+ {
+ if (obj is ListBoxItem { Tag: VehicleTexture t, IsEnabled: true })
+ list.Add(t);
+ }
+ return list;
+ }
+
private bool PassesFilters(VehicleTexture t, string search, bool hasSearch)
{
if (_categoryFilter != null && !_categoryFilter(CategoryOf(t)))
@@ -449,6 +627,13 @@ public partial class Depot : UserControl
!string.Equals(ClassOf(t), _classFilter, StringComparison.OrdinalIgnoreCase))
return false;
+ // Hide subsequent multi-unit cars; adding the lead expands the whole set.
+ if (_db.IsSetFollower(t))
+ return false;
+
+ if (StarterNG.Classes.Settings.Instance.HideArchivalVehicles && t.ResolvedArchived)
+ return false;
+
if (hasSearch && !Matches(t, search))
return false;
@@ -496,9 +681,15 @@ public partial class Depot : UserControl
private string BrowserLabel(VehicleTexture texture, IReadOnlyList? set)
{
- string name = !string.IsNullOrEmpty(texture.TextureMini)
- ? texture.TextureMini!
- : Base(texture.Skinfile);
+ // When texture_mini is missing or only the class/category mini, use the
+ // skin basename so the list doesn't show the category name as the vehicle.
+ string name;
+ if (!string.IsNullOrEmpty(texture.TextureMini) &&
+ !string.Equals(texture.TextureMini, texture.ResolvedClass, StringComparison.OrdinalIgnoreCase))
+ name = texture.TextureMini!;
+ else
+ name = Base(texture.Skinfile);
+
if (!string.IsNullOrEmpty(texture.Meta?.Operator))
name += $" · {texture.Meta!.Operator}";
if (set is { Count: > 1 })
@@ -506,10 +697,20 @@ public partial class Depot : UserControl
return name;
}
- // Clicking "Add" appends to the consist. If the texture belongs to an
- // auto-consist set, the whole set is added as one locked unit; otherwise the
- // single vehicle is added.
+ // Clicking "Add" inserts after the selected vehicle (or appends). If the
+ // texture belongs to an auto-consist set, the whole set is one locked unit.
private void AddTexture(VehicleTexture texture)
+ {
+ int at = _consist.Count;
+ if (_selected != null)
+ {
+ int si = _consist.IndexOf(_selected);
+ if (si >= 0) at = si + 1;
+ }
+ InsertTextureAt(texture, at);
+ }
+
+ private void InsertTextureAt(VehicleTexture texture, int at)
{
var set = _db.ResolveSet(texture);
var unit = set ?? new List { texture };
@@ -522,19 +723,36 @@ public partial class Depot : UserControl
Flipped = false
};
- // insert after the selected vehicle, or append when nothing is selected
- int at = _consist.Count;
- if (_selected != null)
- {
- int si = _consist.IndexOf(_selected);
- if (si >= 0) at = si + 1;
- }
+ at = Math.Clamp(at, 0, _consist.Count);
+ MatchOccupancy(item, at);
_consist.Insert(at, item);
_selected = item;
AutoConnectAll();
RebuildConsist();
}
+ // Like the Pascal MatchOccupancy: only powered units get a driver, and only
+ // when the consist has no crew yet (or the unit is placed at the front).
+ private void MatchOccupancy(ConsistItem item, int position)
+ {
+ item.Driver = eDriverType.Nobody;
+ var lead = item.Cars.FirstOrDefault();
+ if (lead is null) return;
+
+ var tex = _db.TextureForSkin(lead.SkinFile);
+ string? cat = tex != null ? CategoryOf(tex) : null;
+ if (!IsPoweredCategory(cat))
+ return;
+
+ bool staffed = _consist.Any(i =>
+ i.Driver is eDriverType.Headdriver or eDriverType.Reardriver);
+ if (position == 0 || !staffed)
+ item.Driver = eDriverType.Headdriver;
+ }
+
+ private static bool IsPoweredCategory(string? c) =>
+ c is "e" or "s" or "p" or "z" or "a";
+
// ------------------------------------------------------------- scenery combos
// Lists the consists of the scenery currently selected in the Scenarios tab,
@@ -551,16 +769,17 @@ public partial class Depot : UserControl
if (_listScenery != null)
{
+ bool showAi = StarterNG.Classes.Settings.Instance.ShowAiVehicles;
+ bool drivableOnly = StarterNG.Classes.Settings.Instance.DrivableOnly;
+
foreach (var trainset in _listScenery.Trainsets)
{
- if (trainset.Vehicles.Count == 0)
- continue;
- if (trainset.Vehicles.All(v => string.IsNullOrWhiteSpace(v.Name)))
- continue;
+ if (IsAiTrainset(trainset) && !showAi) continue;
+ if (trainset.Decor && drivableOnly) continue;
var entry = new ListBoxItem
{
- Content = ConsistLabel(trainset),
+ Content = ConsistListContent(trainset),
Tag = trainset
};
// right-click: warehouse save/load, clipboard copy/paste, clear-all
@@ -588,9 +807,14 @@ public partial class Depot : UserControl
AppState.Instance.CurrentScenery = _listScenery;
AppState.Instance.CurrentTrainset = trainset;
+ AppState.Instance.StartingVehicleName = TrainsetDisplay.DefaultStartingVehicle(trainset);
LoadTrainset(trainset);
}
+ private static bool IsAiTrainset(Trainset trainset) =>
+ !string.IsNullOrEmpty(trainset.Description) &&
+ trainset.Description.TrimStart().StartsWith("-", StringComparison.Ordinal);
+
// Shift+Delete clears the selected scenery consist (same as the context-menu
// "remove all vehicles" entry).
private void SceneryConsistList_OnKeyDown(object? sender, KeyEventArgs e)
@@ -626,13 +850,30 @@ public partial class Depot : UserControl
PopulateSceneryConsists();
}
- // Plain-text label for a scenery consist (no thumbnails).
+ // List caption for a scenery consist — grows with the list width (ellipsis).
+ private static Control ConsistListContent(Trainset trainset) =>
+ new TextBlock
+ {
+ Text = ConsistLabel(trainset),
+ TextTrimming = TextTrimming.CharacterEllipsis,
+ TextWrapping = TextWrapping.NoWrap
+ };
+
private static string ConsistLabel(Trainset trainset)
{
- string text = string.Join(" + ", trainset.Vehicles.Select(v => v.SkinFile));
- if (string.IsNullOrWhiteSpace(text))
- text = string.Join(" + ", trainset.Vehicles.Select(v => v.Name));
- return text.Length > 80 ? text[..77] + "…" : text;
+ string? text = TrainsetDisplay.CompositionText(trainset);
+ if (text != null)
+ return text;
+ return string.Format(App.Loc["EmptyTrainset"], trainset.Track ?? "");
+ }
+
+ private void RefreshConsistListLabels()
+ {
+ foreach (var obj in sceneryConsistList.Items)
+ {
+ if (obj is ListBoxItem { Tag: Trainset ts } item)
+ item.Content = ConsistListContent(ts);
+ }
}
// ------------------------------------------------------------ consist edits
@@ -660,7 +901,32 @@ public partial class Depot : UserControl
{
if (string.IsNullOrWhiteSpace(baseName)) baseName = "vehicle";
baseName = baseName.Replace(' ', '_');
- return $"{baseName}_{++_nameCounter}";
+
+ var used = new HashSet(StringComparer.OrdinalIgnoreCase);
+ foreach (var item in _consist)
+ foreach (var c in item.Cars)
+ if (!string.IsNullOrEmpty(c.Name))
+ used.Add(c.Name!);
+
+ var scn = _listScenery ?? AppState.Instance.CurrentScenery;
+ if (scn != null)
+ {
+ foreach (string n in scn.LooseVehicleNames)
+ used.Add(n);
+ foreach (var ts in scn.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)
@@ -714,7 +980,10 @@ public partial class Depot : UserControl
eDriverType.Reardriver => eDriverType.Passenger,
_ => eDriverType.Nobody
};
+ if (ReferenceEquals(_selected, item))
+ SyncStartingVehicle();
RebuildConsist();
+ AppState.Instance.NotifyChanged(); // crew change may enable/disable Start
}
// Breaks a grouped unit into individual single-car items.
@@ -742,17 +1011,17 @@ public partial class Depot : UserControl
private void RebuildConsist()
{
- // The first vehicle drives from cab A by default (unless a crew is set).
- if (_consist.Count > 0 && _consist.All(i => i.Driver == eDriverType.Nobody))
- _consist[0].Driver = eDriverType.Headdriver;
-
consistStack.Children.Clear();
for (int i = 0; i < _consist.Count; i++)
{
var item = _consist[i];
- consistStack.Children.Add(BuildCard(item));
+ consistStack.Children.Add(BuildCard(item, i));
+ // Coupler between units, plus a trailing coupler after the last unit
+ // so end-of-train coupling / lights can be set (#10).
if (i < _consist.Count - 1)
consistStack.Children.Add(BuildCoupler(item));
+ else
+ consistStack.Children.Add(BuildCoupler(item, trailing: true));
}
emptyHint.IsVisible = _consist.Count == 0;
@@ -780,6 +1049,7 @@ public partial class Depot : UserControl
}
}
_editingTrainset.Vehicles = flat;
+ RefreshConsistListLabels();
}
// Resolves the .fiz physics for a consist car: the .fiz is named after the
@@ -830,42 +1100,24 @@ public partial class Depot : UserControl
}
}
- // Train totals (length / mass / Vmax / load) read from the .fiz files.
+ // Train totals — same Pascal mass rules as Scenarios (IncludeVehicleToMass + 200 t retry).
private void UpdateTrainStats()
{
- if (_consist.Count == 0)
+ if (_editingTrainset is null)
{
trainStats.Text = "";
return;
}
- double lengthM = 0, massKg = 0, loadKg = 0;
- double vmax = double.PositiveInfinity;
-
- foreach (var item in _consist)
- foreach (var c in item.Cars)
- {
- var p = PhysicsFor(c);
- if (p != null)
- {
- lengthM += p.Length;
- massKg += p.Mass;
- if (p.VMax > 0) vmax = Math.Min(vmax, p.VMax);
- }
- if (c.LoadCount > 0 && !string.IsNullOrEmpty(c.LoadType))
- loadKg += (double)c.LoadCount * LoadWeight(c.LoadType);
- }
-
- var inv = System.Globalization.CultureInfo.InvariantCulture;
- string v = double.IsInfinity(vmax) ? "—" : $"{vmax.ToString("0", inv)} km/h";
- trainStats.Text =
- $"{App.Loc["Length"]}: {lengthM.ToString("0.#", inv)} m · " +
- $"{App.Loc["Mass"]}: {(massKg / 1000.0).ToString("0.#", inv)} t · " +
- $"Vmax: {v} · " +
- $"{App.Loc["Load"]}: {(loadKg / 1000.0).ToString("0.#", inv)} t";
+ trainStats.Text = TrainsetDisplay.FormatStats(
+ _editingTrainset,
+ PhysicsFor,
+ App.Loc["Length"], App.Loc["Mass"], App.Loc["VehicleCount"], App.Loc["Track"],
+ car => _db.TextureForSkin(car.SkinFile) is { } t ? CategoryOf(t) : null,
+ LoadWeight);
}
- private Control BuildCard(ConsistItem item)
+ private Control BuildCard(ConsistItem item, int index)
{
bool selected = ReferenceEquals(_selected, item);
@@ -925,7 +1177,6 @@ public partial class Depot : UserControl
controls.Children.Add(SmallButton("◀", App.Loc["TipMoveLeft"], () => MoveLeft(item)));
controls.Children.Add(DriverButton(item));
controls.Children.Add(SmallButton("⇄", App.Loc["TipFlip"], () => FlipItem(item)));
- controls.Children.Add(SmallButton("✕", App.Loc["TipRemove"], () => RemoveItem(item)));
controls.Children.Add(SmallButton("▶", App.Loc["TipMoveRight"], () => MoveRight(item)));
var inner = new StackPanel { Spacing = 4 };
@@ -958,58 +1209,96 @@ public partial class Depot : UserControl
BorderBrush = selected ? CardBorderSel : CardBorder,
Background = selected ? CardBgSel : Brushes.Transparent,
Cursor = _hand,
- Child = inner
+ Child = inner,
+ Tag = item
};
card.PointerPressed += (_, e) =>
{
- // left button only - right-click is reserved for the context popup,
- // and rebuilding here would detach the card it anchors to
+ // left button only - right-click is reserved for the context popup
if (!e.GetCurrentPoint(card).Properties.IsLeftButtonPressed)
return;
+ // Buttons on the card (split / remove) handle their own clicks.
+ if (e.Source is Control src && src.GetVisualAncestors().OfType