From bd5993a7fdec39f6e7dd7421a236cab740d1d2e6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 17:59:39 +0000 Subject: [PATCH 1/2] Add read-only zoom/offset workaround for Avalonia PanAndZoom via callbacks - Add readOnlyZoomAndOffsets parameter to ViewTabViewModel constructor (default false) - Add ApplyTransformCallback and PanStepCallback callback properties - Modify Fill, Fit, Zoom100, DefferedFill to use callbacks in read-only mode - Modify StartScrollingAsync to use step-per-second translation via callback - Pass readOnlyZoomAndOffsets=true in Avalonia DI registration - Remove PanelAttributesAP bindings from Avalonia ZoomBorder XAML - Register ZoomBorder matrix callbacks in MainWindow.Methods.axaml.cs - WPF version remains completely unaffected Agent-Logs-Url: https://github.com/JohnnyJF10/TgaBuilder/sessions/dd11e2f8-35b2-4466-9eed-5693e68e6e9b Co-authored-by: JohnnyJF10 <83164789+JohnnyJF10@users.noreply.github.com> --- TgaBuilderAvaloniaUi/App.DI.axaml.cs | 6 +- .../View/MainWindow.Methods.axaml.cs | 24 +++ TgaBuilderAvaloniaUi/View/MainWindow.axaml | 10 -- TgaBuilderAvaloniaUi/View/MainWindow.axaml.cs | 13 ++ .../ViewModel/Tabs/ViewTabViewModel.cs | 157 +++++++++++++----- 5 files changed, 159 insertions(+), 51 deletions(-) diff --git a/TgaBuilderAvaloniaUi/App.DI.axaml.cs b/TgaBuilderAvaloniaUi/App.DI.axaml.cs index 1e96c9a..61144c3 100644 --- a/TgaBuilderAvaloniaUi/App.DI.axaml.cs +++ b/TgaBuilderAvaloniaUi/App.DI.axaml.cs @@ -250,12 +250,14 @@ private void AddTabVMsToProvider(IServiceCollection services) services.AddTransient(sp => new ViewTabViewModel( visualPanelSize: sp.GetServices() .ElementAt((int)PresenterType.Source), - panel: sp.GetRequiredService())); + panel: sp.GetRequiredService(), + readOnlyZoomAndOffsets: true)); services.AddTransient(sp => new ViewTabViewModel( visualPanelSize: sp.GetServices() .ElementAt((int)PresenterType.Target), - panel: sp.GetRequiredService())); + panel: sp.GetRequiredService(), + readOnlyZoomAndOffsets: true)); } private void AddViewVMsToProvider(IServiceCollection services) diff --git a/TgaBuilderAvaloniaUi/View/MainWindow.Methods.axaml.cs b/TgaBuilderAvaloniaUi/View/MainWindow.Methods.axaml.cs index 450bd10..24cb8bc 100644 --- a/TgaBuilderAvaloniaUi/View/MainWindow.Methods.axaml.cs +++ b/TgaBuilderAvaloniaUi/View/MainWindow.Methods.axaml.cs @@ -1,8 +1,11 @@  +using Avalonia; using Avalonia.Controls; using Avalonia.Controls.PanAndZoom; +using Avalonia.Threading; using Avalonia.VisualTree; using System; +using TgaBuilderLib.ViewModel; namespace TgaBuilderAvaloniaUi.View { @@ -27,5 +30,26 @@ public ZoomBorder GetPanelFromImage(Image image) public void SetPanelFromImage(Image image) => CurrentPanel = GetPanelFromImage(image); + + public void RegisterZoomBorderCallbacks(ViewTabViewModel viewTab, ZoomBorder panel) + { + viewTab.ApplyTransformCallback = (zoom, translateX, translateY) => + { + Dispatcher.UIThread.Post(() => + { + var matrix = Matrix.CreateScale(zoom, zoom) * + Matrix.CreateTranslation(translateX, translateY); + panel.SetMatrix(matrix); + }); + }; + + viewTab.PanStepCallback = (deltaX, deltaY) => + { + Dispatcher.UIThread.Post(() => + { + panel.SetMatrix(panel.Matrix * Matrix.CreateTranslation(deltaX, deltaY)); + }); + }; + } } } diff --git a/TgaBuilderAvaloniaUi/View/MainWindow.axaml b/TgaBuilderAvaloniaUi/View/MainWindow.axaml index b6aad4a..423a2e8 100644 --- a/TgaBuilderAvaloniaUi/View/MainWindow.axaml +++ b/TgaBuilderAvaloniaUi/View/MainWindow.axaml @@ -430,9 +430,6 @@ x:Name="SourcePanel" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" - ap:PanelAttributesAP.OffsetXAtr="{Binding MultipliedOffsetX}" - ap:PanelAttributesAP.OffsetYAtr="{Binding MultipliedOffsetY}" - ap:PanelAttributesAP.ZoomAtr="{Binding Zoom}" ap:PanelMouseAP.ScrollCommand="{Binding ScrollCommand}" ap:SizeObserverAP.ObserveSize="True" ap:SizeObserverAP.ObservedHeight="{Binding VisualPanelSize.ViewportHeight, UpdateSourceTrigger=PropertyChanged, Mode=OneWayToSource}" @@ -448,8 +445,6 @@ PanButton="Middle" Stretch="None" ZoomSpeed="1.5" - OffsetX="{Binding MultipliedOffsetX, Mode=OneWayToSource}" - OffsetY="{Binding MultipliedOffsetY, Mode=OneWayToSource}" ZoomX="{Binding Zoom, Mode=OneWayToSource}" ZoomY="{Binding Zoom, Mode=OneWayToSource}"> @@ -1083,9 +1078,6 @@ x:Name="TargetPanel" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" - ap:PanelAttributesAP.OffsetXAtr="{Binding MultipliedOffsetX}" - ap:PanelAttributesAP.OffsetYAtr="{Binding MultipliedOffsetY}" - ap:PanelAttributesAP.ZoomAtr="{Binding Zoom}" ap:PanelMouseAP.ScrollCommand="{Binding ScrollCommand}" ap:SizeObserverAP.ObserveSize="True" ap:SizeObserverAP.ObservedHeight="{Binding VisualPanelSize.ViewportHeight, Mode=OneWayToSource}" @@ -1101,8 +1093,6 @@ PanButton="Middle" Stretch="None" ZoomSpeed="1.5" - OffsetX="{Binding MultipliedOffsetX, Mode=OneWayToSource}" - OffsetY="{Binding MultipliedOffsetY, Mode=OneWayToSource}" ZoomX="{Binding Zoom, Mode=OneWayToSource}" ZoomY="{Binding Zoom, Mode=OneWayToSource}"> + { + var sourcePanel = this.FindControl("SourcePanel"); + var targetPanel = this.FindControl("TargetPanel"); + if (sourcePanel != null) + RegisterZoomBorderCallbacks(vm.SourceViewTab, sourcePanel); + if (targetPanel != null) + RegisterZoomBorderCallbacks(vm.DestinationViewTab, targetPanel); + }; + } } [Obsolete("For designer use only")] diff --git a/TgaBuilderLib/ViewModel/Tabs/ViewTabViewModel.cs b/TgaBuilderLib/ViewModel/Tabs/ViewTabViewModel.cs index 5925b04..03ff9fb 100644 --- a/TgaBuilderLib/ViewModel/Tabs/ViewTabViewModel.cs +++ b/TgaBuilderLib/ViewModel/Tabs/ViewTabViewModel.cs @@ -9,9 +9,11 @@ public class ViewTabViewModel : ViewModelBase { public ViewTabViewModel( PanelVisualSizeViewModel visualPanelSize, - TexturePanelViewModelBase panel) + TexturePanelViewModelBase panel, + bool readOnlyZoomAndOffsets = false) { _panel = panel; + _readOnlyZoomAndOffsets = readOnlyZoomAndOffsets; VisualPanelSize = visualPanelSize; _panel.PresenterChanged += (_, _) => _ = DefferedFill(); @@ -25,6 +27,7 @@ public ViewTabViewModel( public bool IsScrolling { get; set; } = false; private (double X, double Y) _scrollDirection; + private readonly bool _readOnlyZoomAndOffsets; private TexturePanelViewModelBase _panel; private double _offsetX; @@ -99,6 +102,20 @@ public double HorizonatlMargin public ICommand Zoom100Command => _100PercentCommand ??= new RelayCommand(Zoom100); public ICommand ScrollCommand => _scrollCommand ??= new(DoPanelScrolling); + /// + /// Callback to apply a full view transformation (zoom + translate). + /// Parameters: (zoom, translateX, translateY) in screen-space coordinates. + /// Used when zoom and offsets are read-only (e.g. Avalonia PanAndZoom). + /// + public Action? ApplyTransformCallback { get; set; } + + /// + /// Callback to apply an incremental pan step. + /// Parameters: (deltaX, deltaY) in screen-space coordinates. + /// Used when zoom and offsets are read-only (e.g. Avalonia PanAndZoom). + /// + public Action? PanStepCallback { get; set; } + // A makeshift workaround to avoid a race condition when the panel is resized. @@ -107,8 +124,7 @@ public async Task DefferedFill() { await Task.Delay(20); - Zoom = 1; - Zoom = _panel.Presenter.PixelWidth < _panel.Presenter.PixelHeight + var zoom = _panel.Presenter.PixelWidth < _panel.Presenter.PixelHeight ? Math.Max( VisualPanelSize.ViewportWidth / _panel.Presenter.PixelWidth, VisualPanelSize.ViewportHeight / _panel.Presenter.PixelHeight) @@ -116,17 +132,30 @@ public async Task DefferedFill() VisualPanelSize.ViewportWidth / _panel.Presenter.PixelWidth, VisualPanelSize.ViewportHeight / _panel.Presenter.PixelHeight); - await Task.Delay(20); - OffsetY -= 1000; - OffsetY = 0; + if (_readOnlyZoomAndOffsets) + { + _panel.Zoom = zoom; + OnPropertyChanged(nameof(Zoom)); + OnContentActualSizeChanged(); + await Task.Delay(20); + ApplyTransformCallback?.Invoke(zoom, 0, 0); + } + else + { + Zoom = 1; + Zoom = zoom; + + await Task.Delay(20); + OffsetY -= 1000; + OffsetY = 0; - Debug.WriteLine("DefferedFill called, OffsetY reset to 0"); + Debug.WriteLine("DefferedFill called, OffsetY reset to 0"); + } } public void Fill() { - Zoom = 1.0; - Zoom = _panel.Presenter.PixelWidth < _panel.Presenter.PixelHeight + var zoom = _panel.Presenter.PixelWidth < _panel.Presenter.PixelHeight ? Math.Max( VisualPanelSize.ViewportWidth / _panel.Presenter.PixelWidth, VisualPanelSize.ViewportHeight / _panel.Presenter.PixelHeight) @@ -134,31 +163,68 @@ public void Fill() VisualPanelSize.ViewportWidth / _panel.Presenter.PixelWidth, VisualPanelSize.ViewportHeight / _panel.Presenter.PixelHeight); - OffsetX = 0; - OffsetY = 0; - OnPropertyChanged(nameof(MultipliedOffsetX)); - OnPropertyChanged(nameof(MultipliedOffsetY)); + if (_readOnlyZoomAndOffsets) + { + _panel.Zoom = zoom; + OnPropertyChanged(nameof(Zoom)); + OnContentActualSizeChanged(); + ApplyTransformCallback?.Invoke(zoom, 0, 0); + } + else + { + Zoom = 1.0; + Zoom = zoom; + + OffsetX = 0; + OffsetY = 0; + OnPropertyChanged(nameof(MultipliedOffsetX)); + OnPropertyChanged(nameof(MultipliedOffsetY)); + } } public void Fit() { - Zoom = 1.0; - Zoom = _panel.Presenter.PixelWidth < _panel.Presenter.PixelHeight + var zoom = _panel.Presenter.PixelWidth < _panel.Presenter.PixelHeight ? Math.Min( VisualPanelSize.ViewportWidth / _panel.Presenter.PixelWidth, VisualPanelSize.ViewportHeight / _panel.Presenter.PixelHeight) : Math.Max( VisualPanelSize.ViewportWidth / _panel.Presenter.PixelWidth, VisualPanelSize.ViewportHeight / _panel.Presenter.PixelHeight); + + if (_readOnlyZoomAndOffsets) + { + _panel.Zoom = zoom; + OnPropertyChanged(nameof(Zoom)); + OnContentActualSizeChanged(); + ApplyTransformCallback?.Invoke(zoom, 0, 0); + } + else + { + Zoom = 1.0; + Zoom = zoom; + } } public void Zoom100() { - Zoom = 1.0; - OffsetX = (_panel.Presenter.PixelWidth - VisualPanelSize.ViewportWidth) / 2; - OffsetY = (_panel.Presenter.PixelHeight - VisualPanelSize.ViewportHeight) / 2; - OnPropertyChanged(nameof(MultipliedOffsetX)); - OnPropertyChanged(nameof(MultipliedOffsetY)); + if (_readOnlyZoomAndOffsets) + { + _panel.Zoom = 1.0; + OnPropertyChanged(nameof(Zoom)); + OnContentActualSizeChanged(); + double offsetX = (_panel.Presenter.PixelWidth - VisualPanelSize.ViewportWidth) / 2; + double offsetY = (_panel.Presenter.PixelHeight - VisualPanelSize.ViewportHeight) / 2; + ApplyTransformCallback?.Invoke(1.0, -offsetX, -offsetY); + } + else + { + Zoom = 1.0; + OffsetX = (_panel.Presenter.PixelWidth - VisualPanelSize.ViewportWidth) / 2; + OffsetY = (_panel.Presenter.PixelHeight - VisualPanelSize.ViewportHeight) / 2; + OnPropertyChanged(nameof(MultipliedOffsetX)); + OnPropertyChanged(nameof(MultipliedOffsetY)); + } } private void SetPanelZoom(double zoom) @@ -216,33 +282,46 @@ private async Task StartScrollingAsync() { IsScrolling = true; - Stopwatch stopwatch = _stopwatch ?? new(); - stopwatch.Start(); + if (_readOnlyZoomAndOffsets) + { + while (IsScrolling) + { + double deltaX = -_scrollDirection.X * SCROLL_SPEED_PIX_PER_SEC; + double deltaY = -_scrollDirection.Y * SCROLL_SPEED_PIX_PER_SEC; + PanStepCallback?.Invoke(deltaX, deltaY); + await Task.Delay(1000); + } + } + else + { + Stopwatch stopwatch = _stopwatch ?? new(); + stopwatch.Start(); - long lastTicks = stopwatch.ElapsedTicks; + long lastTicks = stopwatch.ElapsedTicks; - while (IsScrolling) - { - long nowTicks = stopwatch.ElapsedTicks; - double elapsedSeconds = (nowTicks - lastTicks) / (double)Stopwatch.Frequency; - lastTicks = nowTicks; + while (IsScrolling) + { + long nowTicks = stopwatch.ElapsedTicks; + double elapsedSeconds = (nowTicks - lastTicks) / (double)Stopwatch.Frequency; + lastTicks = nowTicks; - double deltaX = _scrollDirection.X * SCROLL_SPEED_PIX_PER_SEC * elapsedSeconds / Zoom; - double deltaY = _scrollDirection.Y * SCROLL_SPEED_PIX_PER_SEC * elapsedSeconds / Zoom; + double deltaX = _scrollDirection.X * SCROLL_SPEED_PIX_PER_SEC * elapsedSeconds / Zoom; + double deltaY = _scrollDirection.Y * SCROLL_SPEED_PIX_PER_SEC * elapsedSeconds / Zoom; - OffsetX += deltaX; - OffsetY += deltaY; + OffsetX += deltaX; + OffsetY += deltaY; - OnPropertyChanged(nameof(MultipliedOffsetX)); - OnPropertyChanged(nameof(MultipliedOffsetY)); + OnPropertyChanged(nameof(MultipliedOffsetX)); + OnPropertyChanged(nameof(MultipliedOffsetY)); - maxX = Math.Max(0, _panel.Presenter.PixelWidth - (VisualPanelSize.ViewportWidth / Zoom)); - maxY = Math.Max(0, _panel.Presenter.PixelHeight - (VisualPanelSize.ViewportHeight / Zoom)); + maxX = Math.Max(0, _panel.Presenter.PixelWidth - (VisualPanelSize.ViewportWidth / Zoom)); + maxY = Math.Max(0, _panel.Presenter.PixelHeight - (VisualPanelSize.ViewportHeight / Zoom)); - OffsetX = Math.Clamp(OffsetX, 0, maxX); - OffsetY = Math.Clamp(OffsetY, 0, maxY); + OffsetX = Math.Clamp(OffsetX, 0, maxX); + OffsetY = Math.Clamp(OffsetY, 0, maxY); - await Task.Delay(3); + await Task.Delay(3); + } } } } From ce9165bd70009c94085b637b03f3ab674f8c0ba0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Apr 2026 16:13:02 +0000 Subject: [PATCH 2/2] Refactor ViewTabViewModel into IViewTabViewModel interface with WritableViewTabViewModel (WPF) and ReadOnlyViewTabViewModel (Avalonia) implementations. Remove ScrollViewer wrapping ZoomBorder in Avalonia. Replace zoom sliders with zoom in/out buttons in Avalonia. Agent-Logs-Url: https://github.com/JohnnyJF10/TgaBuilder/sessions/8054e0dc-5318-4ad0-a950-15115fde15af Co-authored-by: JohnnyJF10 <83164789+JohnnyJF10@users.noreply.github.com> --- TgaBuilderAvaloniaUi/App.DI.axaml.cs | 14 +- .../View/MainWindow.Methods.axaml.cs | 26 +- TgaBuilderAvaloniaUi/View/MainWindow.axaml | 78 ++---- TgaBuilderAvaloniaUi/View/MainWindow.axaml.cs | 8 +- .../Abstraction/IViewTabViewModel.cs | 42 +++ ...ewModel.cs => ReadOnlyViewTabViewModel.cs} | 199 +++++--------- .../Tabs/WritableViewTabViewModel.cs | 255 ++++++++++++++++++ .../ViewModel/Views/MainViewModel.cs | 10 +- TgaBuilderWpfUi/App.DI.xaml.cs | 8 +- 9 files changed, 432 insertions(+), 208 deletions(-) create mode 100644 TgaBuilderLib/Abstraction/IViewTabViewModel.cs rename TgaBuilderLib/ViewModel/Tabs/{ViewTabViewModel.cs => ReadOnlyViewTabViewModel.cs} (56%) create mode 100644 TgaBuilderLib/ViewModel/Tabs/WritableViewTabViewModel.cs diff --git a/TgaBuilderAvaloniaUi/App.DI.axaml.cs b/TgaBuilderAvaloniaUi/App.DI.axaml.cs index 61144c3..72523ec 100644 --- a/TgaBuilderAvaloniaUi/App.DI.axaml.cs +++ b/TgaBuilderAvaloniaUi/App.DI.axaml.cs @@ -247,17 +247,15 @@ private void AddTabVMsToProvider(IServiceCollection services) panel: sp.GetRequiredService(), messageBoxService: sp.GetRequiredService())); - services.AddTransient(sp => new ViewTabViewModel( + services.AddTransient(sp => new ReadOnlyViewTabViewModel( visualPanelSize: sp.GetServices() .ElementAt((int)PresenterType.Source), - panel: sp.GetRequiredService(), - readOnlyZoomAndOffsets: true)); + panel: sp.GetRequiredService())); - services.AddTransient(sp => new ViewTabViewModel( + services.AddTransient(sp => new ReadOnlyViewTabViewModel( visualPanelSize: sp.GetServices() .ElementAt((int)PresenterType.Target), - panel: sp.GetRequiredService(), - readOnlyZoomAndOffsets: true)); + panel: sp.GetRequiredService())); } private void AddViewVMsToProvider(IServiceCollection services) @@ -302,9 +300,9 @@ private void AddViewVMsToProvider(IServiceCollection services) .ElementAt((int)PresenterType.Target), - sourceViewTab: sp.GetServices() + sourceViewTab: sp.GetServices() .ElementAt((int)PresenterType.Source), - destinationViewTab: sp.GetServices() + destinationViewTab: sp.GetServices() .ElementAt((int)PresenterType.Target), usageData: sp.GetRequiredService())); diff --git a/TgaBuilderAvaloniaUi/View/MainWindow.Methods.axaml.cs b/TgaBuilderAvaloniaUi/View/MainWindow.Methods.axaml.cs index 24cb8bc..be3ff15 100644 --- a/TgaBuilderAvaloniaUi/View/MainWindow.Methods.axaml.cs +++ b/TgaBuilderAvaloniaUi/View/MainWindow.Methods.axaml.cs @@ -31,7 +31,7 @@ public ZoomBorder GetPanelFromImage(Image image) public void SetPanelFromImage(Image image) => CurrentPanel = GetPanelFromImage(image); - public void RegisterZoomBorderCallbacks(ViewTabViewModel viewTab, ZoomBorder panel) + public void RegisterZoomBorderCallbacks(ReadOnlyViewTabViewModel viewTab, ZoomBorder panel) { viewTab.ApplyTransformCallback = (zoom, translateX, translateY) => { @@ -50,6 +50,30 @@ public void RegisterZoomBorderCallbacks(ViewTabViewModel viewTab, ZoomBorder pan panel.SetMatrix(panel.Matrix * Matrix.CreateTranslation(deltaX, deltaY)); }); }; + + viewTab.ZoomStepCallback = (zoomDelta) => + { + Dispatcher.UIThread.Post(() => + { + var currentMatrix = panel.Matrix; + double centerX = panel.Bounds.Width / 2; + double centerY = panel.Bounds.Height / 2; + + var matrix = currentMatrix * + Matrix.CreateTranslation(-centerX, -centerY) * + Matrix.CreateScale(zoomDelta, zoomDelta) * + Matrix.CreateTranslation(centerX, centerY); + panel.SetMatrix(matrix); + }); + }; + + viewTab.ResetViewCallback = () => + { + Dispatcher.UIThread.Post(() => + { + panel.ResetMatrix(); + }); + }; } } } diff --git a/TgaBuilderAvaloniaUi/View/MainWindow.axaml b/TgaBuilderAvaloniaUi/View/MainWindow.axaml index 423a2e8..7ba3a30 100644 --- a/TgaBuilderAvaloniaUi/View/MainWindow.axaml +++ b/TgaBuilderAvaloniaUi/View/MainWindow.axaml @@ -422,10 +422,6 @@ ap:MouseOverInfoAP.InfoText="{Binding PanelHelp, Converter={StaticResource PanelHelpConverter}}" ClipToBounds="True" DropCommand="{Binding FileDropSourceCommand}"> - - - - - + + @@ -1070,10 +1051,6 @@ ap:MouseOverInfoAP.InfoText="{Binding PanelHelp, Converter={StaticResource PanelHelpConverter}}" ClipToBounds="True" DropCommand="{Binding FileDropDestinationCommand}"> - - - - - + + diff --git a/TgaBuilderAvaloniaUi/View/MainWindow.axaml.cs b/TgaBuilderAvaloniaUi/View/MainWindow.axaml.cs index ed1ec2c..898c41f 100644 --- a/TgaBuilderAvaloniaUi/View/MainWindow.axaml.cs +++ b/TgaBuilderAvaloniaUi/View/MainWindow.axaml.cs @@ -47,10 +47,10 @@ public MainWindow(INotifyPropertyChanged mainViewModel) { var sourcePanel = this.FindControl("SourcePanel"); var targetPanel = this.FindControl("TargetPanel"); - if (sourcePanel != null) - RegisterZoomBorderCallbacks(vm.SourceViewTab, sourcePanel); - if (targetPanel != null) - RegisterZoomBorderCallbacks(vm.DestinationViewTab, targetPanel); + if (sourcePanel != null && vm.SourceViewTab is ReadOnlyViewTabViewModel sourceVm) + RegisterZoomBorderCallbacks(sourceVm, sourcePanel); + if (targetPanel != null && vm.DestinationViewTab is ReadOnlyViewTabViewModel targetVm) + RegisterZoomBorderCallbacks(targetVm, targetPanel); }; } } diff --git a/TgaBuilderLib/Abstraction/IViewTabViewModel.cs b/TgaBuilderLib/Abstraction/IViewTabViewModel.cs new file mode 100644 index 0000000..89b4ee5 --- /dev/null +++ b/TgaBuilderLib/Abstraction/IViewTabViewModel.cs @@ -0,0 +1,42 @@ +using System.ComponentModel; +using System.Windows.Input; +using TgaBuilderLib.ViewModel; + +namespace TgaBuilderLib.Abstraction +{ + /// + /// Interface for view tab view models that control zoom and pan behavior + /// for texture panels. Provides the common contract used by both + /// writable (WPF) and read-only (Avalonia) implementations. + /// + public interface IViewTabViewModel : INotifyPropertyChanged + { + bool IsScrolling { get; set; } + + PanelVisualSizeViewModel VisualPanelSize { get; } + + double ContentActualWidth { get; } + double ContentActualHeight { get; } + + double OffsetX { get; set; } + double OffsetY { get; set; } + double Zoom { get; set; } + + double MultipliedOffsetX { get; set; } + double MultipliedOffsetY { get; set; } + + double HorizonatlMargin { get; set; } + + ICommand FillCommand { get; } + ICommand FitCommand { get; } + ICommand Zoom100Command { get; } + ICommand ScrollCommand { get; } + ICommand ZoomInCommand { get; } + ICommand ZoomOutCommand { get; } + + Task DefferedFill(); + void Fill(); + void Fit(); + void Zoom100(); + } +} diff --git a/TgaBuilderLib/ViewModel/Tabs/ViewTabViewModel.cs b/TgaBuilderLib/ViewModel/Tabs/ReadOnlyViewTabViewModel.cs similarity index 56% rename from TgaBuilderLib/ViewModel/Tabs/ViewTabViewModel.cs rename to TgaBuilderLib/ViewModel/Tabs/ReadOnlyViewTabViewModel.cs index 03ff9fb..4ad1c03 100644 --- a/TgaBuilderLib/ViewModel/Tabs/ViewTabViewModel.cs +++ b/TgaBuilderLib/ViewModel/Tabs/ReadOnlyViewTabViewModel.cs @@ -1,19 +1,21 @@ -using System.Diagnostics; - -using System.Windows.Input; +using System.Windows.Input; +using TgaBuilderLib.Abstraction; using TgaBuilderLib.Commands; namespace TgaBuilderLib.ViewModel { - public class ViewTabViewModel : ViewModelBase + /// + /// View tab implementation for Avalonia where zoom and offsets are read-only + /// from the PanAndZoom control. Uses matrix transformation callbacks to + /// apply view changes (zoom, pan) through the view's dispatcher service. + /// + public class ReadOnlyViewTabViewModel : ViewModelBase, IViewTabViewModel { - public ViewTabViewModel( + public ReadOnlyViewTabViewModel( PanelVisualSizeViewModel visualPanelSize, - TexturePanelViewModelBase panel, - bool readOnlyZoomAndOffsets = false) + TexturePanelViewModelBase panel) { _panel = panel; - _readOnlyZoomAndOffsets = readOnlyZoomAndOffsets; VisualPanelSize = visualPanelSize; _panel.PresenterChanged += (_, _) => _ = DefferedFill(); @@ -23,11 +25,11 @@ public ViewTabViewModel( private const int SCROLL_SPEED_PIX_PER_SEC = 420; private const int DRAG_THRESHOLD = 10; private const int SCROLLING_THRESHOLD = 30; + private const double ZOOM_STEP_FACTOR = 1.2; public bool IsScrolling { get; set; } = false; private (double X, double Y) _scrollDirection; - private readonly bool _readOnlyZoomAndOffsets; private TexturePanelViewModelBase _panel; private double _offsetX; @@ -35,22 +37,19 @@ public ViewTabViewModel( private double _horizonatlMargin; - private Stopwatch? _stopwatch; - private RelayCommand? _FillCommand; private RelayCommand? _FitCommand; private RelayCommand? _100PercentCommand; + private RelayCommand? _zoomInCommand; + private RelayCommand? _zoomOutCommand; private RelayCommand<(double X, double Y)>? _scrollCommand; - double maxX; - double maxY; - public PanelVisualSizeViewModel VisualPanelSize { get; set; } - public double ContentActualWidth + public double ContentActualWidth => Math.Min(_panel.Presenter.PixelWidth * Zoom, VisualPanelSize.ViewportWidth); - public double ContentActualHeight + public double ContentActualHeight => Math.Min(_panel.Presenter.PixelHeight * Zoom, VisualPanelSize.ViewportHeight); public double OffsetX @@ -75,7 +74,6 @@ public double MultipliedOffsetX set { OffsetX = -1.0 * value / Zoom; - Debug.WriteLine($"MultipliedOffsetX set to {value}, OffsetX is now {OffsetX}"); OnCallerPropertyChanged(); } } @@ -86,7 +84,6 @@ public double MultipliedOffsetY set { OffsetY = -1.0 * value / Zoom; - Debug.WriteLine($"MultipliedOffsetY set to {value}, OffsetY is now {OffsetY}"); OnCallerPropertyChanged(); } } @@ -102,28 +99,46 @@ public double HorizonatlMargin public ICommand Zoom100Command => _100PercentCommand ??= new RelayCommand(Zoom100); public ICommand ScrollCommand => _scrollCommand ??= new(DoPanelScrolling); + /// + /// Command to zoom in by a fixed step factor. + /// + public ICommand ZoomInCommand => _zoomInCommand ??= new RelayCommand(ZoomIn); + + /// + /// Command to zoom out by a fixed step factor. + /// + public ICommand ZoomOutCommand => _zoomOutCommand ??= new RelayCommand(ZoomOut); + /// /// Callback to apply a full view transformation (zoom + translate). /// Parameters: (zoom, translateX, translateY) in screen-space coordinates. - /// Used when zoom and offsets are read-only (e.g. Avalonia PanAndZoom). /// public Action? ApplyTransformCallback { get; set; } /// /// Callback to apply an incremental pan step. /// Parameters: (deltaX, deltaY) in screen-space coordinates. - /// Used when zoom and offsets are read-only (e.g. Avalonia PanAndZoom). /// public Action? PanStepCallback { get; set; } + /// + /// Callback to apply a zoom step at the center of the viewport. + /// Parameters: (zoomDelta) where values > 1 zoom in, < 1 zoom out. + /// + public Action? ZoomStepCallback { get; set; } + /// + /// Callback to reset the ZoomBorder to its initial state. + /// Used to fix ScrollViewer range issues upon content reset. + /// + public Action? ResetViewCallback { get; set; } - // A makeshift workaround to avoid a race condition when the panel is resized. - // WPF seemingly requires time to set everything up appropriately. public async Task DefferedFill() { await Task.Delay(20); + ResetViewCallback?.Invoke(); + var zoom = _panel.Presenter.PixelWidth < _panel.Presenter.PixelHeight ? Math.Max( VisualPanelSize.ViewportWidth / _panel.Presenter.PixelWidth, @@ -132,25 +147,11 @@ public async Task DefferedFill() VisualPanelSize.ViewportWidth / _panel.Presenter.PixelWidth, VisualPanelSize.ViewportHeight / _panel.Presenter.PixelHeight); - if (_readOnlyZoomAndOffsets) - { - _panel.Zoom = zoom; - OnPropertyChanged(nameof(Zoom)); - OnContentActualSizeChanged(); - await Task.Delay(20); - ApplyTransformCallback?.Invoke(zoom, 0, 0); - } - else - { - Zoom = 1; - Zoom = zoom; - - await Task.Delay(20); - OffsetY -= 1000; - OffsetY = 0; - - Debug.WriteLine("DefferedFill called, OffsetY reset to 0"); - } + _panel.Zoom = zoom; + OnPropertyChanged(nameof(Zoom)); + OnContentActualSizeChanged(); + await Task.Delay(20); + ApplyTransformCallback?.Invoke(zoom, 0, 0); } public void Fill() @@ -163,23 +164,10 @@ public void Fill() VisualPanelSize.ViewportWidth / _panel.Presenter.PixelWidth, VisualPanelSize.ViewportHeight / _panel.Presenter.PixelHeight); - if (_readOnlyZoomAndOffsets) - { - _panel.Zoom = zoom; - OnPropertyChanged(nameof(Zoom)); - OnContentActualSizeChanged(); - ApplyTransformCallback?.Invoke(zoom, 0, 0); - } - else - { - Zoom = 1.0; - Zoom = zoom; - - OffsetX = 0; - OffsetY = 0; - OnPropertyChanged(nameof(MultipliedOffsetX)); - OnPropertyChanged(nameof(MultipliedOffsetY)); - } + _panel.Zoom = zoom; + OnPropertyChanged(nameof(Zoom)); + OnContentActualSizeChanged(); + ApplyTransformCallback?.Invoke(zoom, 0, 0); } public void Fit() @@ -192,39 +180,30 @@ public void Fit() VisualPanelSize.ViewportWidth / _panel.Presenter.PixelWidth, VisualPanelSize.ViewportHeight / _panel.Presenter.PixelHeight); - if (_readOnlyZoomAndOffsets) - { - _panel.Zoom = zoom; - OnPropertyChanged(nameof(Zoom)); - OnContentActualSizeChanged(); - ApplyTransformCallback?.Invoke(zoom, 0, 0); - } - else - { - Zoom = 1.0; - Zoom = zoom; - } + _panel.Zoom = zoom; + OnPropertyChanged(nameof(Zoom)); + OnContentActualSizeChanged(); + ApplyTransformCallback?.Invoke(zoom, 0, 0); } public void Zoom100() { - if (_readOnlyZoomAndOffsets) - { - _panel.Zoom = 1.0; - OnPropertyChanged(nameof(Zoom)); - OnContentActualSizeChanged(); - double offsetX = (_panel.Presenter.PixelWidth - VisualPanelSize.ViewportWidth) / 2; - double offsetY = (_panel.Presenter.PixelHeight - VisualPanelSize.ViewportHeight) / 2; - ApplyTransformCallback?.Invoke(1.0, -offsetX, -offsetY); - } - else - { - Zoom = 1.0; - OffsetX = (_panel.Presenter.PixelWidth - VisualPanelSize.ViewportWidth) / 2; - OffsetY = (_panel.Presenter.PixelHeight - VisualPanelSize.ViewportHeight) / 2; - OnPropertyChanged(nameof(MultipliedOffsetX)); - OnPropertyChanged(nameof(MultipliedOffsetY)); - } + _panel.Zoom = 1.0; + OnPropertyChanged(nameof(Zoom)); + OnContentActualSizeChanged(); + double offsetX = (_panel.Presenter.PixelWidth - VisualPanelSize.ViewportWidth) / 2; + double offsetY = (_panel.Presenter.PixelHeight - VisualPanelSize.ViewportHeight) / 2; + ApplyTransformCallback?.Invoke(1.0, -offsetX, -offsetY); + } + + public void ZoomIn() + { + ZoomStepCallback?.Invoke(ZOOM_STEP_FACTOR); + } + + public void ZoomOut() + { + ZoomStepCallback?.Invoke(1.0 / ZOOM_STEP_FACTOR); } private void SetPanelZoom(double zoom) @@ -282,47 +261,13 @@ private async Task StartScrollingAsync() { IsScrolling = true; - if (_readOnlyZoomAndOffsets) - { - while (IsScrolling) - { - double deltaX = -_scrollDirection.X * SCROLL_SPEED_PIX_PER_SEC; - double deltaY = -_scrollDirection.Y * SCROLL_SPEED_PIX_PER_SEC; - PanStepCallback?.Invoke(deltaX, deltaY); - await Task.Delay(1000); - } - } - else + while (IsScrolling) { - Stopwatch stopwatch = _stopwatch ?? new(); - stopwatch.Start(); - - long lastTicks = stopwatch.ElapsedTicks; - - while (IsScrolling) - { - long nowTicks = stopwatch.ElapsedTicks; - double elapsedSeconds = (nowTicks - lastTicks) / (double)Stopwatch.Frequency; - lastTicks = nowTicks; - - double deltaX = _scrollDirection.X * SCROLL_SPEED_PIX_PER_SEC * elapsedSeconds / Zoom; - double deltaY = _scrollDirection.Y * SCROLL_SPEED_PIX_PER_SEC * elapsedSeconds / Zoom; - - OffsetX += deltaX; - OffsetY += deltaY; - - OnPropertyChanged(nameof(MultipliedOffsetX)); - OnPropertyChanged(nameof(MultipliedOffsetY)); - - maxX = Math.Max(0, _panel.Presenter.PixelWidth - (VisualPanelSize.ViewportWidth / Zoom)); - maxY = Math.Max(0, _panel.Presenter.PixelHeight - (VisualPanelSize.ViewportHeight / Zoom)); - - OffsetX = Math.Clamp(OffsetX, 0, maxX); - OffsetY = Math.Clamp(OffsetY, 0, maxY); - - await Task.Delay(3); - } + double deltaX = -_scrollDirection.X * SCROLL_SPEED_PIX_PER_SEC; + double deltaY = -_scrollDirection.Y * SCROLL_SPEED_PIX_PER_SEC; + PanStepCallback?.Invoke(deltaX, deltaY); + await Task.Delay(1000); } } } -} \ No newline at end of file +} diff --git a/TgaBuilderLib/ViewModel/Tabs/WritableViewTabViewModel.cs b/TgaBuilderLib/ViewModel/Tabs/WritableViewTabViewModel.cs new file mode 100644 index 0000000..25bb447 --- /dev/null +++ b/TgaBuilderLib/ViewModel/Tabs/WritableViewTabViewModel.cs @@ -0,0 +1,255 @@ +using System.Diagnostics; +using System.Windows.Input; +using TgaBuilderLib.Abstraction; +using TgaBuilderLib.Commands; + +namespace TgaBuilderLib.ViewModel +{ + /// + /// View tab implementation for WPF where zoom and offsets are writable + /// via two-way property bindings to the zoom panel control. + /// + public class WritableViewTabViewModel : ViewModelBase, IViewTabViewModel + { + public WritableViewTabViewModel( + PanelVisualSizeViewModel visualPanelSize, + TexturePanelViewModelBase panel) + { + _panel = panel; + VisualPanelSize = visualPanelSize; + + _panel.PresenterChanged += (_, _) => _ = DefferedFill(); + VisualPanelSize.PropertyChanged += (_, _) => OnContentActualSizeChanged(); + } + + private const int SCROLL_SPEED_PIX_PER_SEC = 420; + private const int DRAG_THRESHOLD = 10; + private const int SCROLLING_THRESHOLD = 30; + + public bool IsScrolling { get; set; } = false; + private (double X, double Y) _scrollDirection; + + private TexturePanelViewModelBase _panel; + + private double _offsetX; + private double _offsetY; + + private double _horizonatlMargin; + + private Stopwatch? _stopwatch; + + private RelayCommand? _FillCommand; + private RelayCommand? _FitCommand; + private RelayCommand? _100PercentCommand; + private RelayCommand? _zoomInCommand; + private RelayCommand? _zoomOutCommand; + private RelayCommand<(double X, double Y)>? _scrollCommand; + + double maxX; + double maxY; + + public PanelVisualSizeViewModel VisualPanelSize { get; set; } + + public double ContentActualWidth + => Math.Min(_panel.Presenter.PixelWidth * Zoom, VisualPanelSize.ViewportWidth); + + public double ContentActualHeight + => Math.Min(_panel.Presenter.PixelHeight * Zoom, VisualPanelSize.ViewportHeight); + + public double OffsetX + { + get => _offsetX; + set => SetProperty(ref _offsetX, value, nameof(OffsetX)); + } + public double OffsetY + { + get => _offsetY; + set => SetProperty(ref _offsetY, value, nameof(OffsetY)); + } + public double Zoom + { + get => _panel.Zoom; + set => SetPanelZoom(value); + } + + public double MultipliedOffsetX + { + get => -1.0 * OffsetX * Zoom; + set + { + OffsetX = -1.0 * value / Zoom; + Debug.WriteLine($"MultipliedOffsetX set to {value}, OffsetX is now {OffsetX}"); + OnCallerPropertyChanged(); + } + } + + public double MultipliedOffsetY + { + get => -1.0 * OffsetY * Zoom; + set + { + OffsetY = -1.0 * value / Zoom; + Debug.WriteLine($"MultipliedOffsetY set to {value}, OffsetY is now {OffsetY}"); + OnCallerPropertyChanged(); + } + } + + public double HorizonatlMargin + { + get => _horizonatlMargin; + set => SetProperty(ref _horizonatlMargin, value, nameof(HorizonatlMargin)); + } + + public ICommand FillCommand => _FillCommand ??= new RelayCommand(Fill); + public ICommand FitCommand => _FitCommand ??= new RelayCommand(Fit); + public ICommand Zoom100Command => _100PercentCommand ??= new RelayCommand(Zoom100); + public ICommand ScrollCommand => _scrollCommand ??= new(DoPanelScrolling); + public ICommand ZoomInCommand => _zoomInCommand ??= new RelayCommand(() => Zoom *= 1.2); + public ICommand ZoomOutCommand => _zoomOutCommand ??= new RelayCommand(() => Zoom /= 1.2); + + // A makeshift workaround to avoid a race condition when the panel is resized. + // WPF seemingly requires time to set everything up appropriately. + public async Task DefferedFill() + { + await Task.Delay(20); + + Zoom = 1; + Zoom = _panel.Presenter.PixelWidth < _panel.Presenter.PixelHeight + ? Math.Max( + VisualPanelSize.ViewportWidth / _panel.Presenter.PixelWidth, + VisualPanelSize.ViewportHeight / _panel.Presenter.PixelHeight) + : Math.Min( + VisualPanelSize.ViewportWidth / _panel.Presenter.PixelWidth, + VisualPanelSize.ViewportHeight / _panel.Presenter.PixelHeight); + + await Task.Delay(20); + OffsetY -= 1000; + OffsetY = 0; + + Debug.WriteLine("DefferedFill called, OffsetY reset to 0"); + } + + public void Fill() + { + Zoom = 1.0; + Zoom = _panel.Presenter.PixelWidth < _panel.Presenter.PixelHeight + ? Math.Max( + VisualPanelSize.ViewportWidth / _panel.Presenter.PixelWidth, + VisualPanelSize.ViewportHeight / _panel.Presenter.PixelHeight) + : Math.Min( + VisualPanelSize.ViewportWidth / _panel.Presenter.PixelWidth, + VisualPanelSize.ViewportHeight / _panel.Presenter.PixelHeight); + + OffsetX = 0; + OffsetY = 0; + OnPropertyChanged(nameof(MultipliedOffsetX)); + OnPropertyChanged(nameof(MultipliedOffsetY)); + } + + public void Fit() + { + Zoom = 1.0; + Zoom = _panel.Presenter.PixelWidth < _panel.Presenter.PixelHeight + ? Math.Min( + VisualPanelSize.ViewportWidth / _panel.Presenter.PixelWidth, + VisualPanelSize.ViewportHeight / _panel.Presenter.PixelHeight) + : Math.Max( + VisualPanelSize.ViewportWidth / _panel.Presenter.PixelWidth, + VisualPanelSize.ViewportHeight / _panel.Presenter.PixelHeight); + } + + public void Zoom100() + { + Zoom = 1.0; + OffsetX = (_panel.Presenter.PixelWidth - VisualPanelSize.ViewportWidth) / 2; + OffsetY = (_panel.Presenter.PixelHeight - VisualPanelSize.ViewportHeight) / 2; + OnPropertyChanged(nameof(MultipliedOffsetX)); + OnPropertyChanged(nameof(MultipliedOffsetY)); + } + + private void SetPanelZoom(double zoom) + { + if (zoom == _panel.Zoom) + return; + + _panel.Zoom = zoom; + + OnContentActualSizeChanged(); + OnPropertyChanged(nameof(Zoom)); + } + + private void OnContentActualSizeChanged() + { + OnPropertyChanged(nameof(ContentActualWidth)); + OnPropertyChanged(nameof(ContentActualHeight)); + } + + private void DoPanelScrolling((double X, double Y) pos) + { + int posX = (int)pos.X; + int posY = (int)pos.Y; + + if ((Math.Abs(posX - VisualPanelSize.ViewportWidth) > DRAG_THRESHOLD || + Math.Abs(posY - VisualPanelSize.ViewportHeight) > DRAG_THRESHOLD) && + _panel.CanScroll) + { + (double X, double Y) scrollVector = new(0, 0); + + if (posY < SCROLLING_THRESHOLD) + scrollVector.Y = -1; + else if (posY > VisualPanelSize.ViewportHeight - SCROLLING_THRESHOLD) + scrollVector.Y = 1; + + if (posX < SCROLLING_THRESHOLD) + scrollVector.X = -1; + else if (posX > VisualPanelSize.ViewportWidth - SCROLLING_THRESHOLD) + scrollVector.X = 1; + + if (scrollVector.X != 0 || scrollVector.Y != 0) + { + _scrollDirection = scrollVector; + if (!IsScrolling) + { + _ = StartScrollingAsync(); + } + } + else IsScrolling = false; + } + else IsScrolling = false; + } + + private async Task StartScrollingAsync() + { + IsScrolling = true; + + Stopwatch stopwatch = _stopwatch ?? new(); + stopwatch.Start(); + + long lastTicks = stopwatch.ElapsedTicks; + + while (IsScrolling) + { + long nowTicks = stopwatch.ElapsedTicks; + double elapsedSeconds = (nowTicks - lastTicks) / (double)Stopwatch.Frequency; + lastTicks = nowTicks; + + double deltaX = _scrollDirection.X * SCROLL_SPEED_PIX_PER_SEC * elapsedSeconds / Zoom; + double deltaY = _scrollDirection.Y * SCROLL_SPEED_PIX_PER_SEC * elapsedSeconds / Zoom; + + OffsetX += deltaX; + OffsetY += deltaY; + + OnPropertyChanged(nameof(MultipliedOffsetX)); + OnPropertyChanged(nameof(MultipliedOffsetY)); + + maxX = Math.Max(0, _panel.Presenter.PixelWidth - (VisualPanelSize.ViewportWidth / Zoom)); + maxY = Math.Max(0, _panel.Presenter.PixelHeight - (VisualPanelSize.ViewportHeight / Zoom)); + + OffsetX = Math.Clamp(OffsetX, 0, maxX); + OffsetY = Math.Clamp(OffsetY, 0, maxY); + + await Task.Delay(3); + } + } + } +} diff --git a/TgaBuilderLib/ViewModel/Views/MainViewModel.cs b/TgaBuilderLib/ViewModel/Views/MainViewModel.cs index e3ada25..c67ae15 100644 --- a/TgaBuilderLib/ViewModel/Views/MainViewModel.cs +++ b/TgaBuilderLib/ViewModel/Views/MainViewModel.cs @@ -1,10 +1,8 @@  using System.Windows.Input; using TgaBuilderLib.Abstraction; -using TgaBuilderLib.BitmapOperations; using TgaBuilderLib.Commands; using TgaBuilderLib.Enums; -using TgaBuilderLib.FileHandling; using TgaBuilderLib.Messaging; using TgaBuilderLib.UndoRedo; using TgaBuilderLib.Utils; @@ -30,8 +28,8 @@ public MainViewModel( SourceIOViewModel sourceIO, TargetIOViewModel destinationIO, - ViewTabViewModel sourceViewTab, - ViewTabViewModel destinationViewTab, + IViewTabViewModel sourceViewTab, + IViewTabViewModel destinationViewTab, PlacingTabViewModel placing, EditTabViewModel edits, @@ -140,8 +138,8 @@ public MainViewModel( public FormatTabViewModel SourceFormatTab { get; set; } public FormatTabViewModel DestinationFormatTab { get; set; } - public ViewTabViewModel SourceViewTab { get; set; } - public ViewTabViewModel DestinationViewTab { get; set; } + public IViewTabViewModel SourceViewTab { get; set; } + public IViewTabViewModel DestinationViewTab { get; set; } public SelectionViewModel Selection { get; set; } public AnimationViewModel Animation { get; set; } diff --git a/TgaBuilderWpfUi/App.DI.xaml.cs b/TgaBuilderWpfUi/App.DI.xaml.cs index 0b5dd13..a814155 100644 --- a/TgaBuilderWpfUi/App.DI.xaml.cs +++ b/TgaBuilderWpfUi/App.DI.xaml.cs @@ -245,12 +245,12 @@ private void AddTabVMsToProvider(IServiceCollection services) panel: sp.GetRequiredService(), messageBoxService: sp.GetRequiredService())); - services.AddTransient(sp => new ViewTabViewModel( + services.AddTransient(sp => new WritableViewTabViewModel( visualPanelSize: sp.GetServices() .ElementAt((int)PresenterType.Source), panel: sp.GetRequiredService())); - services.AddTransient(sp => new ViewTabViewModel( + services.AddTransient(sp => new WritableViewTabViewModel( visualPanelSize: sp.GetServices() .ElementAt((int)PresenterType.Target), panel: sp.GetRequiredService())); @@ -298,9 +298,9 @@ private void AddViewVMsToProvider(IServiceCollection services) .ElementAt((int)PresenterType.Target), - sourceViewTab: sp.GetServices() + sourceViewTab: sp.GetServices() .ElementAt((int)PresenterType.Source), - destinationViewTab: sp.GetServices() + destinationViewTab: sp.GetServices() .ElementAt((int)PresenterType.Target), usageData: sp.GetRequiredService()));