Skip to content

Commit 4e2a3b8

Browse files
CopilotLeftofZen
andcommitted
Add populate-from-folder and copy/paste support for region and scenario object lists
Co-authored-by: LeftofZen <7483209+LeftofZen@users.noreply.github.com>
1 parent 2264b84 commit 4e2a3b8

8 files changed

Lines changed: 288 additions & 9 deletions

File tree

Gui/App.axaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@
9999
<DataTemplate DataType="vm:SCV5ViewModel">
100100
<vi:SCV5View />
101101
</DataTemplate>
102+
<DataTemplate DataType="vm:RegionViewModel">
103+
<vi:RegionView />
104+
</DataTemplate>
102105
<DataTemplate DataType="vm:Graphics.ImageViewModel">
103106
<vi:ImageView />
104107
</DataTemplate>

Gui/PlatformSpecific.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Avalonia;
2+
using Avalonia.Controls;
23
using Avalonia.Controls.ApplicationLifetimes;
34
using Avalonia.Platform.Storage;
45
using Common.Logging;
@@ -122,4 +123,28 @@ public static async Task<IReadOnlyList<IStorageFile>> OpenFilePicker(IReadOnlyLi
122123
FileTypeChoices = filetypes,
123124
});
124125
}
126+
127+
public static async Task<string?> GetClipboardTextAsync()
128+
{
129+
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop
130+
&& desktop.MainWindow is { } window)
131+
{
132+
return await TopLevel.GetTopLevel(window)?.Clipboard?.GetTextAsync();
133+
}
134+
135+
return null;
136+
}
137+
138+
public static async Task SetClipboardTextAsync(string text)
139+
{
140+
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop
141+
&& desktop.MainWindow is { } window)
142+
{
143+
var clipboard = TopLevel.GetTopLevel(window)?.Clipboard;
144+
if (clipboard != null)
145+
{
146+
await clipboard.SetTextAsync(text);
147+
}
148+
}
149+
}
125150
}

Gui/ViewModels/Loco/ObjectEditorViewModel.cs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ bool ValidateForOG(bool showPopupOnSuccess)
214214
}
215215
}
216216

217-
public static IViewModel? GetViewModelFromStruct(LocoObject locoObject)
217+
public IViewModel? GetViewModelFromStruct(LocoObject locoObject)
218218
{
219219
var locoStruct = locoObject.Object;
220220
var asm = Assembly
@@ -227,9 +227,21 @@ bool ValidateForOG(bool showPopupOnSuccess)
227227
&& type.BaseType.GetGenericTypeDefinition() == typeof(BaseViewModel<>)
228228
&& type.BaseType.GenericTypeArguments.Single() == locoStruct.GetType());
229229

230-
return asm == null
231-
? null
232-
: (IViewModel?)Activator.CreateInstance(asm, locoStruct);
230+
if (asm == null)
231+
{
232+
return null;
233+
}
234+
235+
// Try to create with (locoStruct, editorContext) for ViewModels that support context-aware features
236+
try
237+
{
238+
return (IViewModel?)Activator.CreateInstance(asm, locoStruct, EditorContext);
239+
}
240+
catch (MissingMethodException)
241+
{
242+
// Fall back to single-argument constructor for ViewModels that don't need the context
243+
return (IViewModel?)Activator.CreateInstance(asm, locoStruct);
244+
}
233245
}
234246

235247
public override void Load()
Lines changed: 99 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,35 @@
11
using Definitions.ObjectModels.Objects.Region;
22
using Definitions.ObjectModels.Types;
3+
using Gui.Models;
4+
using ReactiveUI;
5+
using System.Collections.Generic;
36
using System.ComponentModel;
7+
using System.Linq;
8+
using System.Reactive;
9+
using System.Text.Json;
10+
using System.Threading.Tasks;
411

512
namespace Gui.ViewModels;
613

7-
public class RegionViewModel(RegionObject model)
8-
: BaseViewModel<RegionObject>(model)
14+
public class RegionViewModel : BaseViewModel<RegionObject>
915
{
16+
readonly ObjectEditorContext? editorContext;
17+
18+
public RegionViewModel(RegionObject model, ObjectEditorContext? editorContext = null)
19+
: base(model)
20+
{
21+
this.editorContext = editorContext;
22+
23+
DependentObjects = new BindingList<ObjectModelHeader>(model.DependentObjects);
24+
CargoInfluenceObjects = new BindingList<ObjectModelHeader>(model.CargoInfluenceObjects);
25+
CargoInfluenceTownFilter = new BindingList<CargoInfluenceTownFilterType>(model.CargoInfluenceTownFilter);
26+
27+
PopulateDependentObjectsFromFolderCommand = ReactiveCommand.CreateFromTask(PopulateDependentObjectsFromFolder);
28+
CopyDependentObjectsCommand = ReactiveCommand.CreateFromTask(CopyDependentObjectsAsync);
29+
PasteDependentObjectsCommand = ReactiveCommand.CreateFromTask(PasteDependentObjectsAsync);
30+
ClearDependentObjectsCommand = ReactiveCommand.Create(ClearDependentObjects);
31+
}
32+
1033
public DrivingSide VehiclesDriveOnThe
1134
{
1235
get => Model.VehiclesDriveOnThe;
@@ -19,11 +42,82 @@ public uint8_t pad_07
1942
set => Model.pad_07 = value;
2043
}
2144

22-
public BindingList<ObjectModelHeader> DependentObjects { get; init; } = new(model.DependentObjects);
45+
[Browsable(false)]
46+
public BindingList<ObjectModelHeader> DependentObjects { get; }
2347

2448
[Category("Cargo")]
25-
public BindingList<ObjectModelHeader> CargoInfluenceObjects { get; init; } = new(model.CargoInfluenceObjects);
49+
public BindingList<ObjectModelHeader> CargoInfluenceObjects { get; }
2650

2751
[Category("Cargo")]
28-
public BindingList<CargoInfluenceTownFilterType> CargoInfluenceTownFilter { get; init; } = new(model.CargoInfluenceTownFilter);
52+
public BindingList<CargoInfluenceTownFilterType> CargoInfluenceTownFilter { get; }
53+
54+
[Browsable(false)]
55+
public ReactiveCommand<Unit, Unit> PopulateDependentObjectsFromFolderCommand { get; }
56+
57+
[Browsable(false)]
58+
public ReactiveCommand<Unit, Unit> CopyDependentObjectsCommand { get; }
59+
60+
[Browsable(false)]
61+
public ReactiveCommand<Unit, Unit> PasteDependentObjectsCommand { get; }
62+
63+
[Browsable(false)]
64+
public ReactiveCommand<Unit, Unit> ClearDependentObjectsCommand { get; }
65+
66+
Task PopulateDependentObjectsFromFolder()
67+
{
68+
var objectIndex = editorContext?.ObjectIndex;
69+
if (objectIndex == null)
70+
{
71+
return Task.CompletedTask;
72+
}
73+
74+
DependentObjects.Clear();
75+
foreach (var entry in objectIndex.Objects.Where(x => x.DatChecksum.HasValue))
76+
{
77+
DependentObjects.Add(new ObjectModelHeader(entry.DisplayName, entry.ObjectType, entry.ObjectSource, entry.DatChecksum!.Value));
78+
}
79+
80+
return Task.CompletedTask;
81+
}
82+
83+
async Task CopyDependentObjectsAsync()
84+
{
85+
var json = JsonSerializer.Serialize(DependentObjects.ToList(), JsonSerializerOptions);
86+
await PlatformSpecific.SetClipboardTextAsync(json);
87+
}
88+
89+
async Task PasteDependentObjectsAsync()
90+
{
91+
var text = await PlatformSpecific.GetClipboardTextAsync();
92+
if (string.IsNullOrWhiteSpace(text))
93+
{
94+
return;
95+
}
96+
97+
List<ObjectModelHeader>? headers;
98+
try
99+
{
100+
headers = JsonSerializer.Deserialize<List<ObjectModelHeader>>(text, JsonSerializerOptions);
101+
}
102+
catch (JsonException)
103+
{
104+
return;
105+
}
106+
107+
if (headers == null)
108+
{
109+
return;
110+
}
111+
112+
DependentObjects.Clear();
113+
foreach (var header in headers)
114+
{
115+
DependentObjects.Add(header);
116+
}
117+
}
118+
119+
void ClearDependentObjects()
120+
=> DependentObjects.Clear();
121+
122+
static readonly JsonSerializerOptions JsonSerializerOptions = new() { WriteIndented = false };
29123
}

Gui/ViewModels/Loco/SCV5ViewModel.cs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
using System.Linq;
1919
using System.Reactive;
2020
using System.Reactive.Linq;
21+
using System.Text.Json;
2122
using System.Threading.Tasks;
2223

2324
namespace Gui.ViewModels;
@@ -53,6 +54,9 @@ public ObservableCollection<TileElement> CurrentTileElements
5354
[Reactive]
5455
public GameObjDataFolder LastGameObjDataFolder { get; set; } = GameObjDataFolder.LocomotionSteam;
5556
public ReactiveCommand<GameObjDataFolder, Unit> DownloadMissingObjectsToGameObjDataCommand { get; }
57+
public ReactiveCommand<Unit, Unit> PopulateRequiredObjectsFromFolderCommand { get; }
58+
public ReactiveCommand<Unit, Unit> CopyRequiredObjectsCommand { get; }
59+
public ReactiveCommand<Unit, Unit> PasteRequiredObjectsCommand { get; }
5660

5761
public SCV5ViewModel(FileSystemItem currentFile, ObjectEditorContext editorContext)
5862
: base(currentFile, editorContext)
@@ -61,6 +65,9 @@ public SCV5ViewModel(FileSystemItem currentFile, ObjectEditorContext editorConte
6165
SaveAsIsVisible = false;
6266
Load();
6367
DownloadMissingObjectsToGameObjDataCommand = ReactiveCommand.CreateFromTask<GameObjDataFolder>(DownloadMissingObjects);
68+
PopulateRequiredObjectsFromFolderCommand = ReactiveCommand.CreateFromTask(PopulateRequiredObjectsFromFolder);
69+
CopyRequiredObjectsCommand = ReactiveCommand.CreateFromTask(CopyRequiredObjectsAsync);
70+
PasteRequiredObjectsCommand = ReactiveCommand.CreateFromTask(PasteRequiredObjectsAsync);
6471
}
6572

6673
public override void Load()
@@ -215,6 +222,65 @@ async Task DownloadMissingObjects(GameObjDataFolder targetFolder)
215222
return;
216223
}
217224

225+
Task PopulateRequiredObjectsFromFolder()
226+
{
227+
var objectIndex = EditorContext?.ObjectIndex;
228+
if (objectIndex == null)
229+
{
230+
return Task.CompletedTask;
231+
}
232+
233+
var headers = objectIndex.Objects
234+
.Where(x => x.DatChecksum.HasValue)
235+
.Select(x => new ObjectModelHeaderViewModel(new ObjectModelHeader(x.DisplayName, x.ObjectType, x.ObjectSource, x.DatChecksum!.Value)))
236+
.OrderBy(x => x.Name);
237+
238+
RequiredObjects = new ObservableCollection<ObjectModelHeaderViewModel>([.. headers]);
239+
240+
return Task.CompletedTask;
241+
}
242+
243+
async Task CopyRequiredObjectsAsync()
244+
{
245+
if (RequiredObjects == null)
246+
{
247+
return;
248+
}
249+
250+
var headers = RequiredObjects.Select(x => x.Model).OfType<ObjectModelHeader>().ToList();
251+
var json = JsonSerializer.Serialize(headers, JsonSerializerOptions);
252+
await PlatformSpecific.SetClipboardTextAsync(json);
253+
}
254+
255+
async Task PasteRequiredObjectsAsync()
256+
{
257+
var text = await PlatformSpecific.GetClipboardTextAsync();
258+
if (string.IsNullOrWhiteSpace(text))
259+
{
260+
return;
261+
}
262+
263+
List<ObjectModelHeader>? headers;
264+
try
265+
{
266+
headers = JsonSerializer.Deserialize<List<ObjectModelHeader>>(text, JsonSerializerOptions);
267+
}
268+
catch (JsonException)
269+
{
270+
return;
271+
}
272+
273+
if (headers == null)
274+
{
275+
return;
276+
}
277+
278+
RequiredObjects = new ObservableCollection<ObjectModelHeaderViewModel>(
279+
headers.Select(h => new ObjectModelHeaderViewModel(h)).OrderBy(x => x.Name));
280+
}
281+
282+
static readonly JsonSerializerOptions JsonSerializerOptions = new() { WriteIndented = false };
283+
218284
void DrawMap()
219285
{
220286
(var mapWidth, var mapHeight) = Model.GetMapSize();

Gui/Views/RegionView.axaml

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<UserControl
2+
xmlns="https://github.com/avaloniaui"
3+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4+
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
5+
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
6+
xmlns:vm="using:Gui.ViewModels"
7+
xmlns:vi="using:Gui.Views"
8+
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="600"
9+
x:Class="Gui.Views.RegionView"
10+
x:DataType="vm:RegionViewModel">
11+
12+
<TabControl>
13+
<TabItem Header="Properties">
14+
<ScrollViewer>
15+
<vi:ExtendedPropertyGrid DataContext="{Binding}" IsHeaderVisible="True" IsCategoryVisible="False" IsAutoNameWidth="True" IsQuickFilterVisible="True" />
16+
</ScrollViewer>
17+
</TabItem>
18+
<TabItem Header="Dependent Objects">
19+
<DockPanel>
20+
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Spacing="4" Margin="4">
21+
<Button Command="{Binding PopulateDependentObjectsFromFolderCommand}"
22+
ToolTip.Tip="Clear the list and populate it with all objects from the currently loaded folder">
23+
Populate from current folder
24+
</Button>
25+
<Button Command="{Binding CopyDependentObjectsCommand}"
26+
ToolTip.Tip="Copy the dependent objects list to clipboard as JSON">
27+
Copy list
28+
</Button>
29+
<Button Command="{Binding PasteDependentObjectsCommand}"
30+
ToolTip.Tip="Replace the dependent objects list with objects from clipboard (JSON format)">
31+
Paste list
32+
</Button>
33+
<Button Command="{Binding ClearDependentObjectsCommand}"
34+
Classes="danger"
35+
ToolTip.Tip="Remove all dependent objects from the list">
36+
Clear
37+
</Button>
38+
<TextBlock VerticalAlignment="Center"
39+
Text="{Binding DependentObjects.Count, StringFormat='({0} items)'}" />
40+
</StackPanel>
41+
<DataGrid
42+
ItemsSource="{Binding DependentObjects}"
43+
AutoGenerateColumns="False"
44+
CanUserSortColumns="True"
45+
IsReadOnly="True">
46+
<DataGrid.Columns>
47+
<DataGridTextColumn Header="Name" Binding="{Binding Name}" Width="*" />
48+
<DataGridTextColumn Header="Type" Binding="{Binding ObjectType}" Width="Auto" />
49+
<DataGridTextColumn Header="Source" Binding="{Binding ObjectSource}" Width="Auto" />
50+
<DataGridTextColumn Header="Checksum" Binding="{Binding DatChecksum}" Width="Auto" />
51+
</DataGrid.Columns>
52+
</DataGrid>
53+
</DockPanel>
54+
</TabItem>
55+
</TabControl>
56+
</UserControl>

Gui/Views/RegionView.axaml.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using Avalonia.Controls;
2+
3+
namespace Gui.Views;
4+
5+
public partial class RegionView : UserControl
6+
{
7+
public RegionView()
8+
{
9+
InitializeComponent();
10+
}
11+
}

Gui/Views/SCV5View.axaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,18 @@
5454
</MenuFlyout>
5555
</SplitButton.Flyout>
5656
</SplitButton>
57+
<Button Margin="4" Command="{Binding PopulateRequiredObjectsFromFolderCommand}"
58+
ToolTip.Tip="Clear the list and populate it with all objects from the currently loaded folder">
59+
Populate from current folder
60+
</Button>
61+
<Button Margin="4" Command="{Binding CopyRequiredObjectsCommand}"
62+
ToolTip.Tip="Copy the required objects list to clipboard as JSON">
63+
Copy list
64+
</Button>
65+
<Button Margin="4" Command="{Binding PasteRequiredObjectsCommand}"
66+
ToolTip.Tip="Replace the required objects list with objects from clipboard (JSON format)">
67+
Paste list
68+
</Button>
5769
<TextBlock Margin="4" VerticalAlignment="Center" HorizontalAlignment="Center" Text="{Binding RequiredObjects.Count, StringFormat='Number of objects: {0}'}"></TextBlock>
5870
<!--<TextBlock Margin="4" VerticalAlignment="Center" HorizontalAlignment="Center" Text="" DockPanel.Dock="Top" />-->
5971
</StackPanel>

0 commit comments

Comments
 (0)