2
0
Эх сурвалжийг харах

Merge pull request #731 from PixiEditor/macos

Macos and fixos
Krzysztof Krysiński 7 сар өмнө
parent
commit
fce1454773
100 өөрчлөгдсөн 1056 нэмэгдсэн , 1115 устгасан
  1. 2 1
      .gitignore
  2. 0 38
      Third Party Licenses/Cake.Frosting_3.0.0.txt
  3. 168 0
      Third Party Licenses/ffmpeg-macos.txt
  4. 0 0
      Third Party Licenses/ffmpeg-windows.txt
  5. 1 1
      src/Drawie
  6. 0 426
      src/Installer/installer-setup-x64-light.iss
  7. BIN
      src/Installer/netcorecheck_x64.exe
  8. 1 1
      src/PixiDocks
  9. 46 11
      src/PixiEditor.AnimationRenderer.FFmpeg/FFMpegRenderer.cs
  10. 42 22
      src/PixiEditor.AnimationRenderer.FFmpeg/PixiEditor.AnimationRenderer.FFmpeg.csproj
  11. BIN
      src/PixiEditor.AnimationRenderer.FFmpeg/ThirdParty/MacOS/ffmpeg/ffmpeg
  12. 0 12
      src/PixiEditor.Builder/build/PixiEditor.Builder.csproj
  13. 0 205
      src/PixiEditor.Builder/build/Program.cs
  14. 0 1
      src/PixiEditor.ChangeableDocument/ChangeInfos/Root/ReferenceLayerChangeInfos/SetReferenceLayer_ChangeInfo.cs
  15. 28 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ColorNode.cs
  16. 45 0
      src/PixiEditor.Desktop/Info.plist
  17. 2 12
      src/PixiEditor.Desktop/PixiEditor.Desktop.csproj
  18. BIN
      src/PixiEditor.Desktop/PixiEditor.icns
  19. 4 0
      src/PixiEditor.Extensions/UI/Translator.cs
  20. 14 2
      src/PixiEditor.Linux/LinuxOperatingSystem.cs
  21. BIN
      src/PixiEditor.MSIX/Images/LargeTile.scale-100.png
  22. BIN
      src/PixiEditor.MSIX/Images/LargeTile.scale-125.png
  23. BIN
      src/PixiEditor.MSIX/Images/LargeTile.scale-150.png
  24. BIN
      src/PixiEditor.MSIX/Images/LargeTile.scale-200.png
  25. BIN
      src/PixiEditor.MSIX/Images/LargeTile.scale-400.png
  26. BIN
      src/PixiEditor.MSIX/Images/LockScreenLogo.scale-200.png
  27. BIN
      src/PixiEditor.MSIX/Images/SmallTile.scale-100.png
  28. BIN
      src/PixiEditor.MSIX/Images/SmallTile.scale-125.png
  29. BIN
      src/PixiEditor.MSIX/Images/SmallTile.scale-150.png
  30. BIN
      src/PixiEditor.MSIX/Images/SmallTile.scale-200.png
  31. BIN
      src/PixiEditor.MSIX/Images/SmallTile.scale-400.png
  32. BIN
      src/PixiEditor.MSIX/Images/SplashScreen.scale-100.png
  33. BIN
      src/PixiEditor.MSIX/Images/SplashScreen.scale-125.png
  34. BIN
      src/PixiEditor.MSIX/Images/SplashScreen.scale-150.png
  35. BIN
      src/PixiEditor.MSIX/Images/SplashScreen.scale-200.png
  36. BIN
      src/PixiEditor.MSIX/Images/SplashScreen.scale-400.png
  37. BIN
      src/PixiEditor.MSIX/Images/Square150x150Logo.scale-100.png
  38. BIN
      src/PixiEditor.MSIX/Images/Square150x150Logo.scale-125.png
  39. BIN
      src/PixiEditor.MSIX/Images/Square150x150Logo.scale-150.png
  40. BIN
      src/PixiEditor.MSIX/Images/Square150x150Logo.scale-200.png
  41. BIN
      src/PixiEditor.MSIX/Images/Square150x150Logo.scale-400.png
  42. BIN
      src/PixiEditor.MSIX/Images/Square44x44Logo.altform-lightunplated_targetsize-16.png
  43. BIN
      src/PixiEditor.MSIX/Images/Square44x44Logo.altform-lightunplated_targetsize-24.png
  44. BIN
      src/PixiEditor.MSIX/Images/Square44x44Logo.altform-lightunplated_targetsize-256.png
  45. BIN
      src/PixiEditor.MSIX/Images/Square44x44Logo.altform-lightunplated_targetsize-32.png
  46. BIN
      src/PixiEditor.MSIX/Images/Square44x44Logo.altform-lightunplated_targetsize-48.png
  47. BIN
      src/PixiEditor.MSIX/Images/Square44x44Logo.altform-unplated_targetsize-16.png
  48. BIN
      src/PixiEditor.MSIX/Images/Square44x44Logo.altform-unplated_targetsize-256.png
  49. BIN
      src/PixiEditor.MSIX/Images/Square44x44Logo.altform-unplated_targetsize-32.png
  50. BIN
      src/PixiEditor.MSIX/Images/Square44x44Logo.altform-unplated_targetsize-48.png
  51. BIN
      src/PixiEditor.MSIX/Images/Square44x44Logo.scale-100.png
  52. BIN
      src/PixiEditor.MSIX/Images/Square44x44Logo.scale-125.png
  53. BIN
      src/PixiEditor.MSIX/Images/Square44x44Logo.scale-150.png
  54. BIN
      src/PixiEditor.MSIX/Images/Square44x44Logo.scale-200.png
  55. BIN
      src/PixiEditor.MSIX/Images/Square44x44Logo.scale-400.png
  56. BIN
      src/PixiEditor.MSIX/Images/Square44x44Logo.targetsize-16.png
  57. BIN
      src/PixiEditor.MSIX/Images/Square44x44Logo.targetsize-24.png
  58. BIN
      src/PixiEditor.MSIX/Images/Square44x44Logo.targetsize-24_altform-unplated.png
  59. BIN
      src/PixiEditor.MSIX/Images/Square44x44Logo.targetsize-256.png
  60. BIN
      src/PixiEditor.MSIX/Images/Square44x44Logo.targetsize-32.png
  61. BIN
      src/PixiEditor.MSIX/Images/Square44x44Logo.targetsize-48.png
  62. BIN
      src/PixiEditor.MSIX/Images/StoreLogo.backup.png
  63. BIN
      src/PixiEditor.MSIX/Images/StoreLogo.scale-100.png
  64. BIN
      src/PixiEditor.MSIX/Images/StoreLogo.scale-125.png
  65. BIN
      src/PixiEditor.MSIX/Images/StoreLogo.scale-150.png
  66. BIN
      src/PixiEditor.MSIX/Images/StoreLogo.scale-200.png
  67. BIN
      src/PixiEditor.MSIX/Images/StoreLogo.scale-400.png
  68. BIN
      src/PixiEditor.MSIX/Images/Wide310x150Logo.scale-100.png
  69. BIN
      src/PixiEditor.MSIX/Images/Wide310x150Logo.scale-125.png
  70. BIN
      src/PixiEditor.MSIX/Images/Wide310x150Logo.scale-150.png
  71. BIN
      src/PixiEditor.MSIX/Images/Wide310x150Logo.scale-200.png
  72. BIN
      src/PixiEditor.MSIX/Images/Wide310x150Logo.scale-400.png
  73. 0 66
      src/PixiEditor.MSIX/Package.appxmanifest
  74. 0 197
      src/PixiEditor.MSIX/PixiEditor.MSIX.wapproj
  75. 49 6
      src/PixiEditor.MacOs/MacOperatingSystem.cs
  76. 164 0
      src/PixiEditor.MacOs/MacOsInputKeys.cs
  77. 72 0
      src/PixiEditor.MacOs/MacOsInterop.cs
  78. 79 0
      src/PixiEditor.MacOs/MacOsProcessUtility.cs
  79. 13 0
      src/PixiEditor.MacOs/todo.md
  80. 2 0
      src/PixiEditor.OperatingSystem/IInputKeys.cs
  81. 9 1
      src/PixiEditor.OperatingSystem/IOperatingSystem.cs
  82. 3 1
      src/PixiEditor.OperatingSystem/IProcessUtility.cs
  83. 0 42
      src/PixiEditor.UI.Common/Controls/TextBlock.axaml
  84. 25 2
      src/PixiEditor.UI.Common/Controls/Window.axaml
  85. 4 0
      src/PixiEditor.UI.Common/Styles/TextStyles.axaml
  86. 6 1
      src/PixiEditor.Windows/WindowsInputKeys.cs
  87. 9 3
      src/PixiEditor.Windows/WindowsOperatingSystem.cs
  88. 29 6
      src/PixiEditor.Windows/WindowsProcessUtility.cs
  89. 43 30
      src/PixiEditor.sln
  90. 1 0
      src/PixiEditor/App.axaml
  91. 2 1
      src/PixiEditor/Data/Localization/Languages/en.json
  92. 55 10
      src/PixiEditor/Helpers/Converters/KeyToStringConverter.cs
  93. 31 3
      src/PixiEditor/Helpers/DocumentViewModelBuilder.cs
  94. 26 0
      src/PixiEditor/Helpers/Extensions/ImageExtensions.cs
  95. 1 1
      src/PixiEditor/Helpers/Extensions/PixiParserDocumentEx.cs
  96. 17 0
      src/PixiEditor/Helpers/InputKeyHelpers.cs
  97. 10 5
      src/PixiEditor/Helpers/ThemeResources.cs
  98. 32 7
      src/PixiEditor/Initialization/ClassicDesktopEntry.cs
  99. 1 1
      src/PixiEditor/Models/Commands/Attributes/Commands/CommandAttribute.cs
  100. 20 0
      src/PixiEditor/Models/Commands/Attributes/Commands/CustomOsShortcutAttribute.cs

+ 2 - 1
.gitignore

@@ -338,4 +338,5 @@ Installer/Assets
 
 GitIgnore
 
-Cache/
+Cache/
+.DS_Store

+ 0 - 38
Third Party Licenses/Cake.Frosting_3.0.0.txt

@@ -1,38 +0,0 @@
- 
-MIT License
-SPDX identifier
-MIT
-License text
-
-MIT License
-
-Copyright (c) <year> <copyright holders> 
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of 
-this software and associated documentation files(the "Software"), to deal in the
-Software without restriction, including without limitation the rights to use,
-copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
-Software, and to permit persons to whom the Software is furnished to do so,
-subject to the following conditions:
-
-The above copyright notice and this permission notice (including the next
-paragraph)shall be included in all copies or substantial portions of the
-Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
-FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
-COPYRIGHT HOLDERSBE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
-AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
-WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-SPDX web page
-  * https://spdx.org/licenses/MIT.html
-Notice
-
-This license content is provided by the SPDX project. For more information about
-licenses.nuget.org, see our documentation. 
-
-Data pulled from spdx/license-list-data on February 9, 2023.
-
- 

+ 168 - 0
Third Party Licenses/ffmpeg-macos.txt

@@ -0,0 +1,168 @@
+FFmpeg compiled for MacOs with configuration
+./configure --disable-ffplay                   
+
+GNU LESSER GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+
+  This version of the GNU Lesser General Public License incorporates
+the terms and conditions of version 3 of the GNU General Public
+License, supplemented by the additional permissions listed below.
+
+  0. Additional Definitions.
+
+  As used herein, "this License" refers to version 3 of the GNU Lesser
+General Public License, and the "GNU GPL" refers to version 3 of the GNU
+General Public License.
+
+  "The Library" refers to a covered work governed by this License,
+other than an Application or a Combined Work as defined below.
+
+  An "Application" is any work that makes use of an interface provided
+by the Library, but which is not otherwise based on the Library.
+Defining a subclass of a class defined by the Library is deemed a mode
+of using an interface provided by the Library.
+
+  A "Combined Work" is a work produced by combining or linking an
+Application with the Library.  The particular version of the Library
+with which the Combined Work was made is also called the "Linked
+Version".
+
+  The "Minimal Corresponding Source" for a Combined Work means the
+Corresponding Source for the Combined Work, excluding any source code
+for portions of the Combined Work that, considered in isolation, are
+based on the Application, and not on the Linked Version.
+
+  The "Corresponding Application Code" for a Combined Work means the
+object code and/or source code for the Application, including any data
+and utility programs needed for reproducing the Combined Work from the
+Application, but excluding the System Libraries of the Combined Work.
+
+  1. Exception to Section 3 of the GNU GPL.
+
+  You may convey a covered work under sections 3 and 4 of this License
+without being bound by section 3 of the GNU GPL.
+
+  2. Conveying Modified Versions.
+
+  If you modify a copy of the Library, and, in your modifications, a
+facility refers to a function or data to be supplied by an Application
+that uses the facility (other than as an argument passed when the
+facility is invoked), then you may convey a copy of the modified
+version:
+
+   a) under this License, provided that you make a good faith effort to
+   ensure that, in the event an Application does not supply the
+   function or data, the facility still operates, and performs
+   whatever part of its purpose remains meaningful, or
+
+   b) under the GNU GPL, with none of the additional permissions of
+   this License applicable to that copy.
+
+  3. Object Code Incorporating Material from Library Header Files.
+
+  The object code form of an Application may incorporate material from
+a header file that is part of the Library.  You may convey such object
+code under terms of your choice, provided that, if the incorporated
+material is not limited to numerical parameters, data structure
+layouts and accessors, or small macros, inline functions and templates
+(ten or fewer lines in length), you do both of the following:
+
+   a) Give prominent notice with each copy of the object code that the
+   Library is used in it and that the Library and its use are
+   covered by this License.
+
+   b) Accompany the object code with a copy of the GNU GPL and this license
+   document.
+
+  4. Combined Works.
+
+  You may convey a Combined Work under terms of your choice that,
+taken together, effectively do not restrict modification of the
+portions of the Library contained in the Combined Work and reverse
+engineering for debugging such modifications, if you also do each of
+the following:
+
+   a) Give prominent notice with each copy of the Combined Work that
+   the Library is used in it and that the Library and its use are
+   covered by this License.
+
+   b) Accompany the Combined Work with a copy of the GNU GPL and this license
+   document.
+
+   c) For a Combined Work that displays copyright notices during
+   execution, include the copyright notice for the Library among
+   these notices, as well as a reference directing the user to the
+   copies of the GNU GPL and this license document.
+
+   d) Do one of the following:
+
+       0) Convey the Minimal Corresponding Source under the terms of this
+       License, and the Corresponding Application Code in a form
+       suitable for, and under terms that permit, the user to
+       recombine or relink the Application with a modified version of
+       the Linked Version to produce a modified Combined Work, in the
+       manner specified by section 6 of the GNU GPL for conveying
+       Corresponding Source.
+
+       1) Use a suitable shared library mechanism for linking with the
+       Library.  A suitable mechanism is one that (a) uses at run time
+       a copy of the Library already present on the user's computer
+       system, and (b) will operate properly with a modified version
+       of the Library that is interface-compatible with the Linked
+       Version.
+
+   e) Provide Installation Information, but only if you would otherwise
+   be required to provide such information under section 6 of the
+   GNU GPL, and only to the extent that such information is
+   necessary to install and execute a modified version of the
+   Combined Work produced by recombining or relinking the
+   Application with a modified version of the Linked Version. (If
+   you use option 4d0, the Installation Information must accompany
+   the Minimal Corresponding Source and Corresponding Application
+   Code. If you use option 4d1, you must provide the Installation
+   Information in the manner specified by section 6 of the GNU GPL
+   for conveying Corresponding Source.)
+
+  5. Combined Libraries.
+
+  You may place library facilities that are a work based on the
+Library side by side in a single library together with other library
+facilities that are not Applications and are not covered by this
+License, and convey such a combined library under terms of your
+choice, if you do both of the following:
+
+   a) Accompany the combined library with a copy of the same work based
+   on the Library, uncombined with any other library facilities,
+   conveyed under the terms of this License.
+
+   b) Give prominent notice with the combined library that part of it
+   is a work based on the Library, and explaining where to find the
+   accompanying uncombined form of the same work.
+
+  6. Revised Versions of the GNU Lesser General Public License.
+
+  The Free Software Foundation may publish revised and/or new versions
+of the GNU Lesser General Public License from time to time. Such new
+versions will be similar in spirit to the present version, but may
+differ in detail to address new problems or concerns.
+
+  Each version is given a distinguishing version number. If the
+Library as you received it specifies that a certain numbered version
+of the GNU Lesser General Public License "or any later version"
+applies to it, you have the option of following the terms and
+conditions either of that published version or of any later version
+published by the Free Software Foundation. If the Library as you
+received it does not specify a version number of the GNU Lesser
+General Public License, you may choose any version of the GNU Lesser
+General Public License ever published by the Free Software Foundation.
+
+  If the Library as you received it specifies that a proxy can decide
+whether future versions of the GNU Lesser General Public License shall
+apply, that proxy's public statement of acceptance of any version is
+permanent authorization for you to choose that version for the
+Library.

+ 0 - 0
Third Party Licenses/ffmpeg.txt → Third Party Licenses/ffmpeg-windows.txt


+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit 52a989906b7d9254fa9bab4acf172097d9d0dbef
+Subproject commit 2237f26dbac88d9dffac3e7916ef10198b115c18

+ 0 - 426
src/Installer/installer-setup-x64-light.iss

@@ -1,426 +0,0 @@
-// contribute: https://github.com/DomGries/InnoDependencyInstaller
-// official article: https://codeproject.com/Articles/20868/Inno-Setup-Dependency-Installer
-
-// requires netcorecheck.exe and netcorecheck_x64.exe (see download link below)
-#define UseNetCoreCheck
-#ifdef UseNetCoreCheck
-  ;#define UseDotNet80
-  #define UseDotNet80Desktop
-#endif
-
-// custom setup info
-#define MyAppName "PixiEditor"
-#define MyAppVersion GetFileVersion("..\..\Builds\PixiEditor-x64-light\PixiEditor\PixiEditor.dll")     ;Not perfect solution, it's enviroment dependend
-#define MyAppPublisher "PixiEditor"
-#define MyAppURL "https://github.com/PixiEditor/PixiEditor"
-#define MyAppExeName "PixiEditor.exe"
-#define TargetPlatform "x64-light"
-
-[Setup]
-AppId={{83DE4F2A-1F75-43AE-9546-3184F1C44517}
-AppName={#MyAppName}
-AppVersion={#MyAppVersion}
-AppVerName={#MyAppName} {#MyAppVersion}
-VersionInfoVersion={#MyAppVersion}
-AppPublisher={#MyAppPublisher}
-AppPublisherURL={#MyAppURL}
-AppSupportURL={#MyAppURL}
-AppUpdatesURL={#MyAppURL}
-DefaultDirName={autopf}\{#MyAppName}
-DisableProgramGroupPage=yes
-; The [Icons] "quicklaunchicon" entry uses {userappdata} but its [Tasks] entry has a proper IsAdminInstallMode Check.
-UsedUserAreasWarning=no
-LicenseFile=..\..\LICENSE
-; Uncomment the following line to run in non administrative install mode (install for current user only.)
-;PrivilegesRequired=lowest
-OutputDir=Assets\PixiEditor-{#TargetPlatform}
-OutputBaseFilename=PixiEditor-{#MyAppVersion}-setup-x64
-SetupIconFile=..\icon.ico
-Compression=lzma
-SolidCompression=yes
-WizardStyle=modern
-ChangesAssociations = yes
-
-MinVersion=6.1
-PrivilegesRequired=admin
-ArchitecturesInstallIn64BitMode=x64
-
-// dependency installation requires ready page and ready memo to be enabled (default behaviour)
-DisableReadyPage=no
-DisableReadyMemo=no
-
-
-// shared code for installing the dependencies
-[Code]
-// types and variables
-type
-  TDependency = record
-    Filename: String;
-    Parameters: String;
-    Title: String;
-    URL: String;
-    Checksum: String;
-    ForceSuccess: Boolean;
-    InstallClean: Boolean;
-    RebootAfter: Boolean;
-  end;
-
-  InstallResult = (InstallSuccessful, InstallRebootRequired, InstallError);
-
-var
-  MemoInstallInfo: String;
-  Dependencies: array of TDependency;
-  DelayedReboot, ForceX86: Boolean;
-  DownloadPage: TDownloadWizardPage;
-
-procedure AddDependency(const Filename, Parameters, Title, URL, Checksum: String; const ForceSuccess, InstallClean, RebootAfter: Boolean);
-var
-  Dependency: TDependency;
-  I: Integer;
-begin
-  MemoInstallInfo := MemoInstallInfo + #13#10 + '%1' + Title;
-
-  Dependency.Filename := Filename;
-  Dependency.Parameters := Parameters;
-  Dependency.Title := Title;
-
-  if FileExists(ExpandConstant('{tmp}{\}') + Filename) then begin
-    Dependency.URL := '';
-  end else begin
-    Dependency.URL := URL;
-  end;
-
-  Dependency.Checksum := Checksum;
-  Dependency.ForceSuccess := ForceSuccess;
-  Dependency.InstallClean := InstallClean;
-  Dependency.RebootAfter := RebootAfter;
-
-  I := GetArrayLength(Dependencies);
-  SetArrayLength(Dependencies, I + 1);
-  Dependencies[I] := Dependency;
-end;
-
-function IsPendingReboot: Boolean;
-var
-  Value: String;
-begin
-  Result := RegQueryMultiStringValue(HKEY_LOCAL_MACHINE, 'SYSTEM\CurrentControlSet\Control\Session Manager', 'PendingFileRenameOperations', Value) or
-    (RegQueryMultiStringValue(HKEY_LOCAL_MACHINE, 'SYSTEM\CurrentControlSet\Control\Session Manager', 'SetupExecute', Value) and (Value <> ''));
-end;
-
-function InstallProducts: InstallResult;
-var
-  ResultCode, I, ProductCount: Integer;
-begin
-  Result := InstallSuccessful;
-  ProductCount := GetArrayLength(Dependencies);
-  MemoInstallInfo := SetupMessage(msgReadyMemoTasks);
-
-  if ProductCount > 0 then begin
-    DownloadPage.Show;
-
-    for I := 0 to ProductCount - 1 do begin
-      if Dependencies[I].InstallClean and (DelayedReboot or IsPendingReboot) then begin
-        Result := InstallRebootRequired;
-        break;
-      end;
-
-      DownloadPage.SetText(Dependencies[I].Title, '');
-      DownloadPage.SetProgress(I + 1, ProductCount);
-
-      while True do begin
-        ResultCode := 0;
-        if ShellExec('', ExpandConstant('{tmp}{\}') + Dependencies[I].Filename, Dependencies[I].Parameters, '', SW_SHOWNORMAL, ewWaitUntilTerminated, ResultCode) then begin
-          if Dependencies[I].RebootAfter then begin
-            // delay reboot after install if we installed the last dependency anyways
-            if I = ProductCount - 1 then begin
-              DelayedReboot := True;
-            end else begin
-              Result := InstallRebootRequired;
-              MemoInstallInfo := Dependencies[I].Title;
-            end;
-            break;
-          end else if (ResultCode = 0) or Dependencies[I].ForceSuccess then begin
-            break;
-          end else if ResultCode = 3010 then begin
-            // Windows Installer ResultCode 3010: ERROR_SUCCESS_REBOOT_REQUIRED
-            DelayedReboot := True;
-            break;
-          end;
-        end;
-
-        case SuppressibleMsgBox(FmtMessage(SetupMessage(msgErrorFunctionFailed), [Dependencies[I].Title, IntToStr(ResultCode)]), mbError, MB_ABORTRETRYIGNORE, IDIGNORE) of
-          IDABORT: begin
-            Result := InstallError;
-            MemoInstallInfo := MemoInstallInfo + #13#10 + '      ' + Dependencies[I].Title;
-            break;
-          end;
-          IDIGNORE: begin
-            MemoInstallInfo := MemoInstallInfo + #13#10 + '      ' + Dependencies[I].Title;
-            break;
-          end;
-        end;
-      end;
-
-      if Result <> InstallSuccessful then begin
-        break;
-      end;
-    end;
-
-    DownloadPage.Hide;
-  end;
-end;
-
-// Inno Setup event functions
-procedure InitializeWizard;
-begin
-  DownloadPage := CreateDownloadPage(SetupMessage(msgWizardPreparing), SetupMessage(msgPreparingDesc), nil);
-end;
-
-function PrepareToInstall(var NeedsRestart: Boolean): String;
-var
-  I: Integer;
-begin
-  DelayedReboot := False;
-
-  case InstallProducts of
-    InstallError: begin
-      Result := MemoInstallInfo;
-    end;
-    InstallRebootRequired: begin
-      Result := MemoInstallInfo;
-      NeedsRestart := True;
-
-      // write into the registry that the installer needs to be executed again after restart
-      RegWriteStringValue(HKEY_CURRENT_USER, 'SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce', 'InstallBootstrap', ExpandConstant('{srcexe}'));
-    end;
-  end;
-end;
-
-function NeedRestart: Boolean;
-begin
-  Result := DelayedReboot;
-end;
-
-function UpdateReadyMemo(const Space, NewLine, MemoUserInfoInfo, MemoDirInfo, MemoTypeInfo, MemoComponentsInfo, MemoGroupInfo, MemoTasksInfo: String): String;
-begin
-  Result := '';
-  if MemoUserInfoInfo <> '' then begin
-    Result := Result + MemoUserInfoInfo + Newline + NewLine;
-  end;
-  if MemoDirInfo <> '' then begin
-    Result := Result + MemoDirInfo + Newline + NewLine;
-  end;
-  if MemoTypeInfo <> '' then begin
-    Result := Result + MemoTypeInfo + Newline + NewLine;
-  end;
-  if MemoComponentsInfo <> '' then begin
-    Result := Result + MemoComponentsInfo + Newline + NewLine;
-  end;
-  if MemoGroupInfo <> '' then begin
-    Result := Result + MemoGroupInfo + Newline + NewLine;
-  end;
-  if MemoTasksInfo <> '' then begin
-    Result := Result + MemoTasksInfo;
-  end;
-
-  if MemoInstallInfo <> '' then begin
-    if MemoTasksInfo = '' then begin
-      Result := Result + SetupMessage(msgReadyMemoTasks);
-    end;
-    Result := Result + FmtMessage(MemoInstallInfo, [Space]);
-  end;
-end;
-
-function NextButtonClick(const CurPageID: Integer): Boolean;
-var
-  I, ProductCount: Integer;
-  Retry: Boolean;
-begin
-  Result := True;
-
-  if (CurPageID = wpReady) and (MemoInstallInfo <> '') then begin
-    DownloadPage.Show;
-
-    ProductCount := GetArrayLength(Dependencies);
-    for I := 0 to ProductCount - 1 do begin
-      if Dependencies[I].URL <> '' then begin
-        DownloadPage.Clear;
-        DownloadPage.Add(Dependencies[I].URL, Dependencies[I].Filename, Dependencies[I].Checksum);
-
-        Retry := True;
-        while Retry do begin
-          Retry := False;
-
-          try
-            DownloadPage.Download;
-          except
-            if GetExceptionMessage = SetupMessage(msgErrorDownloadAborted) then begin
-              Result := False;
-              I := ProductCount;
-            end else begin
-              case SuppressibleMsgBox(AddPeriod(GetExceptionMessage), mbError, MB_ABORTRETRYIGNORE, IDIGNORE) of
-                IDABORT: begin
-                  Result := False;
-                  I := ProductCount;
-                end;
-                IDRETRY: begin
-                  Retry := True;
-                end;
-              end;
-            end;
-          end;
-        end;
-      end;
-    end;
-
-    DownloadPage.Hide;
-  end;
-end;
-
-// architecture helper functions
-function IsX64: Boolean;
-begin
-  Result := not ForceX86 and Is64BitInstallMode;
-end;
-
-function GetString(const x86, x64: String): String;
-begin
-  if IsX64 then begin
-    Result := x64;
-  end else begin
-    Result := x86;
-  end;
-end;
-
-function GetArchitectureSuffix: String;
-begin
-  Result := GetString('', '_x64');
-end;
-
-function GetArchitectureTitle: String;
-begin
-  Result := GetString(' (x86)', ' (x64)');
-end;
-
-function CompareVersion(const Version1, Version2: String): Integer;
-var
-  Position, Number1, Number2: Integer;
-begin
-  Result := 0;
-  while (Version1 <> '') or (Version2 <> '') do begin
-    Position := Pos('.', Version1);
-    if Position > 0 then begin
-      Number1 := StrToIntDef(Copy(Version1, 1, Position - 1), 0);
-      Delete(Version1, 1, Position);
-    end else if Version1 <> '' then begin
-      Number1 := StrToIntDef(Version1, 0);
-      Version1 := '';
-    end else begin
-      Number1 := 0;
-    end;
-
-    Position := Pos('.', Version2);
-    if Position > 0 then begin
-      Number2 := StrToIntDef(Copy(Version2, 1, Position - 1), 0);
-      Delete(Version2, 1, Position);
-    end else if Version2 <> '' then begin
-      Number2 := StrToIntDef(Version2, 0);
-      Version2 := '';
-    end else begin
-      Number2 := 0;
-    end;
-
-    if Number1 < Number2 then begin
-      Result := -1;
-      break;
-    end else if Number1 > Number2 then begin
-      Result := 1;
-      break;
-    end;
-  end;
-end;
-
-#ifdef UseNetCoreCheck
-// https://github.com/dotnet/deployment-tools/tree/master/src/clickonce/native/projects/NetCoreCheck
-function IsNetCoreInstalled(const Version: String): Boolean;
-var
-  ResultCode: Integer;
-begin
-  if not FileExists(ExpandConstant('{tmp}{\}') + 'netcorecheck' + GetArchitectureSuffix + '.exe') then begin
-    ExtractTemporaryFile('netcorecheck' + GetArchitectureSuffix + '.exe');
-  end;
-  Result := ShellExec('', ExpandConstant('{tmp}{\}') + 'netcorecheck' + GetArchitectureSuffix + '.exe', Version, '', SW_HIDE, ewWaitUntilTerminated, ResultCode) and (ResultCode = 0);
-end;
-#endif
-
-// custom setup content
-[Languages]
-Name: en; MessagesFile: "compiler:Default.isl"
-Name: nl; MessagesFile: "compiler:Languages\Dutch.isl"
-Name: de; MessagesFile: "compiler:Languages\German.isl"
-
-[Files]
-#ifdef UseNetCoreCheck
-// download netcorecheck.exe: https://go.microsoft.com/fwlink/?linkid=2135256
-// download netcorecheck_x64.exe: https://go.microsoft.com/fwlink/?linkid=2135504
-Source: "netcorecheck_x64.exe"; Flags: dontcopy noencryption
-#endif
-
-Source: "..\..\Builds\PixiEditor-{#TargetPlatform}\PixiEditor\PixiEditor.exe"; DestDir: "{app}"; Flags: ignoreversion
-Source: "..\..\Builds\PixiEditor-{#TargetPlatform}\PixiEditor\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
-
-[Icons]
-Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
-Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
-Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: quicklaunchicon
-
-[Tasks]
-Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
-Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked; OnlyBelowVersion: 6.1; Check: not IsAdminInstallMode
-
-[Run]
-Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
-
-[Registry]
-
-Root: HKCR; Subkey: ".pixi";                             ValueData: "{#MyAppName}";          Flags: uninsdeletevalue; ValueType: string;  ValueName: ""
-Root: HKCR; Subkey: "{#MyAppName}";                     ValueData: "Program {#MyAppName}";  Flags: uninsdeletekey;   ValueType: string;  ValueName: ""
-Root: HKCR; Subkey: "{#MyAppName}\DefaultIcon";             ValueData: "{app}\{#MyAppExeName},0";               ValueType: string;  ValueName: ""
-Root: HKCR; Subkey: "{#MyAppName}\shell\open\command";  ValueData: """{app}\{#MyAppExeName}"" ""%1""";  ValueType: string;  ValueName: ""
-
-// lospec-palette URL protocol association
-Root: HKCR; Subkey: "lospec-palette";                   ValueData: "{#MyAppName}";  Flags: uninsdeletevalue; ValueType: string;  ValueName: ""
-Root: HKCR; Subkey: "lospec-palette";                   ValueData: "";  Flags: uninsdeletekey;   ValueType: string;  ValueName: "URL Protocol"
-Root: HKCR; Subkey: "lospec-palette\shell\open\command";  ValueData: """{app}\{#MyAppExeName}"" ""%1""";  ValueType: string;  ValueName: ""
-
-[Code]
-function InitializeSetup: Boolean;
-var
-  Version: String;
-begin
-
-#ifdef UseDotNet80
-  // https://dotnet.microsoft.com/download/dotnet/8.0
-  if not IsNetCoreInstalled('Microsoft.NETCore.App 8.0.0') then begin
-    AddDependency('dotnet80' + GetArchitectureSuffix + '.exe',
-      '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
-      '.NET Runtime 8.0' + GetArchitectureTitle,
-      GetString('https://download.visualstudio.microsoft.com/download/pr/53e9e41c-b362-4598-9985-45f989518016/53c5e1919ba2fe23273f2abaff65595b/dotnet-runtime-8.0.11-win-x64.exe', 'https://download.visualstudio.microsoft.com/download/pr/53e9e41c-b362-4598-9985-45f989518016/53c5e1919ba2fe23273f2abaff65595b/dotnet-runtime-8.0.11-win-x64.exe'),
-      '', False, False, False);
-  end;
-#endif
-
-#ifdef UseDotNet80Desktop
-  // https://dotnet.microsoft.com/download/dotnet/8.0
-  if not IsNetCoreInstalled('Microsoft.WindowsDesktop.App 8.0.0') then begin
-    AddDependency('dotnet80desktop' + GetArchitectureSuffix + '.exe',
-      '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
-      '.NET Desktop Runtime 8.0' + GetArchitectureTitle,
-      GetString('https://download.visualstudio.microsoft.com/download/pr/27bcdd70-ce64-4049-ba24-2b14f9267729/d4a435e55182ce5424a7204c2cf2b3ea/windowsdesktop-runtime-8.0.11-win-x64.exe', 'https://download.visualstudio.microsoft.com/download/pr/27bcdd70-ce64-4049-ba24-2b14f9267729/d4a435e55182ce5424a7204c2cf2b3ea/windowsdesktop-runtime-8.0.11-win-x64.exe'),
-      '', False, False, False);
-  end;
-#endif
-
-  Result := True;
-end;

BIN
src/Installer/netcorecheck_x64.exe


+ 1 - 1
src/PixiDocks

@@ -1 +1 @@
-Subproject commit 5f14bdf0e46dd470e46a88ce5f58de4e02c68e94
+Subproject commit b83ba013241e6d6b6d280eea8836e49c5c6b9f81

+ 46 - 11
src/PixiEditor.AnimationRenderer.FFmpeg/FFMpegRenderer.cs

@@ -1,4 +1,5 @@
-using System.Drawing;
+using System.Diagnostics;
+using System.Drawing;
 using System.Reflection;
 using FFMpegCore;
 using FFMpegCore.Arguments;
@@ -22,10 +23,16 @@ public class FFMpegRenderer : IAnimationRenderer
     {
         string path = $"ThirdParty/{IOperatingSystem.Current.Name}/ffmpeg";
 
-        GlobalFFOptions.Configure(new FFOptions()
+        string binaryPath = Path.Combine(Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory), path);
+
+        GlobalFFOptions.Configure(new FFOptions() { BinaryFolder = binaryPath });
+
+        if (IOperatingSystem.Current.IsUnix)
         {
-            BinaryFolder = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), path),
-        });
+            MakeExecutableIfNeeded(binaryPath);
+        }
+
+        string paletteTempPath = Path.Combine(Path.GetDirectoryName(outputPath), "RenderTemp", "palette.png");
 
         try
         {
@@ -38,7 +45,6 @@ public class FFMpegRenderer : IAnimationRenderer
 
             RawVideoPipeSource streamPipeSource = new(frames) { FrameRate = FrameRate, };
 
-            string paletteTempPath = Path.Combine(Path.GetDirectoryName(outputPath), "RenderTemp", "palette.png");
 
             if (!Directory.Exists(Path.GetDirectoryName(paletteTempPath)))
             {
@@ -63,20 +69,50 @@ public class FFMpegRenderer : IAnimationRenderer
             var result = await outputArgs.CancellableThrough(cancellationToken)
                 .NotifyOnProgress(progressCallback, totalTimeSpan).ProcessAsynchronously();
 
-            if (RequiresPaletteGeneration())
+            DisposeStream(frames);
+
+            return result;
+        }
+        finally
+        {
+            if (RequiresPaletteGeneration() && File.Exists(paletteTempPath))
             {
                 File.Delete(paletteTempPath);
                 Directory.Delete(Path.GetDirectoryName(paletteTempPath));
             }
+        }
+    }
 
-            DisposeStream(frames);
+    private static void MakeExecutableIfNeeded(string binaryPath)
+    {
+        string filePath = Path.Combine(binaryPath, "ffmpeg");
 
-            return result;
+        if (!File.Exists(filePath))
+        {
+            throw new FileNotFoundException("FFmpeg binary not found");
+        }
+
+        try
+        {
+            var process = IOperatingSystem.Current.ProcessUtility.Execute($"{filePath}", "-version");
+
+            bool exited = process.WaitForExit(500);
+
+            if (!exited)
+            {
+                throw new InvalidOperationException("Failed to perform FFmpeg check");
+            }
+
+            if (process.ExitCode == 0)
+            {
+                return;
+            }
+
+            IOperatingSystem.Current.ProcessUtility.Execute("chmod", $"+x {filePath}");
         }
         catch (Exception e)
         {
-            Console.WriteLine(e);
-            return false;
+            IOperatingSystem.Current.ProcessUtility.Execute("chmod", $"+x {filePath}");
         }
     }
 
@@ -117,7 +153,6 @@ public class FFMpegRenderer : IAnimationRenderer
             .OutputToFile(outputPath, true, options =>
             {
                 options.WithFramerate(FrameRate)
-                    .WithConstantRateFactor(18)
                     .WithVideoBitrate(1800)
                     .WithVideoCodec("mpeg4")
                     .ForcePixelFormat("yuv420p");

+ 42 - 22
src/PixiEditor.AnimationRenderer.FFmpeg/PixiEditor.AnimationRenderer.FFmpeg.csproj

@@ -1,26 +1,46 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
-    <PropertyGroup>
-        <TargetFramework>net8.0</TargetFramework>
-        <ImplicitUsings>enable</ImplicitUsings>
-        <Nullable>enable</Nullable>
-        <Configurations>Release;Debug</Configurations>
-        <Platforms>arm64;x64</Platforms>
-    </PropertyGroup>
-
-    <ItemGroup>
-      <ProjectReference Include="..\PixiEditor.AnimationRenderer.Core\PixiEditor.AnimationRenderer.Core.csproj" />
-      <ProjectReference Include="..\PixiEditor.OperatingSystem\PixiEditor.OperatingSystem.csproj" />
-    </ItemGroup>
-
-    <ItemGroup>
-      <PackageReference Include="FFMpegCore" Version="5.1.0" />
-    </ItemGroup>
-  
-    <ItemGroup>
-      <Content Include="ThirdParty\Windows\ffmpeg\**">
-        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
-      </Content>
-    </ItemGroup>
+  <PropertyGroup>
+    <TargetFramework>net8.0</TargetFramework>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>enable</Nullable>
+    <Configurations>Release;Debug</Configurations>
+    <Platforms>arm64;x64</Platforms>
+    <RuntimeIdentifiers>win-x64;win-arm64;osx-x64;osx-arm64;linux-x64;linux-arm64</RuntimeIdentifiers>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\Drawie\src\Drawie.Backend.Core\Drawie.Backend.Core.csproj"/>
+    <ProjectReference Include="..\PixiEditor.AnimationRenderer.Core\PixiEditor.AnimationRenderer.Core.csproj"/>
+    <ProjectReference Include="..\PixiEditor.OperatingSystem\PixiEditor.OperatingSystem.csproj"/>
+  </ItemGroup>
+
+  <ItemGroup>
+    <PackageReference Include="FFMpegCore" Version="5.1.0"/>
+  </ItemGroup>
+
+  <ItemGroup Condition="'$(RuntimeIdentifier)' == 'win-x64'">
+    <Content Include="ThirdParty\Windows\ffmpeg\**">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
+  </ItemGroup>
+
+  <ItemGroup Condition="'$(RuntimeIdentifier)' == 'win-arm64'">
+    <Content Include="ThirdParty\Windows\ffmpeg\**">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
+  </ItemGroup>
+
+  <ItemGroup Condition="'$(RuntimeIdentifier)' == 'osx-x64'">
+    <Content Include="ThirdParty\MacOS\ffmpeg\**">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
+  </ItemGroup>
+
+  <ItemGroup Condition="'$(RuntimeIdentifier)' == 'osx-arm64'">
+    <Content Include="ThirdParty\MacOS\ffmpeg\**">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
+  </ItemGroup>
 
 </Project>

BIN
src/PixiEditor.AnimationRenderer.FFmpeg/ThirdParty/MacOS/ffmpeg/ffmpeg


+ 0 - 12
src/PixiEditor.Builder/build/PixiEditor.Builder.csproj

@@ -1,12 +0,0 @@
-<Project Sdk="Microsoft.NET.Sdk">
-    <PropertyGroup>
-        <OutputType>Exe</OutputType>
-        <TargetFramework>net8.0</TargetFramework>
-        <RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
-        <AssemblyName>PixiEditor.Builder</AssemblyName>
-        <RootNamespace>PixiEditor.Builder</RootNamespace>
-    </PropertyGroup>
-    <ItemGroup>
-        <PackageReference Include="Cake.Frosting" Version="5.0.0" />
-    </ItemGroup>
-</Project>

+ 0 - 205
src/PixiEditor.Builder/build/Program.cs

@@ -1,205 +0,0 @@
-using System.IO;
-using Cake.Common.Build;
-using Cake.Common.Tools.DotNet;
-using Cake.Common.Tools.DotNet.Publish;
-using Cake.Core;
-using Cake.Core.Diagnostics;
-using Cake.Frosting;
-using Path = System.IO.Path;
-
-namespace PixiEditor.Cake.Builder;
-
-public static class Program
-{
-    public static int Main(string[] args)
-    {
-        return new CakeHost()
-            .UseContext<BuildContext>()
-            .Run(args);
-    }
-}
-
-public class BuildContext : FrostingContext
-{
-    public string PathToProject { get; set; } = "../PixiEditor/PixiEditor.csproj";
-
-    public string[] ExtensionProjectsToInclude { get; set; } = [];
-
-    public string CrashReportWebhookUrl { get; set; }
-
-    public string AnalyticsUrl { get; set; }
-
-    public string BackedUpConstants { get; set; }
-
-    public string BuildConfiguration { get; set; } = "Release";
-
-    public string OutputDirectory { get; set; } = "Builds";
-
-    public bool SelfContained { get; set; } = false;
-
-    public string Runtime { get; set; }
-
-    public BuildContext(ICakeContext context)
-        : base(context)
-    {
-        CrashReportWebhookUrl = GetArgumentOrDefault(context, "crash-report-webhook-url", string.Empty);
-        AnalyticsUrl = GetArgumentOrDefault(context, "analytics-url", string.Empty);
-
-        bool hasCustomProjectPath = context.Arguments.HasArgument("project-path");
-        if (hasCustomProjectPath)
-        {
-            PathToProject = context.Arguments.GetArgument("project-path");
-        }
-
-        bool hasCustomExtensionProjects = context.Arguments.HasArgument("extension-projects");
-        if (hasCustomExtensionProjects)
-        {
-            ExtensionProjectsToInclude = context.Arguments.GetArgument("extension-projects").Split(';');
-        }
-
-        bool hasCustomConfiguration = context.Arguments.HasArgument("build-configuration");
-        if (hasCustomConfiguration)
-        {
-            BuildConfiguration = context.Arguments.GetArgument("build-configuration");
-        }
-
-        bool hasCustomOutputDirectory = context.Arguments.HasArgument("o");
-        if (hasCustomOutputDirectory)
-        {
-            OutputDirectory = context.Arguments.GetArgument("o");
-        }
-
-        bool hasSelfContained = context.Arguments.HasArgument("self-contained");
-        if (hasSelfContained)
-        {
-            SelfContained = true;
-        }
-
-        Runtime = context.Arguments.GetArgument("runtime");
-    }
-
-    private static string GetArgumentOrDefault(ICakeContext context, string argumentName, string defaultValue)
-    {
-        var arguments = context.Arguments;
-
-        var hasArgument = arguments.HasArgument(argumentName);
-        return hasArgument ? arguments.GetArgument(argumentName) : defaultValue;
-    }
-}
-
-[TaskName("Default")]
-[IsDependentOn(typeof(CopyExtensionsTask))]
-public sealed class DefaultTask : FrostingTask<BuildContext>
-{
-    public override void Run(BuildContext context)
-    {
-        context.Log.Information("Built project successfully!");
-    }
-}
-
-[TaskName("ReplaceSpecialStrings")]
-public sealed class ReplaceSpecialStringsTask : FrostingTask<BuildContext>
-{
-    public override void Run(BuildContext context)
-    {
-        context.Log.Information("Replacing special strings...");
-        string projectPath = context.PathToProject;
-        string filePath = Path.Combine(projectPath, "..", "PixiEditor", "BuildConstants.cs");
-
-        string result;
-        var fileContent = File.ReadAllText(filePath);
-        context.BackedUpConstants = fileContent;
-        result = ReplaceSpecialStrings(context, fileContent);
-
-        File.WriteAllText(filePath, result);
-    }
-
-    private string ReplaceSpecialStrings(BuildContext context, string fileContent)
-    {
-        string result = fileContent
-            .Replace("${crash-report-webhook-url}", context.CrashReportWebhookUrl)
-            .Replace("${analytics-url}", context.AnalyticsUrl);
-
-        return result;
-    }
-}
-
-[TaskName("BuildProject")]
-[IsDependentOn(typeof(ReplaceSpecialStringsTask))]
-public sealed class BuildProjectTask : FrostingTask<BuildContext>
-{
-    public override void Run(BuildContext context)
-    {
-        context.Log.Information("Building project...");
-        string projectPath = context.PathToProject;
-
-        var settings = new DotNetPublishSettings()
-        {
-            Configuration = context.BuildConfiguration,
-            SelfContained = context.SelfContained,
-            Runtime = context.Runtime,
-            OutputDirectory = context.OutputDirectory,
-        };
-
-        context.DotNetPublish(projectPath, settings);
-    }
-
-    public override void Finally(BuildContext context)
-    {
-        context.Log.Information("Cleaning up...");
-        string constantsPath = Path.Combine(context.PathToProject, "..", "PixiEditor", "BuildConstants.cs");
-
-        File.WriteAllText(constantsPath, context.BackedUpConstants);
-    }
-}
-
-[TaskName("BuildExtensions")]
-[IsDependentOn(typeof(BuildProjectTask))]
-public sealed class BuildExtensionsTask : FrostingTask<BuildContext>
-{
-    public override void Run(BuildContext context)
-    {
-        context.Log.Information("Building extensions...");
-        foreach (var project in context.ExtensionProjectsToInclude)
-        {
-            var settings = new DotNetPublishSettings() { Configuration = context.BuildConfiguration, };
-
-            context.DotNetPublish(project, settings);
-        }
-    }
-}
-
-[TaskName("CopyExtensions")]
-[IsDependentOn(typeof(BuildExtensionsTask))]
-public sealed class CopyExtensionsTask : FrostingTask<BuildContext>
-{
-    public override void Run(BuildContext context)
-    {
-        context.Log.Information("Copying extensions...");
-        foreach (var project in context.ExtensionProjectsToInclude)
-        {
-            string outputDir = Path.Combine(context.OutputDirectory, "Extensions");
-            string sourceDir = Path.Combine(project, "bin",
-                context.BuildConfiguration, "wasi-wasm", "Extensions");
-
-            CopyDirectoryContents(sourceDir, outputDir, context);
-        }
-    }
-
-    private void CopyDirectoryContents(string sourceDir, string targetDir, BuildContext context)
-    {
-        if (!Directory.Exists(targetDir))
-        {
-            Directory.CreateDirectory(targetDir);
-        }
-
-        context.Log.Information($"Copying contents of {sourceDir} to {targetDir}");
-
-        foreach (var file in Directory.GetFiles(sourceDir))
-        {
-            string targetFile = Path.Combine(targetDir, Path.GetFileName(file));
-            context.Log.Information($"Copying {file} to {targetFile}");
-            File.Copy(file, targetFile, true);
-        }
-    }
-}

+ 0 - 1
src/PixiEditor.ChangeableDocument/ChangeInfos/Root/ReferenceLayerChangeInfos/SetReferenceLayer_ChangeInfo.cs

@@ -4,5 +4,4 @@ using Drawie.Numerics;
 
 namespace PixiEditor.ChangeableDocument.ChangeInfos.Structure;
 
-// TODO: Make sure Pbgra8888 is all right
 public record class SetReferenceLayer_ChangeInfo(ImmutableArray<byte> ImagePbgra8888Bytes, VecI ImageSize, ShapeCorners Shape) : IChangeInfo;

+ 28 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ColorNode.cs

@@ -0,0 +1,28 @@
+using Drawie.Backend.Core.ColorsImpl;
+using Drawie.Backend.Core.Shaders.Generation.Expressions;
+using PixiEditor.ChangeableDocument.Rendering;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
+
+[NodeInfo("Color")]
+public class ColorNode : Node
+{
+    public FuncInputProperty<Half4> InputColor { get; }
+    public FuncOutputProperty<Half4> Color { get; }
+    
+    public ColorNode()
+    {
+        InputColor = CreateFuncInput<Half4>("InputColor", "COLOR", Colors.White);
+        Color = CreateFuncOutput<Half4>("OutputColor", "COLOR", ctx => ctx.GetValue(InputColor));
+    }
+    
+    protected override void OnExecute(RenderContext context)
+    {
+        
+    }
+
+    public override Node CreateCopy()
+    {
+        return new ColorNode();
+    }
+}

+ 45 - 0
src/PixiEditor.Desktop/Info.plist

@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+    <dict>
+        <key>CFBundleName</key>
+        <string>PixiEditor</string>
+        <key>CFBundleDisplayName</key>
+        <string>PixiEditor</string>
+        <key>CFBundleIdentifier</key>
+        <string>com.pixieditor.macos</string>
+        <key>CFBundleVersion</key>
+        <string>{version-string}</string>
+        <key>CFBundlePackageType</key>
+        <string>APPL</string>
+        <key>CFBundleExecutable</key>
+        <string>PixiEditor</string>
+        <key>CFBundleIconFile</key>
+        <string>PixiEditor.icns</string>
+        <key>CFBundleShortVersionString</key>
+        <string>{version-string}</string>
+        <key>CFBundleSignature</key>
+        <string>????</string>
+        <key>NSPrincipalClass</key>
+        <string>NSApplication</string>
+        <key>NSHighResolutionCapable</key>
+        <key>CFBundleDocumentTypes</key>
+        <array>
+            <dict>
+                <key>CFBundleTypeName</key>
+                <string>Pixi Document</string>
+                <key>CFBundleTypeExtensions</key>
+                <array>
+                    <string>pixi</string>
+                </array>
+                <key>CFBundleTypeIconFile</key>
+                <string>PixiEditor.icns</string>
+                <key>CFBundleTypeRole</key>
+                <string>Editor</string>
+                <key>LSHandlerRank</key>
+                <string>Owner</string>
+            </dict>
+        </array>
+        <true/>
+    </dict>
+</plist>

+ 2 - 12
src/PixiEditor.Desktop/PixiEditor.Desktop.csproj

@@ -10,7 +10,6 @@
     <RootNamespace>PixiEditor.Desktop</RootNamespace>
     <ApplicationIcon>..\PixiEditor\Images\favicon.ico</ApplicationIcon>
   </PropertyGroup>
-  
 
   <PropertyGroup>
     <ApplicationManifest>app.manifest</ApplicationManifest>
@@ -27,7 +26,7 @@
   </ItemGroup>
 
   <ItemGroup>
-    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" />
+    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0"/>
   </ItemGroup>
 
   <ItemGroup>
@@ -37,16 +36,7 @@
   </ItemGroup>
 
   <ItemGroup>
-    <ProjectReference Include="..\PixiEditor\PixiEditor.csproj" />
+    <ProjectReference Include="..\PixiEditor\PixiEditor.csproj"/>
   </ItemGroup>
 
-  <Target Name="Rename" AfterTargets="AfterBuild">
-    <Move SourceFiles="$(OutDir)PixiEditor.Desktop.exe" DestinationFiles="$(OutDir)PixiEditor.exe" />
-    <Message Text="Renamed build executable file." Importance="high" />
-  </Target>
-
-  <Target Name="Rename" AfterTargets="Publish">
-    <Move SourceFiles="$(PublishDir)PixiEditor.Desktop.exe" DestinationFiles="$(PublishDir)PixiEditor.exe" />
-    <Message Text="Renamed published executable file." Importance="high" />
-  </Target>
 </Project>

BIN
src/PixiEditor.Desktop/PixiEditor.icns


+ 4 - 0
src/PixiEditor.Extensions/UI/Translator.cs

@@ -215,6 +215,10 @@ public class Translator : Control
         {
             contentControl.Bind(ContentControl.ContentProperty, valueObservable);
         }
+        else if (d is NativeMenuItem nativeMenuItem)
+        {
+            nativeMenuItem.Bind(NativeMenuItem.HeaderProperty, valueObservable);
+        }
 #if DEBUG
         else
         {

+ 14 - 2
src/PixiEditor.Linux/LinuxOperatingSystem.cs

@@ -12,7 +12,9 @@ public sealed class LinuxOperatingSystem : IOperatingSystem
     public string AnalyticsName => LinuxOSInformation.FromReleaseFile().ToString();
     public IInputKeys InputKeys { get; }
     public IProcessUtility ProcessUtility { get; }
-    
+
+    public string ExecutableExtension { get; } = string.Empty;
+
     public void OpenUri(string uri)
     {
         throw new NotImplementedException();
@@ -23,11 +25,21 @@ public sealed class LinuxOperatingSystem : IOperatingSystem
         throw new NotImplementedException();
     }
 
-    public bool HandleNewInstance(Dispatcher? dispatcher, Action<string> openInExistingAction, IApplicationLifetime lifetime)
+    public bool HandleNewInstance(Dispatcher? dispatcher, Action<string, bool> openInExistingAction, IApplicationLifetime lifetime)
     {
         return true;
     }
 
+    public void HandleActivatedWithFile(FileActivatedEventArgs fileActivatedEventArgs)
+    {
+        throw new NotImplementedException();
+    }
+
+    public void HandleActivatedWithUri(ProtocolActivatedEventArgs openUriEventArgs)
+    {
+        throw new NotImplementedException();
+    }
+
     class LinuxOSInformation
     {
         const string FilePath = "/etc/os-release";

BIN
src/PixiEditor.MSIX/Images/LargeTile.scale-100.png


BIN
src/PixiEditor.MSIX/Images/LargeTile.scale-125.png


BIN
src/PixiEditor.MSIX/Images/LargeTile.scale-150.png


BIN
src/PixiEditor.MSIX/Images/LargeTile.scale-200.png


BIN
src/PixiEditor.MSIX/Images/LargeTile.scale-400.png


BIN
src/PixiEditor.MSIX/Images/LockScreenLogo.scale-200.png


BIN
src/PixiEditor.MSIX/Images/SmallTile.scale-100.png


BIN
src/PixiEditor.MSIX/Images/SmallTile.scale-125.png


BIN
src/PixiEditor.MSIX/Images/SmallTile.scale-150.png


BIN
src/PixiEditor.MSIX/Images/SmallTile.scale-200.png


BIN
src/PixiEditor.MSIX/Images/SmallTile.scale-400.png


BIN
src/PixiEditor.MSIX/Images/SplashScreen.scale-100.png


BIN
src/PixiEditor.MSIX/Images/SplashScreen.scale-125.png


BIN
src/PixiEditor.MSIX/Images/SplashScreen.scale-150.png


BIN
src/PixiEditor.MSIX/Images/SplashScreen.scale-200.png


BIN
src/PixiEditor.MSIX/Images/SplashScreen.scale-400.png


BIN
src/PixiEditor.MSIX/Images/Square150x150Logo.scale-100.png


BIN
src/PixiEditor.MSIX/Images/Square150x150Logo.scale-125.png


BIN
src/PixiEditor.MSIX/Images/Square150x150Logo.scale-150.png


BIN
src/PixiEditor.MSIX/Images/Square150x150Logo.scale-200.png


BIN
src/PixiEditor.MSIX/Images/Square150x150Logo.scale-400.png


BIN
src/PixiEditor.MSIX/Images/Square44x44Logo.altform-lightunplated_targetsize-16.png


BIN
src/PixiEditor.MSIX/Images/Square44x44Logo.altform-lightunplated_targetsize-24.png


BIN
src/PixiEditor.MSIX/Images/Square44x44Logo.altform-lightunplated_targetsize-256.png


BIN
src/PixiEditor.MSIX/Images/Square44x44Logo.altform-lightunplated_targetsize-32.png


BIN
src/PixiEditor.MSIX/Images/Square44x44Logo.altform-lightunplated_targetsize-48.png


BIN
src/PixiEditor.MSIX/Images/Square44x44Logo.altform-unplated_targetsize-16.png


BIN
src/PixiEditor.MSIX/Images/Square44x44Logo.altform-unplated_targetsize-256.png


BIN
src/PixiEditor.MSIX/Images/Square44x44Logo.altform-unplated_targetsize-32.png


BIN
src/PixiEditor.MSIX/Images/Square44x44Logo.altform-unplated_targetsize-48.png


BIN
src/PixiEditor.MSIX/Images/Square44x44Logo.scale-100.png


BIN
src/PixiEditor.MSIX/Images/Square44x44Logo.scale-125.png


BIN
src/PixiEditor.MSIX/Images/Square44x44Logo.scale-150.png


BIN
src/PixiEditor.MSIX/Images/Square44x44Logo.scale-200.png


BIN
src/PixiEditor.MSIX/Images/Square44x44Logo.scale-400.png


BIN
src/PixiEditor.MSIX/Images/Square44x44Logo.targetsize-16.png


BIN
src/PixiEditor.MSIX/Images/Square44x44Logo.targetsize-24.png


BIN
src/PixiEditor.MSIX/Images/Square44x44Logo.targetsize-24_altform-unplated.png


BIN
src/PixiEditor.MSIX/Images/Square44x44Logo.targetsize-256.png


BIN
src/PixiEditor.MSIX/Images/Square44x44Logo.targetsize-32.png


BIN
src/PixiEditor.MSIX/Images/Square44x44Logo.targetsize-48.png


BIN
src/PixiEditor.MSIX/Images/StoreLogo.backup.png


BIN
src/PixiEditor.MSIX/Images/StoreLogo.scale-100.png


BIN
src/PixiEditor.MSIX/Images/StoreLogo.scale-125.png


BIN
src/PixiEditor.MSIX/Images/StoreLogo.scale-150.png


BIN
src/PixiEditor.MSIX/Images/StoreLogo.scale-200.png


BIN
src/PixiEditor.MSIX/Images/StoreLogo.scale-400.png


BIN
src/PixiEditor.MSIX/Images/Wide310x150Logo.scale-100.png


BIN
src/PixiEditor.MSIX/Images/Wide310x150Logo.scale-125.png


BIN
src/PixiEditor.MSIX/Images/Wide310x150Logo.scale-150.png


BIN
src/PixiEditor.MSIX/Images/Wide310x150Logo.scale-200.png


BIN
src/PixiEditor.MSIX/Images/Wide310x150Logo.scale-400.png


+ 0 - 66
src/PixiEditor.MSIX/Package.appxmanifest

@@ -1,66 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-
-<Package
-  xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
-  xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
-  xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
-  IgnorableNamespaces="uap rescap">
-
-  <Identity
-    Name="56069PixiEditorOrganizati.PixiEditor"
-    Publisher="CN=0AFA75AD-56A3-481D-B5E4-D3C6274DD38A"
-    Version="1.2.5.0" />
-
-  <Properties>
-    <DisplayName>PixiEditor</DisplayName>
-    <PublisherDisplayName>PixiEditor Organization</PublisherDisplayName>
-    <Logo>Images\StoreLogo.png</Logo>
-  </Properties>
-
-  <Dependencies>
-    <TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.0.0" MaxVersionTested="10.0.0.0" />
-    <TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.14393.0" MaxVersionTested="10.0.14393.0" />
-  </Dependencies>
-
-  <Resources>
-    <Resource Language="x-generate"/>
-  </Resources>
-
-  <Applications>
-    <Application Id="App"
-      Executable="$targetnametoken$.exe"
-      EntryPoint="$targetentrypoint$">
-      <uap:VisualElements
-        DisplayName="PixiEditor"
-        Description="PixiEditor is pixel-art editing software. Create beautiful sprites for your games, animations (coming soon!), and edit images. All packed in an eye-friendly dark theme."
-        BackgroundColor="transparent"
-        Square150x150Logo="Images\Square150x150Logo.png"
-        Square44x44Logo="Images\Square44x44Logo.png">
-        <uap:DefaultTile Wide310x150Logo="Images\Wide310x150Logo.png"  Square71x71Logo="Images\SmallTile.png" Square310x310Logo="Images\LargeTile.png" ShortName="PixiEditor">
-          <uap:ShowNameOnTiles>
-            <uap:ShowOn Tile="square150x150Logo"/>
-            <uap:ShowOn Tile="wide310x150Logo"/>
-            <uap:ShowOn Tile="square310x310Logo"/>
-          </uap:ShowNameOnTiles>
-        </uap:DefaultTile >
-        <uap:SplashScreen Image="Images\SplashScreen.png" />
-      </uap:VisualElements>
-      <Extensions>
-        <uap:Extension Category="windows.fileTypeAssociation">
-          <uap:FileTypeAssociation Name="pixieditor.pixifile">
-            <uap:SupportedFileTypes>
-              <uap:FileType>.pixi</uap:FileType>
-            </uap:SupportedFileTypes>
-            <uap:DisplayName>PixiEditor</uap:DisplayName>
-            <uap:InfoTip>A file used to save art made in PixiEditor</uap:InfoTip>
-            <uap:EditFlags OpenIsSafe="true"/>
-          </uap:FileTypeAssociation>
-        </uap:Extension>
-      </Extensions>
-    </Application>
-  </Applications>
-
-  <Capabilities>
-    <rescap:Capability Name="runFullTrust" />
-  </Capabilities>
-</Package>

+ 0 - 197
src/PixiEditor.MSIX/PixiEditor.MSIX.wapproj

@@ -1,197 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
-  <PropertyGroup Condition="'$(VisualStudioVersion)' == '' or '$(VisualStudioVersion)' &lt; '15.0'">
-    <VisualStudioVersion>15.0</VisualStudioVersion>
-  </PropertyGroup>
-  <ItemGroup Label="ProjectConfigurations">
-    <ProjectConfiguration Include="Debug|x86">
-      <Configuration>Debug</Configuration>
-      <Platform>x86</Platform>
-    </ProjectConfiguration>
-    <ProjectConfiguration Include="Release|x86">
-      <Configuration>Release</Configuration>
-      <Platform>x86</Platform>
-    </ProjectConfiguration>
-    <ProjectConfiguration Include="Debug|x64">
-      <Configuration>Debug</Configuration>
-      <Platform>x64</Platform>
-    </ProjectConfiguration>
-    <ProjectConfiguration Include="Release|x64">
-      <Configuration>Release</Configuration>
-      <Platform>x64</Platform>
-    </ProjectConfiguration>
-    <ProjectConfiguration Include="Debug|ARM">
-      <Configuration>Debug</Configuration>
-      <Platform>ARM</Platform>
-    </ProjectConfiguration>
-    <ProjectConfiguration Include="Release|ARM">
-      <Configuration>Release</Configuration>
-      <Platform>ARM</Platform>
-    </ProjectConfiguration>
-    <ProjectConfiguration Include="Debug|ARM64">
-      <Configuration>Debug</Configuration>
-      <Platform>ARM64</Platform>
-    </ProjectConfiguration>
-    <ProjectConfiguration Include="Release|ARM64">
-      <Configuration>Release</Configuration>
-      <Platform>ARM64</Platform>
-    </ProjectConfiguration>
-    <ProjectConfiguration Include="Debug|AnyCPU">
-      <Configuration>Debug</Configuration>
-      <Platform>AnyCPU</Platform>
-    </ProjectConfiguration>
-    <ProjectConfiguration Include="Release|AnyCPU">
-      <Configuration>Release</Configuration>
-      <Platform>AnyCPU</Platform>
-    </ProjectConfiguration>
-  </ItemGroup>
-  <PropertyGroup>
-    <WapProjPath Condition="'$(WapProjPath)'==''">$(MSBuildExtensionsPath)\Microsoft\DesktopBridge\</WapProjPath>
-  </PropertyGroup>
-  <Import Project="$(WapProjPath)\Microsoft.DesktopBridge.props" />
-  <PropertyGroup>
-    <ProjectGuid>1f97f972-f9e8-4f35-a8b5-3f71408d2230</ProjectGuid>
-    <TargetPlatformVersion>10.0.22621.0</TargetPlatformVersion>
-    <TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
-    <DefaultLanguage>en-US</DefaultLanguage>
-    <AppxPackageSigningEnabled>False</AppxPackageSigningEnabled>
-    <EntryPointProjectUniqueName>..\PixiEditor\PixiEditor.csproj</EntryPointProjectUniqueName>
-    <GenerateAppInstallerFile>False</GenerateAppInstallerFile>
-    <AppxPackageSigningTimestampDigestAlgorithm>SHA256</AppxPackageSigningTimestampDigestAlgorithm>
-    <AppxAutoIncrementPackageRevision>False</AppxAutoIncrementPackageRevision>
-    <GenerateTestArtifacts>True</GenerateTestArtifacts>
-    <AppxBundlePlatforms>x86|x64</AppxBundlePlatforms>
-    <GenerateTemporaryStoreCertificate>True</GenerateTemporaryStoreCertificate>
-    <HoursBetweenUpdateChecks>0</HoursBetweenUpdateChecks>
-  </PropertyGroup>
-  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
-    <OutputPath>bin\Debug\</OutputPath>
-    <AppxBundle>Always</AppxBundle>
-  </PropertyGroup>
-  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x64' ">
-    <OutputPath>bin\x64\Debug\</OutputPath>
-    <PlatformTarget>x64</PlatformTarget>
-    <AppxBundle>Always</AppxBundle>
-  </PropertyGroup>
-  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x86' ">
-    <OutputPath>bin\x86\Debug\</OutputPath>
-    <PlatformTarget>x86</PlatformTarget>
-    <AppxBundle>Always</AppxBundle>
-  </PropertyGroup>
-  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
-    <OutputPath>bin\Release\</OutputPath>
-    <AppxBundle>Always</AppxBundle>
-  </PropertyGroup>
-  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x64' ">
-    <OutputPath>bin\x64\Release\</OutputPath>
-    <PlatformTarget>x64</PlatformTarget>
-    <AppxBundle>Always</AppxBundle>
-  </PropertyGroup>
-  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' ">
-    <OutputPath>bin\x86\Release\</OutputPath>
-    <PlatformTarget>x86</PlatformTarget>
-    <AppxBundle>Always</AppxBundle>
-  </PropertyGroup>
-  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Steam|x86' ">
-    <OutputPath>bin\x86\Steam\</OutputPath>
-    <PlatformTarget>x86</PlatformTarget>
-  </PropertyGroup>
-  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Steam|AnyCPU' ">
-    <OutputPath>bin\Steam\</OutputPath>
-  </PropertyGroup>
-  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Steam|x64' ">
-    <OutputPath>bin\x64\Steam\</OutputPath>
-    <PlatformTarget>x64</PlatformTarget>
-  </PropertyGroup>
-  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM'">
-    <AppxBundle>Always</AppxBundle>
-  </PropertyGroup>
-  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
-    <AppxBundle>Always</AppxBundle>
-  </PropertyGroup>
-  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">
-    <AppxBundle>Always</AppxBundle>
-  </PropertyGroup>
-  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM'">
-    <AppxBundle>Always</AppxBundle>
-  </PropertyGroup>
-  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'DevRelease|x86' ">
-    <OutputPath>bin\x86\DevRelease\</OutputPath>
-    <PlatformTarget>x86</PlatformTarget>
-  </PropertyGroup>
-  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'DevRelease|x64' ">
-    <OutputPath>bin\x64\DevRelease\</OutputPath>
-    <PlatformTarget>x64</PlatformTarget>
-  </PropertyGroup>
-  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'DevRelease|AnyCPU' ">
-    <OutputPath>bin\DevRelease\</OutputPath>
-  </PropertyGroup>
-  <ItemGroup>
-    <AppxManifest Include="Package.appxmanifest">
-      <SubType>Designer</SubType>
-    </AppxManifest>
-  </ItemGroup>
-  <ItemGroup>
-    <Content Include="Images\LargeTile.scale-100.png" />
-    <Content Include="Images\LargeTile.scale-125.png" />
-    <Content Include="Images\LargeTile.scale-150.png" />
-    <Content Include="Images\LargeTile.scale-200.png" />
-    <Content Include="Images\LargeTile.scale-400.png" />
-    <Content Include="Images\SmallTile.scale-100.png" />
-    <Content Include="Images\SmallTile.scale-125.png" />
-    <Content Include="Images\SmallTile.scale-150.png" />
-    <Content Include="Images\SmallTile.scale-200.png" />
-    <Content Include="Images\SmallTile.scale-400.png" />
-    <Content Include="Images\SplashScreen.scale-100.png" />
-    <Content Include="Images\SplashScreen.scale-125.png" />
-    <Content Include="Images\SplashScreen.scale-150.png" />
-    <Content Include="Images\SplashScreen.scale-200.png" />
-    <Content Include="Images\LockScreenLogo.scale-200.png" />
-    <Content Include="Images\SplashScreen.scale-400.png" />
-    <Content Include="Images\Square150x150Logo.scale-100.png" />
-    <Content Include="Images\Square150x150Logo.scale-125.png" />
-    <Content Include="Images\Square150x150Logo.scale-150.png" />
-    <Content Include="Images\Square150x150Logo.scale-200.png" />
-    <Content Include="Images\Square150x150Logo.scale-400.png" />
-    <Content Include="Images\Square44x44Logo.altform-lightunplated_targetsize-16.png" />
-    <Content Include="Images\Square44x44Logo.altform-lightunplated_targetsize-24.png" />
-    <Content Include="Images\Square44x44Logo.altform-lightunplated_targetsize-256.png" />
-    <Content Include="Images\Square44x44Logo.altform-lightunplated_targetsize-32.png" />
-    <Content Include="Images\Square44x44Logo.altform-lightunplated_targetsize-48.png" />
-    <Content Include="Images\Square44x44Logo.altform-unplated_targetsize-16.png" />
-    <Content Include="Images\Square44x44Logo.altform-unplated_targetsize-256.png" />
-    <Content Include="Images\Square44x44Logo.altform-unplated_targetsize-32.png" />
-    <Content Include="Images\Square44x44Logo.altform-unplated_targetsize-48.png" />
-    <Content Include="Images\Square44x44Logo.scale-100.png" />
-    <Content Include="Images\Square44x44Logo.scale-125.png" />
-    <Content Include="Images\Square44x44Logo.scale-150.png" />
-    <Content Include="Images\Square44x44Logo.scale-200.png" />
-    <Content Include="Images\Square44x44Logo.scale-400.png" />
-    <Content Include="Images\Square44x44Logo.targetsize-16.png" />
-    <Content Include="Images\Square44x44Logo.targetsize-24.png" />
-    <Content Include="Images\Square44x44Logo.targetsize-24_altform-unplated.png" />
-    <Content Include="Images\Square44x44Logo.targetsize-256.png" />
-    <Content Include="Images\Square44x44Logo.targetsize-32.png" />
-    <Content Include="Images\Square44x44Logo.targetsize-48.png" />
-    <Content Include="Images\StoreLogo.scale-100.png" />
-    <Content Include="Images\StoreLogo.scale-125.png" />
-    <Content Include="Images\StoreLogo.scale-150.png" />
-    <Content Include="Images\StoreLogo.scale-200.png" />
-    <Content Include="Images\StoreLogo.scale-400.png" />
-    <Content Include="Images\Wide310x150Logo.scale-100.png" />
-    <Content Include="Images\Wide310x150Logo.scale-125.png" />
-    <Content Include="Images\Wide310x150Logo.scale-150.png" />
-    <Content Include="Images\Wide310x150Logo.scale-200.png" />
-    <Content Include="Images\Wide310x150Logo.scale-400.png" />
-    <None Include="Package.StoreAssociation.xml" />
-  </ItemGroup>
-  <Import Project="$(WapProjPath)\Microsoft.DesktopBridge.targets" />
-  <ItemGroup>
-    <PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.19041.1" PrivateAssets="all" />
-  </ItemGroup>
-  <ItemGroup>
-    <ProjectReference Include="..\PixiEditor\PixiEditor.csproj">
-      <SkipGetTargetFrameworkProperties>True</SkipGetTargetFrameworkProperties>
-    </ProjectReference>
-  </ItemGroup>
-</Project>

+ 49 - 6
src/PixiEditor.MacOs/MacOperatingSystem.cs

@@ -1,4 +1,5 @@
-using Avalonia.Controls.ApplicationLifetimes;
+using System.Text;
+using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Threading;
 using PixiEditor.OperatingSystem;
 
@@ -10,20 +11,62 @@ public sealed class MacOperatingSystem : IOperatingSystem
 
     public string AnalyticsId => "macOS";
     
-    public IInputKeys InputKeys { get; }
-    public IProcessUtility ProcessUtility { get; }
+    public IInputKeys InputKeys { get; } = new MacOsInputKeys();
+    public IProcessUtility ProcessUtility { get; } = new MacOsProcessUtility();
+
+    private List<Uri> activationUris;
+
+    public string ExecutableExtension { get; } = string.Empty;
+
     public void OpenUri(string uri)
     {
-        throw new NotImplementedException();
+        ProcessUtility.ShellExecute(uri);
     }
 
     public void OpenFolder(string path)
     {
-        throw new NotImplementedException();
+        ProcessUtility.ShellExecute(Path.GetDirectoryName(path));
     }
 
-    public bool HandleNewInstance(Dispatcher? dispatcher, Action<string> openInExistingAction, IApplicationLifetime lifetime)
+    public bool HandleNewInstance(Dispatcher? dispatcher, Action<string, bool> openInExistingAction, IApplicationLifetime lifetime)
     {
+        StringBuilder args = new StringBuilder();
+        
+        if(activationUris != null)
+        {
+            foreach (var uri in activationUris)
+            {
+                args.Append('"');
+                args.Append(uri.AbsolutePath);
+                args.Append('"');
+                args.Append(' ');
+            }
+        }
+        
+        dispatcher.Invoke(() => openInExistingAction(args.ToString(), true));
         return true;
     }
+
+    public void HandleActivatedWithFile(FileActivatedEventArgs fileActivatedEventArgs)
+    {
+        if(activationUris == null)
+        {
+            activationUris = [];
+        }
+        
+        foreach (var file in fileActivatedEventArgs.Files)
+        {
+           activationUris.Add(file.Path);
+        }
+    }
+
+    public void HandleActivatedWithUri(ProtocolActivatedEventArgs openUriEventArgs)
+    {
+        if(activationUris == null)
+        {
+            activationUris = [];
+        }
+        
+        activationUris.Add(openUriEventArgs.Uri);
+    }
 }

+ 164 - 0
src/PixiEditor.MacOs/MacOsInputKeys.cs

@@ -0,0 +1,164 @@
+using Avalonia.Input;
+using PixiEditor.OperatingSystem;
+
+namespace PixiEditor.MacOs;
+
+public class MacOsInputKeys : IInputKeys
+{
+    public string GetKeyboardKey(Key key, bool forceInvariant = false)
+    {
+        switch (key)
+        {
+            case Key.LWin: return "\u2318";
+            case Key.RWin: return "\u2318";
+            case Key.LeftCtrl: return "\u2303";
+            case Key.RightCtrl: return "\u2303";
+            case Key.LeftAlt: return "\u2325";
+            case Key.RightAlt: return "\u2325";
+            case Key.LeftShift: return "\u21E7";
+            case Key.RightShift: return "\u21E7";
+            case Key.CapsLock: return "\u21EA";
+            case Key.Escape: return "\u238B";
+            case Key.Return: return "\u23CE";
+            case Key.Back: return "\u232B";
+            case Key.Tab: return "\u21E5";
+        }
+
+        if (key == Key.None) return string.Empty;
+
+        ushort? virtualKeyCode = GetVirtualKeyCode(key);
+        if (virtualKeyCode == null) return string.Empty;
+
+        string result = MacOsInterop.GetSymbolFromKey(virtualKeyCode.Value, 0).ToUpper();
+        if (result.Length == 1 && char.IsControl(result[0])) return key.ToString();
+        
+        return result;
+    }
+
+    public bool ModifierUsesSymbol(KeyModifiers modifier)
+    {
+        return true;
+    }
+
+    private ushort? GetVirtualKeyCode(Key key)
+    {
+        return key switch
+        {
+            Key.A => 0x00,
+            Key.S => 0x01,
+            Key.D => 0x02,
+            Key.F => 0x03,
+            Key.H => 0x04,
+            Key.G => 0x05,
+            Key.Z => 0x06,
+            Key.X => 0x07,
+            Key.C => 0x08,
+            Key.V => 0x09,
+            Key.B => 0x0B,
+            Key.Q => 0x0C,
+            Key.W => 0x0D,
+            Key.E => 0x0E,
+            Key.R => 0x0F,
+            Key.Y => 0x10,
+            Key.T => 0x11,
+            Key.D1 => 0x12,
+            Key.D2 => 0x13,
+            Key.D3 => 0x14,
+            Key.D4 => 0x15,
+            Key.D6 => 0x16,
+            Key.D5 => 0x17,
+            Key.OemPlus => 0x18, // '=',
+            Key.D9 => 0x19,
+            Key.D7 => 0x1A,
+            Key.OemMinus => 0x1B, // '-'
+            Key.D8 => 0x1C,
+            Key.D0 => 0x1D,
+            Key.OemCloseBrackets => 0x1E, // ']'
+            Key.O => 0x1F,
+            Key.U => 0x20,
+            Key.OemOpenBrackets => 0x21, // '['
+            Key.I => 0x22,
+            Key.P => 0x23,
+            Key.Enter => 0x24, // Return
+            Key.L => 0x25,
+            Key.J => 0x26,
+            Key.OemQuotes => 0x27, // "'"
+            Key.K => 0x28,
+            Key.OemSemicolon => 0x29, // ';'
+            Key.OemBackslash => 0x2A, // '\'
+            Key.OemComma => 0x2B, // ','
+            Key.OemQuestion => 0x2C, // '/'
+            Key.N => 0x2D,
+            Key.M => 0x2E,
+            Key.OemPeriod => 0x2F, // '.'
+            Key.Tab => 0x30,
+            Key.Space => 0x31,
+            Key.OemTilde => 0x32 // '~'
+            ,
+            Key.Back => 0x33 // Delete
+            ,
+            Key.Escape => 0x35,
+            Key.LWin => 0x37 // Cmd (Apple)
+            ,
+            Key.LeftShift => 0x38,
+            Key.CapsLock => 0x39,
+            Key.LeftAlt => 0x3A // Option
+            ,
+            Key.LeftCtrl => 0x3B // Control
+            ,
+            Key.RightShift => 0x3C,
+            Key.RightAlt => 0x3D // Right Option
+            ,
+            Key.RightCtrl => 0x3E // Right Control
+            ,
+            Key.F17 => 0x40,
+            Key.VolumeUp => 0x48,
+            Key.VolumeDown => 0x49,
+            Key.VolumeMute => 0x4A,
+            Key.F18 => 0x4F,
+            Key.F19 => 0x50,
+            Key.NumPad0 => 0x52,
+            Key.NumPad1 => 0x53,
+            Key.NumPad2 => 0x54,
+            Key.NumPad3 => 0x55,
+            Key.NumPad4 => 0x56,
+            Key.NumPad5 => 0x57,
+            Key.NumPad6 => 0x58,
+            Key.NumPad7 => 0x59,
+            Key.NumPad8 => 0x5B,
+            Key.NumPad9 => 0x5C,
+            Key.F5 => 0x60,
+            Key.F6 => 0x61,
+            Key.F7 => 0x62,
+            Key.F3 => 0x63,
+            Key.F8 => 0x64,
+            Key.F9 => 0x65,
+            Key.F11 => 0x67,
+            Key.F13 => 0x69,
+            Key.F16 => 0x6A,
+            Key.F14 => 0x6B,
+            Key.F10 => 0x6D,
+            Key.F12 => 0x6F,
+            Key.F15 => 0x71,
+            Key.Help => 0x72,
+            Key.Home => 0x73,
+            Key.PageUp => 0x74,
+            Key.Delete => 0x75 // Below the Help key
+            ,
+            Key.F4 => 0x76,
+            Key.End => 0x77,
+            Key.F2 => 0x78,
+            Key.PageDown => 0x79,
+            Key.F1 => 0x7A,
+            Key.Left => 0x7B // Left Arrow
+            ,
+            Key.Right => 0x7C // Right Arrow
+            ,
+            Key.Down => 0x7D // Down Arrow
+            ,
+            Key.Up => 0x7E // Up Arrow
+            ,
+            _ => null
+        };
+    }
+}

+ 72 - 0
src/PixiEditor.MacOs/MacOsInterop.cs

@@ -0,0 +1,72 @@
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace PixiEditor.MacOs;
+
+internal static class MacOsInterop
+{
+    private const string CoreFoundationLib = "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation";
+    private const string CarbonLib = "/System/Library/Frameworks/Carbon.framework/Carbon";
+    private static readonly IntPtr kTISPropertyInputSourceID = CFStringCreate("TISPropertyUnicodeKeyLayoutData");
+
+    [DllImport(CarbonLib)]
+    private static extern IntPtr TISCopyCurrentKeyboardLayoutInputSource();
+    
+    [DllImport(CarbonLib)]
+    private static extern IntPtr TISGetInputSourceProperty(IntPtr inputSource, IntPtr propertyKey);
+
+    [DllImport(CoreFoundationLib)]
+    private static extern IntPtr CFDataGetBytePtr(IntPtr data);
+
+    [DllImport(CarbonLib)]
+    private static extern short UCKeyTranslate(
+        IntPtr keyLayout,
+        ushort virtualKeyCode,
+        ushort keyAction,
+        UInt32 modifierKeyState,
+        UInt32 keyboardType,
+        uint keyTranslateOptions,
+        ref uint deadKeyState,
+        int maxLength,
+        out int actualLength,
+        ushort[] unicodeString);
+
+    public static string? GetSymbolFromKey(ushort virtualKeyCode, uint modifiers)
+    {
+        IntPtr layout = TISCopyCurrentKeyboardLayoutInputSource();
+        if (layout == IntPtr.Zero)
+            return null;
+
+        
+        IntPtr layoutData = TISGetInputSourceProperty(layout, kTISPropertyInputSourceID);
+        if (layoutData == IntPtr.Zero)
+            return null;
+
+        IntPtr keyLayoutPtr = CFDataGetBytePtr(layoutData);
+        if (keyLayoutPtr == IntPtr.Zero)
+            return null;
+
+        // Translate the key code into a symbol
+        var state = 0u;
+        var output = new ushort[255];
+        UCKeyTranslate(keyLayoutPtr, virtualKeyCode, 3 /* kUCKeyActionDisplay */, modifiers, 0, 0, ref state, output.Length, out var actualLength, output);
+
+        return actualLength > 0 ? UshortArrayToString(output, (int)actualLength) : null;
+    }
+    
+    private static string UshortArrayToString(ushort[] array, int length)
+    {
+        // Convert ushort[] to byte[] for Encoding.Unicode.GetString
+        byte[] bytes = new byte[length * 2];
+        Buffer.BlockCopy(array, 0, bytes, 0, length * 2);
+        return Encoding.Unicode.GetString(bytes, 0, length * 2);
+    }
+    
+    [DllImport(CoreFoundationLib)]
+    private static extern IntPtr CFStringCreateWithCString(IntPtr allocator, string cStr, UInt32 encoding);
+
+    private static IntPtr CFStringCreate(string str)
+    {
+        return CFStringCreateWithCString(IntPtr.Zero, str, 134217984);
+    }
+}

+ 79 - 0
src/PixiEditor.MacOs/MacOsProcessUtility.cs

@@ -0,0 +1,79 @@
+using System.Diagnostics;
+using PixiEditor.OperatingSystem;
+
+namespace PixiEditor.MacOs;
+
+internal class MacOsProcessUtility : IProcessUtility
+{
+    public Process RunAsAdmin(string path)
+    {
+        return RunAsAdmin(path, true);
+    }
+
+    public Process RunAsAdmin(string path, bool createWindow)
+    {
+        ProcessStartInfo startInfo = new ProcessStartInfo
+        {
+            FileName = path,
+            Verb = "runas",
+            UseShellExecute = createWindow,
+            CreateNoWindow = !createWindow,
+            RedirectStandardOutput = !createWindow,
+            RedirectStandardError = !createWindow,
+            WindowStyle = createWindow ? ProcessWindowStyle.Normal : ProcessWindowStyle.Hidden
+        };
+
+        Process p = new Process();
+        p.StartInfo = startInfo;
+
+        p.Start();
+        return p;
+    }
+
+    public bool IsRunningAsAdministrator()
+    {
+        return Environment.IsPrivilegedProcess;
+    }
+
+    public static Process ShellExecute(string url)
+    {
+        return Process.Start(new ProcessStartInfo { FileName = url, UseShellExecute = true, });
+    }
+
+    Process IProcessUtility.ShellExecute(string url)
+    {
+        return ShellExecute(url);
+    }
+
+    Process IProcessUtility.ShellExecute(string url, string args)
+    {
+        return ShellExecute(url, args);
+    }
+
+    Process IProcessUtility.Execute(string path, string args)
+    {
+        return Execute(path, args);
+    }
+
+    public static Process Execute(string path, string args)
+    {
+        return Process.Start(new ProcessStartInfo
+        {
+            FileName = path,
+            Arguments = args,
+            UseShellExecute = false,
+            RedirectStandardOutput = true,
+            RedirectStandardError = true,
+        });
+    }
+
+    public static Process ShellExecute(string url, string args)
+    {
+        return 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);
+}

+ 13 - 0
src/PixiEditor.MacOs/todo.md

@@ -0,0 +1,13 @@
+- [x] Input keys
+- [x] Default shortcuts
+- [x] Package - builder
+- [ ] FFmpeg
+- [x] Single instance
+- [ ] File associations (pixi, other formats, lospec protocol)
+- [ ] Autoupdates
+- [x] Process handling
+- [x] Check if extensions work
+- [x] Native menu
+- [x] Crash dialog
+- [ ] Deploy pipelines
+- [x] OpenGL fixes

+ 2 - 0
src/PixiEditor.OperatingSystem/IInputKeys.cs

@@ -8,4 +8,6 @@ public interface IInputKeys
     ///     Returns the character of the <paramref name="key"/> mapped to the users keyboard layout
     /// </summary>
     public string GetKeyboardKey(Key key, bool forceInvariant = false);
+
+    public bool ModifierUsesSymbol(KeyModifiers modifier);
 }

+ 9 - 1
src/PixiEditor.OperatingSystem/IOperatingSystem.cs

@@ -13,6 +13,12 @@ public interface IOperatingSystem
 
     public IInputKeys InputKeys { get; }
     public IProcessUtility ProcessUtility { get; }
+    public bool IsMacOs => Name == "MacOS";
+    public bool IsWindows => Name == "Windows";
+    public bool IsLinux => Name == "Linux";
+    public bool IsMiscellaneous => !IsMacOs && !IsWindows && !IsLinux;
+    public string ExecutableExtension { get; }
+    public bool IsUnix => IsMacOs || IsLinux;
 
     public static void RegisterOS(IOperatingSystem operatingSystem)
     {
@@ -26,5 +32,7 @@ public interface IOperatingSystem
 
     public void OpenUri(string uri);
     public void OpenFolder(string path);
-    public bool HandleNewInstance(Dispatcher? dispatcher, Action<string> openInExistingAction, IApplicationLifetime lifetime);
+    public bool HandleNewInstance(Dispatcher? dispatcher, Action<string, bool> openInExistingAction, IApplicationLifetime lifetime);
+    public void HandleActivatedWithFile(FileActivatedEventArgs fileActivatedEventArgs);
+    public void HandleActivatedWithUri(ProtocolActivatedEventArgs openUriEventArgs);
 }

+ 3 - 1
src/PixiEditor.OperatingSystem/IProcessUtility.cs

@@ -7,5 +7,7 @@ public interface IProcessUtility
     public Process RunAsAdmin(string path);
     public Process RunAsAdmin(string path, bool createWindow);
     public bool IsRunningAsAdministrator();
-    public void ShellExecute(string toExecute);
+    public Process ShellExecute(string toExecute);
+    public Process ShellExecute(string toExecute, string args);
+    public Process Execute(string path, string args);
 }

+ 0 - 42
src/PixiEditor.UI.Common/Controls/TextBlock.axaml

@@ -1,42 +0,0 @@
-<Styles xmlns="https://github.com/avaloniaui"
-        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
-    <Design.PreviewWith>
-        <Border Padding="20">
-            <StackPanel Orientation="Vertical">
-                <TextBlock Classes="h1" Text="h1"/>
-                <TextBlock Classes="h2" Text="h2"/>
-                <TextBlock Classes="h3" Text="h3"/>
-                <TextBlock Classes="h4" Text="h4"/>
-            </StackPanel>
-        </Border>
-    </Design.PreviewWith>
-
-    <Style Selector="TextBlock">
-        <Setter Property="FontSize" Value="16"/>
-    </Style>
-
-    <Style Selector="TextBlock.h1">
-        <Setter Property="FontSize" Value="40"/>
-        <Setter Property="FontWeight" Value="Bold"/>
-    </Style>
-
-    <Style Selector="TextBlock.h2">
-        <Setter Property="FontSize" Value="32"/>
-        <Setter Property="FontWeight" Value="Bold"/>
-    </Style>
-
-    <Style Selector="TextBlock.h3">
-        <Setter Property="FontSize" Value="24"/>
-        <Setter Property="FontWeight" Value="Bold"/>
-    </Style>
-
-    <Style Selector="TextBlock.h4">
-        <Setter Property="FontSize" Value="20"/>
-        <Setter Property="FontWeight" Value="Bold"/>
-    </Style>
-
-    <Style Selector="TextBlock.h5">
-        <Setter Property="FontSize" Value="16"/>
-        <Setter Property="FontWeight" Value="Bold"/>
-    </Style>
-</Styles>

+ 25 - 2
src/PixiEditor.UI.Common/Controls/Window.axaml

@@ -1,5 +1,6 @@
 <ResourceDictionary xmlns="https://github.com/avaloniaui"
                     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+                    xmlns:platform="clr-namespace:Avalonia.Platform;assembly=Avalonia.Controls"
                     x:ClassModifier="internal">
     <ControlTheme x:Key="{x:Type Window}"
                   TargetType="Window">
@@ -8,7 +9,18 @@
         <Setter Property="Foreground" Value="{DynamicResource ThemeForegroundBrush}" />
         <Setter Property="FontSize" Value="{DynamicResource FontSizeNormal}" />
         <Setter Property="WindowStartupLocation" Value="CenterScreen" />
-        <Setter Property="ExtendClientAreaChromeHints" Value="NoChrome" />
+        <Setter Property="ExtendClientAreaChromeHints">
+            <Setter.Value>
+                <OnPlatform>
+                    <OnPlatform.Default>
+                        <platform:ExtendClientAreaChromeHints>NoChrome</platform:ExtendClientAreaChromeHints>
+                    </OnPlatform.Default>
+                    <OnPlatform.macOS>
+                        <platform:ExtendClientAreaChromeHints>Default,NoChrome,OSXThickTitleBar</platform:ExtendClientAreaChromeHints>
+                    </OnPlatform.macOS>
+                </OnPlatform>
+            </Setter.Value>
+        </Setter>
         <Setter Property="ExtendClientAreaToDecorationsHint" Value="True" />
         <Setter Property="ExtendClientAreaTitleBarHeightHint" Value="36" />
         <Setter Property="Template">
@@ -35,7 +47,18 @@
             </ControlTemplate>
         </Setter>
         <Style Selector="^Window[WindowState=Maximized] /template/ ContentPresenter#PART_ContentPresenter">
-            <Setter Property="Padding" Value="8"/>
+            <Setter Property="Padding">
+                <Setter.Value>
+                    <OnPlatform>
+                        <OnPlatform.Default>
+                            <Thickness>8</Thickness>
+                        </OnPlatform.Default>
+                        <OnPlatform.macOS>
+                            <Thickness>0</Thickness>
+                        </OnPlatform.macOS>
+                    </OnPlatform>
+                </Setter.Value>
+            </Setter>
         </Style>
     </ControlTheme>
 </ResourceDictionary>

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

@@ -5,6 +5,10 @@
             <TextBlock Classes="h1"/>
         </Border>
     </Design.PreviewWith>
+    
+    <Style Selector="TextBlock">
+        <Setter Property="VerticalAlignment" Value="Center"/>
+    </Style>
 
     <Style Selector="TextBlock.h1">
         <Setter Property="FontSize" Value="{DynamicResource Header1}"/>

+ 6 - 1
src/PixiEditor.Windows/WindowsInputKeys.cs

@@ -19,7 +19,7 @@ public class WindowsInputKeys : IInputKeys
     private static nint? invariantLayout;
 
     /// <summary>
-    /// Returns the charcter of the <paramref name="key"/> mapped to the users keyboard layout
+    /// Returns the character of the <paramref name="key"/> mapped to the users keyboard layout
     /// </summary>
     public string GetKeyboardKey(Key key, bool forceInvariant = false) => key switch
     {
@@ -32,6 +32,11 @@ public class WindowsInputKeys : IInputKeys
         _ => GetMappedKey(key, forceInvariant),
     };
 
+    public bool ModifierUsesSymbol(KeyModifiers modifier)
+    {
+        return false;
+    }
+
     private static string GetMappedKey(Key key, bool forceInvariant)
     {
         int virtualKey = KeyInterop.VirtualKeyFromKey(key);

+ 9 - 3
src/PixiEditor.Windows/WindowsOperatingSystem.cs

@@ -27,6 +27,8 @@ public sealed class WindowsOperatingSystem : IOperatingSystem
         Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
         "PixiEditor", ".passedArgs");
 
+    public string ExecutableExtension { get; } = ".exe";
+
     public void OpenUri(string uri)
     {
         WindowsProcessUtility.ShellExecute(uri);
@@ -51,7 +53,7 @@ public sealed class WindowsOperatingSystem : IOperatingSystem
         WindowsProcessUtility.ShellExecuteEV(dirName);
     }
 
-    public bool HandleNewInstance(Dispatcher? dispatcher, Action<string> openInExistingAction, IApplicationLifetime lifetime)
+    public bool HandleNewInstance(Dispatcher? dispatcher, Action<string, bool> openInExistingAction, IApplicationLifetime lifetime)
     {
         bool isOwned;
         _mutex = new Mutex(true, UniqueMutexName, out isOwned);
@@ -69,7 +71,7 @@ public sealed class WindowsOperatingSystem : IOperatingSystem
                 {
                     while (_eventWaitHandle.WaitOne())
                     {
-                        dispatcher.Invoke(() => openInExistingAction(passedArgsFile));
+                        dispatcher.Invoke(() => openInExistingAction(passedArgsFile, false));
                     }
                 })
             {
@@ -89,7 +91,11 @@ public sealed class WindowsOperatingSystem : IOperatingSystem
         (lifetime as IClassicDesktopStyleApplicationLifetime)!.Shutdown();
         return false;
     }
-    
+
+    public void HandleActivatedWithFile(FileActivatedEventArgs fileActivatedEventArgs) { }
+
+    public void HandleActivatedWithUri(ProtocolActivatedEventArgs openUriEventArgs) { }
+
     private string?[] WrapSpaces(string[] args)
     {
         string?[] wrappedArgs = new string?[args.Length];

+ 29 - 6
src/PixiEditor.Windows/WindowsProcessUtility.cs

@@ -37,23 +37,46 @@ public class WindowsProcessUtility : IProcessUtility
         return new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator);
     }
 
-    public static void ShellExecute(string url)
+    public static Process ShellExecute(string url)
     {
-        Process.Start(new ProcessStartInfo
+        return Process.Start(new ProcessStartInfo
         {
             FileName = url,
             UseShellExecute = true,
         });
     }
 
-    void IProcessUtility.ShellExecute(string url)
+    Process IProcessUtility.ShellExecute(string url)
     {
-        ShellExecute(url);
+        return ShellExecute(url);
+    }
+    
+    Process IProcessUtility.ShellExecute(string url, string args)
+    {
+        return ShellExecute(url, args);
+    }
+
+
+    Process IProcessUtility.Execute(string path, string args)
+    {
+        return Execute(path, args);
+    }
+    
+    public static Process Execute(string path, string args)
+    {
+        return Process.Start(new ProcessStartInfo
+        {
+            FileName = path,
+            Arguments = args,
+            UseShellExecute = false,
+            RedirectStandardOutput = true,
+            RedirectStandardError = true,
+        });
     }
 
-    public static void ShellExecute(string url, string args)
+    public static Process ShellExecute(string url, string args)
     {
-        Process.Start(new ProcessStartInfo
+        return Process.Start(new ProcessStartInfo
         {
             FileName = url,
             Arguments = args,

+ 43 - 30
src/PixiEditor.sln

@@ -14,8 +14,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BuildConfiguration", "Build
 		stylecop.json = stylecop.json
 	EndProjectSection
 EndProject
-Project("{C7167F0D-BC9F-4E6E-AFE1-012C56B48DB5}") = "PixiEditor.MSIX", "PixiEditor.MSIX\PixiEditor.MSIX.wapproj", "{1F97F972-F9E8-4F35-A8B5-3F71408D2230}"
-EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChunkyImageLib", "ChunkyImageLib\ChunkyImageLib.csproj", "{6A9DA760-1E47-414C-B8E8-3B4927F18131}"
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChunkyImageLibVis", "ChunkyImageLibVis\ChunkyImageLibVis.csproj", "{510ED47C-2455-4DCE-A561-1074725E1236}"
@@ -28,8 +26,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PixiEditor.Zoombox", "PixiE
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PixiEditor.Gen", "PixiEditor.Gen\PixiEditor.Gen.csproj", "{1DC5B4C4-6902-4659-AE7E-17FDA0403DEB}"
 EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PixiEditor.Builder", "PixiEditor.Builder\build\PixiEditor.Builder.csproj", "{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}"
-EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PixiEditor.Platform.Steam", "PixiEditor.Platform.Steam\PixiEditor.Platform.Steam.csproj", "{9BCD0764-9C16-4A2A-B153-C676FEF38887}"
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PixiEditor.Platform", "PixiEditor.Platform\PixiEditor.Platform.csproj", "{2BDEB8C6-F22D-43EA-A309-B3387A803689}"
@@ -164,24 +160,8 @@ Global
 		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.Steam|x64.Build.0 = Release|Any CPU
 		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.DevRelease|x64.ActiveCfg = Debug|Any CPU
 		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.DevRelease|x64.Build.0 = Debug|Any CPU
-		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.Debug|x64.ActiveCfg = Debug|x64
-		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.Debug|x64.Build.0 = Debug|x64
-		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.Debug|x64.Deploy.0 = Debug|x64
-		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.DevRelease|x64.ActiveCfg = Release|x64
-		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.DevRelease|x64.Build.0 = Release|x64
-		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.DevRelease|x64.Deploy.0 = Release|x64
-		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.MSIX Debug|x64.ActiveCfg = Debug|x64
-		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.MSIX Debug|x64.Build.0 = Debug|x64
-		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.MSIX Debug|x64.Deploy.0 = Debug|x64
-		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.MSIX|x64.ActiveCfg = Debug|x64
-		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.MSIX|x64.Build.0 = Debug|x64
-		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.MSIX|x64.Deploy.0 = Debug|x64
-		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.Release|x64.ActiveCfg = Release|x64
-		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.Release|x64.Build.0 = Release|x64
-		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.Release|x64.Deploy.0 = Release|x64
-		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.Steam|x64.ActiveCfg = Debug|x64
-		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.Debug|ARM64.ActiveCfg = Debug|Any CPU
-		{1F97F972-F9E8-4F35-A8B5-3F71408D2230}.Debug|ARM64.Build.0 = Debug|Any CPU
+		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.Release|ARM64.ActiveCfg = Release|Any CPU
+		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8}.Release|ARM64.Build.0 = Release|Any CPU
 		{6A9DA760-1E47-414C-B8E8-3B4927F18131}.Debug|x64.ActiveCfg = Debug|Any CPU
 		{6A9DA760-1E47-414C-B8E8-3B4927F18131}.Debug|x64.Build.0 = Debug|Any CPU
 		{6A9DA760-1E47-414C-B8E8-3B4927F18131}.MSIX Debug|x64.ActiveCfg = Debug|Any CPU
@@ -198,6 +178,8 @@ Global
 		{6A9DA760-1E47-414C-B8E8-3B4927F18131}.DevRelease|x64.Build.0 = Debug|Any CPU
 		{6A9DA760-1E47-414C-B8E8-3B4927F18131}.DevSteam|x64.ActiveCfg = Release|Any CPU
 		{6A9DA760-1E47-414C-B8E8-3B4927F18131}.DevSteam|x64.Build.0 = Release|Any CPU
+		{6A9DA760-1E47-414C-B8E8-3B4927F18131}.Release|ARM64.ActiveCfg = Release|Any CPU
+		{6A9DA760-1E47-414C-B8E8-3B4927F18131}.Release|ARM64.Build.0 = Release|Any CPU
 		{510ED47C-2455-4DCE-A561-1074725E1236}.Debug|x64.ActiveCfg = Debug|x64
 		{510ED47C-2455-4DCE-A561-1074725E1236}.Debug|x64.Build.0 = Debug|x64
 		{510ED47C-2455-4DCE-A561-1074725E1236}.DevRelease|x64.ActiveCfg = DevRelease|x64
@@ -226,6 +208,8 @@ Global
 		{294FD171-9536-474C-A679-83F0266275FB}.Steam|x64.Build.0 = Steam|Any CPU
 		{294FD171-9536-474C-A679-83F0266275FB}.Debug|ARM64.ActiveCfg = Debug|Any CPU
 		{294FD171-9536-474C-A679-83F0266275FB}.Debug|ARM64.Build.0 = Debug|Any CPU
+		{294FD171-9536-474C-A679-83F0266275FB}.Release|ARM64.ActiveCfg = Release|Any CPU
+		{294FD171-9536-474C-A679-83F0266275FB}.Release|ARM64.Build.0 = Release|Any CPU
 		{758DF7DF-A8B1-4409-B79A-018E542B7251}.Debug|x64.ActiveCfg = Debug|Any CPU
 		{758DF7DF-A8B1-4409-B79A-018E542B7251}.Debug|x64.Build.0 = Debug|Any CPU
 		{758DF7DF-A8B1-4409-B79A-018E542B7251}.DevRelease|x64.ActiveCfg = DevRelease|Any CPU
@@ -242,6 +226,8 @@ Global
 		{758DF7DF-A8B1-4409-B79A-018E542B7251}.Debug|ARM64.Build.0 = Debug|Any CPU
 		{758DF7DF-A8B1-4409-B79A-018E542B7251}.DevSteam|x64.ActiveCfg = DevRelease|Any CPU
 		{758DF7DF-A8B1-4409-B79A-018E542B7251}.DevSteam|x64.Build.0 = DevRelease|Any CPU
+		{758DF7DF-A8B1-4409-B79A-018E542B7251}.Release|ARM64.ActiveCfg = Release|Any CPU
+		{758DF7DF-A8B1-4409-B79A-018E542B7251}.Release|ARM64.Build.0 = Release|Any CPU
 		{69DD5830-C682-49FB-B1A5-D2A506EEA06B}.Debug|x64.ActiveCfg = Debug|Any CPU
 		{69DD5830-C682-49FB-B1A5-D2A506EEA06B}.Debug|x64.Build.0 = Debug|Any CPU
 		{69DD5830-C682-49FB-B1A5-D2A506EEA06B}.MSIX Debug|x64.ActiveCfg = Debug|Any CPU
@@ -258,6 +244,8 @@ Global
 		{69DD5830-C682-49FB-B1A5-D2A506EEA06B}.DevRelease|x64.Build.0 = Debug|Any CPU
 		{69DD5830-C682-49FB-B1A5-D2A506EEA06B}.DevSteam|x64.ActiveCfg = Release|Any CPU
 		{69DD5830-C682-49FB-B1A5-D2A506EEA06B}.DevSteam|x64.Build.0 = Release|Any CPU
+		{69DD5830-C682-49FB-B1A5-D2A506EEA06B}.Release|ARM64.ActiveCfg = Release|Any CPU
+		{69DD5830-C682-49FB-B1A5-D2A506EEA06B}.Release|ARM64.Build.0 = Release|Any CPU
 		{1DC5B4C4-6902-4659-AE7E-17FDA0403DEB}.Debug|x64.ActiveCfg = Debug|Any CPU
 		{1DC5B4C4-6902-4659-AE7E-17FDA0403DEB}.Debug|x64.Build.0 = Debug|Any CPU
 		{1DC5B4C4-6902-4659-AE7E-17FDA0403DEB}.MSIX Debug|x64.ActiveCfg = Debug|Any CPU
@@ -274,6 +262,8 @@ Global
 		{1DC5B4C4-6902-4659-AE7E-17FDA0403DEB}.DevRelease|x64.Build.0 = Debug|Any CPU
 		{1DC5B4C4-6902-4659-AE7E-17FDA0403DEB}.DevSteam|x64.ActiveCfg = Release|Any CPU
 		{1DC5B4C4-6902-4659-AE7E-17FDA0403DEB}.DevSteam|x64.Build.0 = Release|Any CPU
+		{1DC5B4C4-6902-4659-AE7E-17FDA0403DEB}.Release|ARM64.ActiveCfg = Release|Any CPU
+		{1DC5B4C4-6902-4659-AE7E-17FDA0403DEB}.Release|ARM64.Build.0 = Release|Any CPU
 		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.Debug|x64.ActiveCfg = Debug|Any CPU
 		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.Debug|x64.Build.0 = Debug|Any CPU
 		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.DevRelease|x64.ActiveCfg = Debug|Any CPU
@@ -290,6 +280,8 @@ Global
 		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.Debug|ARM64.Build.0 = Debug|Any CPU
 		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.DevSteam|x64.ActiveCfg = Release|Any CPU
 		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.DevSteam|x64.Build.0 = Release|Any CPU
+		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.Release|ARM64.ActiveCfg = Release|Any CPU
+		{9BCD0764-9C16-4A2A-B153-C676FEF38887}.Release|ARM64.Build.0 = Release|Any CPU
 		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.Debug|x64.ActiveCfg = Debug|Any CPU
 		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.Debug|x64.Build.0 = Debug|Any CPU
 		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.DevRelease|x64.ActiveCfg = Debug|Any CPU
@@ -306,6 +298,8 @@ Global
 		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.Debug|ARM64.Build.0 = Debug|Any CPU
 		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.DevSteam|x64.ActiveCfg = Release|Any CPU
 		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.DevSteam|x64.Build.0 = Release|Any CPU
+		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.Release|ARM64.ActiveCfg = Release|Any CPU
+		{2BDEB8C6-F22D-43EA-A309-B3387A803689}.Release|ARM64.Build.0 = Release|Any CPU
 		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6}.Debug|x64.ActiveCfg = Debug|Any CPU
 		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6}.Debug|x64.Build.0 = Debug|Any CPU
 		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6}.DevRelease|x64.ActiveCfg = Debug|Any CPU
@@ -333,6 +327,8 @@ Global
 		{7A12C96B-8B5C-45E1-9EF6-0B1DA7F270DE}.Steam|x64.ActiveCfg = Debug|Any CPU
 		{7A12C96B-8B5C-45E1-9EF6-0B1DA7F270DE}.Debug|ARM64.ActiveCfg = Debug|Any CPU
 		{7A12C96B-8B5C-45E1-9EF6-0B1DA7F270DE}.Debug|ARM64.Build.0 = Debug|Any CPU
+		{7A12C96B-8B5C-45E1-9EF6-0B1DA7F270DE}.Release|ARM64.ActiveCfg = Release|Any CPU
+		{7A12C96B-8B5C-45E1-9EF6-0B1DA7F270DE}.Release|ARM64.Build.0 = Release|Any CPU
 		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.Debug|x64.ActiveCfg = Debug|Any CPU
 		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.Debug|x64.Build.0 = Debug|Any CPU
 		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.DevRelease|x64.ActiveCfg = Debug|Any CPU
@@ -349,6 +345,8 @@ Global
 		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.Debug|ARM64.Build.0 = Debug|Any CPU
 		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.DevSteam|x64.ActiveCfg = Release|Any CPU
 		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.DevSteam|x64.Build.0 = Release|Any CPU
+		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.Release|ARM64.ActiveCfg = Release|Any CPU
+		{1249EE2B-EB0D-411C-B311-53A7A22B7743}.Release|ARM64.Build.0 = Release|Any CPU
 		{FA98BFA6-2E83-41C6-9102-76875B261F51}.Debug|x64.ActiveCfg = Debug|Any CPU
 		{FA98BFA6-2E83-41C6-9102-76875B261F51}.Debug|x64.Build.0 = Debug|Any CPU
 		{FA98BFA6-2E83-41C6-9102-76875B261F51}.DevRelease|x64.ActiveCfg = Debug|Any CPU
@@ -365,6 +363,8 @@ Global
 		{FA98BFA6-2E83-41C6-9102-76875B261F51}.Steam|x64.Build.0 = Release|Any CPU
 		{FA98BFA6-2E83-41C6-9102-76875B261F51}.Debug|ARM64.ActiveCfg = Debug|Any CPU
 		{FA98BFA6-2E83-41C6-9102-76875B261F51}.Debug|ARM64.Build.0 = Debug|Any CPU
+		{FA98BFA6-2E83-41C6-9102-76875B261F51}.Release|ARM64.ActiveCfg = Release|Any CPU
+		{FA98BFA6-2E83-41C6-9102-76875B261F51}.Release|ARM64.Build.0 = Release|Any CPU
 		{16519035-0FF4-456F-B3F0-0ACA16E6920C}.Debug|x64.ActiveCfg = Debug|Any CPU
 		{16519035-0FF4-456F-B3F0-0ACA16E6920C}.Debug|x64.Build.0 = Debug|Any CPU
 		{16519035-0FF4-456F-B3F0-0ACA16E6920C}.DevRelease|x64.ActiveCfg = Debug|Any CPU
@@ -381,6 +381,8 @@ Global
 		{16519035-0FF4-456F-B3F0-0ACA16E6920C}.Steam|x64.Build.0 = Release|Any CPU
 		{16519035-0FF4-456F-B3F0-0ACA16E6920C}.Debug|ARM64.ActiveCfg = Debug|Any CPU
 		{16519035-0FF4-456F-B3F0-0ACA16E6920C}.Debug|ARM64.Build.0 = Debug|Any CPU
+		{16519035-0FF4-456F-B3F0-0ACA16E6920C}.Release|ARM64.ActiveCfg = Release|Any CPU
+		{16519035-0FF4-456F-B3F0-0ACA16E6920C}.Release|ARM64.Build.0 = Release|Any CPU
 		{3DF64622-87E3-4870-B694-05D565251BB9}.Debug|x64.ActiveCfg = Debug|Any CPU
 		{3DF64622-87E3-4870-B694-05D565251BB9}.Debug|x64.Build.0 = Debug|Any CPU
 		{3DF64622-87E3-4870-B694-05D565251BB9}.DevRelease|x64.ActiveCfg = Debug|Any CPU
@@ -429,6 +431,8 @@ Global
 		{8F4FFC91-BE9F-4476-A372-FBD952865F15}.Steam|x64.Build.0 = Release|Any CPU
 		{8F4FFC91-BE9F-4476-A372-FBD952865F15}.Debug|ARM64.ActiveCfg = Debug|Any CPU
 		{8F4FFC91-BE9F-4476-A372-FBD952865F15}.Debug|ARM64.Build.0 = Debug|Any CPU
+		{8F4FFC91-BE9F-4476-A372-FBD952865F15}.Release|ARM64.ActiveCfg = Release|Any CPU
+		{8F4FFC91-BE9F-4476-A372-FBD952865F15}.Release|ARM64.Build.0 = Release|Any CPU
 		{71907779-F1D1-4AA6-BA11-E990DB089841}.Debug|x64.ActiveCfg = Debug|Any CPU
 		{71907779-F1D1-4AA6-BA11-E990DB089841}.Debug|x64.Build.0 = Debug|Any CPU
 		{71907779-F1D1-4AA6-BA11-E990DB089841}.DevRelease|x64.ActiveCfg = Debug|Any CPU
@@ -461,6 +465,8 @@ Global
 		{B30622ED-9177-4930-8E64-2B2352D4D8DC}.Steam|x64.Build.0 = Release|Any CPU
 		{B30622ED-9177-4930-8E64-2B2352D4D8DC}.Debug|ARM64.ActiveCfg = Debug|Any CPU
 		{B30622ED-9177-4930-8E64-2B2352D4D8DC}.Debug|ARM64.Build.0 = Debug|Any CPU
+		{B30622ED-9177-4930-8E64-2B2352D4D8DC}.Release|ARM64.ActiveCfg = Release|Any CPU
+		{B30622ED-9177-4930-8E64-2B2352D4D8DC}.Release|ARM64.Build.0 = Release|Any CPU
 		{43C03D0E-EF50-4225-A268-CB9B8E0E8622}.Debug|x64.ActiveCfg = Debug|Any CPU
 		{43C03D0E-EF50-4225-A268-CB9B8E0E8622}.Debug|x64.Build.0 = Debug|Any CPU
 		{43C03D0E-EF50-4225-A268-CB9B8E0E8622}.DevRelease|x64.ActiveCfg = Debug|Any CPU
@@ -477,6 +483,8 @@ Global
 		{43C03D0E-EF50-4225-A268-CB9B8E0E8622}.Steam|x64.Build.0 = Release|Any CPU
 		{43C03D0E-EF50-4225-A268-CB9B8E0E8622}.Debug|ARM64.ActiveCfg = Debug|Any CPU
 		{43C03D0E-EF50-4225-A268-CB9B8E0E8622}.Debug|ARM64.Build.0 = Debug|Any CPU
+		{43C03D0E-EF50-4225-A268-CB9B8E0E8622}.Release|ARM64.ActiveCfg = Release|Any CPU
+		{43C03D0E-EF50-4225-A268-CB9B8E0E8622}.Release|ARM64.Build.0 = Release|Any CPU
 		{5848FCF1-E127-4CE3-8A25-F37032819F8D}.Debug|x64.ActiveCfg = Debug|Any CPU
 		{5848FCF1-E127-4CE3-8A25-F37032819F8D}.Debug|x64.Build.0 = Debug|Any CPU
 		{5848FCF1-E127-4CE3-8A25-F37032819F8D}.DevRelease|x64.ActiveCfg = Debug|Any CPU
@@ -493,6 +501,8 @@ Global
 		{5848FCF1-E127-4CE3-8A25-F37032819F8D}.Steam|x64.Build.0 = Release|Any CPU
 		{5848FCF1-E127-4CE3-8A25-F37032819F8D}.Debug|ARM64.ActiveCfg = Debug|Any CPU
 		{5848FCF1-E127-4CE3-8A25-F37032819F8D}.Debug|ARM64.Build.0 = Debug|Any CPU
+		{5848FCF1-E127-4CE3-8A25-F37032819F8D}.Release|ARM64.ActiveCfg = Release|Any CPU
+		{5848FCF1-E127-4CE3-8A25-F37032819F8D}.Release|ARM64.Build.0 = Release|Any CPU
 		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}.Debug|x64.ActiveCfg = Debug|Any CPU
 		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}.Debug|x64.Build.0 = Debug|Any CPU
 		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}.DevRelease|x64.ActiveCfg = Debug|Any CPU
@@ -509,6 +519,8 @@ Global
 		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}.Steam|x64.Build.0 = Release|Any CPU
 		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}.Debug|ARM64.ActiveCfg = Debug|Any CPU
 		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}.Debug|ARM64.Build.0 = Debug|Any CPU
+		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}.Release|ARM64.ActiveCfg = Release|Any CPU
+		{5E8F82CF-F48A-40B2-99E3-9BBB8725866A}.Release|ARM64.Build.0 = Release|Any CPU
 		{9FCCD0CF-FF76-4638-A712-803EFBBC641F}.Debug|x64.ActiveCfg = Debug|Any CPU
 		{9FCCD0CF-FF76-4638-A712-803EFBBC641F}.Debug|x64.Build.0 = Debug|Any CPU
 		{9FCCD0CF-FF76-4638-A712-803EFBBC641F}.DevRelease|x64.ActiveCfg = Debug|Any CPU
@@ -525,6 +537,8 @@ Global
 		{9FCCD0CF-FF76-4638-A712-803EFBBC641F}.Steam|x64.Build.0 = Release|Any CPU
 		{9FCCD0CF-FF76-4638-A712-803EFBBC641F}.Debug|ARM64.ActiveCfg = Debug|Any CPU
 		{9FCCD0CF-FF76-4638-A712-803EFBBC641F}.Debug|ARM64.Build.0 = Debug|Any CPU
+		{9FCCD0CF-FF76-4638-A712-803EFBBC641F}.Release|ARM64.ActiveCfg = Release|Any CPU
+		{9FCCD0CF-FF76-4638-A712-803EFBBC641F}.Release|ARM64.Build.0 = Release|Any CPU
 		{DA3AF3CC-43B2-4871-BDEC-CBE9222A8269}.Debug|x64.ActiveCfg = Debug|Any CPU
 		{DA3AF3CC-43B2-4871-BDEC-CBE9222A8269}.Debug|x64.Build.0 = Debug|Any CPU
 		{DA3AF3CC-43B2-4871-BDEC-CBE9222A8269}.DevRelease|x64.ActiveCfg = Debug|Any CPU
@@ -557,6 +571,8 @@ Global
 		{E46F2824-3CDA-40CB-AA57-8A4387E6B188}.Steam|x64.Build.0 = Release|Any CPU
 		{E46F2824-3CDA-40CB-AA57-8A4387E6B188}.Debug|ARM64.ActiveCfg = Debug|Any CPU
 		{E46F2824-3CDA-40CB-AA57-8A4387E6B188}.Debug|ARM64.Build.0 = Debug|Any CPU
+		{E46F2824-3CDA-40CB-AA57-8A4387E6B188}.Release|ARM64.ActiveCfg = Release|Any CPU
+		{E46F2824-3CDA-40CB-AA57-8A4387E6B188}.Release|ARM64.Build.0 = Release|Any CPU
 		{AE200ADC-9E85-4275-A373-E975CD6D518C}.Debug|x64.ActiveCfg = Debug|Any CPU
 		{AE200ADC-9E85-4275-A373-E975CD6D518C}.Debug|x64.Build.0 = Debug|Any CPU
 		{AE200ADC-9E85-4275-A373-E975CD6D518C}.DevRelease|x64.ActiveCfg = Debug|Any CPU
@@ -571,6 +587,7 @@ Global
 		{AE200ADC-9E85-4275-A373-E975CD6D518C}.Release|x64.Build.0 = Release|Any CPU
 		{AE200ADC-9E85-4275-A373-E975CD6D518C}.Steam|x64.ActiveCfg = Release|Any CPU
 		{AE200ADC-9E85-4275-A373-E975CD6D518C}.Steam|x64.Build.0 = Release|Any CPU
+		{AE200ADC-9E85-4275-A373-E975CD6D518C}.Release|ARM64.ActiveCfg = Release|Any CPU
 		{F2E992CA-12E3-49F3-B16F-2CEF5B191493}.Debug|x64.ActiveCfg = Debug|x64
 		{F2E992CA-12E3-49F3-B16F-2CEF5B191493}.Debug|x64.Build.0 = Debug|x64
 		{F2E992CA-12E3-49F3-B16F-2CEF5B191493}.Steam|x64.ActiveCfg = Steam|x64
@@ -583,6 +600,8 @@ Global
 		{F2E992CA-12E3-49F3-B16F-2CEF5B191493}.DevRelease|x64.Build.0 = DevRelease|x64
 		{F2E992CA-12E3-49F3-B16F-2CEF5B191493}.DevSteam|x64.ActiveCfg = DevSteam|x64
 		{F2E992CA-12E3-49F3-B16F-2CEF5B191493}.DevSteam|x64.Build.0 = DevSteam|x64
+		{F2E992CA-12E3-49F3-B16F-2CEF5B191493}.Release|ARM64.ActiveCfg = Release|arm64
+		{F2E992CA-12E3-49F3-B16F-2CEF5B191493}.Release|ARM64.Build.0 = Release|arm64
 		{D72E70F3-BF37-432F-B78B-5B247C873852}.Debug|ARM64.ActiveCfg = Debug|Any CPU
 		{D72E70F3-BF37-432F-B78B-5B247C873852}.Debug|ARM64.Build.0 = Debug|Any CPU
 		{D72E70F3-BF37-432F-B78B-5B247C873852}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -695,12 +714,6 @@ Global
 		{059904E2-9ACC-41E6-B78D-F0BC03CA9D78}.Steam|ARM64.Build.0 = Debug|Any CPU
 		{059904E2-9ACC-41E6-B78D-F0BC03CA9D78}.Steam|x64.ActiveCfg = Release|Any CPU
 		{059904E2-9ACC-41E6-B78D-F0BC03CA9D78}.Steam|x64.Build.0 = Release|Any CPU
-		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.Debug|x64.ActiveCfg = Release|Any CPU
-		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.Debug|x64.Build.0 = Release|Any CPU
-		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.Steam|x64.ActiveCfg = Release|Any CPU
-		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.Steam|x64.Build.0 = Release|Any CPU
-		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.Debug|ARM64.ActiveCfg = Debug|Any CPU
-		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2}.Debug|ARM64.Build.0 = Debug|Any CPU
 		{2B4A9926-0532-4C59-9289-37775A7499A4}.Debug|x64.ActiveCfg = Debug|Any CPU
 		{2B4A9926-0532-4C59-9289-37775A7499A4}.Debug|x64.Build.0 = Debug|Any CPU
 		{2B4A9926-0532-4C59-9289-37775A7499A4}.Debug|ARM64.ActiveCfg = Debug|Any CPU
@@ -821,6 +834,8 @@ Global
 		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.DevRelease|x64.Build.0 = Debug|Any CPU
 		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Release|x64.ActiveCfg = Release|Any CPU
 		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Release|x64.Build.0 = Release|Any CPU
+		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Release|ARM64.ActiveCfg = Release|Any CPU
+		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE}.Release|ARM64.Build.0 = Release|Any CPU
 		{786E1F87-4A10-493E-88BD-3F2461DBFCA0}.Debug|x64.ActiveCfg = Debug|Any CPU
 		{786E1F87-4A10-493E-88BD-3F2461DBFCA0}.Debug|x64.Build.0 = Debug|Any CPU
 		{786E1F87-4A10-493E-88BD-3F2461DBFCA0}.Debug|ARM64.ActiveCfg = Debug|Any CPU
@@ -1248,14 +1263,12 @@ Global
 	GlobalSection(NestedProjects) = preSolution
 		{41B40602-2E8C-4B76-9BDB-B9FDE686ACCE} = {68C3DA2D-D2EA-426E-A866-0019E425C816}
 		{80BB2920-3DC0-406C-9E2B-30B08D5CC7A8} = {1E816135-76C1-4255-BE3C-BF17895A65AA}
-		{1F97F972-F9E8-4F35-A8B5-3F71408D2230} = {68C3DA2D-D2EA-426E-A866-0019E425C816}
 		{6A9DA760-1E47-414C-B8E8-3B4927F18131} = {1E816135-76C1-4255-BE3C-BF17895A65AA}
 		{510ED47C-2455-4DCE-A561-1074725E1236} = {5AFBF881-C054-4CE4-8159-8D4017FFD27A}
 		{294FD171-9536-474C-A679-83F0266275FB} = {1E816135-76C1-4255-BE3C-BF17895A65AA}
 		{758DF7DF-A8B1-4409-B79A-018E542B7251} = {1E816135-76C1-4255-BE3C-BF17895A65AA}
 		{69DD5830-C682-49FB-B1A5-D2A506EEA06B} = {1E816135-76C1-4255-BE3C-BF17895A65AA}
 		{1DC5B4C4-6902-4659-AE7E-17FDA0403DEB} = {1E816135-76C1-4255-BE3C-BF17895A65AA}
-		{7AEE19FA-A4F8-4ACA-9E39-401AA1F603C2} = {68C3DA2D-D2EA-426E-A866-0019E425C816}
 		{9BCD0764-9C16-4A2A-B153-C676FEF38887} = {9A81B795-66AB-4743-9284-90565941343D}
 		{2BDEB8C6-F22D-43EA-A309-B3387A803689} = {9A81B795-66AB-4743-9284-90565941343D}
 		{8EF48E6C-8219-4EE2-87C6-5176D8D092E6} = {9A81B795-66AB-4743-9284-90565941343D}

+ 1 - 0
src/PixiEditor/App.axaml

@@ -5,6 +5,7 @@
              xmlns:templates="clr-namespace:ColorPicker.AvaloniaUI.Templates;assembly=ColorPicker.AvaloniaUI"
              xmlns:avalonia="clr-namespace:PixiDocks.Avalonia;assembly=PixiDocks.Avalonia"
              x:Class="PixiEditor.App"
+             Name="PixiEditor"
              RequestedThemeVariant="Dark">
     <!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
     <Application.DataTemplates>

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

@@ -834,5 +834,6 @@
   "PRESERVE_ALPHA": "Preserve alpha",
   "BLUR_FILTER_NODE": "Gaussian Blur Filter",
   "LENGTH": "Length",
-  "GREATER_THAN_OR_EQUAL": "Greater than or equal"
+  "GREATER_THAN_OR_EQUAL": "Greater than or equal",
+  "COLOR_NODE": "Color"
 }

+ 55 - 10
src/PixiEditor/Helpers/Converters/KeyToStringConverter.cs

@@ -1,23 +1,68 @@
 using System.Globalization;
+using System.Text;
 using Avalonia.Input;
 using PixiEditor.Extensions.Common.Localization;
+using PixiEditor.Extensions.Helpers;
+using PixiEditor.Models.Input;
+using PixiEditor.OperatingSystem;
 
 namespace PixiEditor.Helpers.Converters;
 
 internal class KeyToStringConverter
     : SingleInstanceConverter<KeyToStringConverter>
 {
-    public override object Convert(object value, Type targetType, object parameter, CultureInfo culture) =>
-        value switch
+    public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+    {
+        return value switch
         {
-            Key key => (object)InputKeyHelpers.GetKeyboardKey(key),
-            KeyModifiers modifier => modifier switch
-            {
-                KeyModifiers.Control => new LocalizedString("CTRL_KEY"),
-                KeyModifiers.Shift => new LocalizedString("SHIFT_KEY"),
-                KeyModifiers.Alt => new LocalizedString("ALT_KEY"),
-                _ => modifier.ToString()
-            },
+            Key key => ConvertKey(key),
+            KeyModifiers modifier => ConvertModifier(modifier),
+            KeyGesture gesture => ConvertKeyCombination(gesture),
             _ => string.Empty
         };
+    }
+
+    private static string ConvertKey(Key key)
+    {
+        return InputKeyHelpers.GetKeyboardKey(key);
+    }
+
+    private static string ConvertModifier(KeyModifiers modifier)
+    {
+        if (IOperatingSystem.Current.InputKeys.ModifierUsesSymbol(modifier))
+        {
+            return InputKeyHelpers.GetKeyboardKey(InputKeyHelpers.ModifierToKey(modifier));
+        }
+
+        return modifier switch
+        {
+            KeyModifiers.Control => new LocalizedString("CTRL_KEY"),
+            KeyModifiers.Shift => new LocalizedString("SHIFT_KEY"),
+            KeyModifiers.Alt => new LocalizedString("ALT_KEY"),
+            _ => modifier.ToString()
+        };
+    }
+
+    private string ConvertKeyCombination(KeyGesture value)
+    {
+        var flags = value.KeyModifiers.GetFlags().OrderByDescending(x => x != KeyModifiers.Alt);
+        var builder = new StringBuilder();
+        
+        foreach (var modifier in flags)
+        {
+            if (modifier == KeyModifiers.None) continue;
+
+            string mod = ConvertModifier(modifier);
+
+            builder.Append($"{mod}+");
+        }
+        
+        if (value.Key != Key.None)
+        {
+            builder.Append(ConvertKey(value.Key));
+        }
+        
+        builder.Append('‎'); // left-to-right marker ensures Avalonia does not reverse the string when using punctuations as key
+        return builder.ToString();
+    }
 }

+ 31 - 3
src/PixiEditor/Helpers/DocumentViewModelBuilder.cs

@@ -87,7 +87,7 @@ internal class DocumentViewModelBuilder
         return this;
     }
 
-    public DocumentViewModelBuilder WithAnimationData(AnimationData? animationData)
+    public DocumentViewModelBuilder WithAnimationData(AnimationData? animationData, NodeGraph documentGraph)
     {
         AnimationData = new AnimationDataBuilder();
 
@@ -96,7 +96,7 @@ internal class DocumentViewModelBuilder
             AnimationData.WithFrameRate(animationData.FrameRate);
             AnimationData.WithOnionFrames(animationData.OnionFrames);
             AnimationData.WithOnionOpacity(animationData.OnionOpacity);
-            BuildKeyFrames(animationData.KeyFrameGroups.ToList(), AnimationData.KeyFrameGroups);
+            BuildKeyFrames(animationData.KeyFrameGroups.ToList(), AnimationData.KeyFrameGroups, documentGraph);
         }
 
         return this;
@@ -132,7 +132,7 @@ internal class DocumentViewModelBuilder
         return this;
     }
 
-    private static void BuildKeyFrames(List<KeyFrameGroup> root, List<KeyFrameBuilder> data)
+    private static void BuildKeyFrames(List<KeyFrameGroup> root, List<KeyFrameBuilder> data, NodeGraph documentGraph)
     {
         foreach (KeyFrameGroup group in root)
         {
@@ -148,6 +148,34 @@ internal class DocumentViewModelBuilder
 
             data?.Add(builder);
         }
+        
+        TryAddMissingKeyFrames(root, data, documentGraph);
+    }
+
+    private static void TryAddMissingKeyFrames(List<KeyFrameGroup> groups, List<KeyFrameBuilder>? data, NodeGraph documentGraph)
+    {
+        if (data == null)
+        {
+            return;
+        }
+
+        foreach (var node in documentGraph.AllNodes)
+        {
+            if (node.KeyFrames.Length > 1 && data.All(x => x.NodeId != node.Id))
+            {
+                GroupKeyFrameBuilder builder = new GroupKeyFrameBuilder()
+                .WithNodeId(node.Id);
+                
+                foreach (var keyFrame in node.KeyFrames)
+                {
+                    builder.WithChild<KeyFrameBuilder>(x => x
+                        .WithKeyFrameId(keyFrame.Id)
+                        .WithNodeId(node.Id));
+                }   
+                
+                data.Add(builder);
+            }
+        }
     }
 
     public class ReferenceLayerBuilder

+ 26 - 0
src/PixiEditor/Helpers/Extensions/ImageExtensions.cs

@@ -0,0 +1,26 @@
+using Avalonia;
+using Avalonia.Media;
+using Avalonia.Media.Imaging;
+
+namespace PixiEditor.Helpers.Extensions;
+
+public static class ImageExtensions
+{
+    public static Bitmap? ToBitmap(this IImage? image, PixelSize dimensions)
+    {
+        if (image is null)
+        {
+            return null;
+        }
+        
+        RenderTargetBitmap renderTarget = new RenderTargetBitmap(dimensions);
+        var context = renderTarget.CreateDrawingContext();
+        
+        Rect rect = new Rect(0, 0, dimensions.Width, dimensions.Height);
+        image.Draw(context, rect, rect);
+        
+        context.Dispose();
+        
+        return renderTarget;
+    }
+}

+ 1 - 1
src/PixiEditor/Helpers/Extensions/PixiParserDocumentEx.cs

@@ -38,7 +38,7 @@ internal static class PixiParserDocumentEx
             .WithSwatches(document.Swatches, x => new(x.R, x.G, x.B))
             .WithReferenceLayer(document.ReferenceLayer, BuildReferenceLayer, encoder)
             .WithGraph(document.Graph, BuildGraph)
-            .WithAnimationData(document.AnimationData));
+            .WithAnimationData(document.AnimationData, document.Graph));
     }
 
     private static void BuildGraph(NodeGraph graph, NodeGraphBuilder graphBuilder)

+ 17 - 0
src/PixiEditor/Helpers/InputKeyHelpers.cs

@@ -10,4 +10,21 @@ internal static class InputKeyHelpers
     /// </summary>
     public static string GetKeyboardKey(Key key, bool forceInvariant = false) =>
         IOperatingSystem.Current.InputKeys.GetKeyboardKey(key, forceInvariant);
+
+    public static bool ModifierUsesSymbol(KeyModifiers modifier)
+    {
+        return IOperatingSystem.Current.InputKeys.ModifierUsesSymbol(modifier);
+    }
+
+    public static Key ModifierToKey(KeyModifiers modifier)
+    {
+        return modifier switch
+        {
+            KeyModifiers.Alt => Key.LeftAlt,
+            KeyModifiers.Control => Key.LeftCtrl,
+            KeyModifiers.Shift => Key.LeftShift,
+            KeyModifiers.Meta => Key.LWin,
+            _ => Key.None
+        };
+    }
 }

+ 10 - 5
src/PixiEditor/Helpers/ThemeResources.cs

@@ -8,15 +8,20 @@ namespace PixiEditor.Helpers;
 
 public static class ThemeResources
 {
+    public static string? ThemeFontFamilyName { get; } = "FiraSans";
+
     public static Font ThemeFont =>
-        Font.FromFamilyName("FiraSans") ?? Font.CreateDefault();
+        Font.FromFamilyName(ThemeFontFamilyName) ?? Font.CreateDefault();
 
     public static Color ForegroundColor =>
-        ResourceLoader.GetResource<SolidColorBrush>("ThemeForegroundBrush", Application.Current.ActualThemeVariant).Color.ToColor();
-    
+        ResourceLoader.GetResource<SolidColorBrush>("ThemeForegroundBrush", Application.Current.ActualThemeVariant)
+            .Color.ToColor();
+
     public static Color BackgroundColor =>
-        ResourceLoader.GetResource<SolidColorBrush>("ThemeBackgroundBrush", Application.Current.ActualThemeVariant).Color.ToColor();
+        ResourceLoader.GetResource<SolidColorBrush>("ThemeBackgroundBrush", Application.Current.ActualThemeVariant)
+            .Color.ToColor();
 
     public static Color BorderMidColor =>
-        ResourceLoader.GetResource<SolidColorBrush>("ThemeBorderMidBrush", Application.Current.ActualThemeVariant).Color.ToColor();
+        ResourceLoader.GetResource<SolidColorBrush>("ThemeBorderMidBrush", Application.Current.ActualThemeVariant).Color
+            .ToColor();
 }

+ 32 - 7
src/PixiEditor/Initialization/ClassicDesktopEntry.cs

@@ -4,6 +4,7 @@ using System.Linq;
 using System.Text.RegularExpressions;
 using System.Threading;
 using System.Threading.Tasks;
+using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Threading;
@@ -30,15 +31,37 @@ internal class ClassicDesktopEntry
     public ClassicDesktopEntry(IClassicDesktopStyleApplicationLifetime desktop)
     {
         this.desktop = desktop;
+        IActivatableLifetime? activable =
+            (IActivatableLifetime?)App.Current.TryGetFeature(typeof(IActivatableLifetime));
+        if (activable != null)
+        {
+            activable.Activated += ActivableOnActivated;
+        }
+
         desktop.Startup += Start;
         desktop.ShutdownRequested += ShutdownRequested;
     }
 
+    private void ActivableOnActivated(object? sender, ActivatedEventArgs e)
+    {
+        if (e.Kind == ActivationKind.File && e is FileActivatedEventArgs fileActivatedEventArgs)
+        {
+            IOperatingSystem.Current.HandleActivatedWithFile(fileActivatedEventArgs);
+        }
+        else if (e.Kind == ActivationKind.OpenUri && e is ProtocolActivatedEventArgs openUriEventArgs)
+        {
+            IOperatingSystem.Current.HandleActivatedWithUri(openUriEventArgs);
+        }
+    }
+
     private void Start(object? sender, ControlledApplicationLifetimeStartupEventArgs e)
     {
         StartupArgs.Args = e.Args.ToList();
         string arguments = string.Join(' ', e.Args);
 
+        Dispatcher dispatcher = Dispatcher.UIThread;
+        InitOperatingSystem();
+
         if (ParseArgument("--crash (\"?)([A-z0-9:\\/\\ -_.]+)\\1", arguments, out Group[] groups))
         {
             try
@@ -63,9 +86,6 @@ internal class ClassicDesktopEntry
             return;
         }
 
-        Dispatcher dispatcher = Dispatcher.UIThread;
-        InitOperatingSystem();
-
 #if !STEAM
         if (!HandleNewInstance(dispatcher))
         {
@@ -136,17 +156,22 @@ internal class ClassicDesktopEntry
         return IOperatingSystem.Current.HandleNewInstance(dispatcher, OpenInExisting, desktop);
     }
 
-    private void OpenInExisting(string passedArgsFile)
+    private void OpenInExisting(string passedArgs, bool isInline)
     {
         if (desktop.MainWindow is MainWindow mainWindow)
         {
             mainWindow.BringIntoView();
             List<string> args = new List<string>();
-            if (File.Exists(passedArgsFile))
+            if (isInline)
+            {
+                args = CommandLineHelpers.SplitCommandLine(passedArgs)
+                    .ToList();
+            }
+            else if (File.Exists(passedArgs))
             {
-                args = CommandLineHelpers.SplitCommandLine(File.ReadAllText(passedArgsFile))
+                args = CommandLineHelpers.SplitCommandLine(File.ReadAllText(passedArgs))
                     .ToList();
-                File.Delete(passedArgsFile);
+                File.Delete(passedArgs);
             }
 
             StartupArgs.Args = args;

+ 1 - 1
src/PixiEditor/Models/Commands/Attributes/Commands/CommandAttribute.cs

@@ -28,7 +28,7 @@ internal partial class Command
         /// Gets or sets the default shortcut modfiers keys for this command
         /// </summary>
         public KeyModifiers Modifiers { get; set; }
-
+        
         /// <summary>
         /// Gets or sets the name of the icon evaluator for this command
         /// </summary>

+ 20 - 0
src/PixiEditor/Models/Commands/Attributes/Commands/CustomOsShortcutAttribute.cs

@@ -0,0 +1,20 @@
+using Avalonia.Input;
+
+namespace PixiEditor.Models.Commands.Attributes.Commands;
+
+[AttributeUsage(AttributeTargets.Method)]
+internal class CustomOsShortcutAttribute : Attribute
+{
+    public string TargetCommand { get; }
+    public string ValidOs { get; }
+    public Key Key { get; }
+    public KeyModifiers Modifiers { get; }
+    
+    public CustomOsShortcutAttribute(string targetCommand, string validOs, Key key, KeyModifiers modifiers)
+    {
+        TargetCommand = targetCommand;
+        ValidOs = validOs;
+        Key = key;
+        Modifiers = modifiers;
+    }
+}

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно