Browse Source

Merge pull request #319 from tomaszkot/#277

#278 Support more export formats
Krzysztof Krysiński 3 years ago
parent
commit
342cd9e19e

+ 37 - 0
PixiEditor/Helpers/Converters/EnumBooleanConverter.cs

@@ -0,0 +1,37 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+
+namespace PixiEditor.Helpers.Converters
+{
+    public 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 DependencyProperty.UnsetValue;
+
+            if (Enum.IsDefined(value.GetType(), value) == false)
+                return DependencyProperty.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 DependencyProperty.UnsetValue;
+
+            return Enum.Parse(targetType, parameterString);
+        }
+        #endregion
+    }
+}

+ 29 - 0
PixiEditor/Helpers/Converters/EnumToStringConverter.cs

@@ -0,0 +1,29 @@
+using PixiEditor.Models.Enums;
+using System;
+
+namespace PixiEditor.Helpers.Converters
+{
+  internal class EnumToStringConverter : SingleInstanceConverter<EnumToStringConverter>
+  {
+    public override object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
+    {
+      try
+      {
+        var type = value.GetType();
+        if (type == typeof(SizeUnit))
+        {
+          var valueCasted = (SizeUnit)value;
+          if (valueCasted == SizeUnit.Percentage)
+            return "%";
+          
+          return "px";
+        }
+        return Enum.GetName((value.GetType()), value);
+      }
+      catch
+      {
+        return string.Empty;
+      }
+    }
+  }
+}

+ 17 - 0
PixiEditor/Helpers/SizeCalculator.cs

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

+ 15 - 0
PixiEditor/Models/Constants.cs

@@ -0,0 +1,15 @@
+namespace PixiEditor.Models
+{
+    internal class Constants
+    {
+        public const int DefaultCanvasSize = 64;
+        public const int MaxPreviewWidth = 2048;
+        public const int MaxPreviewHeight = 2048;
+
+        public const int MaxCanvasWidth = 9999;
+        public const int MaxCanvasHeight = 9999;
+
+        public const string NativeExtensionNoDot = "pixi";
+        public const string NativeExtension = "." + NativeExtensionNoDot;
+    }
+}

+ 6 - 5
PixiEditor/Models/DataHolders/RecentlyOpenedDocument.cs

@@ -105,18 +105,19 @@ namespace PixiEditor.Models.DataHolders
                 {
                     corrupt = true;
                 }
+                
+                if (bitmap == null)//prevent crash
+                    return null;
 
-                const int MaxWidthInPixels = 2048;
-                const int MaxHeightInPixels = 2048;
                 ImageFileMaxSizeChecker imageFileMaxSizeChecker = new ImageFileMaxSizeChecker()
                 {
-                    MaxAllowedWidthInPixels = MaxWidthInPixels,
-                    MaxAllowedHeightInPixels = MaxHeightInPixels,
+                    MaxAllowedWidthInPixels = Constants.MaxPreviewWidth,
+                    MaxAllowedHeightInPixels = Constants.MaxPreviewHeight,
                 };
 
                 return imageFileMaxSizeChecker.IsFileUnderMaxSize(bitmap) ?
                     bitmap
-                    : bitmap.Resize(width: MaxWidthInPixels, height: MaxHeightInPixels, WriteableBitmapExtensions.Interpolation.Bilinear);
+                    : bitmap.Resize(width: Constants.MaxPreviewWidth, height: Constants.MaxPreviewHeight, WriteableBitmapExtensions.Interpolation.Bilinear);
             }
 
             return null;

+ 17 - 0
PixiEditor/Models/Dialogs/ExportFileDialog.cs

@@ -1,10 +1,13 @@
 using PixiEditor.Views;
+using System.Drawing.Imaging;
 using System.Windows;
 
 namespace PixiEditor.Models.Dialogs
 {
     public class ExportFileDialog : CustomDialog
     {
+        ImageFormat _chosenFormat;
+
         private int fileHeight;
 
         private string filePath;
@@ -56,6 +59,19 @@ namespace PixiEditor.Models.Dialogs
             }
         }
 
+        public ImageFormat ChosenFormat
+        {
+            get => _chosenFormat;
+            set
+            {
+                if (_chosenFormat != value)
+                {
+                    _chosenFormat = value;
+                    RaisePropertyChanged(nameof(ChosenFormat));
+                }
+            }
+        }
+
         public override bool ShowDialog()
         {
             ExportFilePopup popup = new ExportFilePopup
@@ -69,6 +85,7 @@ namespace PixiEditor.Models.Dialogs
                 FileWidth = popup.SaveWidth;
                 FileHeight = popup.SaveHeight;
                 FilePath = popup.SavePath;
+                ChosenFormat = popup.SaveFormat;
             }
 
             return (bool)popup.DialogResult;

+ 2 - 4
PixiEditor/Models/Dialogs/NewFileDialog.cs

@@ -5,11 +5,9 @@ namespace PixiEditor.Models.Dialogs
 {
     public class NewFileDialog : CustomDialog
     {
-        public const int defaultSize = 64;
+        private int height = IPreferences.Current.GetPreference("DefaultNewFileHeight", Constants.DefaultCanvasSize);
 
-        private int height = IPreferences.Current.GetPreference("DefaultNewFileHeight", defaultSize);
-
-        private int width = IPreferences.Current.GetPreference("DefaultNewFileWidth", defaultSize);
+        private int width = IPreferences.Current.GetPreference("DefaultNewFileWidth", Constants.DefaultCanvasSize);
 
         public int Width
         {

+ 16 - 22
PixiEditor/Models/Dialogs/ResizeDocumentDialog.cs

@@ -50,41 +50,35 @@ namespace PixiEditor.Models.Dialogs
             return OpenResizeCanvas ? ShowResizeCanvasDialog() : ShowResizeDocumentCanvas();
         }
 
-        private bool ShowResizeDocumentCanvas()
-        {
-            ResizeDocumentPopup popup = new ResizeDocumentPopup
+        bool ShowDialog<T>()
+            where T : ResizeablePopup, new()
+        {
+            var popup = new T()
             {
-                NewHeight = Height,
-                NewWidth = Width
+                NewAbsoluteHeight = Height,
+                NewAbsoluteWidth = Width,
+                NewPercentageSize = 100,
+                NewSelectedUnit = SizeUnit.Pixel
             };
 
             popup.ShowDialog();
             if (popup.DialogResult == true)
             {
-                Width = popup.NewWidth;
-                Height = popup.NewHeight;
+                Width = popup.NewAbsoluteWidth;
+                Height = popup.NewAbsoluteHeight;
             }
 
             return (bool)popup.DialogResult;
         }
 
-        private bool ShowResizeCanvasDialog()
+        private bool ShowResizeDocumentCanvas()
         {
-            ResizeCanvasPopup popup = new ResizeCanvasPopup
-            {
-                NewHeight = Height,
-                NewWidth = Width
-            };
-
-            popup.ShowDialog();
-            if (popup.DialogResult == true)
-            {
-                Width = popup.NewWidth;
-                Height = popup.NewHeight;
-                ResizeAnchor = popup.SelectedAnchorPoint;
-            }
+            return ShowDialog<ResizeDocumentPopup>();
+        }
 
-            return (bool)popup.DialogResult;
+        private bool ShowResizeCanvasDialog()
+        {
+            return ShowDialog<ResizeCanvasPopup>();
         }
     }
 }

+ 4 - 0
PixiEditor/Models/Enums/SizeUnit.cs

@@ -0,0 +1,4 @@
+namespace PixiEditor.Models.Enums
+{
+  public enum SizeUnit { Pixel, Percentage }
+}

+ 70 - 13
PixiEditor/Models/IO/Exporter.cs

@@ -4,8 +4,12 @@ using PixiEditor.Models.DataHolders;
 using PixiEditor.Models.Dialogs;
 using SkiaSharp;
 using System;
+using System.Collections.Generic;
+using System.Drawing.Imaging;
 using System.IO;
 using System.IO.Compression;
+using System.Linq;
+using System.Reflection;
 using System.Runtime.InteropServices;
 using System.Windows;
 using System.Windows.Media.Imaging;
@@ -14,6 +18,8 @@ namespace PixiEditor.Models.IO
 {
     public class Exporter
     {
+        static ImageFormat[] _formats = new[] { ImageFormat.Png, ImageFormat.Jpeg, ImageFormat.Bmp, ImageFormat.Gif, ImageFormat.Tiff };
+        
         /// <summary>
         ///     Saves document as .pixi file that contains all document data.
         /// </summary>
@@ -21,10 +27,11 @@ namespace PixiEditor.Models.IO
         /// <param name="path">Path where file was saved.</param>
         public static bool SaveAsEditableFileWithDialog(Document document, out string path)
         {
+            var pixi = GetFormattedString("PixiEditor File", Constants.NativeExtensionNoDot);
             SaveFileDialog dialog = new SaveFileDialog
             {
-                Filter = "PixiEditor Files | *.pixi",
-                DefaultExt = "pixi"
+                Filter = pixi + "|" + BuildFilter(),
+                FilterIndex = 0
             };
             if ((bool)dialog.ShowDialog())
             {
@@ -36,6 +43,23 @@ namespace PixiEditor.Models.IO
             return false;
         }
 
+        public static string BuildFilter()
+        {
+          var filter = string.Join("|", Formats.Select(i => GetFormattedString(i)));
+          return filter;
+        }
+
+        public static string GetFormattedString(ImageFormat imageFormat)
+        {
+            var formatLower = imageFormat.ToString().ToLower();
+            return GetFormattedString(imageFormat.ToString() + " Image", formatLower);
+        }
+
+        private static string GetFormattedString(string imageFormat, string formatLower)
+        {
+            return $"{imageFormat}|*.{formatLower}";
+        }
+
         /// <summary>
         /// Saves editable file to chosen path and returns it.
         /// </summary>
@@ -44,10 +68,43 @@ namespace PixiEditor.Models.IO
         /// <returns>Path.</returns>
         public static string SaveAsEditableFile(Document document, string path)
         {
-            Parser.PixiParser.Serialize(ParserHelpers.ToSerializable(document), path);
+            if (Path.GetExtension(path) != Constants.NativeExtension)
+            {
+                var chosenFormat = ParseImageFormat(Path.GetExtension(path));
+                var bitmap = document.Renderer.FinalBitmap;
+                SaveAs(encodersFactory[chosenFormat](), path, bitmap.PixelWidth, bitmap.PixelHeight, bitmap);
+            }
+            else
+            {
+                Parser.PixiParser.Serialize(ParserHelpers.ToSerializable(document), path);
+            }
+
             return path;
         }
 
+        public static ImageFormat ParseImageFormat(string fileExtension)
+        {
+            fileExtension = fileExtension.Replace(".", "");
+            return (ImageFormat)typeof(ImageFormat)
+                    .GetProperty(fileExtension, BindingFlags.Public | BindingFlags.Static | BindingFlags.IgnoreCase)
+                    .GetValue(null);
+        }
+
+        //static Dictionary<ImageFormat, Action<ExportFileDialog, WriteableBitmap>> encoders = new Dictionary<ImageFormat, Action<ExportFileDialog, WriteableBitmap>>();
+        //TODO remove static methods/members
+        static Dictionary<ImageFormat, Func<BitmapEncoder>> encodersFactory = new Dictionary<ImageFormat, Func<BitmapEncoder>>();
+
+        public static ImageFormat[] Formats { get => _formats; }
+
+        static Exporter()
+        {
+            encodersFactory[ImageFormat.Png] = () => { return new PngBitmapEncoder(); };
+            encodersFactory[ImageFormat.Jpeg] = () => { return new JpegBitmapEncoder(); };
+            encodersFactory[ImageFormat.Bmp] = () => { return new BmpBitmapEncoder(); };
+            encodersFactory[ImageFormat.Gif] = () => { return new GifBitmapEncoder(); };
+            encodersFactory[ImageFormat.Tiff] = () => { return new TiffBitmapEncoder(); };
+        }
+
         /// <summary>
         ///     Creates ExportFileDialog to get width, height and path of file.
         /// </summary>
@@ -55,15 +112,15 @@ namespace PixiEditor.Models.IO
         /// <param name="fileDimensions">Size of file.</param>
         public static void Export(WriteableBitmap bitmap, Size fileDimensions)
         {
-            ExportFileDialog info = new ExportFileDialog(fileDimensions);
+          ExportFileDialog info = new ExportFileDialog(fileDimensions);
 
-            // If OK on dialog has been clicked
-            if (info.ShowDialog())
-            {
-                SaveAsPng(info.FilePath, info.FileWidth, info.FileHeight, bitmap);
-            }
+          // If OK on dialog has been clicked
+          if (info.ShowDialog())
+          {
+            if(encodersFactory.ContainsKey(info.ChosenFormat))
+              SaveAs(encodersFactory[info.ChosenFormat](), info.FilePath, info.FileWidth, info.FileHeight, bitmap);
+          }
         }
-
         public static void SaveAsGZippedBytes(string path, Surface surface)
         {
             SaveAsGZippedBytes(path, surface, SKRectI.Create(0, 0, surface.Width, surface.Height));
@@ -94,18 +151,18 @@ namespace PixiEditor.Models.IO
         /// <summary>
         ///     Saves image to PNG file.
         /// </summary>
+        /// <param name="encoder">encoder to do the job.</param>
         /// <param name="savePath">Save file path.</param>
         /// <param name="exportWidth">File width.</param>
         /// <param name="exportHeight">File height.</param>
         /// <param name="bitmap">Bitmap to save.</param>
-        public static void SaveAsPng(string savePath, int exportWidth, int exportHeight, WriteableBitmap bitmap)
+        private static void SaveAs(BitmapEncoder encoder, string savePath, int exportWidth, int exportHeight, WriteableBitmap bitmap)
         {
             try
             {
                 bitmap = bitmap.Resize(exportWidth, exportHeight, WriteableBitmapExtensions.Interpolation.NearestNeighbor);
-                using (FileStream stream = new FileStream(savePath, FileMode.Create))
+                using (var stream = new FileStream(savePath, FileMode.Create))
                 {
-                    PngBitmapEncoder encoder = new PngBitmapEncoder();
                     encoder.Frames.Add(BitmapFrame.Create(bitmap));
                     encoder.Save(stream);
                 }

+ 2 - 2
PixiEditor/Models/IO/ImageFileMaxSizeChecker.cs

@@ -4,8 +4,8 @@ namespace PixiEditor.Models.IO
 {
     internal class ImageFileMaxSizeChecker
     {
-        public int MaxAllowedWidthInPixels { get; init; } = 2048;
-        public int MaxAllowedHeightInPixels { get; init; } = 2048;
+        public int MaxAllowedWidthInPixels { get; init; } = Constants.MaxPreviewWidth;
+        public int MaxAllowedHeightInPixels { get; init; } = Constants.MaxPreviewHeight;
 
         public ImageFileMaxSizeChecker()
         {

+ 25 - 3
PixiEditor/ViewModels/SaveFilePopupViewModel.cs

@@ -1,5 +1,11 @@
 using Microsoft.Win32;
 using PixiEditor.Helpers;
+using PixiEditor.Models.IO;
+using System;
+using System.Drawing.Imaging;
+using System.IO;
+using System.Linq;
+using System.Reflection;
 using System.Windows;
 
 namespace PixiEditor.ViewModels
@@ -7,6 +13,7 @@ namespace PixiEditor.ViewModels
     internal class SaveFilePopupViewModel : ViewModelBase
     {
         private string _filePath;
+        private ImageFormat _chosenFormat;
 
         public SaveFilePopupViewModel()
         {
@@ -27,11 +34,24 @@ namespace PixiEditor.ViewModels
                 if (_filePath != value)
                 {
                     _filePath = value;
-                    RaisePropertyChanged("FilePath");
+                    RaisePropertyChanged(nameof(FilePath));
                 }
             }
         }
 
+        public ImageFormat ChosenFormat 
+        { 
+            get => _chosenFormat;
+            set
+            {
+                if (_chosenFormat != value)
+                {
+                    _chosenFormat = value;
+                    RaisePropertyChanged(nameof(ChosenFormat));
+                }
+            }
+        }
+                
         /// <summary>
         ///     Command that handles Path choosing to save file
         /// </summary>
@@ -41,13 +61,14 @@ namespace PixiEditor.ViewModels
             {
                 Title = "Export path",
                 CheckPathExists = true,
-                DefaultExt = "PNG Image (.png) | *.png",
-                Filter = "PNG Image (.png) | *.png"
+                DefaultExt = "." + Exporter.Formats.First().ToString().ToLower(),
+                Filter = Exporter.BuildFilter()
             };
             if (path.ShowDialog() == true)
             {
                 if (string.IsNullOrEmpty(path.FileName) == false)
                 {
+                    ChosenFormat = Exporter.ParseImageFormat(Path.GetExtension(path.SafeFileName));
                     return path.FileName;
                 }
             }
@@ -71,6 +92,7 @@ namespace PixiEditor.ViewModels
             if (path == null)
                 return;
             FilePath = path;
+            
             ((Window)parameter).DialogResult = true;
             CloseButton(parameter);
         }

+ 1 - 2
PixiEditor/ViewModels/SubViewModels/Main/FileViewModel.cs

@@ -244,8 +244,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Main
         {
             bool paramIsAsNew = parameter != null && parameter.ToString()?.ToLower() == "asnew";
             if (paramIsAsNew ||
-                string.IsNullOrEmpty(Owner.BitmapManager.ActiveDocument.DocumentFilePath) ||
-                !Owner.BitmapManager.ActiveDocument.DocumentFilePath.EndsWith(".pixi"))
+                string.IsNullOrEmpty(Owner.BitmapManager.ActiveDocument.DocumentFilePath)) 
             {
                 Owner.BitmapManager.ActiveDocument.SaveWithDialog();
             }

+ 4 - 3
PixiEditor/ViewModels/SubViewModels/UserPreferences/Settings/FileSettings.cs

@@ -1,4 +1,5 @@
-using PixiEditor.Models.Dialogs;
+using PixiEditor.Models;
+using PixiEditor.Models.Dialogs;
 
 namespace PixiEditor.ViewModels.SubViewModels.UserPreferences.Settings
 {
@@ -12,7 +13,7 @@ namespace PixiEditor.ViewModels.SubViewModels.UserPreferences.Settings
             set => RaiseAndUpdatePreference(ref showStartupWindow, value);
         }
 
-        private int defaultNewFileWidth = GetPreference("DefaultNewFileWidth", NewFileDialog.defaultSize);
+        private int defaultNewFileWidth = GetPreference("DefaultNewFileWidth", Constants.DefaultCanvasSize);
 
         public int DefaultNewFileWidth
         {
@@ -25,7 +26,7 @@ namespace PixiEditor.ViewModels.SubViewModels.UserPreferences.Settings
             }
         }
 
-        private int defaultNewFileHeight = GetPreference("DefaultNewFileHeight", NewFileDialog.defaultSize);
+        private int defaultNewFileHeight = GetPreference("DefaultNewFileHeight", Constants.DefaultCanvasSize);
 
         public int DefaultNewFileHeight
         {

+ 7 - 0
PixiEditor/Views/Dialogs/ExportFilePopup.xaml.cs

@@ -1,4 +1,5 @@
 using PixiEditor.ViewModels;
+using System.Drawing.Imaging;
 using System.Windows;
 using System.Windows.Input;
 
@@ -52,5 +53,11 @@ namespace PixiEditor.Views
             get => dataContext.FilePath;
             set => dataContext.FilePath = value;
         }
+
+        public ImageFormat SaveFormat 
+        {
+            get => dataContext.ChosenFormat;
+            set => dataContext.ChosenFormat = value;
+        }
     }
 }

+ 18 - 9
PixiEditor/Views/Dialogs/ResizeCanvasPopup.xaml

@@ -1,4 +1,4 @@
-<Window x:Class="PixiEditor.Views.ResizeCanvasPopup"
+<base:ResizeablePopup x:Class="PixiEditor.Views.ResizeCanvasPopup"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
@@ -7,9 +7,13 @@
         xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
         xmlns:behaviors="clr-namespace:PixiEditor.Helpers.Behaviours"
         xmlns:dial="clr-namespace:PixiEditor.Views.Dialogs"
-        mc:Ignorable="d" Name="window"
+        xmlns:base="clr-namespace:PixiEditor.Views"
+        mc:Ignorable="d" 
+        x:Name="window"
         Title="ResizeCanvasPopup" ShowInTaskbar="False" WindowStartupLocation="CenterScreen"
-        Height="350" Width="300" MinHeight="350" MinWidth="300" WindowStyle="None">
+        Height="420" Width="320" MinHeight="380" MinWidth="300" 
+        WindowStyle="None"
+        >
 
     <WindowChrome.WindowChrome>
         <WindowChrome CaptionHeight="32"  GlassFrameThickness="0.1"
@@ -33,12 +37,17 @@
                 Style="{StaticResource DarkRoundButton}" Content="Resize" Click="Button_Click" IsDefault="True" />
 
         <StackPanel HorizontalAlignment="Center" Margin="0,30,0,0" Background="{StaticResource MainColor}"
-                    VerticalAlignment="Top" Grid.Row="1" Width="230" Height="225">
-            <local:SizePicker Margin="0,18,0,0" Width="200"
+                    VerticalAlignment="Top" Grid.Row="1" Width="250" Height="300">
+            <local:SizePicker Margin="0,18,0,0" 
+                              Width="240"
+                              Height="170"
                               x:Name="sizePicker"
-                              PreserveAspectRatio="False"
-                              ChosenHeight="{Binding NewHeight, Mode=TwoWay, ElementName=window}"
-                              ChosenWidth="{Binding NewWidth, Mode=TwoWay, ElementName=window}" />
+                              ChosenHeight="{Binding NewAbsoluteHeight, Mode=TwoWay, ElementName=window}"
+                              ChosenWidth="{Binding NewAbsoluteWidth, Mode=TwoWay, ElementName=window}" 
+                              ChosenPercentageSize="{Binding NewPercentageSize, Mode=TwoWay, ElementName=window}"
+                              SelectedUnit="{ Binding NewSelectedUnit, Mode=TwoWay, ElementName=window}"
+                              SizeUnitSelectionVisibility="Visible"
+                              />
             <Separator Margin="10,5,10,0" Background="{StaticResource AccentColor}" Height="1" />
             <DockPanel>
                 <Label Content="Anchor point:" Foreground="White" Margin="25,5,0,0" HorizontalAlignment="Left"
@@ -49,4 +58,4 @@
             </DockPanel>
         </StackPanel>
     </DockPanel>
-</Window>
+</base:ResizeablePopup>

+ 2 - 25
PixiEditor/Views/Dialogs/ResizeCanvasPopup.xaml.cs

@@ -7,21 +7,14 @@ namespace PixiEditor.Views
     /// <summary>
     ///     Interaction logic for ResizeCanvasPopup.xaml
     /// </summary>
-    public partial class ResizeCanvasPopup : Window
+    public partial class ResizeCanvasPopup : ResizeablePopup
     {
         // Using a DependencyProperty as the backing store for SelectedAnchorPoint.  This enables animation, styling, binding, etc...
         public static readonly DependencyProperty SelectedAnchorPointProperty =
             DependencyProperty.Register("SelectedAnchorPoint", typeof(AnchorPoint), typeof(ResizeCanvasPopup),
                 new PropertyMetadata(AnchorPoint.Top | AnchorPoint.Left));
 
-        // Using a DependencyProperty as the backing store for NewHeight.  This enables animation, styling, binding, etc...
-        public static readonly DependencyProperty NewHeightProperty =
-            DependencyProperty.Register("NewHeight", typeof(int), typeof(ResizeCanvasPopup), new PropertyMetadata(0));
-
-        // Using a DependencyProperty as the backing store for NewWidth.  This enables animation, styling, binding, etc...
-        public static readonly DependencyProperty NewWidthProperty =
-            DependencyProperty.Register("NewWidth", typeof(int), typeof(ResizeCanvasPopup), new PropertyMetadata(0));
-
+        
         public ResizeCanvasPopup()
         {
             InitializeComponent();
@@ -29,28 +22,12 @@ namespace PixiEditor.Views
             Loaded += (_, _) => sizePicker.FocusWidthPicker();
         }
 
-
         public AnchorPoint SelectedAnchorPoint
         {
             get => (AnchorPoint)GetValue(SelectedAnchorPointProperty);
             set => SetValue(SelectedAnchorPointProperty, value);
         }
 
-
-        public int NewHeight
-        {
-            get => (int)GetValue(NewHeightProperty);
-            set => SetValue(NewHeightProperty, value);
-        }
-
-
-        public int NewWidth
-        {
-            get => (int)GetValue(NewWidthProperty);
-            set => SetValue(NewWidthProperty, value);
-        }
-
-
         private void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e)
         {
             e.CanExecute = true;

+ 12 - 7
PixiEditor/Views/Dialogs/ResizeDocumentPopup.xaml

@@ -1,4 +1,4 @@
-<Window x:Class="PixiEditor.Views.ResizeDocumentPopup"
+<base:ResizeablePopup x:Class="PixiEditor.Views.ResizeDocumentPopup"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
@@ -7,9 +7,10 @@
         xmlns:local="clr-namespace:PixiEditor.Views" 
         xmlns:behaviors="clr-namespace:PixiEditor.Helpers.Behaviours"
         xmlns:dial="clr-namespace:PixiEditor.Views.Dialogs"
-        mc:Ignorable="d" Name="window"
+        mc:Ignorable="d" x:Name="window"
         Title="ResizeDocumentPopup" ShowInTaskbar="False" WindowStartupLocation="CenterScreen"
-        Height="250" Width="300" MaxHeight="250" MaxWidth="300"
+        Height="300" Width="320" MaxHeight="300" MaxWidth="300"
+        xmlns:base="clr-namespace:PixiEditor.Views"
         WindowStyle="None">
 
     <WindowChrome.WindowChrome>
@@ -33,11 +34,15 @@
         <Button DockPanel.Dock="Bottom" Width="70" HorizontalAlignment="Center" Margin="15"
                 Style="{StaticResource DarkRoundButton}" Content="Resize" Click="Button_Click" IsDefault="True" />
 
-        <local:SizePicker HorizontalAlignment="Center" Width="230" Height="125" Margin="0,30,0,0"
+        <local:SizePicker HorizontalAlignment="Center" Width="240" Height="180" Margin="0,30,0,0"
             x:Name="sizePicker"
             PreserveAspectRatio="True"
-            ChosenHeight="{Binding Path=NewHeight, Mode=TwoWay, ElementName=window}"
-            ChosenWidth="{Binding Path=NewWidth, Mode=TwoWay, ElementName=window}" />
+            ChosenHeight="{Binding NewAbsoluteHeight, Mode=TwoWay, ElementName=window}"
+            ChosenWidth="{Binding NewAbsoluteWidth, Mode=TwoWay, ElementName=window}" 
+            ChosenPercentageSize="{Binding NewPercentageSize, Mode=TwoWay, ElementName=window}"
+            SelectedUnit="{ Binding NewSelectedUnit, Mode=TwoWay, ElementName=window}"
+            SizeUnitSelectionVisibility="Visible"
+            />
 
     </DockPanel>
-</Window>
+    </base:ResizeablePopup>

+ 1 - 21
PixiEditor/Views/Dialogs/ResizeDocumentPopup.xaml.cs

@@ -6,14 +6,8 @@ namespace PixiEditor.Views
     /// <summary>
     ///     Interaction logic for ResizeDocumentPopup.xaml
     /// </summary>
-    public partial class ResizeDocumentPopup : Window
+    public partial class ResizeDocumentPopup : ResizeablePopup
     {
-        public static readonly DependencyProperty NewHeightProperty =
-            DependencyProperty.Register("NewHeight", typeof(int), typeof(ResizeDocumentPopup), new PropertyMetadata(0));
-
-        public static readonly DependencyProperty NewWidthProperty =
-            DependencyProperty.Register("NewWidth", typeof(int), typeof(ResizeDocumentPopup), new PropertyMetadata(0));
-
         public ResizeDocumentPopup()
         {
             InitializeComponent();
@@ -22,20 +16,6 @@ namespace PixiEditor.Views
             Loaded += (_, _) => sizePicker.FocusWidthPicker();
         }
 
-        public int NewHeight
-        {
-            get => (int)GetValue(NewHeightProperty);
-            set => SetValue(NewHeightProperty, value);
-        }
-
-
-        public int NewWidth
-        {
-            get => (int)GetValue(NewWidthProperty);
-            set => SetValue(NewWidthProperty, value);
-        }
-
-
         private void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e)
         {
             e.CanExecute = true;

+ 46 - 0
PixiEditor/Views/Dialogs/ResizeablePopup.cs

@@ -0,0 +1,46 @@
+using PixiEditor.Models.Enums;
+using System.Windows;
+
+namespace PixiEditor.Views
+{
+    public class ResizeablePopup : Window
+    {
+        public static readonly DependencyProperty NewPercentageSizeProperty =
+            DependencyProperty.Register(nameof(NewPercentageSize), typeof(int), typeof(ResizeablePopup), new PropertyMetadata(0));
+
+        public static readonly DependencyProperty NewSelectedUnitProperty =
+            DependencyProperty.Register(nameof(NewSelectedUnit), typeof(SizeUnit), typeof(SizePicker), new PropertyMetadata(SizeUnit.Pixel));
+
+        // Using a DependencyProperty as the backing store for NewAbsoluteHeight.  This enables animation, styling, binding, etc...
+        public static readonly DependencyProperty NewAbsoluteHeightProperty =
+            DependencyProperty.Register(nameof(NewAbsoluteHeight), typeof(int), typeof(ResizeablePopup), new PropertyMetadata(0));
+
+        // Using a DependencyProperty as the backing store for NewAbsoluteWidth.  This enables animation, styling, binding, etc...
+        public static readonly DependencyProperty NewAbsoluteWidthProperty =
+            DependencyProperty.Register(nameof(NewAbsoluteWidth), typeof(int), typeof(ResizeablePopup), new PropertyMetadata(0));
+
+        public int NewPercentageSize
+        {
+            get => (int)GetValue(NewPercentageSizeProperty);
+            set => SetValue(NewPercentageSizeProperty, value);
+        }
+
+        public SizeUnit NewSelectedUnit
+        {
+            get => (SizeUnit)GetValue(NewSelectedUnitProperty);
+            set => SetValue(NewSelectedUnitProperty, value);
+        }
+
+        public int NewAbsoluteHeight
+        {
+            get => (int)GetValue(NewAbsoluteHeightProperty);
+            set => SetValue(NewAbsoluteHeightProperty, value);
+        }
+
+        public int NewAbsoluteWidth
+        {
+            get => (int)GetValue(NewAbsoluteWidthProperty);
+            set => SetValue(NewAbsoluteWidthProperty, value);
+        }
+    }
+}

+ 7 - 3
PixiEditor/Views/UserControls/SizeInput.xaml

@@ -41,7 +41,9 @@
                      Margin="0,0,5,0" VerticalAlignment="Center"
                      x:Name="textBox"
                      Text="{Binding Size, ElementName=uc, Converter={converters:ToolSizeToIntConverter}}"
-                     d:Text="22">
+                     d:Text="22"
+                     MaxLength = "4"
+                     >
                 <i:Interaction.Behaviors>
                     <behaviors:GlobalShortcutFocusBehavior/>
                     <behaviors:TextBoxFocusBehavior 
@@ -52,8 +54,10 @@
             </TextBox>
             <Grid Grid.Column="1" Background="{Binding BorderBrush, ElementName=border}"
                   d:Background="{StaticResource BrighterAccentColor}"/>
-            <TextBlock Text="px" TextAlignment="Right"
-                       Grid.Column="2" Margin="5,0" VerticalAlignment="Center"/>
+                      <TextBlock Text="{Binding Unit, ElementName=uc, Converter={converters:EnumToStringConverter}}" TextAlignment="Right"
+                       Grid.Column="2" Margin="5,0" VerticalAlignment="Center" d:Text="px"
+                       
+                       />
         </Grid>
     </Border>
 </UserControl>

+ 23 - 7
PixiEditor/Views/UserControls/SizeInput.xaml.cs

@@ -1,4 +1,6 @@
-using System.Windows;
+using PixiEditor.Models.Enums;
+using System;
+using System.Windows;
 using System.Windows.Controls;
 using System.Windows.Input;
 
@@ -18,6 +20,19 @@ namespace PixiEditor.Views
         public static readonly DependencyProperty BehaveLikeSmallEmbeddedFieldProperty =
             DependencyProperty.Register(nameof(BehaveLikeSmallEmbeddedField), typeof(bool), typeof(SizeInput), new PropertyMetadata(true));
 
+        public static readonly DependencyProperty UnitProperty =
+            DependencyProperty.Register(nameof(Unit), typeof(SizeUnit), typeof(SizeInput), new PropertyMetadata(SizeUnit.Pixel));
+
+        public SizeInput()
+        {
+            InitializeComponent();
+        }
+
+        private void SizeInput_GotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
+        {
+            textBox.Focus();
+        }
+
         public int Size
         {
             get => (int)GetValue(SizeProperty);
@@ -35,12 +50,7 @@ namespace PixiEditor.Views
             get => (bool)GetValue(BehaveLikeSmallEmbeddedFieldProperty);
             set => SetValue(BehaveLikeSmallEmbeddedFieldProperty, value);
         }
-
-        public SizeInput()
-        {
-            InitializeComponent();
-        }
-
+                
         public void FocusAndSelect()
         {
             textBox.Focus();
@@ -62,6 +72,12 @@ namespace PixiEditor.Views
             e.Handled = true;
         }
 
+        public SizeUnit Unit
+        {
+          get => (SizeUnit)GetValue(UnitProperty);
+          set => SetValue(UnitProperty, value);
+        }
+
         private static void InputSizeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
         {
             int newValue = (int)e.NewValue;

+ 69 - 5
PixiEditor/Views/UserControls/SizePicker.xaml

@@ -5,8 +5,10 @@
              xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
              xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
              xmlns:local="clr-namespace:PixiEditor.Views"
+             xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters"
+             xmlns:enums="clr-namespace:PixiEditor.Models.Enums"
              mc:Ignorable="d"
-             d:DesignHeight="150" d:DesignWidth="300" Name="uc">
+             d:DesignHeight="180" d:DesignWidth="240" Name="uc">
     <i:Interaction.Triggers>
         <i:EventTrigger EventName="Loaded">
             <i:InvokeCommandAction Command="{Binding ElementName=uc, Path=LoadedCommand}"/>
@@ -19,12 +21,69 @@
             <Setter Property="BehaveLikeSmallEmbeddedField" Value="False"/>
             <Setter Property="FontSize" Value="12"/>
             <Setter Property="Margin" Value="10,0,0,0"/>
-            <Setter Property="Width" Value="100"/>
-            <Setter Property="Height" Value="25"/>
+            <Setter Property="Width" Value="80"/>
+            <Setter Property="Height" Value="30"/>
         </Style>
     </UserControl.Resources>
     <Border Background="{StaticResource MainColor}" VerticalAlignment="Stretch">
-        <Grid Height="90" HorizontalAlignment="Center">
+        <DockPanel>
+            <Grid Height="50" HorizontalAlignment="Center" DockPanel.Dock="Top"
+                  Margin="0,20,0,0"
+                  Visibility="{Binding SizeUnitSelectionVisibility, ElementName=uc}"> 
+                <Grid.ColumnDefinitions>
+                    <ColumnDefinition Width="100"/>
+                    <ColumnDefinition/>
+                </Grid.ColumnDefinitions>
+                <Grid.RowDefinitions>
+                    <RowDefinition />
+                    <RowDefinition Height="20"/>
+                </Grid.RowDefinitions>
+
+                <RadioButton    Grid.Row="0" Grid.Column="0"  
+                                x:Name="PercentageRb" 
+                                  Foreground="White" 
+                                  FontSize="12"
+                                  GroupName="Unit"
+                                  Checked="PercentageRb_Checked"
+                                  VerticalContentAlignment="Center"
+                                  IsChecked="{Binding Path=SelectedUnit,  
+                                              ElementName=uc, 
+                                              Converter={converters:EnumBooleanConverter}, 
+                                              ConverterParameter=Percentage
+                                              }"
+                                  >Percentage:</RadioButton>
+                <local:SizeInput Grid.Column="1" Grid.Row="0"
+                             x:Name="PercentageSizePicker"
+                             IsEnabled="{Binding EditingEnabled, ElementName=uc}"
+                             Size="{Binding Path=ChosenPercentageSize, ElementName=uc, Mode=TwoWay}"
+                             Unit="Percentage"
+                                 Margin="-10,0,0,0"
+                             Width="80"
+                             >
+
+                    <i:Interaction.Triggers>
+                        <i:EventTrigger EventName="LostFocus">
+                            <i:InvokeCommandAction Command="{Binding ElementName=uc, Path=WidthLostFocusCommand}"/>
+                        </i:EventTrigger>
+                    </i:Interaction.Triggers>
+                </local:SizeInput>
+
+                <RadioButton Grid.Row="1" Grid.Column="0"  
+                              x:Name="AbsoluteRb" 
+                              Foreground="White" 
+                              FontSize="12"
+                              GroupName="Unit"
+                              Checked="AbsoluteRb_Checked"
+                              VerticalContentAlignment="Center"
+                              IsChecked="{Binding Path=SelectedUnit,  
+                                              ElementName=uc, 
+                                              Converter={converters:EnumBooleanConverter}, 
+                                              ConverterParameter=Pixel}"
+                              >Absolute:</RadioButton>
+               
+            </Grid>
+
+            <Grid Height="90" HorizontalAlignment="Center" DockPanel.Dock="Top">
             <Grid.ColumnDefinitions>
                 <ColumnDefinition Width="40"/>
                 <ColumnDefinition/>
@@ -38,8 +97,11 @@
             <TextBlock Grid.Column="0" Grid.Row="0" Foreground="Snow" Text="Width:" VerticalAlignment="Center" HorizontalAlignment="Left" />
             <local:SizeInput Grid.Column="1" Grid.Row="0"
                              x:Name="WidthPicker"
+                             Width="80"
                              IsEnabled="{Binding EditingEnabled, ElementName=uc}"
-                             Size="{Binding Path=ChosenWidth, ElementName=uc, Mode=TwoWay}">
+                             Size="{Binding Path=ChosenWidth, ElementName=uc, Mode=TwoWay}"
+                             Margin="50,0,0,0"
+                             >
                 <i:Interaction.Triggers>
                     <i:EventTrigger EventName="LostFocus">
                         <i:InvokeCommandAction Command="{Binding ElementName=uc, Path=WidthLostFocusCommand}"/>
@@ -51,6 +113,7 @@
             <local: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}">
                 <i:Interaction.Triggers>
                     <i:EventTrigger EventName="LostFocus">
@@ -68,5 +131,6 @@
                   HorizontalAlignment="Left" 
                   VerticalAlignment="Center" />
         </Grid>
+        </DockPanel>
     </Border>
 </UserControl>

+ 86 - 11
PixiEditor/Views/UserControls/SizePicker.xaml.cs

@@ -1,4 +1,7 @@
 using PixiEditor.Helpers;
+using PixiEditor.Models;
+using PixiEditor.Models.Enums;
+using PixiEditor.ViewModels;
 using System;
 using System.Windows;
 using System.Windows.Controls;
@@ -19,6 +22,38 @@ namespace PixiEditor.Views
         public static readonly DependencyProperty ChosenHeightProperty =
             DependencyProperty.Register(nameof(ChosenHeight), typeof(int), typeof(SizePicker), new PropertyMetadata(1));
 
+        public static readonly DependencyProperty NextControlProperty =
+            DependencyProperty.Register(nameof(NextControl), typeof(FrameworkElement), typeof(SizePicker));
+
+        public static readonly DependencyProperty ChosenPercentageSizeProperty =
+            DependencyProperty.Register(nameof(ChosenPercentageSize), typeof(int), typeof(SizePicker), new PropertyMetadata(1, InputSizeChanged));
+
+        public static readonly DependencyProperty SelectedUnitProperty =
+            DependencyProperty.Register(nameof(SelectedUnit), typeof(SizeUnit), typeof(SizePicker), new PropertyMetadata(SizeUnit.Pixel));
+
+        public static readonly DependencyProperty SizeUnitSelectionVisibilityProperty =
+            DependencyProperty.Register(nameof(SizeUnitSelectionVisibility), typeof(Visibility), typeof(SizePicker), new PropertyMetadata(Visibility.Collapsed));
+
+        System.Drawing.Size? initSize = null;
+               
+        private static void InputSizeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+        {
+            var sizePicker = d as SizePicker;
+            if (!sizePicker.initSize.HasValue)
+                return;
+
+            var newValue = (int)e.NewValue;
+            var newSize = SizeCalculator.CalcAbsoluteFromPercentage(newValue, sizePicker.initSize.Value);
+            if (newSize.Width > Constants.MaxCanvasWidth || newSize.Height > Constants.MaxCanvasHeight)
+            {
+                newSize = new System.Drawing.Size(Constants.MaxCanvasWidth, Constants.MaxCanvasHeight);
+                d.SetValue(ChosenPercentageSizeProperty, SizeCalculator.CalcPercentageFromAbsolute(sizePicker.initSize.Value.Width, newSize.Width));
+            }
+            
+            d.SetValue(ChosenWidthProperty, newSize.Width);
+            d.SetValue(ChosenHeightProperty, newSize.Height);
+        }
+
         public bool EditingEnabled
         {
             get => (bool)GetValue(EditingEnabledProperty);
@@ -37,6 +72,30 @@ namespace PixiEditor.Views
             set => SetValue(ChosenHeightProperty, value);
         }
 
+        public int ChosenPercentageSize
+        {
+            get => (int)GetValue(ChosenPercentageSizeProperty);
+            set => SetValue(ChosenPercentageSizeProperty, value);
+        }
+
+        public SizeUnit SelectedUnit 
+        {
+            get => (SizeUnit)GetValue(SelectedUnitProperty);
+            set => SetValue(SelectedUnitProperty, value);
+        }
+
+        public Visibility SizeUnitSelectionVisibility
+        {
+            get => (Visibility)GetValue(SizeUnitSelectionVisibilityProperty);
+            set => SetValue(SizeUnitSelectionVisibilityProperty, value);
+        }
+
+        public FrameworkElement NextControl
+        {
+            get => (FrameworkElement)GetValue(NextControlProperty);
+            set => SetValue(NextControlProperty, value);
+        }
+
         public bool PreserveAspectRatio
         {
             get => (bool)GetValue(PreserveAspectRatioProperty);
@@ -46,10 +105,7 @@ namespace PixiEditor.Views
         public RelayCommand LoadedCommand { get; private set; }
         public RelayCommand WidthLostFocusCommand { get; private set; }
         public RelayCommand HeightLostFocusCommand { get; private set; }
-
-        private bool initialValuesLoaded = false;
-        private int initW;
-        private int initH;
+                        
         public SizePicker()
         {
             LoadedCommand = new(AfterLoaded);
@@ -60,14 +116,13 @@ namespace PixiEditor.Views
 
         public void FocusWidthPicker()
         {
-            WidthPicker.FocusAndSelect();
+            PercentageSizePicker.FocusAndSelect();
         }
 
         private void AfterLoaded(object parameter)
         {
-            initW = ChosenWidth;
-            initH = ChosenHeight;
-            initialValuesLoaded = true;
+            initSize = new System.Drawing.Size(ChosenWidth, ChosenHeight);
+            EnableSizeEditors();
         }
 
         private void WidthLostFocus(object param) => OnSizeUpdate(true);
@@ -75,17 +130,37 @@ namespace PixiEditor.Views
 
         private void OnSizeUpdate(bool widthUpdated)
         {
-            if (!initialValuesLoaded || !PreserveAspectRatio)
+            if (!initSize.HasValue || !PreserveAspectRatio)
                 return;
 
             if (widthUpdated)
             {
-                ChosenHeight = Math.Clamp(ChosenWidth * initH / initW, 1, HeightPicker.MaxSize);
+                ChosenHeight = Math.Clamp(ChosenWidth * initSize.Value.Height / initSize.Value.Width, 1, HeightPicker.MaxSize);
             }
             else
             {
-                ChosenWidth = Math.Clamp(ChosenHeight * initW / initH, 1, WidthPicker.MaxSize);
+                ChosenWidth = Math.Clamp(ChosenHeight * initSize.Value.Width / initSize.Value.Height, 1, 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;
+        }
     }
 }

+ 29 - 0
PixiEditorTests/HelpersTests/SizeCalculatorTest.cs

@@ -0,0 +1,29 @@
+using PixiEditor.Helpers;
+using Xunit;
+
+namespace PixiEditorTests.HelpersTests
+{
+    public class SizeCalculatorTest
+    {
+        [Theory]
+        [InlineData(50, 64, 64, 32, 32)]
+        [InlineData(100, 64, 64, 64, 64)]
+        [InlineData(200, 128, 128, 256, 256)]
+        public void TestCalculationOfAbsoluteFromPercentageWorks(int percent, int currentWidth, int currentHeight, int expectedWidth, int expectedHeight)
+        {
+            var newSize = SizeCalculator.CalcAbsoluteFromPercentage(percent, new System.Drawing.Size(currentWidth, currentHeight));
+            Assert.Equal(expectedWidth, newSize.Width);
+            Assert.Equal(expectedHeight, newSize.Height);
+        }
+
+        [Theory]
+        [InlineData(32, 64, 50)]
+        [InlineData(32, 32, 100)]
+        [InlineData(64, 32, 200)]
+        public void TestCalculationOfPercentageFromAbsoluteWorks(int currentSize, int initSize, int expectedPerc)
+        {
+            var perc = SizeCalculator.CalcPercentageFromAbsolute(initSize, currentSize);
+            Assert.Equal(perc, expectedPerc);
+        }
+    }
+}