Skip to content

Commit 4b9be4a

Browse files
CopilotLeftofZenCopilot
authored
Add shared RequiredObjectsListViewModel/View; unify Region and SCV5 object list management (#235)
* Initial plan * Add populate-from-folder and copy/paste support for region and scenario object lists Co-authored-by: LeftofZen <7483209+LeftofZen@users.noreply.github.com> * Add individual add/remove buttons for dependent objects list in RegionView Co-authored-by: LeftofZen <7483209+LeftofZen@users.noreply.github.com> * Polish: Delete hotkey, folder picker for populate, resizable columns in RegionView Co-authored-by: LeftofZen <7483209+LeftofZen@users.noreply.github.com> * fix not saving * Extract RequiredObjectsListViewModel/RequiredObjectsView; refactor RegionViewModel and SCV5ViewModel to use shared component Co-authored-by: LeftofZen <7483209+LeftofZen@users.noreply.github.com> * Fix DownloadMissingObjects to iterate RequiredObjects.Items instead of Model.RequiredObjects Co-authored-by: LeftofZen <7483209+LeftofZen@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: LeftofZen <7483209+LeftofZen@users.noreply.github.com> Co-authored-by: Benjamin Sutas <benjamin.sutas@gmail.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent 5570265 commit 4b9be4a

17 files changed

Lines changed: 428 additions & 54 deletions

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: 29 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,32 @@ 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+
var clipboard = TopLevel.GetTopLevel(window)?.Clipboard;
133+
if (clipboard != null)
134+
{
135+
return await clipboard.GetTextAsync();
136+
}
137+
}
138+
139+
return null;
140+
}
141+
142+
public static async Task SetClipboardTextAsync(string text)
143+
{
144+
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop
145+
&& desktop.MainWindow is { } window)
146+
{
147+
var clipboard = TopLevel.GetTopLevel(window)?.Clipboard;
148+
if (clipboard != null)
149+
{
150+
await clipboard.SetTextAsync(text);
151+
}
152+
}
153+
}
125154
}

Gui/Program.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using Avalonia;
22
using Avalonia.Logging;
3-
using Common;
43
using ReactiveUI.Avalonia;
54
using System;
65

Gui/ViewModels/Loco/BaseFileViewModel.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ async Task SaveWrapper()
116116

117117
var result = await box.ShowAsync();
118118

119-
if (result == ButtonResult.Yes)
119+
if (result == ButtonResult.No)
120120
{
121121
return;
122122
}

Gui/ViewModels/Loco/BaseViewModel.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public ReadOnlyObservableCollection<IViewModel> AllViewModels
5353
=> _allViewModelsCollection;
5454

5555
[Browsable(false)]
56-
public string NewGroupName { get; set; } = "New Group";
56+
public string NewGroupName { get; set; } = "<unnamed>";
5757

5858
[Browsable(false)]
5959
public ReactiveCommand<Unit, Unit> AddGroupCommand { get; }

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: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,32 @@
11
using Definitions.ObjectModels.Objects.Region;
22
using Definitions.ObjectModels.Types;
3+
using DynamicData;
4+
using Gui.Models;
35
using System.ComponentModel;
6+
using System.Reactive;
7+
using System.Reactive.Disposables;
8+
using System.Reactive.Disposables.Fluent;
49

510
namespace Gui.ViewModels;
611

7-
public class RegionViewModel(RegionObject model)
8-
: BaseViewModel<RegionObject>(model)
12+
public class RegionViewModel : BaseViewModel<RegionObject>
913
{
14+
readonly CompositeDisposable modelSyncSubscriptions = [];
15+
16+
public RegionViewModel(RegionObject model, ObjectEditorContext? editorContext = null)
17+
: base(model)
18+
{
19+
RequiredObjects = new RequiredObjectsListViewModel(editorContext);
20+
RequiredObjects.AddOrUpdate(model.DependentObjects);
21+
22+
_ = RequiredObjects.Connect()
23+
.Subscribe(Observer.Create<IChangeSet<ObjectModelHeader, uint>>(_ => SyncRequiredObjectsToModel()))
24+
.DisposeWith(modelSyncSubscriptions);
25+
26+
CargoInfluenceObjects = new(model.CargoInfluenceObjects);
27+
CargoInfluenceTownFilter = new(model.CargoInfluenceTownFilter);
28+
}
29+
1030
public DrivingSide VehiclesDriveOnThe
1131
{
1232
get => Model.VehiclesDriveOnThe;
@@ -19,11 +39,29 @@ public uint8_t pad_07
1939
set => Model.pad_07 = value;
2040
}
2141

22-
public BindingList<ObjectModelHeader> DependentObjects { get; init; } = new(model.DependentObjects);
42+
[Browsable(false)]
43+
public RequiredObjectsListViewModel RequiredObjects { get; }
2344

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

2748
[Category("Cargo")]
28-
public BindingList<CargoInfluenceTownFilterType> CargoInfluenceTownFilter { get; init; } = new(model.CargoInfluenceTownFilter);
49+
public BindingList<CargoInfluenceTownFilterType> CargoInfluenceTownFilter { get; }
50+
51+
void SyncRequiredObjectsToModel()
52+
{
53+
Model.DependentObjects.Clear();
54+
Model.DependentObjects.AddRange(RequiredObjects.Items);
55+
}
56+
57+
protected override void Dispose(bool disposing)
58+
{
59+
if (disposing)
60+
{
61+
modelSyncSubscriptions.Dispose();
62+
RequiredObjects.Dispose();
63+
}
64+
65+
base.Dispose(disposing);
66+
}
2967
}

Gui/ViewModels/Loco/SCV5ViewModel.cs

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,7 @@ public class SCV5ViewModel : BaseFileViewModel<S5File>
2727
//[Reactive]
2828
//public S5File? Model { get; set; }
2929

30-
[Reactive]
31-
public ObservableCollection<ObjectModelHeaderViewModel>? RequiredObjects { get; set; }
30+
public RequiredObjectsListViewModel RequiredObjects { get; }
3231

3332
[Reactive]
3433
public ObservableCollection<ObjectModelHeaderViewModel>? PackedObjects { get; set; }
@@ -57,6 +56,7 @@ public ObservableCollection<TileElement> CurrentTileElements
5756
public SCV5ViewModel(FileSystemItem currentFile, ObjectEditorContext editorContext)
5857
: base(currentFile, editorContext)
5958
{
59+
RequiredObjects = new RequiredObjectsListViewModel(editorContext);
6060
SaveIsVisible = false;
6161
SaveAsIsVisible = false;
6262
Load();
@@ -74,11 +74,10 @@ public override void Load()
7474
return;
7575
}
7676

77-
var ro = Model.RequiredObjects
77+
var headers = Model.RequiredObjects
7878
.Where(x => x.Checksum != 0)
79-
.Select(x => new ObjectModelHeaderViewModel(x.Convert()))
80-
.OrderBy(x => x.Name);
81-
RequiredObjects = new ObservableCollection<ObjectModelHeaderViewModel>([.. ro]);
79+
.Select(x => x.Convert());
80+
RequiredObjects.Replace(headers);
8281

8382
var po = Model.PackedObjects.ConvertAll(x => new ObjectModelHeaderViewModel(x.Item1.Convert())).OrderBy(x => x.Name);
8483
PackedObjects = new ObservableCollection<ObjectModelHeaderViewModel>([.. po]);
@@ -140,43 +139,43 @@ async Task DownloadMissingObjects(GameObjDataFolder targetFolder)
140139
// technically should check if the index is downloaded and valid now
141140
}
142141

143-
foreach (var obj in Model.RequiredObjects)
142+
foreach (var obj in RequiredObjects.Items)
144143
{
145-
if (OriginalObjectFiles.GetFileSource(obj.Name, obj.Checksum, obj.ObjectSource) is ObjectSource.LocomotionSteam or ObjectSource.LocomotionGoG)
144+
if (OriginalObjectFiles.GetFileSource(obj.Name, obj.DatChecksum, obj.ObjectSource.Convert()) is ObjectSource.LocomotionSteam or ObjectSource.LocomotionGoG)
146145
{
147146
continue;
148147
}
149148

150-
if (gameFolderIndex.Objects.Contains(x => x.DisplayName == obj.Name && x.DatChecksum == obj.Checksum))
149+
if (gameFolderIndex.Objects.Contains(x => x.DisplayName == obj.Name && x.DatChecksum == obj.DatChecksum))
151150
{
152151
continue;
153152
}
154153

155154
// obj is missing - we need to download
156-
logger.Info($"Scenario {CurrentFile.DisplayName} has missing object. Name=\"{obj.Name}\" Checksum={obj.Checksum} ObjectType={obj.ObjectType} ");
155+
logger.Info($"Scenario {CurrentFile.DisplayName} has missing object. Name=\"{obj.Name}\" Checksum={obj.DatChecksum} ObjectType={obj.ObjectType} ");
157156

158157
var onlineObj = EditorContext.ObjectIndexOnline
159158
.Objects
160-
.FirstOrDefault(x => x.DisplayName == obj.Name && x.DatChecksum == obj.Checksum); // ideally would be SingleOrDefault but unfortunately DAT is not unique
159+
.FirstOrDefault(x => x.DisplayName == obj.Name && x.DatChecksum == obj.DatChecksum); // ideally would be SingleOrDefault but unfortunately DAT is not unique
161160

162161
if (onlineObj == null)
163162
{
164-
logger.Error($"Couldn't find a matching object in the online index. Name=\"{obj.Name}\" Checksum={obj.Checksum} ObjectType={obj.ObjectType} ");
163+
logger.Error($"Couldn't find a matching object in the online index. Name=\"{obj.Name}\" Checksum={obj.DatChecksum} ObjectType={obj.ObjectType} ");
165164

166165
// Add this missing object to the server's missing objects list
167166
var missingEntry = new DtoObjectMissingPost(
168167
obj.Name,
169-
obj.Checksum,
170-
obj.ObjectType.Convert());
168+
obj.DatChecksum,
169+
obj.ObjectType);
171170

172171
var result = await EditorContext.ObjectServiceClient.AddMissingObjectAsync(missingEntry);
173172
if (result != null)
174173
{
175-
logger.Info($"Successfully added missing object to server: Id={result.Id} Name=\"{obj.Name}\" Checksum=({obj.Checksum})");
174+
logger.Info($"Successfully added missing object to server: Id={result.Id} Name=\"{obj.Name}\" Checksum=({obj.DatChecksum})");
176175
}
177176
else
178177
{
179-
logger.Error($"Failed to add missing object to server: Name=\"{obj.Name}\" Checksum=({obj.Checksum})");
178+
logger.Error($"Failed to add missing object to server: Name=\"{obj.Name}\" Checksum=({obj.DatChecksum})");
180179
}
181180

182181
continue;

Gui/ViewModels/ObjectMetadataViewModel.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1-
using Common.Logging;
21
using Definitions;
32
using Definitions.DTO;
43
using Definitions.ObjectModels;
54
using Gui.Models;
6-
using Microsoft.AspNetCore.Components.Forms;
75
using ReactiveUI;
86
using System;
97
using System.Collections.Generic;

0 commit comments

Comments
 (0)