Browse Source

Implemented export file dialog

Krzysztof Krysiński 1 year ago
parent
commit
bf4588bdb0

+ 0 - 1
src/PixiEditor.AvaloniaUI/App.axaml

@@ -18,5 +18,4 @@
         <StyleInclude Source="/Styles/PixiEditor.Layers.axaml"/>
         <StyleInclude Source="/Styles/PixiEditorPopupTemplate.axaml"/>
     </Application.Styles>
-
 </Application>

+ 3 - 1
src/PixiEditor.AvaloniaUI/Data/Localization/Languages/en.json

@@ -584,5 +584,7 @@
   "USE_SECONDARY_COLOR": "Use secondary color",
   "RIGHT_CLICK_MODE": "Right click mode",
   "ADD_PRIMARY_COLOR_TO_PALETTE": "Add primary color to palette",
-  "ADD_PRIMARY_COLOR_TO_PALETTE_DESCRIPTIVE": "Add primary color to current palette"
+  "ADD_PRIMARY_COLOR_TO_PALETTE_DESCRIPTIVE": "Add primary color to current palette",
+
+  "EXPORT_SAVE_TITLE": "Choose a location to save the image"
 }

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

@@ -19,6 +19,7 @@
       <AvaloniaResource Include="Images\Lock-alpha.svg" />
       <None Remove="Images\Merge-downwards.svg" />
       <AvaloniaResource Include="Images\Merge-downwards.svg" />
+      <None Remove="Nunito.ttf" />
     </ItemGroup>
 
     <ItemGroup>

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

@@ -18,11 +18,13 @@ using PixiEditor.AvaloniaUI.Models.Dialogs;
 using PixiEditor.AvaloniaUI.Models.IO;
 using PixiEditor.AvaloniaUI.Models.UserData;
 using PixiEditor.AvaloniaUI.ViewModels.Document;
+using PixiEditor.AvaloniaUI.Views;
 using PixiEditor.AvaloniaUI.Views.Dialogs;
 using PixiEditor.AvaloniaUI.Views.Windows;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Extensions.Common.UserPreferences;
+using PixiEditor.OperatingSystem;
 using PixiEditor.Parser;
 
 namespace PixiEditor.AvaloniaUI.ViewModels.SubViewModels;
@@ -346,7 +348,7 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
     /// </summary>
     /// <param name="parameter">CommandProperty.</param>
     [Command.Basic("PixiEditor.File.Export", "EXPORT", "EXPORT_IMAGE", CanExecute = "PixiEditor.HasDocument", Key = Key.E, Modifiers = KeyModifiers.Control)]
-    public void ExportFile()
+    public async Task ExportFile()
     {
         try
         {
@@ -354,16 +356,15 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
             if (doc is null)
                 return;
 
-            //TODO: Implement ExportFileDialog
-            /*ExportFileDialog info = new ExportFileDialog(doc.SizeBindable);
-            if (info.ShowDialog())
+            ExportFileDialog info = new ExportFileDialog(MainWindow.Current, doc.SizeBindable) { SuggestedName = Path.GetFileNameWithoutExtension(doc.FileName) };
+            if (await info.ShowDialog())
             {
                 SaveResult result = Exporter.TrySaveUsingDataFromDialog(doc, info.FilePath, info.ChosenFormat, out string finalPath, new(info.FileWidth, info.FileHeight));
                 if (result == SaveResult.Success)
-                    ProcessHelper.OpenInExplorer(finalPath);
+                    IOperatingSystem.Current.OpenFolder(finalPath);
                 else
                     ShowSaveError((DialogSaveResult)result);
-            }*/
+            }
         }
         catch (RecoverableException e)
         {

+ 101 - 0
src/PixiEditor.AvaloniaUI/Views/Dialogs/ExportFileDialog.cs

@@ -0,0 +1,101 @@
+using System.Threading.Tasks;
+using Avalonia.Controls;
+using PixiEditor.AvaloniaUI.Models.Dialogs;
+using PixiEditor.AvaloniaUI.Models.Files;
+using PixiEditor.DrawingApi.Core.Numerics;
+
+namespace PixiEditor.AvaloniaUI.Views.Dialogs;
+
+internal class ExportFileDialog : CustomDialog
+{
+    FileType _chosenFormat;
+
+    private int fileHeight;
+
+    private string filePath;
+
+    private int fileWidth;
+
+    private string suggestedName;
+
+    public ExportFileDialog(Window owner, VecI size) : base(owner)
+    {
+        FileWidth = size.X;
+        FileHeight = size.Y;
+    }
+
+    public int FileWidth
+    {
+        get => fileWidth;
+        set
+        {
+            if (fileWidth != value)
+            {
+                this.SetProperty(ref fileWidth, value);
+            }
+        }
+    }
+
+    public int FileHeight
+    {
+        get => fileHeight;
+        set
+        {
+            if (fileHeight != value)
+            {
+                this.SetProperty(ref fileHeight, value);
+            }
+        }
+    }
+
+    public string FilePath
+    {
+        get => filePath;
+        set
+        {
+            if (filePath != value)
+            {
+                this.SetProperty(ref filePath, value);
+            }
+        }
+    }
+
+    public FileType ChosenFormat
+    {
+        get => _chosenFormat;
+        set
+        {
+            if (_chosenFormat != value)
+            {
+                this.SetProperty(ref _chosenFormat, value);
+            }
+        }
+    }
+
+    public string SuggestedName
+    {
+        get => suggestedName;
+        set
+        {
+            if (suggestedName != value)
+            {
+                this.SetProperty(ref suggestedName, value);
+            }
+        }
+    }
+    public override async Task<bool> ShowDialog()
+    {
+        ExportFilePopup popup = new ExportFilePopup(FileWidth, FileHeight) { SuggestedName = SuggestedName };
+        bool result = await popup.ShowDialog<bool>(OwnerWindow);
+
+        if (result)
+        {
+            FileWidth = popup.SaveWidth;
+            FileHeight = popup.SaveHeight;
+            FilePath = popup.SavePath;
+            ChosenFormat = popup.SaveFormat;
+        }
+
+        return result;
+    }
+}

+ 44 - 0
src/PixiEditor.AvaloniaUI/Views/Dialogs/ExportFilePopup.axaml

@@ -0,0 +1,44 @@
+<dialogs:PixiEditorPopup 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:dialogs="clr-namespace:PixiEditor.AvaloniaUI.Views.Dialogs"
+        xmlns:input="clr-namespace:PixiEditor.AvaloniaUI.Views.Input"
+        xmlns:ui="clr-namespace:PixiEditor.AvaloniaUI.Helpers.UI"
+        xmlns:ui1="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
+        CanResize="False"
+        CanMinimize="False"
+        SizeToContent="WidthAndHeight"
+        Name="saveFilePopup"
+        x:Class="PixiEditor.AvaloniaUI.Views.Dialogs.ExportFilePopup"
+        Title="EXPORT_IMAGE">
+    <DockPanel Background="{DynamicResource ThemeBackgroundBrush}">
+        <Button DockPanel.Dock="Bottom" HorizontalAlignment="Center" IsDefault="True"
+                Margin="15" ui1:Translator.Key="EXPORT" Command="{Binding ExportCommand, ElementName=saveFilePopup}" />
+
+        <Border HorizontalAlignment="Center" Margin="15,30,15,0" Background="{DynamicResource ThemeBackgroundBrush1}"
+                VerticalAlignment="Stretch" CornerRadius="10">
+            <Grid MinHeight="205" MinWidth="240">
+                <Grid.RowDefinitions>
+                    <RowDefinition/>
+                    <RowDefinition Height="Auto"/>
+                </Grid.RowDefinitions>
+                <input:SizePicker Margin="0,15,0,0"
+                                         x:Name="sizePicker"
+                                         IsSizeUnitSelectionVisible="True"
+                                         VerticalAlignment="Top"
+                                         ChosenHeight="{Binding Path=SaveHeight, Mode=TwoWay, ElementName=saveFilePopup}"
+                                         ChosenWidth="{Binding Path=SaveWidth, Mode=TwoWay, ElementName=saveFilePopup}" />
+                    <TextBlock Grid.Row="1" Margin="5,0,5,10" VerticalAlignment="Bottom" Classes="hyperlink" TextWrapping="Wrap"
+                               Width="220" TextAlignment="Center" Text="{Binding SizeHint, Mode=OneTime, ElementName=saveFilePopup}">
+                        <Interaction.Behaviors>
+                            <EventTriggerBehavior EventName="PointerPressed">
+                                <InvokeCommandAction Command="{Binding SetBestPercentageCommand, ElementName=saveFilePopup}"/>
+                            </EventTriggerBehavior>
+                        </Interaction.Behaviors>
+                        <TextBlock Text="&#xE869;" FontFamily="{DynamicResource Feather}"/>
+                    </TextBlock>
+            </Grid>
+        </Border>
+    </DockPanel>
+</dialogs:PixiEditorPopup>

+ 158 - 0
src/PixiEditor.AvaloniaUI/Views/Dialogs/ExportFilePopup.axaml.cs

@@ -0,0 +1,158 @@
+using System.Threading.Tasks;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.Platform.Storage;
+using CommunityToolkit.Mvvm.Input;
+using PixiEditor.AvaloniaUI.Helpers;
+using PixiEditor.AvaloniaUI.Models.Files;
+using PixiEditor.Extensions.Common.Localization;
+
+namespace PixiEditor.AvaloniaUI.Views.Dialogs;
+
+public partial class ExportFilePopup : PixiEditorPopup
+{
+    public int SaveWidth
+    {
+        get => (int)GetValue(SaveWidthProperty);
+        set => SetValue(SaveWidthProperty, value);
+    }
+
+
+    public int SaveHeight
+    {
+        get => (int)GetValue(SaveHeightProperty);
+        set => SetValue(SaveHeightProperty, value);
+    }
+
+    public string? SavePath
+    {
+        get => (string)GetValue(SavePathProperty);
+        set => SetValue(SavePathProperty, value);
+    }
+
+    public FileType SaveFormat
+    {
+        get => (FileType)GetValue(SaveFormatProperty);
+        set => SetValue(SaveFormatProperty, value);
+    }
+
+    public static readonly StyledProperty<int> SaveHeightProperty =
+        AvaloniaProperty.Register<ExportFilePopup, int>(nameof(SaveHeight), 32);
+
+    public static readonly StyledProperty<int> SaveWidthProperty =
+        AvaloniaProperty.Register<ExportFilePopup, int>(nameof(SaveWidth), 32);
+
+    public static readonly StyledProperty<RelayCommand> SetBestPercentageCommandProperty =
+        AvaloniaProperty.Register<ExportFilePopup, RelayCommand>(nameof(SetBestPercentageCommand));
+
+    public static readonly StyledProperty<string?> SavePathProperty =
+        AvaloniaProperty.Register<ExportFilePopup, string?>(nameof(SavePath), "");
+
+    public static readonly StyledProperty<FileType> SaveFormatProperty =
+        AvaloniaProperty.Register<ExportFilePopup, FileType>(nameof(SaveFormat), FileType.Png);
+
+    public static readonly StyledProperty<AsyncRelayCommand> ExportCommandProperty =
+        AvaloniaProperty.Register<ExportFilePopup, AsyncRelayCommand>(
+            nameof(ExportCommand));
+
+    public static readonly StyledProperty<string> SuggestedNameProperty = AvaloniaProperty.Register<ExportFilePopup, string>(
+        nameof(SuggestedName));
+
+    public string SuggestedName
+    {
+        get => GetValue(SuggestedNameProperty);
+        set => SetValue(SuggestedNameProperty, value);
+    }
+
+    public AsyncRelayCommand ExportCommand
+    {
+        get => GetValue(ExportCommandProperty);
+        set => SetValue(ExportCommandProperty, value);
+    }
+
+    public RelayCommand SetBestPercentageCommand
+    {
+        get => (RelayCommand)GetValue(SetBestPercentageCommandProperty);
+        set => SetValue(SetBestPercentageCommandProperty, value);
+    }
+
+    public string SizeHint => new LocalizedString("EXPORT_SIZE_HINT", GetBestPercentage());
+
+    public ExportFilePopup(int imageWidth, int imageHeight)
+    {
+        SaveWidth = imageWidth;
+        SaveHeight = imageHeight;
+
+        InitializeComponent();
+        DataContext = this;
+        Loaded += (_, _) => sizePicker.FocusWidthPicker();
+
+        SaveWidth = imageWidth;
+        SaveHeight = imageHeight;
+
+        SetBestPercentageCommand = new RelayCommand(SetBestPercentage);
+        ExportCommand = new AsyncRelayCommand(Export);
+    }
+
+    private async Task Export()
+    {
+        SavePath = await ChoosePath();
+        if (SavePath != null)
+        {
+            Close(true);
+        }
+    }
+
+    /// <summary>
+    ///     Command that handles Path choosing to save file
+    /// </summary>
+    private async Task<string?> ChoosePath()
+    {
+        FilePickerSaveOptions options = new FilePickerSaveOptions
+        {
+            Title = new LocalizedString("EXPORT_SAVE_TITLE"),
+            SuggestedFileName = SuggestedName,
+            SuggestedStartLocation = await GetTopLevel(this).StorageProvider.TryGetWellKnownFolderAsync(WellKnownFolder.Documents),
+            FileTypeChoices = SupportedFilesHelper.BuildSaveFilter(false),
+            ShowOverwritePrompt = true
+        };
+
+        IStorageFile file = await GetTopLevel(this).StorageProvider.SaveFilePickerAsync(options);
+        if (file != null)
+        {
+            if (string.IsNullOrEmpty(file.Name) == false)
+            {
+                SaveFormat = SupportedFilesHelper.GetSaveFileType(false, file);
+                if (SaveFormat == FileType.Unset)
+                {
+                    return null;
+                }
+
+                string fileName = SupportedFilesHelper.FixFileExtension(file.Path.AbsolutePath, SaveFormat);
+
+                return fileName;
+            }
+        }
+        return null;
+    }
+
+    private int GetBestPercentage()
+    {
+        int maxDim = Math.Max(SaveWidth, SaveHeight);
+        for (int i = 16; i >= 1; i--)
+        {
+            if (maxDim * i <= 1280)
+                return i * 100;
+        }
+
+        return 100;
+    }
+
+    private void SetBestPercentage()
+    {
+        sizePicker.ChosenPercentageSize = GetBestPercentage();
+        sizePicker.PercentageRb.IsChecked = true;
+        sizePicker.PercentageLostFocus();
+    }
+}

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

@@ -16,7 +16,6 @@
     <DockPanel Background="{DynamicResource ThemeBackgroundBrush}" Focusable="True">
         <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=popup}">
             <Button.CommandParameter>
                 <system:Boolean>True</system:Boolean>

+ 1 - 1
src/PixiEditor.AvaloniaUI/Views/Input/SizePicker.axaml

@@ -29,7 +29,7 @@
             <Setter Property="Height" Value="25"/>
         </Style>
     </UserControl.Styles>
-    <Border Background="{DynamicResource ThemeBackgroundBrush}" VerticalAlignment="Stretch" CornerRadius="10" Padding="15,0">
+    <Border 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}">

+ 1 - 1
src/PixiEditor.AvaloniaUI/Views/Windows/HelloTherePopup.axaml

@@ -232,7 +232,7 @@
                                                                 <Button Command="{xaml:Command Name=PixiEditor.File.RemoveRecent, UseProvided=True}"
                                                                         CommandParameter="{Binding FilePath}"
                                                                         ToolTip.Tip="Remove from list">
-                                                                    <TextBlock Text="" FontFamily="{DynamicResource Feather}"
+                                                                    <TextBlock Text="" FontFamily="{StaticResource Feather}"
                                                                                TextAlignment="Center" FontSize="20"/>
                                                                 </Button>
                                                             </StackPanel>

+ 1 - 1
src/PixiEditor.UI.Common/Accents/Base.axaml

@@ -84,7 +84,7 @@
             <system:Double x:Key="DockFontSizeNormal">12</system:Double>
 
             <FontFamily x:Key="ContentControlThemeFontFamily">fonts:Inter#Inter, $Default</FontFamily>
-            <FontFamily x:Key="Feather">avares://PixiEditor.UI.Common;/Fonts/feather.tff</FontFamily>
+            <FontFamily x:Key="Feather">avares://PixiEditor.UI.Common;/Fonts/feather.ttf</FontFamily>
             <system:Double x:Key="FontSizeSmall">10</system:Double>
             <system:Double x:Key="FontSizeNormal">12</system:Double>
             <system:Double x:Key="FontSizeLarge">16</system:Double>

BIN
src/PixiEditor.UI.Common/Fonts/feather.ttf


+ 4 - 1
src/PixiEditor.UI.Common/PixiEditor.UI.Common.csproj

@@ -10,7 +10,6 @@
       <AvaloniaResource Include="Assets\ChevronDown.png" />
       <AvaloniaResource Include="Assets\PixiEditorLogo.png" />
       <AvaloniaResource Include="Assets\Processing.gif" />
-      <AvaloniaResource Include="Fonts\feather.ttf" />
       <None Remove="Assets\Fonts\Oxygen-Bold.ttf" />
       <None Remove="Assets\Fonts\Oxygen-Light.ttf" />
       <None Remove="Assets\Fonts\Oxygen-Regular.ttf" />
@@ -29,4 +28,8 @@
       <UpToDateCheckInput Remove="Animations\Processing.axaml" />
       <UpToDateCheckInput Remove="Controls\Dock\Accents\Base.axaml" />
     </ItemGroup>
+
+    <ItemGroup>
+      <AvaloniaResource Include="Fonts\feather.ttf" />
+    </ItemGroup>
 </Project>

+ 10 - 0
src/PixiEditor.UI.Common/Styles/TextStyles.axaml

@@ -25,4 +25,14 @@
     <Style Selector="TextBlock.h5">
         <Setter Property="FontSize" Value="{DynamicResource Header5}"/>
     </Style>
+
+    <Style Selector="TextBlock.hyperlink">
+        <Setter Property="Foreground" Value="{DynamicResource ThemeForegroundSecondaryBrush}"/>
+    </Style>
+
+    <Style Selector="TextBlock.hyperlink:pointerover">
+        <Setter Property="Foreground" Value="{DynamicResource ThemeForegroundBrush}"/>
+        <Setter Property="TextDecorations" Value="Underline"/>
+        <Setter Property="Cursor" Value="Hand"/>
+    </Style>
 </Styles>

+ 17 - 2
src/PixiEditor.Windows/WindowsOperatingSystem.cs

@@ -1,4 +1,5 @@
-using PixiEditor.Helpers;
+using System.Runtime.InteropServices;
+using PixiEditor.Helpers;
 using PixiEditor.OperatingSystem;
 
 namespace PixiEditor.Windows;
@@ -18,6 +19,20 @@ public class WindowsOperatingSystem : IOperatingSystem
 
     public void OpenFolder(string path)
     {
-        WindowsProcessUtility.ShellExecuteEV(path);
+        string dirName = Path.GetDirectoryName(path);
+        string fileName = Path.GetFileName(path);
+
+        if (dirName == null)
+        {
+            return;
+        }
+
+        if (!string.IsNullOrEmpty(fileName))
+        {
+            WindowsProcessUtility.SelectInFileExplorer(path);
+            return;
+        }
+
+        WindowsProcessUtility.ShellExecuteEV(dirName);
     }
 }

+ 48 - 1
src/PixiEditor.Windows/WindowsProcessUtility.cs

@@ -1,4 +1,5 @@
 using System.Diagnostics;
+using System.Runtime.InteropServices;
 using System.Security.Principal;
 using PixiEditor.OperatingSystem;
 
@@ -27,9 +28,55 @@ public class WindowsProcessUtility : IProcessUtility
         Process.Start(new ProcessStartInfo
         {
             FileName = url,
-            UseShellExecute = true
+            UseShellExecute = true,
+        });
+    }
+
+    public static void ShellExecute(string url, string args)
+    {
+        Process.Start(new ProcessStartInfo
+        {
+            FileName = url,
+            Arguments = args,
+            UseShellExecute = true,
         });
     }
 
     public static void ShellExecuteEV(string path) => ShellExecute(Environment.ExpandEnvironmentVariables(path));
+
+    public static void ShellExecuteEV(string path, string args) => ShellExecute(Environment.ExpandEnvironmentVariables(path), args);
+
+    public static void SelectInFileExplorer(string fullPath)
+    {
+        if (string.IsNullOrEmpty(fullPath))
+            throw new ArgumentNullException("fullPath");
+
+        fullPath = Path.GetFullPath(fullPath);
+
+        IntPtr pidlList = NativeMethods.ILCreateFromPathW(fullPath);
+        if (pidlList != IntPtr.Zero)
+        {
+            try
+            {
+                // Open parent folder and select item
+                Marshal.ThrowExceptionForHR(NativeMethods.SHOpenFolderAndSelectItems(pidlList, 0, IntPtr.Zero, 0));
+            }
+            finally
+            {
+                NativeMethods.ILFree(pidlList);
+            }
+        }
+    }
+
+    static class NativeMethods
+    {
+        [DllImport("shell32.dll", ExactSpelling = true)]
+        public static extern void ILFree(IntPtr pidlList);
+
+        [DllImport("shell32.dll", CharSet = CharSet.Unicode, ExactSpelling = true)]
+        public static extern IntPtr ILCreateFromPathW(string pszPath);
+
+        [DllImport("shell32.dll", ExactSpelling = true)]
+        public static extern int SHOpenFolderAndSelectItems(IntPtr pidlList, uint cild, IntPtr children, uint dwFlags);
+    }
 }