Browse Source

Merge pull request #959 from PixiEditor/fixes/28.05.2025

Fixed saving palette not handled
Krzysztof Krysiński 2 months ago
parent
commit
17cd1e5867

+ 3 - 0
src/PixiEditor.ChangeableDocument/Rendering/DocumentRenderer.cs

@@ -5,6 +5,7 @@ using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using Drawie.Backend.Core;
+using Drawie.Backend.Core.Bridge;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces;
@@ -211,6 +212,7 @@ public class DocumentRenderer : IPreviewRenderable, IDisposable
     public void RenderDocument(DrawingSurface toRenderOn, KeyFrameTime frameTime, VecI renderSize,
         string? customOutput = null)
     {
+        var ctx = DrawingBackendApi.Current.RenderingDispatcher.EnsureContext();
         IsBusy = true;
 
         if (renderTexture == null || renderTexture.Size != renderSize)
@@ -262,6 +264,7 @@ public class DocumentRenderer : IPreviewRenderable, IDisposable
         renderTexture.DrawingSurface.Canvas.Restore();
         toRenderOn.Canvas.Restore();
 
+        ctx.Dispose();
         IsBusy = false;
     }
 

+ 1 - 1
src/PixiEditor.MacOs/MacOsProcessUtility.cs

@@ -9,7 +9,7 @@ internal class MacOsProcessUtility : IProcessUtility
     {
         string script = $"""
 
-                                     do shell script "'{path}' {args}" with administrator privileges
+                                     do shell script "/bin/bash '{path}' {args}" with administrator privileges
                          """;
         ProcessStartInfo startInfo = new ProcessStartInfo
         {

+ 0 - 10
src/PixiEditor.UpdateInstaller.Exe/PixiEditor.UpdateInstaller.Exe.csproj

@@ -29,14 +29,4 @@
     <Move SourceFiles="$(PublishDir)PixiEditor.UpdateInstaller.Exe.exe" DestinationFiles="$(PublishDir)PixiEditor.UpdateInstaller.exe"/>
     <Message Text="Renamed published executable file." Importance="high"/>
   </Target>
-
-  <Target Name="RenameBuildUnix" AfterTargets="AfterBuild" Condition="'$(RuntimeIdentifier)'!='win-x64' and '$(RuntimeIdentifier)'!='win-arm64'">
-    <Move SourceFiles="$(OutDir)PixiEditor.UpdateInstaller.Exe" DestinationFiles="$(OutDir)PixiEditor.UpdateInstaller"/>
-    <Message Text="Renamed build executable file." Importance="high"/>
-  </Target>
-
-  <Target Name="RenamePublishUnix" AfterTargets="Publish" Condition="'$(RuntimeIdentifier)'!='win-x64' and '$(RuntimeIdentifier)'!='win-arm64'">
-    <Move SourceFiles="$(PublishDir)PixiEditor.UpdateInstaller.Exe" DestinationFiles="$(PublishDir)PixiEditor.UpdateInstaller"/>
-    <Message Text="Renamed published executable file." Importance="high"/>
-  </Target>
 </Project>

+ 21 - 31
src/PixiEditor.UpdateInstaller.Exe/Program.cs

@@ -27,20 +27,17 @@ catch (Exception ex)
 {
     log.AppendLine($"{DateTime.Now}: Error during update installation: {ex.Message}");
     string errorLogPath = Path.Combine(logDirectory, "ErrorLog.txt");
-    File.AppendAllText(errorLogPath, $"Error PixiEditor.UpdateInstaller: {DateTime.Now}\n{ex.Message}\n{ex.StackTrace}\n-----\n");
+    File.AppendAllText(errorLogPath,
+        $"Error PixiEditor.UpdateInstaller: {DateTime.Now}\n{ex.Message}\n{ex.StackTrace}\n-----\n");
 }
 finally
 {
-    if (startAfterUpdate)
+    try
     {
-        log.AppendLine($"{DateTime.Now}: Starting PixiEditor after update.");
-        if (OperatingSystem.IsMacOS())
-        {
-            StartPixiEditorOnMacOS(controller);
-        }
-        else
+        if (startAfterUpdate)
         {
-            string binaryName = OperatingSystem.IsWindows() ? "PixiEditor.exe" : "PixiEditor";
+            log.AppendLine($"{DateTime.Now}: Starting PixiEditor after update.");
+            string binaryName = "PixiEditor.exe";
             string path = Path.Join(controller.UpdateDirectory, binaryName);
             if (File.Exists(path))
             {
@@ -49,7 +46,7 @@ finally
             }
             else
             {
-                binaryName = OperatingSystem.IsWindows() ? "PixiEditor.Desktop.exe" : "PixiEditor.Desktop";
+                binaryName = "PixiEditor.Desktop.exe";
                 path = Path.Join(controller.UpdateDirectory, binaryName);
                 if (File.Exists(path))
                 {
@@ -62,7 +59,14 @@ finally
             }
         }
     }
-    
+    catch (Exception ex)
+    {
+        log.AppendLine($"{DateTime.Now}: Error starting PixiEditor: {ex.Message}");
+        string errorLogPath = Path.Combine(logDirectory, "ErrorLog.txt");
+        File.AppendAllText(errorLogPath,
+            $"Error starting PixiEditor: {DateTime.Now}\n{ex.Message}\n{ex.StackTrace}\n-----\n");
+    }
+
     try
     {
         string updateLogPath = Path.Combine(logDirectory, "UpdateLog.txt");
@@ -75,37 +79,22 @@ finally
 
     void StartPixiEditor(string pixiEditorExecutablePath)
     {
-        if (OperatingSystem.IsWindows())
-        {
-            Process.Start(new ProcessStartInfo(pixiEditorExecutablePath) { UseShellExecute = true });
-        }
-        else if (OperatingSystem.IsLinux())
-        {
-            string display = Environment.GetEnvironmentVariable("DISPLAY");
-            Process.Start(new ProcessStartInfo
-            {
-                FileName = "/bin/bash",
-                Arguments = "-c \"DISPLAY=" + display + " " + pixiEditorExecutablePath + "\" & disown",
-                UseShellExecute = false,
-            });
-        }
-        else
-        {
-            log.AppendLine($"{DateTime.Now}: Unsupported operating system for starting PixiEditor.");
-        }
+        Process.Start(new ProcessStartInfo(pixiEditorExecutablePath) { UseShellExecute = true });
     }
 }
 
+/*
 void StartPixiEditorOnMacOS(UpdateController controller)
 {
     string pixiEditorExecutablePath = Path.Combine(controller.UpdateDirectory, "PixiEditor.app");
     if (Directory.Exists(pixiEditorExecutablePath))
     {
-        log.AppendLine($"{DateTime.Now}: Starting PixiEditor with open -a PixiEditor");
+        log.AppendLine($"{DateTime.Now}: Starting PixiEditor with open {pixiEditorExecutablePath}");
         Process.Start(new ProcessStartInfo
         {
             FileName = "open",
-            Arguments = $"-a PixiEditor",
+            Arguments = $"\"{pixiEditorExecutablePath}\"",
+            UseShellExecute = true
         });
     }
     else
@@ -113,3 +102,4 @@ void StartPixiEditorOnMacOS(UpdateController controller)
         log.AppendLine($"{DateTime.Now}: PixiEditor.app not found at {pixiEditorExecutablePath}");
     }
 }
+*/

+ 1 - 2
src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/ViewModels/UpdateController.cs

@@ -44,8 +44,7 @@ public class UpdateController
 
     public void InstallUpdate(StringBuilder log)
     {
-        string extension = OperatingSystem.IsLinux() ? "tar.gz" : ".zip";
-        string[] files = Directory.GetFiles(UpdateDownloader.DownloadLocation, $"update-*{extension}");
+        string[] files = Directory.GetFiles(UpdateDownloader.DownloadLocation, $"update-*.zip");
         log.AppendLine($"Found {files.Length} update files.");
 
         if (files.Length > 0)

+ 14 - 11
src/PixiEditor.UpdateModule/UpdateDownloader.cs

@@ -11,6 +11,8 @@ public static class UpdateDownloader
 {
     public static string DownloadLocation { get; } = Path.Join(Path.GetTempPath(), "PixiEditor");
 
+    public static event Action<double> ProgressChanged;
+
     public static async Task DownloadReleaseZip(ReleaseInfo release, string contentType, string extension)
     {
         Asset? matchingAsset = GetMatchingAsset(release, contentType);
@@ -20,16 +22,17 @@ public static class UpdateDownloader
             throw new FileNotFoundException("No matching update for your system found.");
         }
 
-        using HttpClient client = new HttpClient();
-        client.DefaultRequestHeaders.Add("User-Agent", "PixiEditor");
-        client.DefaultRequestHeaders.Add("Accept", "application/octet-stream");
-        var response = await client.GetAsync(matchingAsset.Url);
-        if (response.StatusCode == HttpStatusCode.OK)
+        using WebClient client = new WebClient();
+        client.Headers.Add("User-Agent", "PixiEditor");
+        client.Headers.Add("Accept", "application/octet-stream");
+        client.DownloadProgressChanged += (sender, args) =>
         {
-            byte[] bytes = await response.Content.ReadAsByteArrayAsync();
-            CreateTempDirectory();
-            await File.WriteAllBytesAsync(Path.Join(DownloadLocation, $"update-{release.TagName}.{extension}"), bytes);
-        }
+            ProgressChanged?.Invoke(args.ProgressPercentage);
+        };
+
+        var bytes = await client.DownloadDataTaskAsync(matchingAsset.Url);
+        CreateTempDirectory();
+        await File.WriteAllBytesAsync(Path.Join(DownloadLocation, $"update-{release.TagName}.{extension}"), bytes);
     }
 
     public static async Task DownloadInstaller(ReleaseInfo info)
@@ -70,8 +73,8 @@ public static class UpdateDownloader
                                                       && x.Name.Contains(archOld));
         }
 
-        string arch = OperatingSystem.IsWindows() ? "x64" : 
-                      OperatingSystem.IsLinux() ? "amd64" : "universal";
+        string arch = OperatingSystem.IsWindows() ? "x64" :
+            OperatingSystem.IsLinux() ? "amd64" : "universal";
         string os = OperatingSystem.IsWindows() ? "win" : OperatingSystem.IsLinux() ? "linux" : "macos";
         return release.Assets.FirstOrDefault(x => x.ContentType.Contains(assetType)
                                                   && x.Name.Contains(arch) && x.Name.Contains(os));

+ 4 - 47
src/PixiEditor.UpdateModule/UpdateInstaller.cs

@@ -59,58 +59,15 @@ public class UpdateInstaller
 
         Directory.CreateDirectory(UpdateFilesPath);
 
-        bool isZip = ArchiveFileName.EndsWith(".zip");
-        if (isZip)
-        {
-            log.AppendLine($"Extracting {ArchiveFileName} to {UpdateFilesPath}");
-            ZipFile.ExtractToDirectory(ArchiveFileName, UpdateFilesPath, true);
-        }
-        else
-        {
-            log.AppendLine($"Extracting {ArchiveFileName} to {UpdateFilesPath} using GZipStream");
-            using FileStream fs = new(ArchiveFileName, FileMode.Open, FileAccess.Read);
-            using GZipStream gz = new(fs, CompressionMode.Decompress, leaveOpen: true);
-
-            TarFile.ExtractToDirectory(gz, UpdateFilesPath, overwriteFiles: true);
-        }
-
-        if (OperatingSystem.IsMacOS())
-        {
-            string appFile = Directory.GetDirectories(UpdateFilesPath, "PixiEditor.app", SearchOption.TopDirectoryOnly)
-                .FirstOrDefault();
-            if (string.IsNullOrEmpty(appFile))
-            {
-                log.AppendLine("PixiEditor.app not found in the update files. Installation failed.");
-                string[] allFiles = Directory.GetFiles(UpdateFilesPath, "*.*", SearchOption.TopDirectoryOnly);
-                foreach (string file in allFiles)
-                {
-                    log.AppendLine($"Found file: {file}");
-                }
-
-                throw new FileNotFoundException("PixiEditor.app not found in the update files.");
-            }
-
-
-            log.AppendLine($"Moving {appFile} to {TargetDirectory}");
-            string targetAppDirectory = Path.Combine(TargetDirectory, "PixiEditor.app");
-            if (Directory.Exists(targetAppDirectory))
-            {
-                log.AppendLine($"Removing existing PixiEditor.app at {targetAppDirectory}");
-                Directory.Delete(targetAppDirectory, true);
-            }
-
-            Directory.Move(appFile, targetAppDirectory);
-
-            Cleanup(log);
-            return;
-        }
+        log.AppendLine($"Extracting {ArchiveFileName} to {UpdateFilesPath}");
+        ZipFile.ExtractToDirectory(ArchiveFileName, UpdateFilesPath, true);
 
         string[] extractedFiles = Directory.GetFiles(UpdateFilesPath, "*", SearchOption.AllDirectories);
         log.AppendLine($"Extracted {extractedFiles.Length} files to {UpdateFilesPath}");
         log.AppendLine("Files extracted");
 
         string dirWithFiles = UpdateFilesPath;
-        string binName = OperatingSystem.IsWindows() ? "PixiEditor.exe" : "PixiEditor";
+        string binName = "PixiEditor.exe";
         if (!File.Exists(Path.Combine(UpdateFilesPath, binName)))
         {
             dirWithFiles = Directory.GetDirectories(UpdateFilesPath)[0];
@@ -155,7 +112,7 @@ public class UpdateInstaller
         }
 
         string updateInstallerFile = Path.Join(Path.GetTempPath(), "PixiEditor",
-            "PixiEditor.UpdateInstaller" + (OperatingSystem.IsWindows() ? ".exe" : ""));
+            "PixiEditor.UpdateInstaller.exe");
         logger.AppendLine($"Looking for: {updateInstallerFile}");
         if (File.Exists(updateInstallerFile))
         {

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

@@ -1047,5 +1047,6 @@
   "EXPORT_OUTPUT": "Export Output",
   "RENDER_OUTPUT_SIZE": "Render Output Size",
   "RENDER_OUTPUT_CENTER": "Render Output Center",
-  "COLOR_PICKER": "Color Picker"
+  "COLOR_PICKER": "Color Picker",
+  "UNAUTHORIZED_ACCESS": "Unauthorized access"
 }

+ 1 - 22
src/PixiEditor/Initialization/ClassicDesktopEntry.cs

@@ -226,28 +226,7 @@ internal class ClassicDesktopEntry
         if (vm is null)
             return;
 
-        if (vm.DocumentManagerSubViewModel.Documents.Any(x => !x.AllChangesSaved))
-        {
-            e.Cancel = true;
-            Task.Run(async () =>
-            {
-                await Dispatcher.UIThread.InvokeAsync(async () =>
-                {
-                    ConfirmationType confirmation = await ConfirmationDialog.Show(
-                        new LocalizedString("SESSION_UNSAVED_DATA", "Shutdown"),
-                        $"Shutdown");
-
-                    if (confirmation == ConfirmationType.Yes)
-                    {
-                        desktop.Shutdown();
-                    }
-                    else
-                    {
-                        e.Cancel = true;
-                    }
-                });
-            });
-        }
+        vm.OnShutdown(e, () => desktop.Shutdown(0));
     }
 
     private void AttachGlobalShortcutBehavior(BehaviorCollection collection)

+ 2 - 2
src/PixiEditor/Models/Commands/CommandController.cs

@@ -80,7 +80,7 @@ internal class CommandController
             {
                 if (Commands.ContainsKey(command))
                 {
-                    ReplaceShortcut(Commands[command], shortcut.KeyCombination, false);
+                    ReplaceShortcut(Commands[command], AdjustForOS(shortcut.KeyCombination, null), false);
                 }
             }
         }
@@ -706,7 +706,7 @@ internal class CommandController
         if (IOperatingSystem.Current.IsMacOs)
         {
             KeyCombination newCombination = combination;
-            if (combination.Modifiers.HasFlag(KeyModifiers.Control))
+            if (combination.Modifiers.HasFlag(KeyModifiers.Control) && !combination.Modifiers.HasFlag(KeyModifiers.Meta))
             {
                 newCombination.Modifiers &= ~KeyModifiers.Control;
                 newCombination.Modifiers |= KeyModifiers.Meta;

+ 3 - 0
src/PixiEditor/Models/Commands/Templates/Providers/Parsers/KeyDefinition.cs

@@ -63,6 +63,9 @@ public record HumanReadableKeyCombination(string key, string[] modifiers = null)
                 case "win":
                     modifiers |= KeyModifiers.Meta;
                     break;
+                case "cmd":
+                    modifiers |= KeyModifiers.Meta;
+                    break;
             }
         }
         

+ 1 - 2
src/PixiEditor/Models/Rendering/SceneRenderer.cs

@@ -4,10 +4,8 @@ using Drawie.Backend.Core.Numerics;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.ChangeableDocument.Rendering;
 using Drawie.Backend.Core.Surfaces;
-using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Numerics;
 using PixiEditor.ChangeableDocument.Changeables.Animations;
-using PixiEditor.ChangeableDocument.Changeables.Graph;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Workspace;
@@ -55,6 +53,7 @@ internal class SceneRenderer : IDisposable
 
             var rendered = RenderGraph(target, resolution, targetOutput, finalGraph);
             cachedTextures[adjustedTargetOutput] = rendered;
+            return;
         }
 
         var cachedTexture = cachedTextures[adjustedTargetOutput];

+ 2 - 2
src/PixiEditor/ViewModels/Document/DocumentViewModel.cs

@@ -971,10 +971,10 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
             {
                 if (maybeMember is IRasterizable rasterizable)
                 {
-                    using Texture texture = Texture.ForDisplay(SizeBindable);
+                    using Surface texture = new Surface(SizeBindable);
                     using Paint paint = new Paint();
                     rasterizable.Rasterize(texture.DrawingSurface, paint);
-                    return texture.GetSRGBPixel(pos);
+                    return texture.GetSrgbPixel(pos);
                 }
             }
             else

+ 7 - 2
src/PixiEditor/ViewModels/SubViewModels/IoViewModel.cs

@@ -123,6 +123,9 @@ internal class IoViewModel : SubViewModel<ViewModelMain>
 
     private void OnKeyDown(object? sender, FilteredKeyEventArgs args)
     {
+        if (args.Key == Key.None)
+            return;
+
         ProcessShortcutDown(args.IsRepeat, args.Key, args.Modifiers);
         Owner.DocumentManagerSubViewModel.ActiveDocument?.EventInlet.OnKeyDown(args.Key);
     }
@@ -174,7 +177,8 @@ internal class IoViewModel : SubViewModel<ViewModelMain>
         }
 
         if (isRepeat && Owner.ShortcutController.LastCommands != null &&
-            Owner.ShortcutController.LastCommands.Any(x => x is Command.ToolCommand cmd && cmd.Shortcut == new KeyCombination(key, argsModifiers)))
+            Owner.ShortcutController.LastCommands.Any(x =>
+                x is Command.ToolCommand cmd && cmd.Shortcut == new KeyCombination(key, argsModifiers)))
         {
             Owner.ToolsSubViewModel.HandleToolRepeatShortcutDown();
         }
@@ -358,7 +362,8 @@ internal class IoViewModel : SubViewModel<ViewModelMain>
         var tools = Owner.ToolsSubViewModel;
 
         var rightCanUp = (button == MouseButton.Right) &&
-                         tools.RightClickMode is RightClickMode.Erase or RightClickMode.SecondaryColor or RightClickMode.ColorPicker;
+                         tools.RightClickMode is RightClickMode.Erase or RightClickMode.SecondaryColor
+                             or RightClickMode.ColorPicker;
 
         if (button == MouseButton.Left || rightCanUp)
         {

+ 53 - 15
src/PixiEditor/ViewModels/SubViewModels/UpdateViewModel.cs

@@ -10,6 +10,7 @@ using Avalonia;
 using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Threading;
 using CommunityToolkit.Mvvm.Input;
+using PixiEditor.Extensions.CommonApi.UserPreferences;
 using PixiEditor.Views.Dialogs;
 using PixiEditor.Extensions.CommonApi.UserPreferences.Settings.PixiEditor;
 using PixiEditor.Helpers;
@@ -25,6 +26,7 @@ namespace PixiEditor.ViewModels.SubViewModels;
 
 internal class UpdateViewModel : SubViewModel<ViewModelMain>
 {
+    private double currentProgress;
     public UpdateChecker UpdateChecker { get; set; }
 
     public List<UpdateChannel> UpdateChannels { get; } = new List<UpdateChannel>();
@@ -107,15 +109,25 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
         get => _updateState == UpdateState.UpToDate;
     }
 
+    public double CurrentProgress
+    {
+        get => currentProgress;
+        set
+        {
+            currentProgress = value;
+            OnPropertyChanged(nameof(CurrentProgress));
+        }
+    }
+
     public string ZipExtension => IOperatingSystem.Current.IsLinux ? "tar.gz" : "zip";
-    public string ZipContentType => IOperatingSystem.Current.IsLinux ? "octet-stream" : "zip";
+    public string ZipContentType => IOperatingSystem.Current.IsLinux ? "x-gzip" : "zip";
     public string InstallerExtension => IOperatingSystem.Current.IsWindows ? "exe" : "dmg";
 
     public string BinaryExtension => IOperatingSystem.Current.IsWindows ? ".exe" : string.Empty;
 
     public bool SelfUpdatingAvailable =>
 #if UPDATE
-        PixiEditorSettings.Update.CheckUpdatesOnStartup.Value && OsSupported() && !InstallDirReadOnly();
+        PixiEditorSettings.Update.CheckUpdatesOnStartup.Value && OsSupported();
 #else
         false;
 #endif
@@ -137,6 +149,13 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
 
         Owner.OnStartupEvent += Owner_OnStartupEvent;
         Owner.OnClose += Owner_OnClose;
+        UpdateDownloader.ProgressChanged += d =>
+        {
+            Dispatcher.UIThread.InvokeAsync(() =>
+            {
+                CurrentProgress = d;
+            });
+        };
         PixiEditorSettings.Update.UpdateChannel.ValueChanged += (_, value) =>
         {
             string prevChannel = UpdateChecker.Channel.ApiUrl;
@@ -207,6 +226,7 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
             try
             {
                 UpdateState = UpdateState.Downloading;
+                CurrentProgress = 0;
                 if (updateCompatible || !IOperatingSystem.Current.IsWindows)
                 {
                     await UpdateDownloader.DownloadReleaseZip(UpdateChecker.LatestReleaseInfo, ZipContentType,
@@ -256,12 +276,24 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
         Install(true);
     }
 
-    [Command.Debug("PixiEditor.Update.DebugInstall", "Debug Install Update", "(DEBUG) Install update zip file without checking for updates")]
+    [Command.Debug("PixiEditor.Update.DebugInstall", "Debug Install Update",
+        "(DEBUG) Install update zip file without checking for updates")]
     public void DebugInstall()
     {
         UpdateChecker.SetLatestReleaseInfo(new ReleaseInfo(true) { TagName = "2.2.2.2" });
         Install(true);
     }
+    
+    [Command.Debug("PixiEditor.Update.DebugDownload", "Debug Download Update",
+        "(DEBUG) Download update file")]
+    public void DebugDownload()
+    {
+        Dispatcher.UIThread.InvokeAsync(async () =>
+        {
+            await CheckForUpdate();
+            await Download();
+        });
+    }
 
     private void Install(bool startAfterUpdate)
     {
@@ -299,11 +331,14 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
         {
             if (Path.Exists(updaterPath))
             {
-                File.Copy(updaterPath, Path.Join(UpdateDownloader.DownloadLocation, $"PixiEditor.UpdateInstaller" + BinaryExtension),
+                string updateLocation = Path.GetDirectoryName(Process.GetCurrentProcess().MainModule?.FileName);
+
+                File.Copy(updaterPath,
+                    Path.Join(UpdateDownloader.DownloadLocation, $"PixiEditor.UpdateInstaller" + BinaryExtension),
                     true);
-                updaterPath = Path.Join(UpdateDownloader.DownloadLocation, $"PixiEditor.UpdateInstaller" + BinaryExtension);
-                File.WriteAllText(Path.Join(UpdateDownloader.DownloadLocation, "update-location.txt"),
-                    Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? string.Empty);
+                updaterPath = Path.Join(UpdateDownloader.DownloadLocation,
+                    $"PixiEditor.UpdateInstaller" + BinaryExtension);
+                File.WriteAllText(Path.Join(UpdateDownloader.DownloadLocation, "update-location.txt"), updateLocation);
             }
         }
         catch (IOException)
@@ -358,19 +393,18 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
     {
         try
         {
-            
             if (IOperatingSystem.Current.IsLinux)
             {
                 bool hasWritePermissions = !InstallDirReadOnly();
                 if (hasWritePermissions)
                 {
-                    IOperatingSystem.Current.ProcessUtility.ShellExecute(updateExeFile, args);
+                    args = "bash " + updateExeFile + " " + args + " &";
+                    IOperatingSystem.Current.ProcessUtility.ShellExecute("nohup", args);
                     Shutdown();
                 }
                 else
                 {
                     NoticeDialog.Show("COULD_NOT_UPDATE_WITHOUT_ADMIN", "INSUFFICIENT_PERMISSIONS");
-                    return;
                 }
             }
             else
@@ -382,7 +416,6 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
                     {
                         Dispatcher.UIThread.Invoke(() =>
                         {
-
                             if (t.IsCompletedSuccessfully && proc.ExitCode == 0)
                             {
                                 Shutdown();
@@ -399,7 +432,6 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
                     Shutdown();
                 }
             }
-
         }
         catch (Win32Exception)
         {
@@ -437,7 +469,7 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
     [Conditional("UPDATE")]
     private async void ConditionalUPDATE()
     {
-        if (PixiEditorSettings.Update.CheckUpdatesOnStartup.Value && OsSupported() && !InstallDirReadOnly())
+        if (PixiEditorSettings.Update.CheckUpdatesOnStartup.Value && OsSupported())
         {
             try
             {
@@ -471,7 +503,8 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
 
     private bool OsSupported()
     {
-        return IOperatingSystem.Current.IsWindows || IOperatingSystem.Current.IsLinux || IOperatingSystem.Current.IsMacOs;
+        return IOperatingSystem.Current.IsWindows || IOperatingSystem.Current.IsLinux ||
+               IOperatingSystem.Current.IsMacOs;
     }
 
     private void EnsureUpdateFilesDeleted()
@@ -495,14 +528,19 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
 
     private void InitUpdateChecker()
     {
+        string platformName = IPlatform.Current.Name;
 #if UPDATE
         UpdateChannels.Add(new UpdateChannel("Release", "PixiEditor", "PixiEditor"));
         UpdateChannels.Add(new UpdateChannel("Development", "PixiEditor", "PixiEditor-development-channel"));
 #else
-        string platformName = IPlatform.Current.Name;
         UpdateChannels.Add(new UpdateChannel(platformName, "", ""));
 #endif
 
+        if (IPreferences.Current.GetLocalPreference<bool>("UseTestingUpdateChannel", false))
+        {
+            UpdateChannels.Add(new UpdateChannel("Testing", "PixiEditor", "PixiEditor-testing-channel"));
+        }
+
         string updateChannel = PixiEditorSettings.Update.UpdateChannel.Value;
 
         string version = VersionHelpers.GetCurrentAssemblyVersionString();

+ 24 - 0
src/PixiEditor/ViewModels/ViewModelMain.cs

@@ -1,6 +1,7 @@
 using System.ComponentModel;
 using System.Linq;
 using System.Threading.Tasks;
+using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Threading;
 using CommunityToolkit.Mvvm.Input;
 using Microsoft.Extensions.DependencyInjection;
@@ -356,6 +357,29 @@ internal partial class ViewModelMain : ViewModelBase, ICommandsHandler
         return false;
     }
 
+
+    public void OnShutdown(ShutdownRequestedEventArgs shutdownRequestedEventArgs, Action shutdown)
+    {
+        shutdownRequestedEventArgs.Cancel = true;
+        Dispatcher.UIThread.InvokeAsync(async () =>
+        {
+            ResetNextSessionFiles();
+            UserWantsToClose = await DisposeAllDocumentsWithSaveConfirmation();
+
+            if (UserWantsToClose)
+            {
+                var analytics = Services.GetService<AnalyticsPeriodicReporter>();
+                if (analytics != null)
+                {
+                    await analytics.StopAsync();
+                }
+
+                OnClose?.Invoke();
+                shutdown();
+            }
+        });
+    }
+
     private void OnActiveDocumentChanged(object sender, DocumentChangedEventArgs e)
     {
         NotifyToolActionDisplayChanged();

+ 49 - 44
src/PixiEditor/Views/Main/CommandSearch/CommandSearchControl.axaml

@@ -12,25 +12,29 @@
              mc:Ignorable="d"
              Foreground="White"
              d:DesignHeight="450" d:DesignWidth="600"
-             Width="600"
              x:Name="uc">
-    <Grid x:Name="mainGrid">
-        <Grid.RowDefinitions>
-            <RowDefinition Height="Auto" />
-            <RowDefinition Height="*" />
-            <RowDefinition Height="Auto" />
-        </Grid.RowDefinitions>
+    <Grid Background="Transparent" Tapped="InputElement_OnTapped">
+        <Grid Height="700" 
+              MinWidth="600"
+              Width="600"
+              MaxWidth="920" x:Name="mainGrid" Tapped="MainGrid_OnTapped">
+            <Grid.RowDefinitions>
+                <RowDefinition Height="Auto" />
+                <RowDefinition Height="*" />
+                <RowDefinition Height="Auto" />
+            </Grid.RowDefinitions>
 
-        <TextBox Text="{Binding SearchTerm, Mode=TwoWay, ElementName=uc}"
-                 FontSize="17"
-                 Padding="5"
-                 CornerRadius="5,5,0,0"
-                 x:Name="textBox">
-            <Interaction.Behaviors>
-                <behaviors:TextBoxFocusBehavior SelectOnMouseClick="{Binding SelectAll, ElementName=uc, Mode=OneWay}" />
-                <behaviours:GlobalShortcutFocusBehavior />
-            </Interaction.Behaviors>
-            <!--<TextBox.Styles>
+            <TextBox Text="{Binding SearchTerm, Mode=TwoWay, ElementName=uc}"
+                     FontSize="17"
+                     Padding="5"
+                     CornerRadius="5,5,0,0"
+                     x:Name="textBox">
+                <Interaction.Behaviors>
+                    <behaviors:TextBoxFocusBehavior
+                        SelectOnMouseClick="{Binding SelectAll, ElementName=uc, Mode=OneWay}" />
+                    <behaviours:GlobalShortcutFocusBehavior />
+                </Interaction.Behaviors>
+                <!--<TextBox.Styles>
                 <Style Selector="TextBox">
                     <Style.Resources>
                         <Style Selector="Border">
@@ -39,32 +43,33 @@
                     </Style.Resources>
                 </Style>
             </TextBox.Styles>-->
-        </TextBox>
-        <Border Grid.Row="1" BorderThickness="1,0,1,0" BorderBrush="{DynamicResource ThemeBorderMidBrush}"
-                Background="{DynamicResource ThemeBackgroundBrush}">
-            <Grid>
-                <TextBlock Text="{Binding Warnings, ElementName=uc}" TextAlignment="Center" Foreground="Gray"
-                           TextWrapping="Wrap"
-                           Margin="5,5,5,0"
-                           IsVisible="{Binding HasWarnings, ElementName=uc}" />
-                <ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
-                    <ItemsControl ItemsSource="{Binding Results, ElementName=uc}" x:Name="itemscontrol">
-                        <ItemsControl.ItemTemplate>
-                            <DataTemplate DataType="search:SearchResult">
-                                <commandSearch:SearchResultControl
-                                    Result="{Binding}"
-                                    ButtonClickedCommand="{Binding ButtonClickedCommand, ElementName=uc}"
-                                    PointerMoved="SearchResult_MouseMove"/>
-                            </DataTemplate>
-                        </ItemsControl.ItemTemplate>
-                    </ItemsControl>
-                </ScrollViewer>
-            </Grid>
-        </Border>
-        <Border Grid.Row="2" BorderThickness="1" BorderBrush="{DynamicResource ThemeBorderMidBrush}"
-                CornerRadius="0,0,5,5" Background="{DynamicResource ThemeBackgroundBrush1}" Padding="3">
-            <ContentPresenter
-                Content="{Binding SelectedResult.Description, Mode=OneWay, ElementName=uc, FallbackValue={x:Null}}" />
-        </Border>
+            </TextBox>
+            <Border Grid.Row="1" BorderThickness="1,0,1,0" BorderBrush="{DynamicResource ThemeBorderMidBrush}"
+                    Background="{DynamicResource ThemeBackgroundBrush}">
+                <Grid>
+                    <TextBlock Text="{Binding Warnings, ElementName=uc}" TextAlignment="Center" Foreground="Gray"
+                               TextWrapping="Wrap"
+                               Margin="5,5,5,0"
+                               IsVisible="{Binding HasWarnings, ElementName=uc}" />
+                    <ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
+                        <ItemsControl ItemsSource="{Binding Results, ElementName=uc}" x:Name="itemscontrol">
+                            <ItemsControl.ItemTemplate>
+                                <DataTemplate DataType="search:SearchResult">
+                                    <commandSearch:SearchResultControl
+                                        Result="{Binding}"
+                                        ButtonClickedCommand="{Binding ButtonClickedCommand, ElementName=uc}"
+                                        PointerMoved="SearchResult_MouseMove" />
+                                </DataTemplate>
+                            </ItemsControl.ItemTemplate>
+                        </ItemsControl>
+                    </ScrollViewer>
+                </Grid>
+            </Border>
+            <Border Grid.Row="2" BorderThickness="1" BorderBrush="{DynamicResource ThemeBorderMidBrush}"
+                    CornerRadius="0,0,5,5" Background="{DynamicResource ThemeBackgroundBrush1}" Padding="3">
+                <ContentPresenter
+                    Content="{Binding SelectedResult.Description, Mode=OneWay, ElementName=uc, FallbackValue={x:Null}}" />
+            </Border>
+        </Grid>
     </Grid>
 </UserControl>

+ 12 - 2
src/PixiEditor/Views/Main/CommandSearch/CommandSearchControl.axaml.cs

@@ -120,7 +120,7 @@ internal partial class CommandSearchControl : UserControl, INotifyPropertyChange
         if (e.Sender is not CommandSearchControl control) return;
         if (e.NewValue.Value)
         {
-            Dispatcher.UIThread.Invoke(
+            Dispatcher.UIThread.Post(
                 () =>
                 {
                     control.textBox.Focus();
@@ -133,7 +133,7 @@ internal partial class CommandSearchControl : UserControl, INotifyPropertyChange
                     {
                         control.textBox.CaretIndex = control.SearchTerm?.Length ?? 0;
                     }
-                }, DispatcherPriority.Render);
+                }, DispatcherPriority.Input);
         }
     }
 
@@ -298,4 +298,14 @@ internal partial class CommandSearchControl : UserControl, INotifyPropertyChange
         CommandSearchControl control = ((CommandSearchControl)e.Sender);
         control.UpdateSearchResults();
     }
+
+    private void InputElement_OnTapped(object? sender, TappedEventArgs e)
+    {
+        Hide();
+    }
+
+    private void MainGrid_OnTapped(object? sender, TappedEventArgs e)
+    {
+        e.Handled = true;
+    }
 }

+ 3 - 0
src/PixiEditor/Views/Main/MainTitleBar.axaml

@@ -66,6 +66,9 @@
                                         Background="{DynamicResource ThemeAccentBrush}"
                                         Command="{Binding UpdateViewModel.DownloadCommand}"
                                         IsVisible="{Binding UpdateViewModel.IsUpdateAvailable}" />
+                                <ProgressBar IsVisible="{Binding UpdateViewModel.IsDownloading}" 
+                                             ShowProgressText="True" Value="{Binding UpdateViewModel.CurrentProgress}"
+                                              Maximum="100" Minimum="0"/>
                                 <Button ui:Translator.Key="SWITCH_TO_NEW_VERSION"
                                         Background="{DynamicResource ThemeAccentBrush}"
                                         Command="{Binding UpdateViewModel.InstallCommand}"

+ 1 - 3
src/PixiEditor/Views/MainView.axaml

@@ -35,8 +35,6 @@
         <commandSearch:CommandSearchControl
             IsVisible="{Binding SearchSubViewModel.SearchWindowOpen, Mode=TwoWay}"
             SearchTerm="{Binding SearchSubViewModel.SearchTerm, Mode=TwoWay}"
-            HorizontalAlignment="Center"
-            Height="700"
-            MaxWidth="920" />
+            />
     </Grid>
 </UserControl>

+ 8 - 0
src/PixiEditor/Views/Palettes/PaletteViewer.axaml.cs

@@ -214,6 +214,14 @@ internal partial class PaletteViewer : UserControl
             {
                 NoticeDialog.Show("COULD_NOT_SAVE_PALETTE", "ERROR");
             }
+            catch (UnauthorizedAccessException unauthorizedAccessException)
+            {
+                NoticeDialog.Show(new LocalizedString("UNAUTHORIZED_ACCESS", file.Path.LocalPath), "ERROR");
+            }
+            catch (Exception ex)
+            {
+                NoticeDialog.Show(new LocalizedString("ERROR_SAVING_PALETTE", ex.Message), "ERROR");
+            }
         });
     }
 

+ 2 - 1
src/PixiEditor/Views/Rendering/Scene.cs

@@ -465,7 +465,8 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
                 }
             }
 
-            Cursor = finalCursor;
+            if(Cursor.ToString() != finalCursor.ToString())
+                Cursor = finalCursor;
             e.Handled = args.Handled;
         }
     }