Browse Source

Size picker and NewFileDialog

Krzysztof Krysiński 1 year ago
parent
commit
f58524a88c

+ 36 - 0
src/PixiEditor.AvaloniaUI/Helpers/Converters/EnumBooleanConverter.cs

@@ -0,0 +1,36 @@
+using Avalonia;
+
+namespace PixiEditor.AvaloniaUI.Helpers.Converters;
+
+internal class EnumBooleanConverter : SingleInstanceConverter<EnumBooleanConverter>
+{
+    #region IValueConverter Members
+    public override object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
+    {
+        string parameterString = parameter as string;
+        if (parameterString == null)
+            return AvaloniaProperty.UnsetValue;
+
+        if (Enum.IsDefined(value.GetType(), value) == false)
+            return AvaloniaProperty.UnsetValue;
+
+        object parameterValue = Enum.Parse(value.GetType(), parameterString);
+
+        return parameterValue.Equals(value);
+    }
+
+    public override object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
+    {
+        string parameterString = parameter as string;
+        if (parameterString == null)
+            return AvaloniaProperty.UnsetValue;
+
+        if ((bool)value)
+        {
+            return Enum.Parse(targetType, parameterString);
+        }
+
+        return AvaloniaProperty.UnsetValue;
+    }
+    #endregion
+}

+ 17 - 0
src/PixiEditor.AvaloniaUI/Helpers/SizeCalculator.cs

@@ -0,0 +1,17 @@
+namespace PixiEditor.AvaloniaUI.Helpers;
+
+internal static class SizeCalculator
+{
+    public static System.Drawing.Size CalcAbsoluteFromPercentage(float percentage, System.Drawing.Size currentSize)
+    {
+        float percFactor = percentage / 100f;
+        float newWidth = currentSize.Width * percFactor;
+        float newHeight = currentSize.Height * percFactor;
+        return new System.Drawing.Size((int)MathF.Round(newWidth), (int)MathF.Round(newHeight));
+    }
+
+    public static int CalcPercentageFromAbsolute(int initAbsoluteSize, int currentAbsoluteSize)
+    {
+        return (int)((float)currentAbsoluteSize * 100) / initAbsoluteSize;
+    }
+}

+ 6 - 0
src/PixiEditor.AvaloniaUI/PixiEditor.AvaloniaUI.csproj

@@ -92,4 +92,10 @@
       <Resource Include="Images\ChevronDown.png" />
     </ItemGroup>
   
+    <ItemGroup>
+      <Reference Include="Microsoft.Xaml.Behaviors">
+        <HintPath>..\..\..\..\Users\flubb\.nuget\packages\microsoft.xaml.behaviors.wpf\1.1.31\lib\net5.0-windows7.0\Microsoft.Xaml.Behaviors.dll</HintPath>
+      </Reference>
+    </ItemGroup>
+  
 </Project>

+ 10 - 7
src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/FileViewModel.cs

@@ -3,6 +3,7 @@ using System.IO;
 using System.Linq;
 using System.Threading.Tasks;
 using Avalonia;
+using Avalonia.Controls;
 using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Input;
 using Avalonia.Platform.Storage;
@@ -10,12 +11,14 @@ using ChunkyImageLib;
 using Newtonsoft.Json.Linq;
 using PixiEditor.AvaloniaUI.Exceptions;
 using PixiEditor.AvaloniaUI.Helpers;
+using PixiEditor.AvaloniaUI.Helpers.Extensions;
 using PixiEditor.AvaloniaUI.Models.Commands.Attributes.Commands;
 using PixiEditor.AvaloniaUI.Models.Controllers;
 using PixiEditor.AvaloniaUI.Models.Dialogs;
 using PixiEditor.AvaloniaUI.Models.IO;
 using PixiEditor.AvaloniaUI.Models.UserData;
 using PixiEditor.AvaloniaUI.ViewModels.Document;
+using PixiEditor.AvaloniaUI.Views.Dialogs;
 using PixiEditor.AvaloniaUI.Views.Windows;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.Extensions.Common.Localization;
@@ -270,16 +273,16 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
     [Command.Basic("PixiEditor.File.New", "NEW_IMAGE", "CREATE_NEW_IMAGE", Key = Key.N, Modifiers = KeyModifiers.Control)]
     public async Task CreateFromNewFileDialog()
     {
-        //TODO: Implement NewFileDialog
-        /*NewFileDialog newFile = new NewFileDialog();
-        if (newFile.ShowDialog())
-        {*/
+        Window mainWindow = (Application.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime).MainWindow;
+        NewFileDialog newFile = new NewFileDialog(mainWindow);
+        if (await newFile.ShowDialog())
+        {
             NewDocument(b => b
-                .WithSize(64/*newFile.Width*/, /*newFile.Height*/64)
+                .WithSize(newFile.Width, newFile.Height)
                 .WithLayer(l => l
                     .WithName(new LocalizedString("BASE_LAYER_NAME"))
-                    .WithSurface(new Surface(new VecI(/*newFile.Width*/64, /*newFile.Height*/64)))));
-        //}
+                    .WithSurface(new Surface(new VecI(newFile.Width, newFile.Height)))));
+        }
     }
 
     public DocumentViewModel NewDocument(Action<DocumentViewModelBuilder> builder)

+ 47 - 0
src/PixiEditor.AvaloniaUI/Views/Dialogs/NewFileDialog.cs

@@ -0,0 +1,47 @@
+using System.Threading.Tasks;
+using Avalonia.Controls;
+using PixiEditor.AvaloniaUI.Models;
+using PixiEditor.AvaloniaUI.Models.Dialogs;
+using PixiEditor.Extensions.Common.UserPreferences;
+
+namespace PixiEditor.AvaloniaUI.Views.Dialogs;
+
+internal class NewFileDialog : CustomDialog
+{
+    private int height = IPreferences.Current.GetPreference("DefaultNewFileHeight", Constants.DefaultCanvasSize);
+
+    private int width = IPreferences.Current.GetPreference("DefaultNewFileWidth", Constants.DefaultCanvasSize);
+
+    public int Width
+    {
+        get => width;
+        set => SetProperty(ref width, value);
+    }
+
+    public int Height
+    {
+        get => height;
+        set => SetProperty(ref height, value);
+    }
+
+    public NewFileDialog(Window owner) : base(owner)
+    {
+
+    }
+
+    public override async Task<bool> ShowDialog()
+    {
+        NewFilePopup popup = new()
+        {
+            FileWidth = Width,
+            FileHeight = Height
+        };
+
+        var result = await popup.ShowDialog<bool>(OwnerWindow);
+
+        Height = popup.FileHeight;
+        Width = popup.FileWidth;
+
+        return result;
+    }
+}

+ 60 - 0
src/PixiEditor.AvaloniaUI/Views/Dialogs/NewFilePopup.axaml

@@ -0,0 +1,60 @@
+<Window x:Class="PixiEditor.AvaloniaUI.Views.Dialogs.NewFilePopup"
+        x:ClassModifier="internal"
+        xmlns="https://github.com/avaloniaui"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+        xmlns:local="clr-namespace:PixiEditor.Views"
+        xmlns:vm="clr-namespace:PixiEditor.ViewModels"
+        xmlns:userControls="clr-namespace:PixiEditor.Views.UserControls"
+        xmlns:helpers="clr-namespace:PixiEditor.Helpers"
+        xmlns:ui="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
+        xmlns:subViewModels="clr-namespace:PixiEditor.AvaloniaUI.ViewModels.SubViewModels"
+        xmlns:markupExtensions="clr-namespace:PixiEditor.AvaloniaUI.Helpers.MarkupExtensions"
+        xmlns:behaviours="clr-namespace:PixiEditor.AvaloniaUI.Helpers.Behaviours"
+        xmlns:dialogs="clr-namespace:PixiEditor.AvaloniaUI.Views.Dialogs"
+        xmlns:viewModels="clr-namespace:PixiEditor.AvaloniaUI.ViewModels"
+        xmlns:input="clr-namespace:PixiEditor.AvaloniaUI.Views.Input"
+        xmlns:system="clr-namespace:System;assembly=System.Runtime"
+        ShowInTaskbar="False"
+        SystemDecorations="None"
+        WindowStartupLocation="CenterScreen" 
+        MinHeight="250" Height="250"
+        SizeToContent="Width"
+        Name="newFilePopup" 
+        BorderBrush="Black" BorderThickness="1"
+        FlowDirection="{markupExtensions:Localization FlowDirection}"
+        ui:Translator.Key="CREATE_NEW_IMAGE">
+    <!--<WindowChrome.WindowChrome>
+        <WindowChrome CaptionHeight="32"  GlassFrameThickness="0.1"
+                      ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" />
+    </WindowChrome.WindowChrome>
+
+    <Window.CommandBindings>
+        <CommandBinding Command="{x:Static SystemCommands.CloseWindowCommand}" CanExecute="CommandBinding_CanExecute"
+                        Executed="CommandBinding_Executed_Close" />
+    </Window.CommandBindings>-->
+
+    <DockPanel Background="{DynamicResource ThemeBackgroundBrush1}" Focusable="True">
+        <Interaction.Behaviors>
+            <behaviours:ClearFocusOnClickBehavior/>
+        </Interaction.Behaviors>
+
+        <dialogs:DialogTitleBar DockPanel.Dock="Top"
+            TitleKey="CREATE_NEW_IMAGE" CloseCommand="{Binding DataContext.CancelCommand, ElementName=newFilePopup}"  />
+
+        <Button DockPanel.Dock="Bottom" Margin="0,15,0,15" HorizontalAlignment="Center"
+                IsDefault="True" ui:Translator.Key="CREATE" x:Name="createButton"
+                Classes="DarkRoundButton"
+                Command="{Binding Path=DataContext.SetResultAndCloseCommand, ElementName=newFilePopup}">
+                <Button.CommandParameter>
+                    <system:Boolean>True</system:Boolean>
+                </Button.CommandParameter>
+            </Button>
+
+        <input:SizePicker HorizontalAlignment="Center" MinWidth="230" Height="125" Margin="15,30,15,0"
+                              PreserveAspectRatio="False"
+                              ChosenHeight="{Binding FileHeight, Mode=TwoWay, ElementName=newFilePopup}"
+                              ChosenWidth="{Binding FileWidth, Mode=TwoWay, ElementName=newFilePopup}" 
+                              x:Name="sizePicker"/>
+    </DockPanel>
+</Window>

+ 56 - 0
src/PixiEditor.AvaloniaUI/Views/Dialogs/NewFilePopup.axaml.cs

@@ -0,0 +1,56 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using CommunityToolkit.Mvvm.Input;
+
+namespace PixiEditor.AvaloniaUI.Views.Dialogs;
+
+/// <summary>
+///     Interaction logic for NewFilePopup.xaml.
+/// </summary>
+internal partial class NewFilePopup : Window
+{
+    public static readonly StyledProperty<int> FileHeightProperty =
+        AvaloniaProperty.Register<NewFilePopup, int>(nameof(FileHeight));
+
+    public static readonly StyledProperty<int> FileWidthProperty =
+        AvaloniaProperty.Register<NewFilePopup, int>(nameof(FileWidth));
+
+    public NewFilePopup()
+    {
+        InitializeComponent();
+        DataContext = this;
+        Loaded += OnDialogShown;
+    }
+
+    private void OnDialogShown(object sender, RoutedEventArgs e)
+    {
+        MinWidth = Width;
+        sizePicker.FocusWidthPicker();
+    }
+
+    public int FileHeight
+    {
+        get => (int)GetValue(FileHeightProperty);
+        set => SetValue(FileHeightProperty, value);
+    }
+
+    public int FileWidth
+    {
+        get => (int)GetValue(FileWidthProperty);
+        set => SetValue(FileWidthProperty, value);
+    }
+
+
+    [RelayCommand]
+    private void SetResultAndClose(bool property)
+    {
+        Close(true);
+    }
+
+    [RelayCommand]
+    private void Cancel()
+    {
+        Close(false);
+    }
+}

+ 1 - 1
src/PixiEditor.AvaloniaUI/Views/Dialogs/NoticePopup.axaml

@@ -19,7 +19,7 @@
                       ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" />
     </WindowChrome.WindowChrome>-->
 
-    <DockPanel Background="{StaticResource ThemeBackgroundBrush1}" Focusable="True">
+    <DockPanel Background="{DynamicResource ThemeBackgroundBrush1}" Focusable="True">
         <Interaction.Behaviors>
             <!--<behaviours:ClearFocusOnClickBehavior/>-->
         </Interaction.Behaviors>

+ 138 - 0
src/PixiEditor.AvaloniaUI/Views/Input/SizePicker.axaml

@@ -0,0 +1,138 @@
+<UserControl x:Class="PixiEditor.AvaloniaUI.Views.Input.SizePicker"
+             x:ClassModifier="internal"
+             xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+             xmlns:local="clr-namespace:PixiEditor.Views"
+             xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters"
+             xmlns:enums="clr-namespace:PixiEditor.Models.Enums"
+             xmlns:userControls="clr-namespace:PixiEditor.Views.UserControls"
+             xmlns:ui="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
+             xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
+             xmlns:input="clr-namespace:PixiEditor.AvaloniaUI.Views.Input"
+             xmlns:converters1="clr-namespace:PixiEditor.AvaloniaUI.Helpers.Converters"
+             mc:Ignorable="d" Background="Transparent"
+             d:DesignHeight="200" d:DesignWidth="240" Name="uc">
+    <Interaction.Behaviors>
+        <EventTriggerBehavior EventName="Loaded">
+            <InvokeCommandAction Command="{Binding ElementName=uc, Path=LoadedCommand}"/>
+        </EventTriggerBehavior>
+    </Interaction.Behaviors>
+    <UserControl.Styles>
+        <Style Selector="input|SizeInput">
+            <Setter Property="HorizontalAlignment" Value="Left"/>
+            <Setter Property="MaxSize" Value="9999"/>
+            <Setter Property="BehaveLikeSmallEmbeddedField" Value="False"/>
+            <Setter Property="FontSize" Value="12"/>
+            <Setter Property="Margin" Value="10,0,0,0"/>
+            <Setter Property="Height" Value="25"/>
+        </Style>
+    </UserControl.Styles>
+    <Border Background="{DynamicResource ThemeBackgroundBrush}" VerticalAlignment="Stretch" CornerRadius="10" Padding="15,0">
+        <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
+            <Grid Height="60" HorizontalAlignment="Stretch" DockPanel.Dock="Top"
+                  IsVisible="{Binding IsSizeUnitSelectionVisible, ElementName=uc}">
+                <Grid.RowDefinitions>
+                    <RowDefinition Height="30"/>
+                    <RowDefinition Height="30"/>
+                </Grid.RowDefinitions>
+
+                <Grid HorizontalAlignment="Stretch">
+                <RadioButton Grid.Row="0"
+                             x:Name="PercentageRb" 
+                             Foreground="White" 
+                             FontSize="12"
+                             GroupName="Unit"
+                             Margin="0,0,5,0"
+                             IsCheckedChanged="PercentageRb_Checked"
+                             HorizontalAlignment="Left"
+                             VerticalAlignment="Center"
+                             IsChecked="{Binding Path=SelectedUnit,  
+                                              ElementName=uc, 
+                                              Converter={converters1:EnumBooleanConverter},
+                                              ConverterParameter=Percentage,
+                                              Mode=TwoWay
+                                              }" ui:Translator.Key="PERCENTAGE"/>
+                <input:SizeInput Grid.Row="0"
+                                     VerticalAlignment="Center"
+                                     HorizontalAlignment="Right"
+                                     x:Name="PercentageSizePicker"
+                                     IsEnabled="{Binding EditingEnabled, ElementName=uc}"
+                                     Size="{Binding Path=ChosenPercentageSize, ElementName=uc, Mode=TwoWay}"
+                                     Unit="Percentage"
+                                     Margin="-10,0,0,0"
+                                     MaxSize="9999"
+                                     Width="{Binding Bounds.Width, ElementName=WidthPicker}">
+                    <Interaction.Behaviors>
+                        <EventTriggerBehavior EventName="LostFocus">
+                            <InvokeCommandAction Command="{Binding ElementName=uc, Path=PercentageLostFocusCommand}"/>
+                        </EventTriggerBehavior>
+                    </Interaction.Behaviors>
+                </input:SizeInput>
+                </Grid>
+
+                <RadioButton Grid.Row="1" Grid.Column="0"  
+                             x:Name="AbsoluteRb" 
+                             Foreground="White" 
+                             FontSize="12"
+                             GroupName="Unit"
+                             IsCheckedChanged="AbsoluteRb_Checked"
+                             VerticalAlignment="Center"
+                             IsChecked="{Binding Path=SelectedUnit,  
+                                              ElementName=uc, 
+                                              Converter={converters1:EnumBooleanConverter},
+                                              Mode=TwoWay,
+                                              ConverterParameter=Pixel}" ui:Translator.Key="ABSOLUTE"/>
+
+            </Grid>
+
+            <Grid Height="90" HorizontalAlignment="Center" DockPanel.Dock="Top">
+                <Grid.ColumnDefinitions>
+                    <ColumnDefinition Width="60"/>
+                    <ColumnDefinition/>
+                </Grid.ColumnDefinitions>
+                <Grid.RowDefinitions>
+                    <RowDefinition />
+                    <RowDefinition />
+                    <RowDefinition />
+                </Grid.RowDefinitions>
+
+                <TextBlock Grid.Column="0" Grid.Row="0" Foreground="Snow" ui:Translator.Key="WIDTH" VerticalAlignment="Center" HorizontalAlignment="Left" />
+                <input:SizeInput Grid.Column="1" Grid.Row="0"
+                             x:Name="WidthPicker"
+                             IsEnabled="{Binding EditingEnabled, ElementName=uc}"
+                             Size="{Binding Path=ChosenWidth, ElementName=uc, Mode=TwoWay}"
+                             Margin="50,0,0,0">
+                    <Interaction.Behaviors>
+                        <EventTriggerBehavior EventName="LostFocus">
+                            <InvokeCommandAction Command="{Binding ElementName=uc, Path=WidthLostFocusCommand}"/>
+                        </EventTriggerBehavior>
+                    </Interaction.Behaviors>
+                </input:SizeInput>
+
+                <TextBlock Grid.Column="0" Grid.Row="1" Foreground="Snow" ui:Translator.Key="HEIGHT" VerticalAlignment="Center" HorizontalAlignment="Left"/>
+                <input:SizeInput Grid.Column="1" Grid.Row="1"
+                             x:Name="HeightPicker" 
+                             IsEnabled="{Binding EditingEnabled, ElementName=uc}"
+                             Margin="50,0,0,0"
+                             Size="{Binding ChosenHeight, ElementName=uc, Mode=TwoWay}">
+                    <Interaction.Behaviors>
+                        <EventTriggerBehavior EventName="LostFocus">
+                            <InvokeCommandAction Command="{Binding ElementName=uc, Path=HeightLostFocusCommand}"/>
+                        </EventTriggerBehavior>
+                    </Interaction.Behaviors>
+                </input:SizeInput>
+
+                <CheckBox
+                  Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="2"
+                  Name="aspectRatio" 
+                  IsChecked="{Binding ElementName=uc, Path=PreserveAspectRatio}"
+                  ui:Translator.Key="PRESERVE_ASPECT_RATIO"
+                  Foreground="White" 
+                  HorizontalAlignment="Left" 
+                  VerticalAlignment="Center" />
+            </Grid>
+        </StackPanel>
+    </Border>
+</UserControl>

+ 188 - 0
src/PixiEditor.AvaloniaUI/Views/Input/SizePicker.axaml.cs

@@ -0,0 +1,188 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using CommunityToolkit.Mvvm.Input;
+using PixiEditor.AvaloniaUI.Helpers;
+using PixiEditor.AvaloniaUI.Models;
+using PixiEditor.AvaloniaUI.Models.Dialogs;
+
+namespace PixiEditor.AvaloniaUI.Views.Input;
+
+internal partial class SizePicker : UserControl
+{
+    public static readonly StyledProperty<bool> EditingEnabledProperty =
+        AvaloniaProperty.Register<SizePicker, bool>(nameof(EditingEnabled), true);
+
+    public static readonly StyledProperty<bool> PreserveAspectRatioProperty =
+        AvaloniaProperty.Register<SizePicker, bool>(nameof(PreserveAspectRatio), true);
+
+    public static readonly StyledProperty<int> ChosenWidthProperty =
+        AvaloniaProperty.Register<SizePicker, int>(nameof(ChosenWidth), 1);
+
+    public static readonly StyledProperty<int> ChosenHeightProperty =
+        AvaloniaProperty.Register<SizePicker, int>(nameof(ChosenHeight), 1);
+
+    public static readonly StyledProperty<float> ChosenPercentageSizeProperty =
+        AvaloniaProperty.Register<SizePicker, float>(nameof(ChosenPercentageSize), 100f);
+
+    public static readonly StyledProperty<SizeUnit> SelectedUnitProperty =
+        AvaloniaProperty.Register<SizePicker, SizeUnit>(nameof(SelectedUnit), SizeUnit.Pixel);
+
+    public static readonly StyledProperty<bool> IsSizeUnitSelectionVisibleProperty =
+        AvaloniaProperty.Register<SizePicker, bool>(nameof(IsSizeUnitSelectionVisible), false);
+
+    private System.Drawing.Size? initSize = null;
+
+    public bool EditingEnabled
+    {
+        get => GetValue(EditingEnabledProperty);
+        set => SetValue(EditingEnabledProperty, value);
+    }
+
+    public int ChosenWidth
+    {
+        get => GetValue(ChosenWidthProperty);
+        set => SetValue(ChosenWidthProperty, value);
+    }
+
+    public int ChosenHeight
+    {
+        get => GetValue(ChosenHeightProperty);
+        set => SetValue(ChosenHeightProperty, value);
+    }
+
+    public float ChosenPercentageSize
+    {
+        get => GetValue(ChosenPercentageSizeProperty);
+        set => SetValue(ChosenPercentageSizeProperty, value);
+    }
+
+    public SizeUnit SelectedUnit
+    {
+        get => GetValue(SelectedUnitProperty);
+        set => SetValue(SelectedUnitProperty, value);
+    }
+
+    public bool IsSizeUnitSelectionVisible
+    {
+        get => GetValue(IsSizeUnitSelectionVisibleProperty);
+        set => SetValue(IsSizeUnitSelectionVisibleProperty, value);
+    }
+
+    public bool PreserveAspectRatio
+    {
+        get => GetValue(PreserveAspectRatioProperty);
+        set => SetValue(PreserveAspectRatioProperty, value);
+    }
+
+    public RelayCommand LoadedCommand { get; private set; }
+    public RelayCommand WidthLostFocusCommand { get; private set; }
+    public RelayCommand HeightLostFocusCommand { get; private set; }
+    public RelayCommand PercentageLostFocusCommand { get; private set; }
+
+    public SizePicker()
+    {
+        LoadedCommand = new(AfterLoaded);
+        WidthLostFocusCommand = new(WidthLostFocus);
+        HeightLostFocusCommand = new(HeightLostFocus);
+        PercentageLostFocusCommand = new(PercentageLostFocus);
+
+        InitializeComponent();
+
+        WidthPicker.OnScrollAction = () => OnSizeUpdate(true);
+        HeightPicker.OnScrollAction = () => OnSizeUpdate(false);
+        PercentageSizePicker.OnScrollAction = PercentageLostFocus;
+    }
+
+    public void FocusWidthPicker()
+    {
+        WidthPicker.FocusAndSelect();
+    }
+
+    private void AfterLoaded()
+    {
+        initSize = new System.Drawing.Size(ChosenWidth, ChosenHeight);
+        EnableSizeEditors();
+    }
+
+    private void WidthLostFocus() => OnSizeUpdate(true);
+    private void HeightLostFocus() => OnSizeUpdate(false);
+
+    public void PercentageLostFocus()
+    {
+        if (!initSize.HasValue)
+            return;
+
+        float targetPercentage = GetTargetPercentage(initSize.Value, ChosenPercentageSize);
+        var newSize = SizeCalculator.CalcAbsoluteFromPercentage(targetPercentage, initSize.Value);
+
+        //this shouldn't ever be necessary but just in case
+        newSize.Width = Math.Clamp(newSize.Width, 1, Constants.MaxCanvasSize);
+        newSize.Height = Math.Clamp(newSize.Height, 1, Constants.MaxCanvasSize);
+
+        ChosenPercentageSize = targetPercentage;
+        ChosenWidth = newSize.Width;
+        ChosenHeight = newSize.Height;
+    }
+
+    private static float GetTargetPercentage(System.Drawing.Size initSize, float desiredPercentage)
+    {
+        var potentialSize = SizeCalculator.CalcAbsoluteFromPercentage(desiredPercentage, initSize);
+        // all good
+        if (potentialSize.Width > 0 && potentialSize.Height > 0 && potentialSize.Width <= Constants.MaxCanvasSize &&
+            potentialSize.Height <= Constants.MaxCanvasSize)
+            return desiredPercentage;
+
+        // canvas too small
+        if (potentialSize.Width <= 0 || potentialSize.Height <= 0)
+        {
+            if (potentialSize.Width < potentialSize.Height)
+                return 100f / initSize.Width;
+            else
+                return 100f / initSize.Height;
+        }
+
+        // canvas too big
+        if (potentialSize.Width > potentialSize.Height)
+            return Constants.MaxCanvasSize * 100f / initSize.Width;
+        else
+            return Constants.MaxCanvasSize * 100f / initSize.Height;
+    }
+
+    private void OnSizeUpdate(bool widthUpdated)
+    {
+        if (!initSize.HasValue || !PreserveAspectRatio)
+            return;
+
+        if (widthUpdated)
+        {
+            ChosenHeight = Math.Clamp(ChosenWidth * initSize.Value.Height / initSize.Value.Width, (int)1,
+                (int)HeightPicker.MaxSize);
+        }
+        else
+        {
+            ChosenWidth = Math.Clamp(ChosenHeight * initSize.Value.Width / initSize.Value.Height, (int)1,
+                (int)WidthPicker.MaxSize);
+        }
+    }
+
+    private void PercentageRb_Checked(object sender, RoutedEventArgs e)
+    {
+        EnableSizeEditors();
+    }
+
+    private void AbsoluteRb_Checked(object sender, RoutedEventArgs e)
+    {
+        EnableSizeEditors();
+    }
+
+    private void EnableSizeEditors()
+    {
+        if (PercentageSizePicker != null)
+            PercentageSizePicker.IsEnabled = EditingEnabled && PercentageRb.IsChecked.Value;
+        if (WidthPicker != null)
+            WidthPicker.IsEnabled = EditingEnabled && !PercentageRb.IsChecked.Value;
+        if (HeightPicker != null)
+            HeightPicker.IsEnabled = EditingEnabled && !PercentageRb.IsChecked.Value;
+    }
+}