Skip to content

Commit f38e7cc

Browse files
committed
add better pan and zoom control to image previewer
1 parent af438b6 commit f38e7cc

File tree

5 files changed

+75
-53
lines changed

5 files changed

+75
-53
lines changed

Gui/Gui.csproj

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -38,24 +38,25 @@
3838
</ItemGroup>
3939

4040
<ItemGroup>
41-
<PackageReference Include="Avalonia" Version="11.1.4" />
42-
<PackageReference Include="Avalonia.Controls.ColorPicker" Version="11.1.4" />
43-
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.1.4" />
44-
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.1.4" />
45-
<PackageReference Include="Avalonia.Desktop" Version="11.1.4" />
46-
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.1.4" />
47-
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.1.4" />
41+
<PackageReference Include="Avalonia" Version="11.2.4" />
42+
<PackageReference Include="Avalonia.Controls.ColorPicker" Version="11.2.4" />
43+
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.2.4" />
44+
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.1.5" />
45+
<PackageReference Include="Avalonia.Controls.PanAndZoom" Version="11.2.0" />
46+
<PackageReference Include="Avalonia.Desktop" Version="11.2.4" />
47+
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.4" />
48+
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.2.4" />
4849
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
49-
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.1.4" />
50-
<PackageReference Include="Avalonia.ReactiveUI" Version="11.1.4" />
51-
<PackageReference Include="bodong.Avalonia.PropertyGrid" Version="11.1.4.2" />
52-
<PackageReference Include="bodong.PropertyModels" Version="11.1.4.2" />
53-
<PackageReference Include="Material.Icons.Avalonia" Version="2.1.10" />
50+
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.2.4" />
51+
<PackageReference Include="Avalonia.ReactiveUI" Version="11.2.4" />
52+
<PackageReference Include="bodong.Avalonia.PropertyGrid" Version="11.2.3.5" />
53+
<PackageReference Include="bodong.PropertyModels" Version="11.2.3.5" />
54+
<PackageReference Include="Material.Icons.Avalonia" Version="2.2.0" />
5455
<PackageReference Include="MessageBox.Avalonia" Version="3.2.0" />
5556
<PackageReference Include="NAudio" Version="2.2.1" />
5657
<PackageReference Include="ReactiveUI.Fody" Version="19.5.41" />
5758
<PackageReference Include="ReactiveUI.Validation" Version="4.1.1" />
58-
<PackageReference Include="System.Text.Json" Version="9.0.1" />
59+
<PackageReference Include="System.Text.Json" Version="9.0.2" />
5960
</ItemGroup>
6061

6162
<ItemGroup>

Gui/ViewModels/SubObjectTypes/ImageTableViewModel.cs

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,10 @@ public class ImageTableViewModel : ReactiveObject, IExtraContentViewModel
7474
public IList<Bitmap> SelectedBitmaps { get; set; }
7575

7676
[Reactive] public Bitmap SelectedBitmapPreview { get; set; }
77-
public Avalonia.Size SelectedBitmapPreviewBorder => SelectedBitmapPreview == null
78-
? new Avalonia.Size()
79-
: new Avalonia.Size(SelectedBitmapPreview.Size.Width + 2, SelectedBitmapPreview.Size.Height + 2);
80-
81-
[Reactive] public int AnimationWindowHeight { get; set; }
77+
public Avalonia.Size SelectedBitmapPreviewBorder
78+
=> SelectedBitmapPreview == null
79+
? new Avalonia.Size()
80+
: new Avalonia.Size(SelectedBitmapPreview.Size.Width + 2, SelectedBitmapPreview.Size.Height + 2);
8281

8382
[Reactive]
8483
public int AnimationSpeed { get; set; } = 40;
@@ -120,6 +119,15 @@ public UIG1Element32? SelectedG1Element
120119
? null
121120
: new UIG1Element32(SelectedImageIndex, GetImageName(NameProvider, SelectedImageIndex), G1Provider.G1Elements[SelectedImageIndex]);
122121

122+
public Avalonia.Point SelectedG1ElementOffset
123+
=> SelectedG1Element == null
124+
? new Avalonia.Point()
125+
: new Avalonia.Point(-SelectedG1Element?.G1Element.XOffset ?? 0, -SelectedG1Element?.G1Element.YOffset ?? 0);
126+
public Avalonia.Size SelectedG1ElementSize
127+
=> SelectedG1Element == null
128+
? new Avalonia.Size()
129+
: new Avalonia.Size(SelectedG1Element?.G1Element.Width ?? 0, SelectedG1Element?.G1Element.Height ?? 0);
130+
123131
public ImageTableViewModel(IHasG1Elements g1ElementProvider, IImageTableNameProvider imageNameProvider, PaletteMap paletteMap, IList<Image<Rgba32>> images, ILogger logger)
124132
{
125133
G1Provider = g1ElementProvider;
@@ -138,6 +146,10 @@ public ImageTableViewModel(IHasG1Elements g1ElementProvider, IImageTableNameProv
138146
.Subscribe(_ => Bitmaps = new ObservableCollection<Bitmap?>(G1ImageConversion.CreateAvaloniaImages(Images)));
139147
_ = this.WhenAnyValue(o => o.SelectedImageIndex)
140148
.Subscribe(_ => this.RaisePropertyChanged(nameof(SelectedG1Element)));
149+
_ = this.WhenAnyValue(o => o.SelectedG1Element)
150+
.Subscribe(_ => this.RaisePropertyChanged(nameof(SelectedG1ElementOffset)));
151+
_ = this.WhenAnyValue(o => o.SelectedG1Element)
152+
.Subscribe(_ => this.RaisePropertyChanged(nameof(SelectedG1ElementSize)));
141153
_ = this.WhenAnyValue(o => o.SelectedBitmapPreview)
142154
.Subscribe(_ => this.RaisePropertyChanged(nameof(SelectedBitmapPreviewBorder)));
143155
_ = this.WhenAnyValue(o => o.AnimationSpeed)
@@ -176,7 +188,6 @@ void SelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e)
176188

177189
// ... handle selection changed
178190
SelectedBitmaps = sm.SelectedItems.Cast<Bitmap>().ToList();
179-
AnimationWindowHeight = (int)SelectedBitmaps.Max(x => x.Size.Height) * 2;
180191
}
181192

182193
void UpdateAnimationSpeed()
@@ -429,9 +440,7 @@ public async Task ReplaceImage()
429440
}
430441

431442
void UpdateImage(Image<Rgba32> img, int index, SpriteOffset? offset = null)
432-
{
433-
UpdateImage(img, index, offset?.X, offset?.Y);
434-
}
443+
=> UpdateImage(img, index, offset?.X, offset?.Y);
435444

436445
void UpdateImage(Image<Rgba32> img, int index, short? xOffset, short? yOffset)
437446
{

Gui/ViewModels/SubObjectTypes/UIG1Element32.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ namespace OpenLoco.Gui.ViewModels
66
public record UIG1Element32(
77
[Category("Image")] int ImageIndex,
88
[Category("Image")] string ImageName,
9-
G1Element32 g1Element
9+
G1Element32 G1Element
1010
);
1111
}

Gui/Views/ImageTableView.axaml

Lines changed: 24 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
88
xmlns:pgc="clr-namespace:Avalonia.PropertyGrid.Controls;assembly=Avalonia.PropertyGrid"
99
xmlns:amxc="using:Avalonia.Markup.Xaml.Converters"
10+
xmlns:paz="using:Avalonia.Controls.PanAndZoom"
1011
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
1112
x:Class="OpenLoco.Gui.Views.ImageTableView"
1213
x:DataType="vm:ImageTableViewModel">
@@ -17,22 +18,22 @@
1718

1819
<DockPanel>
1920
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal">
20-
<Button Command="{Binding ImportImagesCommand}" HorizontalAlignment="Stretch" Margin="2" Padding="2" ToolTip.Tip="Import from Directory">
21+
<Button Command="{Binding ImportImagesCommand}" HorizontalAlignment="Stretch" Margin="2" Padding="2" ToolTip.Tip="Import a directory of .pngs">
2122
<DockPanel>
2223
<materialIcons:MaterialIcon Kind="FolderUpload" Width="24" Height="24" Margin="2" />
23-
<TextBlock HorizontalAlignment="Stretch" VerticalAlignment="Center" Margin="4">Import from Directory</TextBlock>
24+
<TextBlock HorizontalAlignment="Stretch" VerticalAlignment="Center" Margin="4">Import</TextBlock>
2425
</DockPanel>
2526
</Button>
26-
<Button Command="{Binding ExportImagesCommand}" HorizontalAlignment="Stretch" Margin="2" Padding="2" ToolTip.Tip="Export to Directory">
27+
<Button Command="{Binding ExportImagesCommand}" HorizontalAlignment="Stretch" Margin="2" Padding="2" ToolTip.Tip="Export all images as .pngs to a directory">
2728
<DockPanel>
2829
<materialIcons:MaterialIcon Kind="FolderDownload" Width="24" Height="24" Margin="2" />
29-
<TextBlock HorizontalAlignment="Stretch" VerticalAlignment="Center" Margin="4">Export to Directory</TextBlock>
30+
<TextBlock HorizontalAlignment="Stretch" VerticalAlignment="Center" Margin="4">Export</TextBlock>
3031
</DockPanel>
3132
</Button>
32-
<Button HorizontalAlignment="Stretch" Margin="2" Padding="2" ToolTip.Tip="Select background colour">
33+
<Button HorizontalAlignment="Stretch" Margin="2" Padding="2" ToolTip.Tip="Background colour">
3334
<DockPanel>
3435
<materialIcons:MaterialIcon Kind="Palette" Width="24" Height="24" Margin="2" />
35-
<TextBlock HorizontalAlignment="Stretch" VerticalAlignment="Center" Margin="4">Select background colour</TextBlock>
36+
<TextBlock HorizontalAlignment="Stretch" VerticalAlignment="Center" Margin="4">Background colour</TextBlock>
3637
</DockPanel>
3738
<Button.Flyout >
3839
<Flyout>
@@ -43,7 +44,7 @@
4344
<Button Command="{Binding CropAllImagesCommand}" HorizontalAlignment="Stretch" Margin="2" Padding="2" ToolTip.Tip="Crops all images and adjusts offsets">
4445
<DockPanel>
4546
<materialIcons:MaterialIcon Kind="Crop" Width="24" Height="24" Margin="2" />
46-
<TextBlock HorizontalAlignment="Stretch" VerticalAlignment="Center" Margin="4">Crop all images</TextBlock>
47+
<TextBlock HorizontalAlignment="Stretch" VerticalAlignment="Center" Margin="4">Crop all</TextBlock>
4748
</DockPanel>
4849
</Button>
4950
<!--<ComboBox ItemsSource="{Binding ColourSwatchesArr}" SelectedItem="{Binding SelectedColourSwatch}" HorizontalAlignment="Stretch" VerticalAlignment="Center" />-->
@@ -95,7 +96,7 @@
9596
</DockPanel>
9697
<Button.Flyout >
9798
<Flyout>
98-
<ColorView Name="ImageColorViewBorder" Palette="" Color="Gray" IsColorModelVisible="False" IsColorSpectrumVisible="False" IsColorComponentsVisible="False" />
99+
<ColorView Name="ImageColorViewBorder" Palette="" Color="Gold" IsColorModelVisible="False" IsColorSpectrumVisible="False" IsColorComponentsVisible="False" />
99100
</Flyout>
100101
</Button.Flyout>
101102
</Button>
@@ -111,29 +112,22 @@
111112
</Button.Flyout>
112113
</Button>
113114
</StackPanel>
114-
<Grid>
115-
<!-- Bounding box -->
116-
<Rectangle Fill="{Binding #ImageColorViewPreviewBackground.Color, ConverterParameter={x:Static Brushes.Transparent}, Converter={StaticResource ColorToBrushConverter}}" Stroke="{Binding #ImageColorViewBorder.Color, ConverterParameter={x:Static Brushes.Transparent}, Converter={StaticResource ColorToBrushConverter}}" StrokeThickness="1.0" Width="{Binding SelectedBitmapPreviewBorder.Width}" Height="{Binding SelectedBitmapPreviewBorder.Height}" Opacity="1.0">
117-
<Rectangle.RenderTransform>
118-
<ScaleTransform ScaleX="{Binding Zoom}" ScaleY="{Binding Zoom}" />
119-
</Rectangle.RenderTransform>
120-
</Rectangle>
121-
<Image Name="AnimationPreviewPP" Source="{Binding SelectedBitmapPreview}" RenderOptions.BitmapInterpolationMode="None" Stretch="None" Height="{Binding AnimationWindowHeight}" Width="256">
122-
<Image.RenderTransform>
123-
<ScaleTransform ScaleX="{Binding Zoom}" ScaleY="{Binding Zoom}" />
124-
</Image.RenderTransform>
125-
</Image>
126-
<!-- Origin point -->
127-
<Rectangle Fill="{Binding #ImageColorViewOriginPoint.Color, ConverterParameter={x:Static Brushes.Transparent}, Converter={StaticResource ColorToBrushConverter}}" Width="1" Height="1" Opacity="1.0">
128-
<Rectangle.RenderTransform>
129-
<ScaleTransform ScaleX="{Binding Zoom}" ScaleY="{Binding Zoom}" />
130-
</Rectangle.RenderTransform>
131-
</Rectangle>
132-
</Grid>
133-
<TextBlock HorizontalAlignment="Center" Text="{Binding AnimationSpeed, StringFormat='Animation FPS: {0}'}"></TextBlock>
115+
<ScrollViewer Grid.Row="4" Grid.Column="1" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto">
116+
<paz:ZoomBorder Name="ZoomBorder" MinWidth="256" MinHeight="256" Stretch="Uniform" ZoomSpeed="1.2" MinZoomX="0.5" MaxZoomX="30" MinZoomY="0.5" MaxZoomY="30" ClipToBounds="True" Focusable="True" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" PanButton="Left" Background="{Binding #ImageColorViewPreviewBackground.Color, ConverterParameter={x:Static Brushes.Transparent}, Converter={StaticResource ColorToBrushConverter}}" >
117+
<Canvas Width="{Binding SelectedG1ElementSize.Width}" Height="{Binding SelectedG1ElementSize.Height}" HorizontalAlignment="Center" VerticalAlignment="Center" Background="Gray">
118+
<!--Bounding box-->
119+
<Rectangle Canvas.Top="-1" Canvas.Left="-1" StrokeThickness="1.0" Width="{Binding SelectedBitmapPreviewBorder.Width}" Height="{Binding SelectedBitmapPreviewBorder.Height}" Opacity="1.0" Fill="{Binding #ImageColorViewPreviewBackground.Color, ConverterParameter={x:Static Brushes.Transparent}, Converter={StaticResource ColorToBrushConverter}}" Stroke="{Binding #ImageColorViewBorder.Color, ConverterParameter={x:Static Brushes.Transparent}, Converter={StaticResource ColorToBrushConverter}}" />
120+
<!--Image-->
121+
<Image Name="AnimationPreviewPP" Source="{Binding SelectedBitmapPreview}" RenderOptions.BitmapInterpolationMode="None" Stretch="None"/>
122+
<!--Origin point-->
123+
<Rectangle Canvas.Left="{Binding SelectedG1ElementOffset.X}" Canvas.Top="{Binding SelectedG1ElementOffset.Y}" Width="1" Height="1" Opacity="1.0" Fill="{Binding #ImageColorViewOriginPoint.Color, ConverterParameter={x:Static Brushes.Transparent}, Converter={StaticResource ColorToBrushConverter}}" />
124+
</Canvas>
125+
</paz:ZoomBorder>
126+
</ScrollViewer>
127+
<TextBlock HorizontalAlignment="Center" Text="{Binding #ZoomBorder.ZoomX, StringFormat='Zoom: {0:F2}x | \'R\' to reset'}"></TextBlock>
128+
<!--<Slider Minimum="1" Maximum="10" Value="{Binding ZoomBorder.ZoomX}" />-->
129+
<TextBlock Margin="0, 16, 0, 0" HorizontalAlignment="Center" Text="{Binding AnimationSpeed, StringFormat='Animation FPS: {0}'}"></TextBlock>
134130
<Slider Minimum="1" Maximum="80" Value="{Binding AnimationSpeed}" />
135-
<TextBlock HorizontalAlignment="Center" Text="{Binding Zoom, StringFormat='Zoom: {0}x'}"></TextBlock>
136-
<Slider Minimum="1" Maximum="10" Value="{Binding Zoom}" />
137131
</StackPanel>
138132
<ScrollViewer>
139133
<pgc:PropertyGrid x:Name="propertyGrid_imageProps" Margin="8" MinWidth="256" DataContext="{Binding SelectedG1Element}" DockPanel.Dock="Right" ShowTitle="False" AllowFilter="False" AllowQuickFilter="False" ShowStyle="Tiled"></pgc:PropertyGrid>

Gui/Views/ImageTableView.axaml.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using Avalonia.Controls;
2+
using Avalonia.Controls.PanAndZoom;
3+
using Avalonia.Input;
24

35
namespace OpenLoco.Gui.Views
46
{
@@ -7,6 +9,22 @@ public partial class ImageTableView : UserControl
79
public ImageTableView()
810
{
911
InitializeComponent();
12+
ZoomBorder = this.Find<ZoomBorder>("ZoomBorder");
13+
if (ZoomBorder != null)
14+
{
15+
ZoomBorder.KeyDown += ZoomBorder_KeyDown;
16+
}
17+
}
18+
19+
void ZoomBorder_KeyDown(object? sender, KeyEventArgs e)
20+
{
21+
ZoomBorder = this.Find<ZoomBorder>("ZoomBorder");
22+
switch (e.Key)
23+
{
24+
case Key.R:
25+
ZoomBorder?.ResetMatrix();
26+
break;
27+
}
1028
}
1129
}
1230
}

0 commit comments

Comments
 (0)