Lay out and drive the depot browser the way the old starter did

Picking a category picks its first class and lists that class, instead of
every vehicle of the category; only categories with something in them are
offered, in the old order. The archival switch has its own line, the class
box shows the thumbnail of the class picked, and the search sits above the
list and looks through every vehicle. The rows are the bare texture names, a
set marked faintly; an empty list says what to do; the thumbnail and Add
share a line so the list keeps its height.
This commit is contained in:
maj00r
2026-10-04 18:44:17 +02:00
parent 9459a21083
commit 6cedf8b6f6
7 changed files with 159 additions and 103 deletions

View File

@@ -30,55 +30,61 @@
<Border Grid.Column="0" Grid.Row="0" TabIndex="2" Background="{StaticResource CarbonPanelBg}"
BorderBrush="{StaticResource CarbonBorderLight}" BorderThickness="1"
CornerRadius="2" Padding="6">
<Grid RowDefinitions="Auto,Auto,*,Auto,Auto">
<DockPanel Grid.Row="0" Margin="0,0,0,6" LastChildFill="True">
<CheckBox Name="hideArchivalCheck" DockPanel.Dock="Right" FontSize="12"
VerticalAlignment="Center" Margin="8,0,0,0"
Content="{Binding [HideArchivalVehicles], Source={x:Static starterNg:App.Loc}}"
IsCheckedChanged="HideArchivalCheck_OnChanged" />
<TextBox Name="searchBox" VerticalAlignment="Center"
PlaceholderText="{Binding [Search], Source={x:Static starterNg:App.Loc}}"
TextChanged="SearchBox_OnTextChanged" />
</DockPanel>
<Grid Grid.Row="1" RowDefinitions="Auto,Auto" RowSpacing="6" Margin="0,0,0,6">
<DockPanel Grid.Row="0" LastChildFill="True">
<Button Name="textureBaseButton" DockPanel.Dock="Right" Classes="Flat"
Width="30" Margin="4,0,0,0" Cursor="Hand"
HorizontalContentAlignment="Center"
Click="TextureBaseButton_OnClick"
ToolTip.Tip="{Binding [OpenTextureBase], Source={x:Static starterNg:App.Loc}}">
<materialIcons:MaterialIcon Kind="Magnify" Width="15" Height="15" />
</Button>
<ComboBox Name="categoryCombo" HorizontalAlignment="Stretch"
PlaceholderText="{Binding [Category], Source={x:Static starterNg:App.Loc}}"
PointerWheelChanged="Combo_OnPointerWheel"
SelectionChanged="CategoryCombo_OnSelectionChanged" />
</DockPanel>
<ComboBox Name="classCombo" Grid.Row="1" HorizontalAlignment="Stretch"
PlaceholderText="{Binding [Class], Source={x:Static starterNg:App.Loc}}"
<!-- The old starter's column, top to bottom: the archival switch, the category
with the catalogue beside it, the class with its thumbnail, then the
vehicles. The search, which it kept in a window of its own, sits above
the list it fills. -->
<Grid RowDefinitions="Auto,Auto,Auto,Auto,*,Auto">
<CheckBox Name="hideArchivalCheck" Grid.Row="0" Margin="0,0,0,6" FontSize="12"
Content="{Binding [HideArchivalVehicles], Source={x:Static starterNg:App.Loc}}"
IsCheckedChanged="HideArchivalCheck_OnChanged" />
<DockPanel Grid.Row="1" Margin="0,0,0,6" LastChildFill="True">
<Button Name="textureBaseButton" DockPanel.Dock="Right" Classes="Flat"
Width="30" Margin="4,0,0,0" Cursor="Hand"
HorizontalContentAlignment="Center"
Click="TextureBaseButton_OnClick"
ToolTip.Tip="{Binding [OpenTextureBase], Source={x:Static starterNg:App.Loc}}">
<materialIcons:MaterialIcon Kind="Magnify" Width="15" Height="15" />
</Button>
<ComboBox Name="categoryCombo" HorizontalAlignment="Stretch"
PlaceholderText="{Binding [Category], Source={x:Static starterNg:App.Loc}}"
PointerWheelChanged="Combo_OnPointerWheel"
SelectionChanged="ClassCombo_OnSelectionChanged" />
</Grid>
SelectionChanged="CategoryCombo_OnSelectionChanged" />
</DockPanel>
<Border Name="miniPreviewPanel" Grid.Row="3" Height="46"
Margin="0,6,0,0"
Background="{StaticResource CarbonInputBg}"
BorderBrush="{StaticResource CarbonBorderLight}" BorderThickness="1"
CornerRadius="2" Cursor="Hand"
PointerPressed="MiniPreview_OnPointerPressed">
<Image Name="miniPreview" Stretch="Uniform" Margin="6" />
</Border>
<ComboBox Name="classCombo" Grid.Row="2" Margin="0,0,0,6" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
PlaceholderText="{Binding [Class], Source={x:Static starterNg:App.Loc}}"
PointerWheelChanged="Combo_OnPointerWheel"
SelectionChanged="ClassCombo_OnSelectionChanged" />
<Button Name="addVehicleButton" Grid.Row="4" Classes="Flat Accent"
Margin="0,8,0,0" Cursor="Hand" IsEnabled="False"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center"
Click="AddVehicleButton_OnClick"
Content="{Binding [AddVehicle], Source={x:Static starterNg:App.Loc}}" />
<TextBox Name="searchBox" Grid.Row="3" Margin="0,0,0,6"
PlaceholderText="{Binding [SearchAllVehicles], Source={x:Static starterNg:App.Loc}}"
TextChanged="SearchBox_OnTextChanged" />
<ListBox Name="vehicleListBox" Grid.Row="2" Classes="Compact"
<ListBox Name="vehicleListBox" Grid.Row="4" Classes="Compact" MinHeight="60"
SelectionChanged="VehicleListBox_OnSelectionChanged"
DoubleTapped="VehicleListBox_OnDoubleTapped"
PointerPressed="VehicleListBox_OnPointerPressed" />
<!-- The thumbnail - drag it onto the consist - and Add share a line, so
the list keeps the height. -->
<Grid Grid.Row="5" ColumnDefinitions="*,Auto" ColumnSpacing="6" Margin="0,6,0,0">
<Border Name="miniPreviewPanel" Height="34"
Background="{StaticResource CarbonInputBg}"
BorderBrush="{StaticResource CarbonBorderLight}" BorderThickness="1"
CornerRadius="2" Cursor="Hand"
PointerPressed="MiniPreview_OnPointerPressed">
<Image Name="miniPreview" Stretch="Uniform" Margin="3" />
</Border>
<Button Name="addVehicleButton" Grid.Column="1" Classes="Flat Accent"
MinWidth="80" Cursor="Hand" IsEnabled="False"
VerticalAlignment="Stretch" HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Click="AddVehicleButton_OnClick"
Content="{Binding [AddVehicle], Source={x:Static starterNg:App.Loc}}" />
</Grid>
</Grid>
</Border>

View File

@@ -94,24 +94,15 @@ public sealed class VehicleBrowser
private const int MiniPreviewHeight = 54;
private const int CompactThumbHeight = 24;
// In the old starter's order: traction first, then the wagons, then the rest.
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"),
("CatEMU", c => c == "z"),
("CatWagonsA", c => c == "A"),
("CatWagonsB", c => c == "B"),
("CatWagonsC", c => c == "C"),
@@ -137,6 +128,15 @@ public sealed class VehicleBrowser
("CatWagonsX", c => c == "X"),
("CatWagonsY", c => c == "Y"),
("CatWagonsZ", c => c == "Z"),
("CatWork", c => c == "r"),
("CatDraisine", c => c == "d"),
("CatTram", c => c == "t"),
("CatCar", c => c == "o"),
("CatBus", c => c == "b"),
("CatTruck", c => c == "c"),
("CatPeople", c => c == "h"),
("CatAnimals", c => c == "f"),
("CatPrototype", c => c == "x"),
("CatOther", IsOtherCat),
};
@@ -148,47 +148,76 @@ public sealed class VehicleBrowser
string.IsNullOrEmpty(c) ||
(c is { Length: 1 } && char.IsLower(c[0]) && !NamedLowerCats.Contains(c));
/// <summary>
/// Only the categories something is listed in - with the archival ones hidden, the
/// ones holding nothing else go too - keeping the pick and its class where they stay.
/// </summary>
public void PopulateCategoryCombo()
{
int keep = categoryCombo.SelectedIndex;
var keep = (categoryCombo.SelectedItem as ComboBoxItem)?.Tag;
string? keepClass = _classFilter;
var present = _db.Textures
.Where(Listable)
.Select(VehicleInfo.CategoryOf)
.Distinct()
.ToList();
_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;
if (present.Any(match))
categoryCombo.Items.Add(new ComboBoxItem { Content = App.Loc[locKey], Tag = match });
categoryCombo.SelectedItem = categoryCombo.Items.OfType<ComboBoxItem>()
.FirstOrDefault(item => ReferenceEquals(item.Tag, keep));
_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();
RebuildClassCombo(keepClass);
}
/// <summary>
/// The open list shows its thumbnails at the size the consist cards use, large or
/// small as set; the closed box keeps a compact line. Called again when that
/// setting changes, so the open list follows.
/// Refills the classes. Within a category one is always picked - the one kept, or
/// else the first - so the list shows its vehicles straight away, as the old starter
/// did, instead of every vehicle of the category at once.
/// </summary>
private void RebuildClassCombo(string? keepClass = null)
{
_suppress = true;
FillClassCombo();
var classes = classCombo.Items.OfType<string>().ToList();
classCombo.SelectedItem =
classes.FirstOrDefault(cls => string.Equals(cls, keepClass, StringComparison.OrdinalIgnoreCase))
?? (_categoryFilter is not null ? classes.FirstOrDefault() : null);
_suppress = false;
_classFilter = classCombo.SelectedItem as string;
Rebuild();
}
/// <summary>A texture the browser lists at all: no set follower, no hidden archival one.</summary>
private bool Listable(VehicleTexture texture) =>
!_db.IsSetFollower(texture) &&
!(AppServices.Current.Settings.HideArchivalVehicles && texture.ResolvedArchived);
/// <summary>
/// The classes with their thumbnail above the name: in the open list at the size
/// the consist cards use, called again when that size changes; in the closed box
/// at a fixed modest one, so the class picked is seen, as in the old starter,
/// without taking the height the vehicle list needs.
/// </summary>
public void InitClassComboTemplates()
{
int listHeight = VehicleCardStyle.ThumbHeight;
int height = VehicleCardStyle.ThumbHeight;
classCombo.ItemTemplate = new FuncDataTemplate<string>(
(cls, _) => ClassComboContent(cls, listHeight, stacked: true), false);
(cls, _) => ClassComboContent(cls, height, stacked: true), false);
classCombo.SelectionBoxItemTemplate = new FuncDataTemplate<string>(
(cls, _) => ClassComboContent(cls, CompactThumbHeight, stacked: false), false);
(cls, _) => ClassComboContent(cls, PickedClassThumbHeight, stacked: true), false);
}
private const int PickedClassThumbHeight = 30;
private void FillClassCombo()
{
classCombo.Items.Clear();
@@ -257,9 +286,8 @@ public sealed class VehicleBrowser
private IEnumerable<string> ClassesForCategory(Func<string?, bool>? category) =>
_db.Textures
.Where(Listable)
.Where(t => category == null || category(VehicleInfo.CategoryOf(t)))
.Where(t => !_db.IsSetFollower(t))
.Where(t => !AppServices.Current.Settings.HideArchivalVehicles || !t.ResolvedArchived)
.Select(VehicleInfo.ClassOf)
.Where(s => !string.IsNullOrEmpty(s))
.Distinct(StringComparer.OrdinalIgnoreCase)
@@ -319,7 +347,10 @@ public sealed class VehicleBrowser
bool hasSearch = search.Length > 0;
if (_categoryFilter == null && _classFilter == null && !hasSearch)
{
AddNote(App.Loc["BrowserHint"]);
return;
}
var matched = _db.Textures
.Where(t => PassesFilters(t, search, hasSearch))
@@ -338,13 +369,16 @@ public sealed class VehicleBrowser
}
if (vehicleListBox.Items.Count == 0)
vehicleListBox.Items.Add(new ListBoxItem
{
Content = App.Loc["NoVehicles"],
IsEnabled = false
});
AddNote(hasSearch ? string.Format(App.Loc["BrowserNoMatch"], search) : App.Loc["NoVehicles"]);
}
private void AddNote(string text) =>
vehicleListBox.Items.Add(new ListBoxItem
{
Content = new TextBlock { Text = text, TextWrapping = TextWrapping.Wrap, Opacity = 0.7 },
IsEnabled = false
});
public void VehicleListBox_OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (vehicleListBox.SelectedItem is ListBoxItem { Tag: VehicleTexture texture })
@@ -489,23 +523,19 @@ public sealed class VehicleBrowser
private bool PassesFilters(VehicleTexture t, string search, bool hasSearch)
{
if (!Listable(t))
return false;
// A search looks through everything; the category and the class narrow the
// browsing, not the finding.
if (hasSearch)
return Matches(t, search);
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 (AppServices.Current.Settings.HideArchivalVehicles && t.ResolvedArchived)
return false;
if (hasSearch && !Matches(t, search))
return false;
return true;
return _classFilter == null ||
string.Equals(VehicleInfo.ClassOf(t), _classFilter, StringComparison.OrdinalIgnoreCase);
}
private bool Matches(VehicleTexture t, string f)
@@ -527,15 +557,20 @@ public sealed class VehicleBrowser
// called a vehicle something none of the other panels did.
private static string BrowserName(VehicleTexture texture) => Consist.Base(texture.Skinfile);
private string BrowserLabel(VehicleTexture texture, IReadOnlyList<VehicleTexture>? set)
// The file name alone, as the old starter listed it - the operator is in the
// texture panel - with a faint count for a set of cars.
private static Control BrowserLabel(VehicleTexture texture, IReadOnlyList<VehicleTexture>? set)
{
string name = BrowserName(texture);
var name = new TextBlock { Text = BrowserName(texture) };
if (set is not { Count: > 1 })
return name;
if (!string.IsNullOrEmpty(texture.Meta?.Operator))
name += $" · {texture.Meta!.Operator}";
if (set is { Count: > 1 })
name += $" [{set.Count}]";
return name;
return new StackPanel
{
Orientation = Orientation.Horizontal,
Spacing = 6,
Children = { name, new TextBlock { Text = $"×{set.Count}", Opacity = 0.55 } }
};
}
public void SearchBox_OnTextChanged(object? sender, TextChangedEventArgs e)
@@ -550,7 +585,7 @@ public sealed class VehicleBrowser
if (_suppress) return;
AppServices.Current.Settings.HideArchivalVehicles = hideArchivalCheck.IsChecked ?? true;
AppServices.Current.SettingsStore.Save();
Rebuild();
PopulateCategoryCombo();
}
public void SelectInBrowser(Dynamic car)

View File

@@ -95,6 +95,9 @@
<String key="Crew">s osádkou</String>
<String key="Reversed">otočeno</String>
<String key="NoVehicles">Ve složce dynamic/**/textures.txt nebyla nalezena žádná vozidla.</String>
<String key="BrowserHint">Vyberte kategorii nebo napište název vozidla.</String>
<String key="BrowserNoMatch">Nic neodpovídá „{0}“.</String>
<String key="SearchAllVehicles">Hledat ve všech vozidlech…</String>
<String key="ConsistEmpty">Přidejte vozidla z databáze vlevo nebo načtěte existující soupravu ze souboru scenérie.</String>
<String key="TipMoveLeft">Posunout doleva</String>
<String key="TipMoveRight">Posunout doprava</String>

View File

@@ -95,6 +95,9 @@
<String key="Crew">with crew</String>
<String key="Reversed">reversed</String>
<String key="NoVehicles">No vehicles found in dynamic/**/textures.txt.</String>
<String key="BrowserHint">Pick a category or type a vehicle's name.</String>
<String key="BrowserNoMatch">Nothing matches “{0}”.</String>
<String key="SearchAllVehicles">Search all vehicles…</String>
<String key="ConsistEmpty">Add vehicles from the database on the left, or load an existing consist from a scenery file.</String>
<String key="TipMoveLeft">Move left</String>
<String key="TipMoveRight">Move right</String>

View File

@@ -95,6 +95,9 @@
<String key="Crew">személyzettel</String>
<String key="Reversed">megfordítva</String>
<String key="NoVehicles">A dynamic/**/textures.txt fájlokban nem található jármű.</String>
<String key="BrowserHint">Válassz kategóriát, vagy írd be a jármű nevét.</String>
<String key="BrowserNoMatch">Semmi sem felel meg ennek: „{0}”.</String>
<String key="SearchAllVehicles">Keresés az összes járműben…</String>
<String key="ConsistEmpty">Adjon hozzá járműveket a bal oldali adatbázisból, vagy töltsön be meglévő szerelvényt egy pályafájlból.</String>
<String key="TipMoveLeft">Mozgatás balra</String>
<String key="TipMoveRight">Mozgatás jobbra</String>

View File

@@ -95,6 +95,9 @@
<String key="Crew">z obsadą</String>
<String key="Reversed">Obrócony</String>
<String key="NoVehicles">Nie znaleziono pojazdów w dynamic/**/textures.txt.</String>
<String key="BrowserHint">Wybierz kategorię albo wpisz nazwę pojazdu.</String>
<String key="BrowserNoMatch">Nic nie pasuje do „{0}”.</String>
<String key="SearchAllVehicles">Szukaj we wszystkich pojazdach…</String>
<String key="ConsistEmpty">Dodaj pojazdy z bazy po lewej stronie lub wczytaj istniejący skład z pliku scenerii.</String>
<String key="TipMoveLeft">Przesuń w lewo</String>
<String key="TipMoveRight">Przesuń w prawo</String>

View File

@@ -95,6 +95,9 @@
<String key="Crew">с бригадой</String>
<String key="Reversed">развёрнут</String>
<String key="NoVehicles">В dynamic/**/textures.txt подвижной состав не найден.</String>
<String key="BrowserHint">Выберите категорию или введите название единицы.</String>
<String key="BrowserNoMatch">Ничего не найдено по запросу «{0}».</String>
<String key="SearchAllVehicles">Искать во всех единицах…</String>
<String key="ConsistEmpty">Добавьте единицы из базы слева или загрузите готовый состав из файла сценерии.</String>
<String key="TipMoveLeft">Сдвинуть влево</String>
<String key="TipMoveRight">Сдвинуть вправо</String>