Skip to content

Commit e9b70fb

Browse files
authored
Improve image viewer (#202)
* rework imagetableviewmodel, create imageviewmodel * imageviewmodel now working * image model improvements
1 parent ccd2f2c commit e9b70fb

15 files changed

+305
-225
lines changed

Definitions/ObjectModels/Types/GraphicsElement.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public enum GraphicsElementFlags : uint16_t
1313
DuplicatePrevious = 1 << 6, // Duplicates the previous element but with adjusted x/y offsets
1414
}
1515

16-
public class GraphicsElement // follows G1Element32
16+
public class GraphicsElement // follows G1Element32, except XOffset and YOffset = are inverted - in loco they're negative but here they're positive
1717
{
1818
public short Width { get; set; }
1919
public short Height { get; set; }

Gui/App.axaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
xmlns:vm="using:Gui.ViewModels"
77
xmlns:vi="using:Gui.Views"
88
xmlns:oldt="using:Dat.Types"
9-
xmlns:pgc="clr-namespace:Avalonia.PropertyGrid.Controls;assembly=Avalonia.PropertyGrid"
9+
xmlns:pgc="clr-namespace:Avalonia.PropertyGrid.Controls;assembly=Avalonia.PropertyGrid"
1010
x:Class="Gui.App"
1111
RequestedThemeVariant="Default">
1212
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
@@ -28,6 +28,9 @@
2828
<DataTemplate DataType="vm:SCV5ViewModel">
2929
<vi:SCV5View />
3030
</DataTemplate>
31+
<DataTemplate DataType="vm:ImageViewModel">
32+
<vi:ImageView />
33+
</DataTemplate>
3134
<DataTemplate DataType="vm:ImageTableViewModel">
3235
<vi:ImageTableView />
3336
</DataTemplate>
@@ -40,9 +43,6 @@
4043
<DataTemplate DataType="vm:FolderTreeViewModel">
4144
<vi:FolderTreeView />
4245
</DataTemplate>
43-
<DataTemplate DataType="vm:UIG1Element32">
44-
<pgc:PropertyGrid DataContext="{Binding}" IsCategoryVisible="False"></pgc:PropertyGrid>
45-
</DataTemplate>
4646
</Application.DataTemplates>
4747

4848
<Application.Styles>

Gui/EditorSettings.cs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,21 @@ public class EditorSettings
1313
{
1414
public string ObjDataDirectory
1515
{
16-
get => objectDirectory;
16+
get;
1717
set
1818
{
19-
objectDirectory = value;
19+
field = value;
2020
ObjDataDirectories ??= [];
21-
_ = ObjDataDirectories.Add(objectDirectory);
21+
_ = ObjDataDirectories.Add(field);
2222
}
2323
}
24-
string objectDirectory;
2524

2625
public HashSet<string> ObjDataDirectories
2726
{
28-
get => objDataDirectories ??= [];
29-
set => objDataDirectories = value;
30-
}
31-
HashSet<string> objDataDirectories = [];
27+
get => field ??= [];
28+
set;
29+
} = [];
30+
3231
public bool AllowSavingAsVanillaObject { get; set; }
3332
public bool AutoObjectDiscoveryAndUpload { get; set; }
3433

Gui/Models/Converters/EnumDescriptionConverter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ public class EnumDescriptionConverter : MarkupExtension, IValueConverter
1010
public override object ProvideValue(IServiceProvider serviceProvider)
1111
=> this;
1212

13-
public object? Convert(object? value, Type targetType, object parameter, CultureInfo culture)
13+
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
1414
{
1515
var type = value?.GetType();
1616
var text = value?.ToString();

Gui/Models/Converters/EnumToBooleanConverter.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ namespace Gui.Models.Converters;
66

77
public class EnumToBooleanConverter : IValueConverter
88
{
9-
public object? Convert(object? value, Type targetType, object parameter, CultureInfo culture)
9+
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
1010
{
1111
if (value is Enum enumValue && parameter is Enum enumParameter)
1212
{
@@ -16,7 +16,7 @@ public class EnumToBooleanConverter : IValueConverter
1616
return false;
1717
}
1818

19-
public object? ConvertBack(object? value, Type targetType, object parameter, CultureInfo culture)
19+
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
2020
{
2121
if (value is bool boolValue && boolValue && parameter is Enum enumParameter)
2222
{
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using Avalonia.Data.Converters;
2+
using System;
3+
4+
namespace Gui.Models.Converters;
5+
6+
public class NegativeValueConverter : IValueConverter
7+
{
8+
public object Convert(object? value, Type targetType, object? parameter, System.Globalization.CultureInfo culture)
9+
{
10+
if (value is int intValue)
11+
{
12+
return -intValue;
13+
}
14+
else if (value is uint uintValue)
15+
{
16+
return -uintValue;
17+
}
18+
else if (value is short shortValue)
19+
{
20+
return -shortValue;
21+
}
22+
else if (value is ushort ushortValue)
23+
{
24+
return -ushortValue;
25+
}
26+
else if (value is sbyte byteValue)
27+
{
28+
return -byteValue;
29+
}
30+
else if (value is byte sbyteValue)
31+
{
32+
return -sbyteValue;
33+
}
34+
35+
return Avalonia.Data.BindingNotification.UnsetValue;
36+
}
37+
38+
public object ConvertBack(object? value, Type targetType, object? parameter, System.Globalization.CultureInfo culture)
39+
=> Convert(value, targetType, parameter, culture);
40+
}

Gui/PlatformSpecific.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ static void FolderOpenInDesktopCore(string directory, string? filename = null)
5959
{
6060
if (!Directory.Exists(directory))
6161
{
62-
throw new ArgumentException("The specified folder does not exist.", nameof(directory));
62+
throw new ArgumentException("The specified folder does not exist. Folder=\"{directory}\"", nameof(directory));
6363
}
6464

6565
// Platform-specific command construction

Gui/ViewModels/SubObjectTypes/ImageTableViewModel.cs

Lines changed: 57 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1+
using Avalonia.Controls.PanAndZoom;
12
using Avalonia.Controls.Selection;
2-
using Avalonia.Media.Imaging;
33
using Avalonia.Threading;
44
using Definitions.ObjectModels;
55
using DynamicData;
@@ -30,20 +30,6 @@ public class ImageTableViewModel : ReactiveObject, IExtraContentViewModel
3030
[Reactive]
3131
public ColourRemapSwatch SelectedSecondarySwatch { get; set; } = ColourRemapSwatch.SecondaryRemap;
3232

33-
readonly DispatcherTimer animationTimer;
34-
int currentFrameIndex;
35-
36-
public IList<Bitmap> SelectedBitmaps { get; set; }
37-
38-
[Reactive] public Bitmap SelectedBitmapPreview { get; set; }
39-
public Avalonia.Size SelectedBitmapPreviewBorder
40-
=> SelectedBitmapPreview == null
41-
? new Avalonia.Size()
42-
: new Avalonia.Size(SelectedBitmapPreview.Size.Width + 2, SelectedBitmapPreview.Size.Height + 2);
43-
44-
[Reactive]
45-
public int AnimationSpeed { get; set; } = 40;
46-
4733
[Reactive]
4834
public ICommand ReplaceImageCommand { get; set; }
4935

@@ -56,29 +42,33 @@ public Avalonia.Size SelectedBitmapPreviewBorder
5642
[Reactive]
5743
public ICommand CropAllImagesCommand { get; set; }
5844

45+
[Reactive]
46+
public ICommand ZeroOffsetAllImagesCommand { get; set; }
47+
48+
[Reactive]
49+
public ICommand CenterOffsetAllImagesCommand { get; set; }
50+
5951
// what is displaying on the ui
6052
[Reactive]
61-
public ObservableCollection<Bitmap?> Bitmaps { get; set; }
53+
public ObservableCollection<ImageViewModel> ImageViewModels { get; set; } = [];
6254

6355
[Reactive]
6456
public int SelectedImageIndex { get; set; } = -1;
6557

6658
[Reactive]
67-
public SelectionModel<Bitmap> SelectionModel { get; set; }
68-
69-
public UIG1Element32? SelectedG1Element
70-
=> SelectedImageIndex == -1 || Model.G1Provider.GraphicsElements.Count == 0 || SelectedImageIndex >= Model.G1Provider.GraphicsElements.Count
71-
? null
72-
: new UIG1Element32(SelectedImageIndex, Model.GetImageName(SelectedImageIndex), Model.G1Provider.GraphicsElements[SelectedImageIndex]);
73-
74-
public Avalonia.Point SelectedG1ElementOffset
75-
=> SelectedG1Element == null
76-
? new Avalonia.Point()
77-
: new Avalonia.Point(-SelectedG1Element?.XOffset ?? 0, -SelectedG1Element?.YOffset ?? 0);
78-
public Avalonia.Size SelectedG1ElementSize
79-
=> SelectedG1Element == null
80-
? new Avalonia.Size()
81-
: new Avalonia.Size(SelectedG1Element?.Width ?? 0, SelectedG1Element?.Height ?? 0);
59+
public SelectionModel<ImageViewModel> SelectionModel { get; set; }
60+
61+
[Reactive]
62+
public ImageViewModel? SelectedImage { get; set; }
63+
64+
readonly DispatcherTimer animationTimer;
65+
int currentFrameIndex;
66+
67+
[Reactive]
68+
public IList<ImageViewModel> SelectedBitmaps { get; set; }
69+
70+
[Reactive]
71+
public int AnimationSpeed { get; set; } = 40;
8272

8373
ImageTableModel Model { get; init; }
8474

@@ -95,17 +85,12 @@ public ImageTableViewModel(ImageTableModel model)
9585
.Subscribe(_ => UpdateBitmaps());
9686

9787
_ = this.WhenAnyValue(o => o.SelectedImageIndex)
98-
.Subscribe(_ => this.RaisePropertyChanged(nameof(SelectedG1Element))); // disabling this line stops mem leak
99-
_ = this.WhenAnyValue(o => o.SelectedG1Element)
100-
.Subscribe(_ => this.RaisePropertyChanged(nameof(SelectedG1ElementOffset)));
101-
_ = this.WhenAnyValue(o => o.SelectedG1Element)
102-
.Subscribe(_ => this.RaisePropertyChanged(nameof(SelectedG1ElementSize)));
103-
_ = this.WhenAnyValue(o => o.SelectedG1Element)
104-
.Subscribe(_ => this.RaisePropertyChanged(nameof(SelectedBitmapPreview)));
105-
_ = this.WhenAnyValue(o => o.SelectedBitmapPreview)
106-
.Subscribe(_ => this.RaisePropertyChanged(nameof(SelectedBitmapPreviewBorder)));
88+
.Where(index => index >= 0 && index < ImageViewModels?.Count)
89+
.Subscribe(_ => SelectedImage = ImageViewModels[SelectedImageIndex]);
90+
10791
_ = this.WhenAnyValue(o => o.AnimationSpeed)
108-
.Subscribe(_ => UpdateAnimationSpeed());
92+
.Where(_ => animationTimer != null)
93+
.Subscribe(_ => animationTimer!.Interval = TimeSpan.FromMilliseconds(1000 / AnimationSpeed));
10994

11095
ImportImagesCommand = ReactiveCommand.CreateFromTask(ImportImages);
11196
ExportImagesCommand = ReactiveCommand.CreateFromTask(ExportImages);
@@ -116,6 +101,24 @@ public ImageTableViewModel(ImageTableModel model)
116101
UpdateBitmaps();
117102
});
118103

104+
ZeroOffsetAllImagesCommand = ReactiveCommand.Create(() =>
105+
{
106+
foreach (var ivm in ImageViewModels)
107+
{
108+
ivm.XOffset = 0;
109+
ivm.YOffset = 0;
110+
}
111+
});
112+
113+
CenterOffsetAllImagesCommand = ReactiveCommand.Create(() =>
114+
{
115+
foreach (var ivm in ImageViewModels)
116+
{
117+
ivm.XOffset = (short)(-ivm.Width / 2);
118+
ivm.YOffset = (short)(-ivm.Height / 2);
119+
}
120+
});
121+
119122
UpdateBitmaps();
120123

121124
// Set up the animation timer
@@ -129,7 +132,7 @@ public ImageTableViewModel(ImageTableModel model)
129132

130133
void SelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e)
131134
{
132-
var sm = (SelectionModel<Bitmap>)sender;
135+
var sm = (SelectionModel<ImageViewModel>)sender;
133136

134137
if (sm.SelectedIndexes.Count > 0)
135138
{
@@ -142,17 +145,7 @@ void SelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e)
142145
}
143146

144147
// ... handle selection changed
145-
SelectedBitmaps = [.. sm.SelectedItems.Cast<Bitmap>()];
146-
}
147-
148-
void UpdateAnimationSpeed()
149-
{
150-
if (animationTimer == null)
151-
{
152-
return;
153-
}
154-
155-
animationTimer.Interval = TimeSpan.FromMilliseconds(1000 / AnimationSpeed);
148+
SelectedBitmaps = [.. sm.SelectedItems.Cast<ImageViewModel>()];
156149
}
157150

158151
void AnimationTimer_Tick(object? sender, EventArgs e)
@@ -167,8 +160,7 @@ void AnimationTimer_Tick(object? sender, EventArgs e)
167160
currentFrameIndex = 0;
168161
}
169162

170-
// Update the displayed image
171-
SelectedBitmapPreview = SelectedBitmaps[currentFrameIndex];
163+
// Update the displayed image viewmodel
172164
SelectedImageIndex = SelectionModel.SelectedIndexes[currentFrameIndex]; // disabling this also makes the memory leaks stop
173165

174166
// Move to the next frame, looping back to the beginning if necessary
@@ -177,7 +169,7 @@ void AnimationTimer_Tick(object? sender, EventArgs e)
177169

178170
void CreateSelectionModel()
179171
{
180-
SelectionModel = new SelectionModel<Bitmap>
172+
SelectionModel = new SelectionModel<ImageViewModel>
181173
{
182174
SingleSelect = false
183175
};
@@ -213,7 +205,15 @@ public async Task ImportImages()
213205
public void UpdateBitmaps()
214206
{
215207
Model.RecalcImages(SelectedPrimarySwatch, SelectedSecondarySwatch);
216-
Bitmaps = [.. G1ImageConversion.CreateAvaloniaImages(Model.Images)];
208+
var newImages = G1ImageConversion.CreateAvaloniaImages(Model.Images);
209+
210+
ImageViewModels.Clear();
211+
var i = 0;
212+
foreach (var image in newImages)
213+
{
214+
ImageViewModels.Add(new ImageViewModel(i, Model.GetImageName(i), Model.G1Provider.GraphicsElements[i], image));
215+
i++;
216+
}
217217
}
218218

219219
public async Task ExportImages()
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using Avalonia.Media.Imaging;
2+
using Definitions.ObjectModels.Types;
3+
using ReactiveUI;
4+
using ReactiveUI.Fody.Helpers;
5+
using System.ComponentModel;
6+
using System;
7+
8+
namespace Gui.ViewModels;
9+
10+
public class ImageViewModel : ReactiveObject
11+
{
12+
[ReadOnly(true)] public int ImageIndex { get; init; }
13+
[ReadOnly(true)] public string ImageName { get; init; }
14+
[ReadOnly(true)] public short Width { get; init; }
15+
[ReadOnly(true)] public short Height { get; init; }
16+
17+
[Reactive] public short XOffset { get; set; }
18+
[Reactive] public short YOffset { get; set; }
19+
20+
public GraphicsElementFlags Flags { get; set; }
21+
public short ZoomOffset { get; set; }
22+
23+
[Reactive, Browsable(false)]
24+
public Bitmap Image { get; set; }
25+
26+
[Browsable(false)]
27+
public Avalonia.Size SelectedBitmapPreviewBorder
28+
=> Image == null
29+
? new Avalonia.Size()
30+
: new Avalonia.Size(Image.Size.Width + 2, Image.Size.Height + 2);
31+
32+
ImageViewModel() { }
33+
34+
public ImageViewModel(int imageIndex, string imageName, GraphicsElement graphicsElement, Bitmap image)
35+
: this()
36+
{
37+
ImageIndex = imageIndex;
38+
ImageName = imageName;
39+
Width = graphicsElement.Width;
40+
Height = graphicsElement.Height;
41+
XOffset = graphicsElement.XOffset;
42+
YOffset = graphicsElement.YOffset;
43+
Flags = graphicsElement.Flags;
44+
ZoomOffset = graphicsElement.ZoomOffset;
45+
Image = image;
46+
47+
_ = this.WhenAnyValue(o => o.Image)
48+
.Subscribe(_ => this.RaisePropertyChanged(nameof(SelectedBitmapPreviewBorder)));
49+
}
50+
}

0 commit comments

Comments
 (0)